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 // Recheck group activity state
58 this.getGroup( item
.getGroup() ).checkActive();
60 this.emit( 'itemUpdate', item
);
64 * Calculate the active state of the filters, based on selected filters in the group.
66 * @param {mw.rcfilters.dm.FilterItem} item Changed item
68 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.reapplyActiveFilters = function ( item
) {
69 var selectedItemsCount
,
70 group
= item
.getGroup(),
73 !this.getGroup( group
).getExclusionType() ||
74 this.getGroup( group
).getExclusionType() === 'default'
77 // If any parameter is selected, but:
78 // - If there are unselected items in the group, they are inactive
79 // - If the entire group is selected, all are inactive
81 // Check what's selected in the group
82 selectedItemsCount
= this.getGroupFilters( group
).filter( function ( filterItem
) {
83 return filterItem
.isSelected();
86 this.getGroupFilters( group
).forEach( function ( filterItem
) {
87 filterItem
.toggleActive(
88 selectedItemsCount
> 0 ?
89 // If some items are selected
91 selectedItemsCount
=== model
.groups
[ group
].getItemCount() ?
92 // If **all** items are selected, they're all inactive
94 // If not all are selected, then the selected are active
95 // and the unselected are inactive
96 filterItem
.isSelected()
98 // No item is selected, everything is active
102 } else if ( this.getGroup( group
).getExclusionType() === 'explicit' ) {
104 // - Go over the list of excluded filters to change their
105 // active states accordingly
107 // For each item in the list, see if there are other selected
108 // filters that also exclude it. If it does, it will still be
111 item
.getExcludedFilters().forEach( function ( filterName
) {
112 var filterItem
= model
.getItemByName( filterName
);
114 // Note to reduce confusion:
115 // - item is the filter whose state changed and should exclude the other filters
116 // in its list of exclusions
117 // - filterItem is the filter that is potentially being excluded by the current item
118 // - anotherExcludingFilter is any other filter that excludes filterItem; we must check
119 // if that filter is selected, because if it is, we should not touch the excluded item
121 // Check if there are any filters (other than the current one)
122 // that also exclude the filterName
123 !model
.excludedByMap
[ filterName
].some( function ( anotherExcludingFilterName
) {
124 var anotherExcludingFilter
= model
.getItemByName( anotherExcludingFilterName
);
127 anotherExcludingFilterName
!== item
.getName() &&
128 anotherExcludingFilter
.isSelected()
132 // Only change the state for filters that aren't
133 // also affected by other excluding selected filters
134 filterItem
.toggleActive( !item
.isSelected() );
141 * Set filters and preserve a group relationship based on
142 * the definition given by an object
144 * @param {Object} filters Filter group definition
146 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.initializeFilters = function ( filters
) {
147 var i
, filterItem
, selectedFilterNames
, excludedFilters
,
150 addToMap = function ( excludedFilters
) {
151 excludedFilters
.forEach( function ( filterName
) {
152 model
.excludedByMap
[ filterName
] = model
.excludedByMap
[ filterName
] || [];
153 model
.excludedByMap
[ filterName
].push( filterItem
.getName() );
160 this.excludedByMap
= {};
162 $.each( filters
, function ( group
, data
) {
163 if ( !model
.groups
[ group
] ) {
164 model
.groups
[ group
] = new mw
.rcfilters
.dm
.FilterGroup( {
168 separator
: data
.separator
,
169 exclusionType
: data
.exclusionType
173 selectedFilterNames
= [];
174 for ( i
= 0; i
< data
.filters
.length
; i
++ ) {
175 excludedFilters
= data
.filters
[ i
].excludes
|| [];
177 filterItem
= new mw
.rcfilters
.dm
.FilterItem( data
.filters
[ i
].name
, {
179 label
: data
.filters
[ i
].label
,
180 description
: data
.filters
[ i
].description
,
181 selected
: data
.filters
[ i
].selected
,
182 excludes
: excludedFilters
,
183 'default': data
.filters
[ i
].default
186 // Map filters and what excludes them
187 addToMap( excludedFilters
);
189 if ( data
.type
=== 'send_unselected_if_any' ) {
190 // Store the default parameter state
191 // For this group type, parameter values are direct
192 model
.defaultParams
[ data
.filters
[ i
].name
] = Number( !!data
.filters
[ i
].default );
194 data
.type
=== 'string_options' &&
195 data
.filters
[ i
].default
197 selectedFilterNames
.push( data
.filters
[ i
].name
);
200 model
.groups
[ group
].addItems( filterItem
);
201 items
.push( filterItem
);
204 if ( data
.type
=== 'string_options' ) {
205 // Store the default parameter group state
206 // For this group, the parameter is group name and value is the names
208 model
.defaultParams
[ group
] = model
.sanitizeStringOptionGroup( group
, selectedFilterNames
).join( model
.groups
[ group
].getSeparator() );
212 this.addItems( items
);
214 this.emit( 'initialize' );
218 * Get the names of all available filters
220 * @return {string[]} An array of filter names
222 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getFilterNames = function () {
223 return this.getItems().map( function ( item
) { return item
.getName(); } );
227 * Get the object that defines groups by their name.
229 * @return {Object} Filter groups
231 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getFilterGroups = function () {
236 * Update the representation of the parameters. These are the back-end
237 * parameters representing the filters, but they represent the given
238 * current state regardless of validity.
240 * This should only run after filters are already set.
242 * @param {Object} params Parameter state
244 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.updateParameters = function ( params
) {
247 $.each( params
, function ( name
, value
) {
248 // Only store the parameters that exist in the system
249 if ( model
.getItemByName( name
) ) {
250 model
.parameters
[ name
] = value
;
256 * Get the value of a specific parameter
258 * @param {string} name Parameter name
259 * @return {number|string} Parameter value
261 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getParamValue = function ( name
) {
262 return this.parameters
[ name
];
266 * Get the current selected state of the filters
268 * @return {Object} Filters selected state
270 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getSelectedState = function () {
272 items
= this.getItems(),
275 for ( i
= 0; i
< items
.length
; i
++ ) {
276 result
[ items
[ i
].getName() ] = items
[ i
].isSelected();
283 * Get the current full state of the filters
285 * @return {Object} Filters full state
287 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getFullState = function () {
289 items
= this.getItems(),
292 for ( i
= 0; i
< items
.length
; i
++ ) {
293 result
[ items
[ i
].getName() ] = {
294 selected
: items
[ i
].isSelected(),
295 active
: items
[ i
].isActive()
303 * Get the default parameters object
305 * @return {Object} Default parameter values
307 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getDefaultParams = function () {
308 return this.defaultParams
;
312 * Set all filter states to default values
314 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.setFiltersToDefaults = function () {
315 var defaultFilterStates
= this.getFiltersFromParameters( this.getDefaultParams() );
317 this.updateFilters( defaultFilterStates
);
321 * Analyze the groups and their filters and output an object representing
322 * the state of the parameters they represent.
324 * @param {Object} [filterGroups] An object defining the filter groups to
325 * translate to parameters. Its structure must follow that of this.groups
326 * see #getFilterGroups
327 * @return {Object} Parameter state object
329 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getParametersFromFilters = function ( filterGroups
) {
330 var i
, filterItems
, anySelected
, values
,
332 groupItems
= filterGroups
|| this.getFilterGroups();
334 $.each( groupItems
, function ( group
, model
) {
335 filterItems
= model
.getItems();
337 if ( model
.getType() === 'send_unselected_if_any' ) {
338 // First, check if any of the items are selected at all.
339 // If none is selected, we're treating it as if they are
341 anySelected
= filterItems
.some( function ( filterItem
) {
342 return filterItem
.isSelected();
345 // Go over the items and define the correct values
346 for ( i
= 0; i
< filterItems
.length
; i
++ ) {
347 result
[ filterItems
[ i
].getName() ] = anySelected
?
348 Number( !filterItems
[ i
].isSelected() ) : 0;
350 } else if ( model
.getType() === 'string_options' ) {
352 for ( i
= 0; i
< filterItems
.length
; i
++ ) {
353 if ( filterItems
[ i
].isSelected() ) {
354 values
.push( filterItems
[ i
].getName() );
358 if ( values
.length
=== 0 || values
.length
=== filterItems
.length
) {
359 result
[ group
] = 'all';
361 result
[ group
] = values
.join( model
.getSeparator() );
370 * Sanitize value group of a string_option groups type
371 * Remove duplicates and make sure to only use valid
375 * @param {string} groupName Group name
376 * @param {string[]} valueArray Array of values
377 * @return {string[]} Array of valid values
379 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.sanitizeStringOptionGroup = function( groupName
, valueArray
) {
381 validNames
= this.getGroupFilters( groupName
).map( function ( filterItem
) {
382 return filterItem
.getName();
385 if ( valueArray
.indexOf( 'all' ) > -1 ) {
386 // If anywhere in the values there's 'all', we
387 // treat it as if only 'all' was selected.
388 // Example: param=valid1,valid2,all
393 // Get rid of any dupe and invalid parameter, only output
395 // Example: param=valid1,valid2,invalid1,valid1
396 // Result: param=valid1,valid2
397 valueArray
.forEach( function ( value
) {
399 validNames
.indexOf( value
) > -1 &&
400 result
.indexOf( value
) === -1
402 result
.push( value
);
410 * Check whether the current filter state is set to all false.
412 * @return {boolean} Current filters are all empty
414 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.areCurrentFiltersEmpty = function () {
415 var currFilters
= this.getSelectedState();
417 return Object
.keys( currFilters
).every( function ( filterName
) {
418 return !currFilters
[ filterName
];
423 * Check whether the default values of the filters are all false.
425 * @return {boolean} Default filters are all false
427 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.areDefaultFiltersEmpty = function () {
430 if ( this.defaultFiltersEmpty
!== null ) {
431 // We only need to do this test once,
432 // because defaults are set once per session
433 defaultFilters
= this.getFiltersFromParameters();
434 this.defaultFiltersEmpty
= Object
.keys( defaultFilters
).every( function ( filterName
) {
435 return !defaultFilters
[ filterName
];
439 return this.defaultFiltersEmpty
;
443 * This is the opposite of the #getParametersFromFilters method; this goes over
444 * the given parameters and translates into a selected/unselected value in the filters.
446 * @param {Object} params Parameters query object
447 * @return {Object} Filter state object
449 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getFiltersFromParameters = function ( params
) {
453 base
= this.getDefaultParams(),
456 params
= $.extend( {}, base
, params
);
458 $.each( params
, function ( paramName
, paramValue
) {
459 // Find the filter item
460 filterItem
= model
.getItemByName( paramName
);
461 // Ignore if no filter item exists
463 groupMap
[ filterItem
.getGroup() ] = groupMap
[ filterItem
.getGroup() ] || {};
465 // Mark the group if it has any items that are selected
466 groupMap
[ filterItem
.getGroup() ].hasSelected
= (
467 groupMap
[ filterItem
.getGroup() ].hasSelected
||
468 !!Number( paramValue
)
471 // Add the relevant filter into the group map
472 groupMap
[ filterItem
.getGroup() ].filters
= groupMap
[ filterItem
.getGroup() ].filters
|| [];
473 groupMap
[ filterItem
.getGroup() ].filters
.push( filterItem
);
474 } else if ( model
.groups
.hasOwnProperty( paramName
) ) {
475 // This parameter represents a group (values are the filters)
476 // this is equivalent to checking if the group is 'string_options'
477 groupMap
[ paramName
] = { filters
: model
.groups
[ paramName
].getItems() };
481 // Now that we know the groups' selection states, we need to go over
482 // the filters in the groups and mark their selected states appropriately
483 $.each( groupMap
, function ( group
, data
) {
484 var paramValues
, filterItem
,
485 allItemsInGroup
= data
.filters
;
487 if ( model
.groups
[ group
].getType() === 'send_unselected_if_any' ) {
488 for ( i
= 0; i
< allItemsInGroup
.length
; i
++ ) {
489 filterItem
= allItemsInGroup
[ i
];
491 result
[ filterItem
.getName() ] = data
.hasSelected
?
492 // Flip the definition between the parameter
493 // state and the filter state
494 // This is what the 'toggleSelected' value of the filter is
495 !Number( params
[ filterItem
.getName() ] ) :
496 // Otherwise, there are no selected items in the
497 // group, which means the state is false
500 } else if ( model
.groups
[ group
].getType() === 'string_options' ) {
501 paramValues
= model
.sanitizeStringOptionGroup( group
, params
[ group
].split( model
.groups
[ group
].getSeparator() ) );
503 for ( i
= 0; i
< allItemsInGroup
.length
; i
++ ) {
504 filterItem
= allItemsInGroup
[ i
];
506 result
[ filterItem
.getName() ] = (
507 // If it is the word 'all'
508 paramValues
.length
=== 1 && paramValues
[ 0 ] === 'all' ||
509 // All values are written
510 paramValues
.length
=== model
.groups
[ group
].getItemCount()
512 // All true (either because all values are written or the term 'all' is written)
513 // is the same as all filters set to false
515 // Otherwise, the filter is selected only if it appears in the parameter values
516 paramValues
.indexOf( filterItem
.getName() ) > -1;
524 * Get the item that matches the given name
526 * @param {string} name Filter name
527 * @return {mw.rcfilters.dm.FilterItem} Filter item
529 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getItemByName = function ( name
) {
530 return this.getItems().filter( function ( item
) {
531 return name
=== item
.getName();
536 * Set all filters to false or empty/all
537 * This is equivalent to display all.
539 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.emptyAllFilters = function () {
542 this.getItems().forEach( function ( filterItem
) {
543 filters
[ filterItem
.getName() ] = false;
547 this.updateFilters( filters
);
551 * Toggle selected state of items by their names
553 * @param {Object} filterDef Filter definitions
555 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.updateFilters = function ( filterDef
) {
556 var name
, filterItem
;
558 for ( name
in filterDef
) {
559 filterItem
= this.getItemByName( name
);
560 filterItem
.toggleSelected( filterDef
[ name
] );
565 * Get a group model from its name
567 * @param {string} groupName Group name
568 * @return {mw.rcfilters.dm.FilterGroup} Group model
570 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getGroup = function ( groupName
) {
571 return this.groups
[ groupName
];
575 * Get all filters within a specified group by its name
577 * @param {string} groupName Group name
578 * @return {mw.rcfilters.dm.FilterItem[]} Filters belonging to this group
580 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.getGroupFilters = function ( groupName
) {
581 return ( this.getGroup( groupName
) && this.getGroup( groupName
).getItems() ) || [];
585 * Find items whose labels match the given string
587 * @param {string} query Search string
588 * @return {Object} An object of items to show
589 * arranged by their group names
591 mw
.rcfilters
.dm
.FiltersViewModel
.prototype.findMatches = function ( query
) {
595 items
= this.getItems();
597 // Normalize so we can search strings regardless of case
598 query
= query
.toLowerCase();
600 // item label starting with the query string
601 for ( i
= 0; i
< items
.length
; i
++ ) {
602 if ( items
[ i
].getLabel().toLowerCase().indexOf( query
) === 0 ) {
603 result
[ items
[ i
].getGroup() ] = result
[ items
[ i
].getGroup() ] || [];
604 result
[ items
[ i
].getGroup() ].push( items
[ i
] );
608 if ( $.isEmptyObject( result
) ) {
609 // item containing the query string in their label, description, or group title
610 for ( i
= 0; i
< items
.length
; i
++ ) {
611 groupTitle
= this.getGroup( items
[ i
].getGroup() ).getTitle();
613 items
[ i
].getLabel().toLowerCase().indexOf( query
) > -1 ||
614 items
[ i
].getDescription().toLowerCase().indexOf( query
) > -1 ||
615 groupTitle
.toLowerCase().indexOf( query
) > -1
617 result
[ items
[ i
].getGroup() ] = result
[ items
[ i
].getGroup() ] || [];
618 result
[ items
[ i
].getGroup() ].push( items
[ i
] );
626 }( mediaWiki
, jQuery
) );