3 * View model for the filters selection and display
5 * @mixins OO.EventEmitter
6 * @mixins OO.EmitterList
10 mw
.rcfilters
.dm
.FiltersViewModel
= function MwRcfiltersDmFiltersViewModel() {
12 OO
.EventEmitter
.call( this );
13 OO
.EmitterList
.call( this );
16 this.excludedByMap
= {};
17 this.defaultParams
= {};
18 this.defaultFiltersEmpty
= null;
21 this.aggregate( { update
: 'filterItemUpdate' } );
22 this.connect( this, { filterItemUpdate
: 'onFilterItemUpdate' } );
26 OO
.initClass( mw
.rcfilters
.dm
.FiltersViewModel
);
27 OO
.mixinClass( mw
.rcfilters
.dm
.FiltersViewModel
, OO
.EventEmitter
);
28 OO
.mixinClass( mw
.rcfilters
.dm
.FiltersViewModel
, OO
.EmitterList
);
35 * Filter list is initialized
40 * @param {mw.rcfilters.dm.FilterItem} item Filter item updated
42 * Filter item has changed
48 * Respond to filter item change.
50 * @param {mw.rcfilters.dm.FilterItem} item Updated filter
53 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.onFilterItemUpdate = function ( item
) {
54 // Reapply the active state of filters
55 this.reapplyActiveFilters( item
);
57 this.emit( 'itemUpdate', item
);
61 * Calculate the active state of the filters, based on selected filters in the group.
63 * @param {mw.rcfilters.dm.FilterItem} item Changed item
65 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.reapplyActiveFilters = function ( item
) {
66 var selectedItemsCount
,
67 group
= item
.getGroup(),
70 !this.groups
[ group
].exclusionType
||
71 this.groups
[ group
].exclusionType
=== 'default'
74 // If any parameter is selected, but:
75 // - If there are unselected items in the group, they are inactive
76 // - If the entire group is selected, all are inactive
78 // Check what's selected in the group
79 selectedItemsCount
= this.groups
[ group
].filters
.filter( function ( filterItem
) {
80 return filterItem
.isSelected();
83 this.groups
[ group
].filters
.forEach( function ( filterItem
) {
84 filterItem
.toggleActive(
85 selectedItemsCount
> 0 ?
86 // If some items are selected
88 selectedItemsCount
=== model
.groups
[ group
].filters
.length
?
89 // If **all** items are selected, they're all inactive
91 // If not all are selected, then the selected are active
92 // and the unselected are inactive
93 filterItem
.isSelected()
95 // No item is selected, everything is active
99 } else if ( this.groups
[ group
].exclusionType
=== 'explicit' ) {
101 // - Go over the list of excluded filters to change their
102 // active states accordingly
104 // For each item in the list, see if there are other selected
105 // filters that also exclude it. If it does, it will still be
108 item
.getExcludedFilters().forEach( function ( filterName
) {
109 var filterItem
= model
.getItemByName( filterName
);
111 // Note to reduce confusion:
112 // - item is the filter whose state changed and should exclude the other filters
113 // in its list of exclusions
114 // - filterItem is the filter that is potentially being excluded by the current item
115 // - anotherExcludingFilter is any other filter that excludes filterItem; we must check
116 // if that filter is selected, because if it is, we should not touch the excluded item
118 // Check if there are any filters (other than the current one)
119 // that also exclude the filterName
120 !model
.excludedByMap
[ filterName
].some( function ( anotherExcludingFilterName
) {
121 var anotherExcludingFilter
= model
.getItemByName( anotherExcludingFilterName
);
124 anotherExcludingFilterName
!== item
.getName() &&
125 anotherExcludingFilter
.isSelected()
129 // Only change the state for filters that aren't
130 // also affected by other excluding selected filters
131 filterItem
.toggleActive( !item
.isSelected() );
138 * Set filters and preserve a group relationship based on
139 * the definition given by an object
141 * @param {Object} filters Filter group definition
143 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.initializeFilters = function ( filters
) {
144 var i
, filterItem
, selectedFilterNames
, excludedFilters
,
147 addToMap = function ( excludedFilters
) {
148 excludedFilters
.forEach( function ( filterName
) {
149 model
.excludedByMap
[ filterName
] = model
.excludedByMap
[ filterName
] || [];
150 model
.excludedByMap
[ filterName
].push( filterItem
.getName() );
157 this.excludedByMap
= {};
159 $.each( filters
, function ( group
, data
) {
160 model
.groups
[ group
] = model
.groups
[ group
] || {};
161 model
.groups
[ group
].filters
= model
.groups
[ group
].filters
|| [];
163 model
.groups
[ group
].title
= data
.title
;
164 model
.groups
[ group
].type
= data
.type
;
165 model
.groups
[ group
].separator
= data
.separator
|| '|';
166 model
.groups
[ group
].exclusionType
= data
.exclusionType
|| 'default';
168 selectedFilterNames
= [];
169 for ( i
= 0; i
< data
.filters
.length
; i
++ ) {
170 excludedFilters
= data
.filters
[ i
].excludes
|| [];
172 filterItem
= new mw
.rcfilters
.dm
.FilterItem( data
.filters
[ i
].name
, {
174 label
: data
.filters
[ i
].label
,
175 description
: data
.filters
[ i
].description
,
176 selected
: data
.filters
[ i
].selected
,
177 excludes
: excludedFilters
,
178 'default': data
.filters
[ i
].default
181 // Map filters and what excludes them
182 addToMap( excludedFilters
);
184 if ( data
.type
=== 'send_unselected_if_any' ) {
185 // Store the default parameter state
186 // For this group type, parameter values are direct
187 model
.defaultParams
[ data
.filters
[ i
].name
] = Number( !!data
.filters
[ i
].default );
189 data
.type
=== 'string_options' &&
190 data
.filters
[ i
].default
192 selectedFilterNames
.push( data
.filters
[ i
].name
);
195 model
.groups
[ group
].filters
.push( filterItem
);
196 items
.push( filterItem
);
199 if ( data
.type
=== 'string_options' ) {
200 // Store the default parameter group state
201 // For this group, the parameter is group name and value is the names
203 model
.defaultParams
[ group
] = model
.sanitizeStringOptionGroup( group
, selectedFilterNames
).join( model
.groups
[ group
].separator
);
207 this.addItems( items
);
209 this.emit( 'initialize' );
213 * Get the names of all available filters
215 * @return {string[]} An array of filter names
217 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getFilterNames = function () {
218 return this.getItems().map( function ( item
) { return item
.getName(); } );
222 * Get the object that defines groups and their filter items.
223 * The structure of this response:
226 * title: {string} Group title
227 * type: {string} Group type
228 * filters: {string[]} Filters in the group
232 * @return {Object} Filter groups
234 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getFilterGroups = function () {
239 * Get the current state of the filters.
241 * Checks whether the filter group is active. This means at least one
242 * filter is selected, but not all filters are selected.
244 * @param {string} groupName Group name
245 * @return {boolean} Filter group is active
247 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.isFilterGroupActive = function ( groupName
) {
249 filters
= this.groups
[ groupName
].filters
;
251 filters
.forEach( function ( filterItem
) {
252 count
+= Number( filterItem
.isSelected() );
257 count
< filters
.length
262 * Update the representation of the parameters. These are the back-end
263 * parameters representing the filters, but they represent the given
264 * current state regardless of validity.
266 * This should only run after filters are already set.
268 * @param {Object} params Parameter state
270 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.updateParameters = function ( params
) {
273 $.each( params
, function ( name
, value
) {
274 // Only store the parameters that exist in the system
275 if ( model
.getItemByName( name
) ) {
276 model
.parameters
[ name
] = value
;
282 * Get the value of a specific parameter
284 * @param {string} name Parameter name
285 * @return {number|string} Parameter value
287 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getParamValue = function ( name
) {
288 return this.parameters
[ name
];
292 * Get the current selected state of the filters
294 * @return {Object} Filters selected state
296 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getSelectedState = function () {
298 items
= this.getItems(),
301 for ( i
= 0; i
< items
.length
; i
++ ) {
302 result
[ items
[ i
].getName() ] = items
[ i
].isSelected();
309 * Get the current full state of the filters
311 * @return {Object} Filters full state
313 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getFullState = function () {
315 items
= this.getItems(),
318 for ( i
= 0; i
< items
.length
; i
++ ) {
319 result
[ items
[ i
].getName() ] = {
320 selected
: items
[ i
].isSelected(),
321 active
: items
[ i
].isActive()
329 * Get the default parameters object
331 * @return {Object} Default parameter values
333 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getDefaultParams = function () {
334 return this.defaultParams
;
338 * Set all filter states to default values
340 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.setFiltersToDefaults = function () {
341 var defaultFilterStates
= this.getFiltersFromParameters( this.getDefaultParams() );
343 this.updateFilters( defaultFilterStates
);
347 * Analyze the groups and their filters and output an object representing
348 * the state of the parameters they represent.
350 * @param {Object} [filterGroups] An object defining the filter groups to
351 * translate to parameters. Its structure must follow that of this.groups
352 * see #getFilterGroups
353 * @return {Object} Parameter state object
355 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getParametersFromFilters = function ( filterGroups
) {
356 var i
, filterItems
, anySelected
, values
,
358 groupItems
= filterGroups
|| this.getFilterGroups();
360 $.each( groupItems
, function ( group
, data
) {
361 filterItems
= data
.filters
;
363 if ( data
.type
=== 'send_unselected_if_any' ) {
364 // First, check if any of the items are selected at all.
365 // If none is selected, we're treating it as if they are
367 anySelected
= filterItems
.some( function ( filterItem
) {
368 return filterItem
.isSelected();
371 // Go over the items and define the correct values
372 for ( i
= 0; i
< filterItems
.length
; i
++ ) {
373 result
[ filterItems
[ i
].getName() ] = anySelected
?
374 Number( !filterItems
[ i
].isSelected() ) : 0;
376 } else if ( data
.type
=== 'string_options' ) {
378 for ( i
= 0; i
< filterItems
.length
; i
++ ) {
379 if ( filterItems
[ i
].isSelected() ) {
380 values
.push( filterItems
[ i
].getName() );
384 if ( values
.length
=== 0 || values
.length
=== filterItems
.length
) {
385 result
[ group
] = 'all';
387 result
[ group
] = values
.join( data
.separator
);
396 * Sanitize value group of a string_option groups type
397 * Remove duplicates and make sure to only use valid
401 * @param {string} groupName Group name
402 * @param {string[]} valueArray Array of values
403 * @return {string[]} Array of valid values
405 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.sanitizeStringOptionGroup = function( groupName
, valueArray
) {
407 validNames
= this.groups
[ groupName
].filters
.map( function ( filterItem
) {
408 return filterItem
.getName();
411 if ( valueArray
.indexOf( 'all' ) > -1 ) {
412 // If anywhere in the values there's 'all', we
413 // treat it as if only 'all' was selected.
414 // Example: param=valid1,valid2,all
419 // Get rid of any dupe and invalid parameter, only output
421 // Example: param=valid1,valid2,invalid1,valid1
422 // Result: param=valid1,valid2
423 valueArray
.forEach( function ( value
) {
425 validNames
.indexOf( value
) > -1 &&
426 result
.indexOf( value
) === -1
428 result
.push( value
);
436 * Check whether the current filter state is set to all false.
438 * @return {boolean} Current filters are all empty
440 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.areCurrentFiltersEmpty = function () {
441 var currFilters
= this.getSelectedState();
443 return Object
.keys( currFilters
).every( function ( filterName
) {
444 return !currFilters
[ filterName
];
449 * Check whether the default values of the filters are all false.
451 * @return {boolean} Default filters are all false
453 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.areDefaultFiltersEmpty = function () {
456 if ( this.defaultFiltersEmpty
!== null ) {
457 // We only need to do this test once,
458 // because defaults are set once per session
459 defaultFilters
= this.getFiltersFromParameters();
460 this.defaultFiltersEmpty
= Object
.keys( defaultFilters
).every( function ( filterName
) {
461 return !defaultFilters
[ filterName
];
465 return this.defaultFiltersEmpty
;
469 * This is the opposite of the #getParametersFromFilters method; this goes over
470 * the given parameters and translates into a selected/unselected value in the filters.
472 * @param {Object} params Parameters query object
473 * @return {Object} Filter state object
475 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getFiltersFromParameters = function ( params
) {
479 base
= this.getDefaultParams(),
482 params
= $.extend( {}, base
, params
);
484 $.each( params
, function ( paramName
, paramValue
) {
485 // Find the filter item
486 filterItem
= model
.getItemByName( paramName
);
487 // Ignore if no filter item exists
489 groupMap
[ filterItem
.getGroup() ] = groupMap
[ filterItem
.getGroup() ] || {};
491 // Mark the group if it has any items that are selected
492 groupMap
[ filterItem
.getGroup() ].hasSelected
= (
493 groupMap
[ filterItem
.getGroup() ].hasSelected
||
494 !!Number( paramValue
)
497 // Add the relevant filter into the group map
498 groupMap
[ filterItem
.getGroup() ].filters
= groupMap
[ filterItem
.getGroup() ].filters
|| [];
499 groupMap
[ filterItem
.getGroup() ].filters
.push( filterItem
);
500 } else if ( model
.groups
.hasOwnProperty( paramName
) ) {
501 // This parameter represents a group (values are the filters)
502 // this is equivalent to checking if the group is 'string_options'
503 groupMap
[ paramName
] = { filters
: model
.groups
[ paramName
].filters
};
507 // Now that we know the groups' selection states, we need to go over
508 // the filters in the groups and mark their selected states appropriately
509 $.each( groupMap
, function ( group
, data
) {
510 var paramValues
, filterItem
,
511 allItemsInGroup
= data
.filters
;
513 if ( model
.groups
[ group
].type
=== 'send_unselected_if_any' ) {
514 for ( i
= 0; i
< allItemsInGroup
.length
; i
++ ) {
515 filterItem
= allItemsInGroup
[ i
];
517 result
[ filterItem
.getName() ] = data
.hasSelected
?
518 // Flip the definition between the parameter
519 // state and the filter state
520 // This is what the 'toggleSelected' value of the filter is
521 !Number( params
[ filterItem
.getName() ] ) :
522 // Otherwise, there are no selected items in the
523 // group, which means the state is false
526 } else if ( model
.groups
[ group
].type
=== 'string_options' ) {
527 paramValues
= model
.sanitizeStringOptionGroup( group
, params
[ group
].split( model
.groups
[ group
].separator
) );
529 for ( i
= 0; i
< allItemsInGroup
.length
; i
++ ) {
530 filterItem
= allItemsInGroup
[ i
];
532 result
[ filterItem
.getName() ] = (
533 // If it is the word 'all'
534 paramValues
.length
=== 1 && paramValues
[ 0 ] === 'all' ||
535 // All values are written
536 paramValues
.length
=== model
.groups
[ group
].filters
.length
538 // All true (either because all values are written or the term 'all' is written)
539 // is the same as all filters set to false
541 // Otherwise, the filter is selected only if it appears in the parameter values
542 paramValues
.indexOf( filterItem
.getName() ) > -1;
550 * Get the item that matches the given name
552 * @param {string} name Filter name
553 * @return {mw.rcfilters.dm.FilterItem} Filter item
555 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getItemByName = function ( name
) {
556 return this.getItems().filter( function ( item
) {
557 return name
=== item
.getName();
562 * Set all filters to false or empty/all
563 * This is equivalent to display all.
565 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.emptyAllFilters = function () {
568 this.getItems().forEach( function ( filterItem
) {
569 filters
[ filterItem
.getName() ] = false;
573 this.updateFilters( filters
);
577 * Toggle selected state of items by their names
579 * @param {Object} filterDef Filter definitions
581 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.updateFilters = function ( filterDef
) {
582 var name
, filterItem
;
584 for ( name
in filterDef
) {
585 filterItem
= this.getItemByName( name
);
586 filterItem
.toggleSelected( filterDef
[ name
] );
591 * Find items whose labels match the given string
593 * @param {string} str Search string
594 * @return {Object} An object of items to show
595 * arranged by their group names
597 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.findMatches = function ( str
) {
600 items
= this.getItems();
602 // Normalize so we can search strings regardless of case
603 str
= str
.toLowerCase();
604 for ( i
= 0; i
< items
.length
; i
++ ) {
605 if ( items
[ i
].getLabel().toLowerCase().indexOf( str
) > -1 ) {
606 result
[ items
[ i
].getGroup() ] = result
[ items
[ i
].getGroup() ] || [];
607 result
[ items
[ i
].getGroup() ].push( items
[ i
] );
613 }( mediaWiki
, jQuery
) );