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
= {};
19 this.aggregate( { update
: 'filterItemUpdate' } );
20 this.connect( this, { filterItemUpdate
: 'onFilterItemUpdate' } );
24 OO
.initClass( mw
.rcfilters
.dm
.FiltersViewModel
);
25 OO
.mixinClass( mw
.rcfilters
.dm
.FiltersViewModel
, OO
.EventEmitter
);
26 OO
.mixinClass( mw
.rcfilters
.dm
.FiltersViewModel
, OO
.EmitterList
);
33 * Filter list is initialized
38 * @param {mw.rcfilters.dm.FilterItem} item Filter item updated
40 * Filter item has changed
46 * Respond to filter item change.
48 * @param {mw.rcfilters.dm.FilterItem} item Updated filter
51 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.onFilterItemUpdate = function ( item
) {
52 // Reapply the active state of filters
53 this.reapplyActiveFilters( item
);
55 this.emit( 'itemUpdate', item
);
59 * Calculate the active state of the filters, based on selected filters in the group.
61 * @param {mw.rcfilters.dm.FilterItem} item Changed item
63 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.reapplyActiveFilters = function ( item
) {
64 var selectedItemsCount
,
65 group
= item
.getGroup(),
68 !this.groups
[ group
].exclusionType
||
69 this.groups
[ group
].exclusionType
=== 'default'
72 // If any parameter is selected, but:
73 // - If there are unselected items in the group, they are inactive
74 // - If the entire group is selected, all are inactive
76 // Check what's selected in the group
77 selectedItemsCount
= this.groups
[ group
].filters
.filter( function ( filterItem
) {
78 return filterItem
.isSelected();
81 this.groups
[ group
].filters
.forEach( function ( filterItem
) {
82 filterItem
.toggleActive(
83 selectedItemsCount
> 0 ?
84 // If some items are selected
86 selectedItemsCount
=== model
.groups
[ group
].filters
.length
?
87 // If **all** items are selected, they're all inactive
89 // If not all are selected, then the selected are active
90 // and the unselected are inactive
91 filterItem
.isSelected()
93 // No item is selected, everything is active
97 } else if ( this.groups
[ group
].exclusionType
=== 'explicit' ) {
99 // - Go over the list of excluded filters to change their
100 // active states accordingly
102 // For each item in the list, see if there are other selected
103 // filters that also exclude it. If it does, it will still be
106 item
.getExcludedFilters().forEach( function ( filterName
) {
107 var filterItem
= model
.getItemByName( filterName
);
109 // Note to reduce confusion:
110 // - item is the filter whose state changed and should exclude the other filters
111 // in its list of exclusions
112 // - filterItem is the filter that is potentially being excluded by the current item
113 // - anotherExcludingFilter is any other filter that excludes filterItem; we must check
114 // if that filter is selected, because if it is, we should not touch the excluded item
116 // Check if there are any filters (other than the current one)
117 // that also exclude the filterName
118 !model
.excludedByMap
[ filterName
].some( function ( anotherExcludingFilterName
) {
119 var anotherExcludingFilter
= model
.getItemByName( anotherExcludingFilterName
);
122 anotherExcludingFilterName
!== item
.getName() &&
123 anotherExcludingFilter
.isSelected()
127 // Only change the state for filters that aren't
128 // also affected by other excluding selected filters
129 filterItem
.toggleActive( !item
.isSelected() );
136 * Set filters and preserve a group relationship based on
137 * the definition given by an object
139 * @param {Object} filters Filter group definition
141 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.initializeFilters = function ( filters
) {
142 var i
, filterItem
, excludedFilters
,
145 addToMap = function ( excludedFilters
) {
146 excludedFilters
.forEach( function ( filterName
) {
147 model
.excludedByMap
[ filterName
] = model
.excludedByMap
[ filterName
] || [];
148 model
.excludedByMap
[ filterName
].push( filterItem
.getName() );
155 this.excludedByMap
= {};
157 $.each( filters
, function ( group
, data
) {
158 model
.groups
[ group
] = model
.groups
[ group
] || {};
159 model
.groups
[ group
].filters
= model
.groups
[ group
].filters
|| [];
161 model
.groups
[ group
].title
= data
.title
;
162 model
.groups
[ group
].type
= data
.type
;
163 model
.groups
[ group
].separator
= data
.separator
|| '|';
164 model
.groups
[ group
].exclusionType
= data
.exclusionType
|| 'default';
166 for ( i
= 0; i
< data
.filters
.length
; i
++ ) {
167 excludedFilters
= data
.filters
[ i
].excludes
|| [];
169 filterItem
= new mw
.rcfilters
.dm
.FilterItem( data
.filters
[ i
].name
, {
171 label
: data
.filters
[ i
].label
,
172 description
: data
.filters
[ i
].description
,
173 selected
: data
.filters
[ i
].selected
,
174 excludes
: excludedFilters
177 // Map filters and what excludes them
178 addToMap( excludedFilters
);
180 model
.groups
[ group
].filters
.push( filterItem
);
181 items
.push( filterItem
);
185 this.addItems( items
);
186 this.emit( 'initialize' );
190 * Get the names of all available filters
192 * @return {string[]} An array of filter names
194 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getFilterNames = function () {
195 return this.getItems().map( function ( item
) { return item
.getName(); } );
199 * Get the object that defines groups and their filter items.
200 * The structure of this response:
203 * title: {string} Group title
204 * type: {string} Group type
205 * filters: {string[]} Filters in the group
209 * @return {Object} Filter groups
211 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getFilterGroups = function () {
216 * Get the current state of the filters.
218 * Checks whether the filter group is active. This means at least one
219 * filter is selected, but not all filters are selected.
221 * @param {string} groupName Group name
222 * @return {boolean} Filter group is active
224 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.isFilterGroupActive = function ( groupName
) {
226 filters
= this.groups
[ groupName
].filters
;
228 filters
.forEach( function ( filterItem
) {
229 count
+= Number( filterItem
.isSelected() );
234 count
< filters
.length
239 * Update the representation of the parameters. These are the back-end
240 * parameters representing the filters, but they represent the given
241 * current state regardless of validity.
243 * This should only run after filters are already set.
245 * @param {Object} params Parameter state
247 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.updateParameters = function ( params
) {
250 $.each( params
, function ( name
, value
) {
251 // Only store the parameters that exist in the system
252 if ( model
.getItemByName( name
) ) {
253 model
.parameters
[ name
] = value
;
259 * Get the value of a specific parameter
261 * @param {string} name Parameter name
262 * @return {number|string} Parameter value
264 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getParamValue = function ( name
) {
265 return this.parameters
[ name
];
269 * Get the current selected state of the filters
271 * @return {Object} Filters selected state
273 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getSelectedState = function () {
275 items
= this.getItems(),
278 for ( i
= 0; i
< items
.length
; i
++ ) {
279 result
[ items
[ i
].getName() ] = items
[ i
].isSelected();
286 * Get the current full state of the filters
288 * @return {Object} Filters full state
290 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getFullState = function () {
292 items
= this.getItems(),
295 for ( i
= 0; i
< items
.length
; i
++ ) {
296 result
[ items
[ i
].getName() ] = {
297 selected
: items
[ i
].isSelected(),
298 active
: items
[ i
].isActive()
306 * Analyze the groups and their filters and output an object representing
307 * the state of the parameters they represent.
309 * @return {Object} Parameter state object
311 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getParametersFromFilters = function () {
312 var i
, filterItems
, anySelected
, values
,
314 groupItems
= this.getFilterGroups();
316 $.each( groupItems
, function ( group
, data
) {
317 filterItems
= data
.filters
;
319 if ( data
.type
=== 'send_unselected_if_any' ) {
320 // First, check if any of the items are selected at all.
321 // If none is selected, we're treating it as if they are
323 anySelected
= filterItems
.some( function ( filterItem
) {
324 return filterItem
.isSelected();
327 // Go over the items and define the correct values
328 for ( i
= 0; i
< filterItems
.length
; i
++ ) {
329 result
[ filterItems
[ i
].getName() ] = anySelected
?
330 Number( !filterItems
[ i
].isSelected() ) : 0;
332 } else if ( data
.type
=== 'string_options' ) {
334 for ( i
= 0; i
< filterItems
.length
; i
++ ) {
335 if ( filterItems
[ i
].isSelected() ) {
336 values
.push( filterItems
[ i
].getName() );
340 if ( values
.length
=== 0 || values
.length
=== filterItems
.length
) {
341 result
[ group
] = 'all';
343 result
[ group
] = values
.join( data
.separator
);
352 * Sanitize value group of a string_option groups type
353 * Remove duplicates and make sure to only use valid
356 * @param {string} groupName Group name
357 * @param {string[]} valueArray Array of values
358 * @return {string[]} Array of valid values
360 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.sanitizeStringOptionGroup = function( groupName
, valueArray
) {
362 validNames
= this.groups
[ groupName
].filters
.map( function ( filterItem
) {
363 return filterItem
.getName();
366 if ( valueArray
.indexOf( 'all' ) > -1 ) {
367 // If anywhere in the values there's 'all', we
368 // treat it as if only 'all' was selected.
369 // Example: param=valid1,valid2,all
374 // Get rid of any dupe and invalid parameter, only output
376 // Example: param=valid1,valid2,invalid1,valid1
377 // Result: param=valid1,valid2
378 valueArray
.forEach( function ( value
) {
380 validNames
.indexOf( value
) > -1 &&
381 result
.indexOf( value
) === -1
383 result
.push( value
);
391 * This is the opposite of the #getParametersFromFilters method; this goes over
392 * the parameters and translates into a selected/unselected value in the filters.
394 * @param {Object} params Parameters query object
395 * @return {Object} Filter state object
397 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getFiltersFromParameters = function ( params
) {
401 base
= this.getParametersFromFilters(),
402 // Start with current state
403 result
= this.getSelectedState();
405 params
= $.extend( {}, base
, params
);
407 $.each( params
, function ( paramName
, paramValue
) {
408 // Find the filter item
409 filterItem
= model
.getItemByName( paramName
);
410 // Ignore if no filter item exists
412 groupMap
[ filterItem
.getGroup() ] = groupMap
[ filterItem
.getGroup() ] || {};
414 // Mark the group if it has any items that are selected
415 groupMap
[ filterItem
.getGroup() ].hasSelected
= (
416 groupMap
[ filterItem
.getGroup() ].hasSelected
||
417 !!Number( paramValue
)
420 // Add the relevant filter into the group map
421 groupMap
[ filterItem
.getGroup() ].filters
= groupMap
[ filterItem
.getGroup() ].filters
|| [];
422 groupMap
[ filterItem
.getGroup() ].filters
.push( filterItem
);
423 } else if ( model
.groups
.hasOwnProperty( paramName
) ) {
424 // This parameter represents a group (values are the filters)
425 // this is equivalent to checking if the group is 'string_options'
426 groupMap
[ paramName
] = { filters
: model
.groups
[ paramName
].filters
};
430 // Now that we know the groups' selection states, we need to go over
431 // the filters in the groups and mark their selected states appropriately
432 $.each( groupMap
, function ( group
, data
) {
433 var paramValues
, filterItem
,
434 allItemsInGroup
= data
.filters
;
436 if ( model
.groups
[ group
].type
=== 'send_unselected_if_any' ) {
437 for ( i
= 0; i
< allItemsInGroup
.length
; i
++ ) {
438 filterItem
= allItemsInGroup
[ i
];
440 result
[ filterItem
.getName() ] = data
.hasSelected
?
441 // Flip the definition between the parameter
442 // state and the filter state
443 // This is what the 'toggleSelected' value of the filter is
444 !Number( params
[ filterItem
.getName() ] ) :
445 // Otherwise, there are no selected items in the
446 // group, which means the state is false
449 } else if ( model
.groups
[ group
].type
=== 'string_options' ) {
450 paramValues
= model
.sanitizeStringOptionGroup( group
, params
[ group
].split( model
.groups
[ group
].separator
) );
452 for ( i
= 0; i
< allItemsInGroup
.length
; i
++ ) {
453 filterItem
= allItemsInGroup
[ i
];
455 result
[ filterItem
.getName() ] = (
456 // If it is the word 'all'
457 paramValues
.length
=== 1 && paramValues
[ 0 ] === 'all' ||
458 // All values are written
459 paramValues
.length
=== model
.groups
[ group
].filters
.length
461 // All true (either because all values are written or the term 'all' is written)
462 // is the same as all filters set to false
464 // Otherwise, the filter is selected only if it appears in the parameter values
465 paramValues
.indexOf( filterItem
.getName() ) > -1;
473 * Get the item that matches the given name
475 * @param {string} name Filter name
476 * @return {mw.rcfilters.dm.FilterItem} Filter item
478 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getItemByName = function ( name
) {
479 return this.getItems().filter( function ( item
) {
480 return name
=== item
.getName();
485 * Toggle selected state of items by their names
487 * @param {Object} filterDef Filter definitions
489 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.updateFilters = function ( filterDef
) {
490 var name
, filterItem
;
492 for ( name
in filterDef
) {
493 filterItem
= this.getItemByName( name
);
494 filterItem
.toggleSelected( filterDef
[ name
] );
499 * Find items whose labels match the given string
501 * @param {string} str Search string
502 * @return {Object} An object of items to show
503 * arranged by their group names
505 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.findMatches = function ( str
) {
508 items
= this.getItems();
510 // Normalize so we can search strings regardless of case
511 str
= str
.toLowerCase();
512 for ( i
= 0; i
< items
.length
; i
++ ) {
513 if ( items
[ i
].getLabel().toLowerCase().indexOf( str
) > -1 ) {
514 result
[ items
[ i
].getGroup() ] = result
[ items
[ i
].getGroup() ] || [];
515 result
[ items
[ i
].getGroup() ].push( items
[ i
] );
521 }( mediaWiki
, jQuery
) );