Merge "Fix mistake in ObjectCache doc"
[lhc/web/wiklou.git] / resources / src / mediawiki.rcfilters / dm / mw.rcfilters.dm.FiltersViewModel.js
1 ( function ( mw, $ ) {
2 /**
3 * View model for the filters selection and display
4 *
5 * @mixins OO.EventEmitter
6 * @mixins OO.EmitterList
7 *
8 * @constructor
9 */
10 mw.rcfilters.dm.FiltersViewModel = function MwRcfiltersDmFiltersViewModel() {
11 // Mixin constructor
12 OO.EventEmitter.call( this );
13 OO.EmitterList.call( this );
14
15 this.groups = {};
16 this.defaultParams = {};
17 this.defaultFiltersEmpty = null;
18 this.highlightEnabled = false;
19
20 // Events
21 this.aggregate( { update: 'filterItemUpdate' } );
22 this.connect( this, { filterItemUpdate: [ 'emit', 'itemUpdate' ] } );
23 };
24
25 /* Initialization */
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 );
29
30 /* Events */
31
32 /**
33 * @event initialize
34 *
35 * Filter list is initialized
36 */
37
38 /**
39 * @event itemUpdate
40 * @param {mw.rcfilters.dm.FilterItem} item Filter item updated
41 *
42 * Filter item has changed
43 */
44
45 /**
46 * @event highlightChange
47 * @param {boolean} Highlight feature is enabled
48 *
49 * Highlight feature has been toggled enabled or disabled
50 */
51
52 /* Methods */
53
54 /**
55 * Re-assess the states of filter items based on the interactions between them
56 *
57 * @param {mw.rcfilters.dm.FilterItem} [item] Changed item. If not given, the
58 * method will go over the state of all items
59 */
60 mw.rcfilters.dm.FiltersViewModel.prototype.reassessFilterInteractions = function ( item ) {
61 var allSelected,
62 model = this,
63 iterationItems = item !== undefined ? [ item ] : this.getItems();
64
65 iterationItems.forEach( function ( checkedItem ) {
66 var allCheckedItems = checkedItem.getSubset().concat( [ checkedItem.getName() ] ),
67 groupModel = checkedItem.getGroupModel();
68
69 // Check for subsets (included filters) plus the item itself:
70 allCheckedItems.forEach( function ( filterItemName ) {
71 var itemInSubset = model.getItemByName( filterItemName );
72
73 itemInSubset.toggleIncluded(
74 // If any of itemInSubset's supersets are selected, this item
75 // is included
76 itemInSubset.getSuperset().some( function ( supersetName ) {
77 return ( model.getItemByName( supersetName ).isSelected() );
78 } )
79 );
80 } );
81
82 // Update coverage for the changed group
83 if ( groupModel.isFullCoverage() ) {
84 allSelected = groupModel.areAllSelected();
85 groupModel.getItems().forEach( function ( filterItem ) {
86 filterItem.toggleFullyCovered( allSelected );
87 } );
88 }
89 } );
90
91 // Check for conflicts
92 // In this case, we must go over all items, since
93 // conflicts are bidirectional and depend not only on
94 // individual items, but also on the selected states of
95 // the groups they're in.
96 this.getItems().forEach( function ( filterItem ) {
97 var inConflict = false,
98 filterItemGroup = filterItem.getGroupModel();
99
100 // For each item, see if that item is still conflicting
101 $.each( model.groups, function ( groupName, groupModel ) {
102 if ( filterItem.getGroupName() === groupName ) {
103 // Check inside the group
104 inConflict = groupModel.areAnySelectedInConflictWith( filterItem );
105 } else {
106 // According to the spec, if two items conflict from two different
107 // groups, the conflict only lasts if the groups **only have selected
108 // items that are conflicting**. If a group has selected items that
109 // are conflicting and non-conflicting, the scope of the result has
110 // expanded enough to completely remove the conflict.
111
112 // For example, see two groups with conflicts:
113 // userExpLevel: [
114 // {
115 // name: 'experienced',
116 // conflicts: [ 'unregistered' ]
117 // }
118 // ],
119 // registration: [
120 // {
121 // name: 'registered',
122 // },
123 // {
124 // name: 'unregistered',
125 // }
126 // ]
127 // If we select 'experienced', then 'unregistered' is in conflict (and vice versa),
128 // because, inherently, 'experienced' filter only includes registered users, and so
129 // both filters are in conflict with one another.
130 // However, the minute we select 'registered', the scope of our results
131 // has expanded to no longer have a conflict with 'experienced' filter, and
132 // so the conflict is removed.
133
134 // In our case, we need to check if the entire group conflicts with
135 // the entire item's group, so we follow the above spec
136 inConflict = (
137 // The foreign group is in conflict with this item
138 groupModel.areAllSelectedInConflictWith( filterItem ) &&
139 // Every selected member of the item's own group is also
140 // in conflict with the other group
141 filterItemGroup.getSelectedItems().every( function ( otherGroupItem ) {
142 return groupModel.areAllSelectedInConflictWith( otherGroupItem );
143 } )
144 );
145 }
146
147 // If we're in conflict, this will return 'false' which
148 // will break the loop. Otherwise, we're not in conflict
149 // and the loop continues
150 return !inConflict;
151 } );
152
153 // Toggle the item state
154 filterItem.toggleConflicted( inConflict );
155 } );
156 };
157
158 /**
159 * Set filters and preserve a group relationship based on
160 * the definition given by an object
161 *
162 * @param {Array} filters Filter group definition
163 */
164 mw.rcfilters.dm.FiltersViewModel.prototype.initializeFilters = function ( filters ) {
165 var i, filterItem, selectedFilterNames,
166 model = this,
167 items = [],
168 addArrayElementsUnique = function ( arr, elements ) {
169 elements = Array.isArray( elements ) ? elements : [ elements ];
170
171 elements.forEach( function ( element ) {
172 if ( arr.indexOf( element ) === -1 ) {
173 arr.push( element );
174 }
175 } );
176
177 return arr;
178 },
179 conflictMap = {},
180 supersetMap = {};
181
182 // Reset
183 this.clearItems();
184 this.groups = {};
185
186 filters.forEach( function ( data ) {
187 var group = data.name;
188
189 if ( !model.groups[ group ] ) {
190 model.groups[ group ] = new mw.rcfilters.dm.FilterGroup( group, {
191 type: data.type,
192 title: mw.msg( data.title ),
193 separator: data.separator,
194 fullCoverage: !!data.fullCoverage
195 } );
196 }
197
198 selectedFilterNames = [];
199 for ( i = 0; i < data.filters.length; i++ ) {
200 data.filters[ i ].subset = data.filters[ i ].subset || [];
201 data.filters[ i ].subset = data.filters[ i ].subset.map( function ( el ) {
202 return el.filter;
203 } );
204
205 filterItem = new mw.rcfilters.dm.FilterItem( data.filters[ i ].name, model.groups[ group ], {
206 group: group,
207 label: mw.msg( data.filters[ i ].label ),
208 description: mw.msg( data.filters[ i ].description ),
209 subset: data.filters[ i ].subset,
210 cssClass: data.filters[ i ].cssClass
211 } );
212
213 // For convenience, we should store each filter's "supersets" -- these are
214 // the filters that have that item in their subset list. This will just
215 // make it easier to go through whether the item has any other items
216 // that affect it (and are selected) at any given time
217 if ( data.filters[ i ].subset ) {
218 data.filters[ i ].subset.forEach( function ( subsetFilterName ) { // eslint-disable-line no-loop-func
219 supersetMap[ subsetFilterName ] = supersetMap[ subsetFilterName ] || [];
220 addArrayElementsUnique(
221 supersetMap[ subsetFilterName ],
222 filterItem.getName()
223 );
224 } );
225 }
226
227 // Conflicts are bi-directional, which means FilterA can define having
228 // a conflict with FilterB, and this conflict should appear in **both**
229 // filter definitions.
230 // We need to remap all the 'conflicts' so they reflect the entire state
231 // in either direction regardless of which filter defined the other as conflicting.
232 if ( data.filters[ i ].conflicts ) {
233 conflictMap[ filterItem.getName() ] = conflictMap[ filterItem.getName() ] || [];
234 addArrayElementsUnique(
235 conflictMap[ filterItem.getName() ],
236 data.filters[ i ].conflicts
237 );
238
239 data.filters[ i ].conflicts.forEach( function ( conflictingFilterName ) { // eslint-disable-line no-loop-func
240 // Add this filter to the conflicts of each of the filters in its list
241 conflictMap[ conflictingFilterName ] = conflictMap[ conflictingFilterName ] || [];
242 addArrayElementsUnique(
243 conflictMap[ conflictingFilterName ],
244 filterItem.getName()
245 );
246 } );
247 }
248
249 if ( data.type === 'send_unselected_if_any' ) {
250 // Store the default parameter state
251 // For this group type, parameter values are direct
252 model.defaultParams[ data.filters[ i ].name ] = Number( !!data.filters[ i ].default );
253 } else if (
254 data.type === 'string_options' &&
255 data.filters[ i ].default
256 ) {
257 selectedFilterNames.push( data.filters[ i ].name );
258 }
259
260 model.groups[ group ].addItems( filterItem );
261 items.push( filterItem );
262 }
263
264 if ( data.type === 'string_options' ) {
265 // Store the default parameter group state
266 // For this group, the parameter is group name and value is the names
267 // of selected items
268 model.defaultParams[ group ] = model.sanitizeStringOptionGroup( group, selectedFilterNames ).join( model.groups[ group ].getSeparator() );
269 }
270 } );
271
272 items.forEach( function ( filterItem ) {
273 // Apply conflict map to the items
274 // Now that we mapped all items and conflicts bi-directionally
275 // we need to apply the definition to each filter again
276 filterItem.setConflicts( conflictMap[ filterItem.getName() ] );
277
278 // Apply the superset map
279 filterItem.setSuperset( supersetMap[ filterItem.getName() ] );
280 } );
281
282 // Add items to the model
283 this.addItems( items );
284
285 this.emit( 'initialize' );
286 };
287
288 /**
289 * Get the names of all available filters
290 *
291 * @return {string[]} An array of filter names
292 */
293 mw.rcfilters.dm.FiltersViewModel.prototype.getFilterNames = function () {
294 return this.getItems().map( function ( item ) { return item.getName(); } );
295 };
296
297 /**
298 * Get the object that defines groups by their name.
299 *
300 * @return {Object} Filter groups
301 */
302 mw.rcfilters.dm.FiltersViewModel.prototype.getFilterGroups = function () {
303 return this.groups;
304 };
305
306 /**
307 * Get the value of a specific parameter
308 *
309 * @param {string} name Parameter name
310 * @return {number|string} Parameter value
311 */
312 mw.rcfilters.dm.FiltersViewModel.prototype.getParamValue = function ( name ) {
313 return this.parameters[ name ];
314 };
315
316 /**
317 * Get the current selected state of the filters
318 *
319 * @return {Object} Filters selected state
320 */
321 mw.rcfilters.dm.FiltersViewModel.prototype.getSelectedState = function () {
322 var i,
323 items = this.getItems(),
324 result = {};
325
326 for ( i = 0; i < items.length; i++ ) {
327 result[ items[ i ].getName() ] = items[ i ].isSelected();
328 }
329
330 return result;
331 };
332
333 /**
334 * Get the current full state of the filters
335 *
336 * @return {Object} Filters full state
337 */
338 mw.rcfilters.dm.FiltersViewModel.prototype.getFullState = function () {
339 var i,
340 items = this.getItems(),
341 result = {};
342
343 for ( i = 0; i < items.length; i++ ) {
344 result[ items[ i ].getName() ] = {
345 selected: items[ i ].isSelected(),
346 conflicted: items[ i ].isConflicted(),
347 included: items[ i ].isIncluded()
348 };
349 }
350
351 return result;
352 };
353
354 /**
355 * Get the default parameters object
356 *
357 * @return {Object} Default parameter values
358 */
359 mw.rcfilters.dm.FiltersViewModel.prototype.getDefaultParams = function () {
360 return this.defaultParams;
361 };
362
363 /**
364 * Set all filter states to default values
365 */
366 mw.rcfilters.dm.FiltersViewModel.prototype.setFiltersToDefaults = function () {
367 var defaultFilterStates = this.getFiltersFromParameters( this.getDefaultParams() );
368
369 this.toggleFiltersSelected( defaultFilterStates );
370 };
371
372 /**
373 * Analyze the groups and their filters and output an object representing
374 * the state of the parameters they represent.
375 *
376 * @param {Object} [filterGroups] An object defining the filter groups to
377 * translate to parameters. Its structure must follow that of this.groups
378 * see #getFilterGroups
379 * @return {Object} Parameter state object
380 */
381 mw.rcfilters.dm.FiltersViewModel.prototype.getParametersFromFilters = function ( filterGroups ) {
382 var i, filterItems, anySelected, values,
383 result = {},
384 groupItems = filterGroups || this.getFilterGroups();
385
386 $.each( groupItems, function ( group, model ) {
387 filterItems = model.getItems();
388
389 if ( model.getType() === 'send_unselected_if_any' ) {
390 // First, check if any of the items are selected at all.
391 // If none is selected, we're treating it as if they are
392 // all false
393 anySelected = filterItems.some( function ( filterItem ) {
394 return filterItem.isSelected();
395 } );
396
397 // Go over the items and define the correct values
398 for ( i = 0; i < filterItems.length; i++ ) {
399 result[ filterItems[ i ].getName() ] = anySelected ?
400 Number( !filterItems[ i ].isSelected() ) : 0;
401 }
402 } else if ( model.getType() === 'string_options' ) {
403 values = [];
404 for ( i = 0; i < filterItems.length; i++ ) {
405 if ( filterItems[ i ].isSelected() ) {
406 values.push( filterItems[ i ].getName() );
407 }
408 }
409
410 if ( values.length === filterItems.length ) {
411 result[ group ] = 'all';
412 } else {
413 result[ group ] = values.join( model.getSeparator() );
414 }
415 }
416 } );
417
418 return result;
419 };
420
421 /**
422 * Get the highlight parameters based on current filter configuration
423 *
424 * @return {object} Object where keys are "<filter name>_color" and values
425 * are the selected highlight colors.
426 */
427 mw.rcfilters.dm.FiltersViewModel.prototype.getHighlightParameters = function () {
428 var result = { highlight: Number( this.isHighlightEnabled() ) };
429
430 this.getItems().forEach( function ( filterItem ) {
431 result[ filterItem.getName() + '_color' ] = filterItem.getHighlightColor();
432 } );
433 return result;
434 };
435
436 /**
437 * Sanitize value group of a string_option groups type
438 * Remove duplicates and make sure to only use valid
439 * values.
440 *
441 * @private
442 * @param {string} groupName Group name
443 * @param {string[]} valueArray Array of values
444 * @return {string[]} Array of valid values
445 */
446 mw.rcfilters.dm.FiltersViewModel.prototype.sanitizeStringOptionGroup = function( groupName, valueArray ) {
447 var result = [],
448 validNames = this.getGroupFilters( groupName ).map( function ( filterItem ) {
449 return filterItem.getName();
450 } );
451
452 if ( valueArray.indexOf( 'all' ) > -1 ) {
453 // If anywhere in the values there's 'all', we
454 // treat it as if only 'all' was selected.
455 // Example: param=valid1,valid2,all
456 // Result: param=all
457 return [ 'all' ];
458 }
459
460 // Get rid of any dupe and invalid parameter, only output
461 // valid ones
462 // Example: param=valid1,valid2,invalid1,valid1
463 // Result: param=valid1,valid2
464 valueArray.forEach( function ( value ) {
465 if (
466 validNames.indexOf( value ) > -1 &&
467 result.indexOf( value ) === -1
468 ) {
469 result.push( value );
470 }
471 } );
472
473 return result;
474 };
475
476 /**
477 * Check whether the current filter state is set to all false.
478 *
479 * @return {boolean} Current filters are all empty
480 */
481 mw.rcfilters.dm.FiltersViewModel.prototype.areCurrentFiltersEmpty = function () {
482 // Check if there are either any selected items or any items
483 // that have highlight enabled
484 return !this.getItems().some( function ( filterItem ) {
485 return filterItem.isSelected() || filterItem.isHighlighted();
486 } );
487 };
488
489 /**
490 * Check whether the default values of the filters are all false.
491 *
492 * @return {boolean} Default filters are all false
493 */
494 mw.rcfilters.dm.FiltersViewModel.prototype.areDefaultFiltersEmpty = function () {
495 var defaultFilters;
496
497 if ( this.defaultFiltersEmpty !== null ) {
498 // We only need to do this test once,
499 // because defaults are set once per session
500 defaultFilters = this.getFiltersFromParameters();
501 this.defaultFiltersEmpty = Object.keys( defaultFilters ).every( function ( filterName ) {
502 return !defaultFilters[ filterName ];
503 } );
504 }
505
506 return this.defaultFiltersEmpty;
507 };
508
509 /**
510 * This is the opposite of the #getParametersFromFilters method; this goes over
511 * the given parameters and translates into a selected/unselected value in the filters.
512 *
513 * @param {Object} params Parameters query object
514 * @return {Object} Filter state object
515 */
516 mw.rcfilters.dm.FiltersViewModel.prototype.getFiltersFromParameters = function ( params ) {
517 var i, filterItem,
518 groupMap = {},
519 model = this,
520 base = this.getDefaultParams(),
521 result = {};
522
523 params = $.extend( {}, base, params );
524
525 $.each( params, function ( paramName, paramValue ) {
526 // Find the filter item
527 filterItem = model.getItemByName( paramName );
528 // Ignore if no filter item exists
529 if ( filterItem ) {
530 groupMap[ filterItem.getGroupName() ] = groupMap[ filterItem.getGroupName() ] || {};
531
532 // Mark the group if it has any items that are selected
533 groupMap[ filterItem.getGroupName() ].hasSelected = (
534 groupMap[ filterItem.getGroupName() ].hasSelected ||
535 !!Number( paramValue )
536 );
537
538 // Add the relevant filter into the group map
539 groupMap[ filterItem.getGroupName() ].filters = groupMap[ filterItem.getGroupName() ].filters || [];
540 groupMap[ filterItem.getGroupName() ].filters.push( filterItem );
541 } else if ( model.groups.hasOwnProperty( paramName ) ) {
542 // This parameter represents a group (values are the filters)
543 // this is equivalent to checking if the group is 'string_options'
544 groupMap[ paramName ] = { filters: model.groups[ paramName ].getItems() };
545 }
546 } );
547
548 // Now that we know the groups' selection states, we need to go over
549 // the filters in the groups and mark their selected states appropriately
550 $.each( groupMap, function ( group, data ) {
551 var paramValues, filterItem,
552 allItemsInGroup = data.filters;
553
554 if ( model.groups[ group ].getType() === 'send_unselected_if_any' ) {
555 for ( i = 0; i < allItemsInGroup.length; i++ ) {
556 filterItem = allItemsInGroup[ i ];
557
558 result[ filterItem.getName() ] = data.hasSelected ?
559 // Flip the definition between the parameter
560 // state and the filter state
561 // This is what the 'toggleSelected' value of the filter is
562 !Number( params[ filterItem.getName() ] ) :
563 // Otherwise, there are no selected items in the
564 // group, which means the state is false
565 false;
566 }
567 } else if ( model.groups[ group ].getType() === 'string_options' ) {
568 paramValues = model.sanitizeStringOptionGroup( group, params[ group ].split( model.groups[ group ].getSeparator() ) );
569
570 for ( i = 0; i < allItemsInGroup.length; i++ ) {
571 filterItem = allItemsInGroup[ i ];
572
573 result[ filterItem.getName() ] = (
574 // If it is the word 'all'
575 paramValues.length === 1 && paramValues[ 0 ] === 'all' ||
576 // All values are written
577 paramValues.length === model.groups[ group ].getItemCount()
578 ) ?
579 // All true (either because all values are written or the term 'all' is written)
580 // is the same as all filters set to false
581 false :
582 // Otherwise, the filter is selected only if it appears in the parameter values
583 paramValues.indexOf( filterItem.getName() ) > -1;
584 }
585 }
586 } );
587 return result;
588 };
589
590 /**
591 * Get the item that matches the given name
592 *
593 * @param {string} name Filter name
594 * @return {mw.rcfilters.dm.FilterItem} Filter item
595 */
596 mw.rcfilters.dm.FiltersViewModel.prototype.getItemByName = function ( name ) {
597 return this.getItems().filter( function ( item ) {
598 return name === item.getName();
599 } )[ 0 ];
600 };
601
602 /**
603 * Set all filters to false or empty/all
604 * This is equivalent to display all.
605 */
606 mw.rcfilters.dm.FiltersViewModel.prototype.emptyAllFilters = function () {
607 this.getItems().forEach( function ( filterItem ) {
608 this.toggleFilterSelected( filterItem.getName(), false );
609 }.bind( this ) );
610 };
611
612 /**
613 * Toggle selected state of one item
614 *
615 * @param {string} name Name of the filter item
616 * @param {boolean} [isSelected] Filter selected state
617 */
618 mw.rcfilters.dm.FiltersViewModel.prototype.toggleFilterSelected = function ( name, isSelected ) {
619 this.getItemByName( name ).toggleSelected( isSelected );
620 };
621
622 /**
623 * Toggle selected state of items by their names
624 *
625 * @param {Object} filterDef Filter definitions
626 */
627 mw.rcfilters.dm.FiltersViewModel.prototype.toggleFiltersSelected = function ( filterDef ) {
628 Object.keys( filterDef ).forEach( function ( name ) {
629 this.toggleFilterSelected( name, filterDef[ name ] );
630 }.bind( this ) );
631 };
632
633 /**
634 * Get a group model from its name
635 *
636 * @param {string} groupName Group name
637 * @return {mw.rcfilters.dm.FilterGroup} Group model
638 */
639 mw.rcfilters.dm.FiltersViewModel.prototype.getGroup = function ( groupName ) {
640 return this.groups[ groupName ];
641 };
642
643 /**
644 * Get all filters within a specified group by its name
645 *
646 * @param {string} groupName Group name
647 * @return {mw.rcfilters.dm.FilterItem[]} Filters belonging to this group
648 */
649 mw.rcfilters.dm.FiltersViewModel.prototype.getGroupFilters = function ( groupName ) {
650 return ( this.getGroup( groupName ) && this.getGroup( groupName ).getItems() ) || [];
651 };
652
653 /**
654 * Find items whose labels match the given string
655 *
656 * @param {string} query Search string
657 * @return {Object} An object of items to show
658 * arranged by their group names
659 */
660 mw.rcfilters.dm.FiltersViewModel.prototype.findMatches = function ( query ) {
661 var i,
662 groupTitle,
663 result = {},
664 items = this.getItems();
665
666 // Normalize so we can search strings regardless of case
667 query = query.toLowerCase();
668
669 // item label starting with the query string
670 for ( i = 0; i < items.length; i++ ) {
671 if ( items[ i ].getLabel().toLowerCase().indexOf( query ) === 0 ) {
672 result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
673 result[ items[ i ].getGroupName() ].push( items[ i ] );
674 }
675 }
676
677 if ( $.isEmptyObject( result ) ) {
678 // item containing the query string in their label, description, or group title
679 for ( i = 0; i < items.length; i++ ) {
680 groupTitle = items[ i ].getGroupModel().getTitle();
681 if (
682 items[ i ].getLabel().toLowerCase().indexOf( query ) > -1 ||
683 items[ i ].getDescription().toLowerCase().indexOf( query ) > -1 ||
684 groupTitle.toLowerCase().indexOf( query ) > -1
685 ) {
686 result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
687 result[ items[ i ].getGroupName() ].push( items[ i ] );
688 }
689 }
690 }
691
692 return result;
693 };
694
695 /**
696 * Get items that are highlighted
697 *
698 * @return {mw.rcfilters.dm.FilterItem[]} Highlighted items
699 */
700 mw.rcfilters.dm.FiltersViewModel.prototype.getHighlightedItems = function () {
701 return this.getItems().filter( function ( filterItem ) {
702 return filterItem.isHighlightSupported() &&
703 filterItem.getHighlightColor();
704 } );
705 };
706
707 /**
708 * Toggle the highlight feature on and off.
709 * Propagate the change to filter items.
710 *
711 * @param {boolean} enable Highlight should be enabled
712 * @fires highlightChange
713 */
714 mw.rcfilters.dm.FiltersViewModel.prototype.toggleHighlight = function ( enable ) {
715 enable = enable === undefined ? !this.highlightEnabled : enable;
716
717 if ( this.highlightEnabled !== enable ) {
718 this.highlightEnabled = enable;
719
720 this.getItems().forEach( function ( filterItem ) {
721 filterItem.toggleHighlight( this.highlightEnabled );
722 }.bind( this ) );
723
724 this.emit( 'highlightChange', this.highlightEnabled );
725 }
726 };
727
728 /**
729 * Check if the highlight feature is enabled
730 * @return {boolean}
731 */
732 mw.rcfilters.dm.FiltersViewModel.prototype.isHighlightEnabled = function () {
733 return !!this.highlightEnabled;
734 };
735
736 /**
737 * Set highlight color for a specific filter item
738 *
739 * @param {string} filterName Name of the filter item
740 * @param {string} color Selected color
741 */
742 mw.rcfilters.dm.FiltersViewModel.prototype.setHighlightColor = function ( filterName, color ) {
743 this.getItemByName( filterName ).setHighlightColor( color );
744 };
745
746 /**
747 * Clear highlight for a specific filter item
748 *
749 * @param {string} filterName Name of the filter item
750 */
751 mw.rcfilters.dm.FiltersViewModel.prototype.clearHighlightColor = function ( filterName ) {
752 this.getItemByName( filterName ).clearHighlightColor();
753 };
754
755 /**
756 * Clear highlight for all filter items
757 */
758 mw.rcfilters.dm.FiltersViewModel.prototype.clearAllHighlightColors = function () {
759 this.getItems().forEach( function ( filterItem ) {
760 filterItem.clearHighlightColor();
761 } );
762 };
763 }( mediaWiki, jQuery ) );