Add namespace names for Atikamekw (atj)
[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 this.invertedNamespaces = false;
20 this.parameterMap = {};
21
22 this.views = {};
23 this.currentView = null;
24
25 // Events
26 this.aggregate( { update: 'filterItemUpdate' } );
27 this.connect( this, { filterItemUpdate: [ 'emit', 'itemUpdate' ] } );
28 };
29
30 /* Initialization */
31 OO.initClass( mw.rcfilters.dm.FiltersViewModel );
32 OO.mixinClass( mw.rcfilters.dm.FiltersViewModel, OO.EventEmitter );
33 OO.mixinClass( mw.rcfilters.dm.FiltersViewModel, OO.EmitterList );
34
35 /* Events */
36
37 /**
38 * @event initialize
39 *
40 * Filter list is initialized
41 */
42
43 /**
44 * @event update
45 *
46 * Model has been updated
47 */
48
49 /**
50 * @event itemUpdate
51 * @param {mw.rcfilters.dm.FilterItem} item Filter item updated
52 *
53 * Filter item has changed
54 */
55
56 /**
57 * @event highlightChange
58 * @param {boolean} Highlight feature is enabled
59 *
60 * Highlight feature has been toggled enabled or disabled
61 */
62
63 /**
64 * @event invertChange
65 * @param {boolean} isInverted Namespace selected is inverted
66 *
67 * Namespace selection is inverted or straight forward
68 */
69
70 /* Methods */
71
72 /**
73 * Re-assess the states of filter items based on the interactions between them
74 *
75 * @param {mw.rcfilters.dm.FilterItem} [item] Changed item. If not given, the
76 * method will go over the state of all items
77 */
78 mw.rcfilters.dm.FiltersViewModel.prototype.reassessFilterInteractions = function ( item ) {
79 var allSelected,
80 model = this,
81 iterationItems = item !== undefined ? [ item ] : this.getItems();
82
83 iterationItems.forEach( function ( checkedItem ) {
84 var allCheckedItems = checkedItem.getSubset().concat( [ checkedItem.getName() ] ),
85 groupModel = checkedItem.getGroupModel();
86
87 // Check for subsets (included filters) plus the item itself:
88 allCheckedItems.forEach( function ( filterItemName ) {
89 var itemInSubset = model.getItemByName( filterItemName );
90
91 itemInSubset.toggleIncluded(
92 // If any of itemInSubset's supersets are selected, this item
93 // is included
94 itemInSubset.getSuperset().some( function ( supersetName ) {
95 return ( model.getItemByName( supersetName ).isSelected() );
96 } )
97 );
98 } );
99
100 // Update coverage for the changed group
101 if ( groupModel.isFullCoverage() ) {
102 allSelected = groupModel.areAllSelected();
103 groupModel.getItems().forEach( function ( filterItem ) {
104 filterItem.toggleFullyCovered( allSelected );
105 } );
106 }
107 } );
108
109 // Check for conflicts
110 // In this case, we must go over all items, since
111 // conflicts are bidirectional and depend not only on
112 // individual items, but also on the selected states of
113 // the groups they're in.
114 this.getItems().forEach( function ( filterItem ) {
115 var inConflict = false,
116 filterItemGroup = filterItem.getGroupModel();
117
118 // For each item, see if that item is still conflicting
119 $.each( model.groups, function ( groupName, groupModel ) {
120 if ( filterItem.getGroupName() === groupName ) {
121 // Check inside the group
122 inConflict = groupModel.areAnySelectedInConflictWith( filterItem );
123 } else {
124 // According to the spec, if two items conflict from two different
125 // groups, the conflict only lasts if the groups **only have selected
126 // items that are conflicting**. If a group has selected items that
127 // are conflicting and non-conflicting, the scope of the result has
128 // expanded enough to completely remove the conflict.
129
130 // For example, see two groups with conflicts:
131 // userExpLevel: [
132 // {
133 // name: 'experienced',
134 // conflicts: [ 'unregistered' ]
135 // }
136 // ],
137 // registration: [
138 // {
139 // name: 'registered',
140 // },
141 // {
142 // name: 'unregistered',
143 // }
144 // ]
145 // If we select 'experienced', then 'unregistered' is in conflict (and vice versa),
146 // because, inherently, 'experienced' filter only includes registered users, and so
147 // both filters are in conflict with one another.
148 // However, the minute we select 'registered', the scope of our results
149 // has expanded to no longer have a conflict with 'experienced' filter, and
150 // so the conflict is removed.
151
152 // In our case, we need to check if the entire group conflicts with
153 // the entire item's group, so we follow the above spec
154 inConflict = (
155 // The foreign group is in conflict with this item
156 groupModel.areAllSelectedInConflictWith( filterItem ) &&
157 // Every selected member of the item's own group is also
158 // in conflict with the other group
159 filterItemGroup.getSelectedItems().every( function ( otherGroupItem ) {
160 return groupModel.areAllSelectedInConflictWith( otherGroupItem );
161 } )
162 );
163 }
164
165 // If we're in conflict, this will return 'false' which
166 // will break the loop. Otherwise, we're not in conflict
167 // and the loop continues
168 return !inConflict;
169 } );
170
171 // Toggle the item state
172 filterItem.toggleConflicted( inConflict );
173 } );
174 };
175
176 /**
177 * Get whether the model has any conflict in its items
178 *
179 * @return {boolean} There is a conflict
180 */
181 mw.rcfilters.dm.FiltersViewModel.prototype.hasConflict = function () {
182 return this.getItems().some( function ( filterItem ) {
183 return filterItem.isSelected() && filterItem.isConflicted();
184 } );
185 };
186
187 /**
188 * Get the first item with a current conflict
189 *
190 * @return {mw.rcfilters.dm.FilterItem} Conflicted item
191 */
192 mw.rcfilters.dm.FiltersViewModel.prototype.getFirstConflictedItem = function () {
193 var conflictedItem;
194
195 $.each( this.getItems(), function ( index, filterItem ) {
196 if ( filterItem.isSelected() && filterItem.isConflicted() ) {
197 conflictedItem = filterItem;
198 return false;
199 }
200 } );
201
202 return conflictedItem;
203 };
204
205 /**
206 * Set filters and preserve a group relationship based on
207 * the definition given by an object
208 *
209 * @param {Array} filters Filter group definition
210 * @param {Object} [namespaces] Namespace definition
211 */
212 mw.rcfilters.dm.FiltersViewModel.prototype.initializeFilters = function ( filters, namespaces ) {
213 var filterItem, filterConflictResult, groupConflictResult,
214 model = this,
215 items = [],
216 namespaceDefinition = [],
217 groupConflictMap = {},
218 filterConflictMap = {},
219 /*!
220 * Expand a conflict definition from group name to
221 * the list of all included filters in that group.
222 * We do this so that the direct relationship in the
223 * models are consistently item->items rather than
224 * mixing item->group with item->item.
225 *
226 * @param {Object} obj Conflict definition
227 * @return {Object} Expanded conflict definition
228 */
229 expandConflictDefinitions = function ( obj ) {
230 var result = {};
231
232 $.each( obj, function ( key, conflicts ) {
233 var filterName,
234 adjustedConflicts = {};
235
236 conflicts.forEach( function ( conflict ) {
237 var filter;
238
239 if ( conflict.filter ) {
240 filterName = model.groups[ conflict.group ].getPrefixedName( conflict.filter );
241 filter = model.getItemByName( filterName );
242
243 // Rename
244 adjustedConflicts[ filterName ] = $.extend(
245 {},
246 conflict,
247 {
248 filter: filterName,
249 item: filter
250 }
251 );
252 } else {
253 // This conflict is for an entire group. Split it up to
254 // represent each filter
255
256 // Get the relevant group items
257 model.groups[ conflict.group ].getItems().forEach( function ( groupItem ) {
258 // Rebuild the conflict
259 adjustedConflicts[ groupItem.getName() ] = $.extend(
260 {},
261 conflict,
262 {
263 filter: groupItem.getName(),
264 item: groupItem
265 }
266 );
267 } );
268 }
269 } );
270
271 result[ key ] = adjustedConflicts;
272 } );
273
274 return result;
275 };
276
277 // Reset
278 this.clearItems();
279 this.groups = {};
280 this.views = {};
281
282 // Filters
283 this.views.default = { name: 'default', label: mw.msg( 'rcfilters-filterlist-title' ) };
284 filters.forEach( function ( data ) {
285 var i,
286 group = data.name;
287
288 if ( !model.groups[ group ] ) {
289 model.groups[ group ] = new mw.rcfilters.dm.FilterGroup( group, {
290 type: data.type,
291 title: data.title ? mw.msg( data.title ) : group,
292 separator: data.separator,
293 fullCoverage: !!data.fullCoverage,
294 whatsThis: {
295 body: data.whatsThisBody,
296 header: data.whatsThisHeader,
297 linkText: data.whatsThisLinkText,
298 url: data.whatsThisUrl
299 }
300 } );
301 }
302
303 // Filters are given to us with msg-keys, we need
304 // to translate those before we hand them off
305 for ( i = 0; i < data.filters.length; i++ ) {
306 data.filters[ i ].label = data.filters[ i ].label ? mw.msg( data.filters[ i ].label ) : data.filters[ i ].name;
307 data.filters[ i ].description = data.filters[ i ].description ? mw.msg( data.filters[ i ].description ) : '';
308 }
309
310 model.groups[ group ].initializeFilters( data.filters, data.default );
311 items = items.concat( model.groups[ group ].getItems() );
312
313 // Prepare conflicts
314 if ( data.conflicts ) {
315 // Group conflicts
316 groupConflictMap[ group ] = data.conflicts;
317 }
318
319 for ( i = 0; i < data.filters.length; i++ ) {
320 // Filter conflicts
321 if ( data.filters[ i ].conflicts ) {
322 filterItem = model.groups[ group ].getItemByParamName( data.filters[ i ].name );
323 filterConflictMap[ filterItem.getName() ] = data.filters[ i ].conflicts;
324 }
325 }
326 } );
327
328 namespaces = namespaces || {};
329 if (
330 mw.config.get( 'wgStructuredChangeFiltersEnableExperimentalViews' ) &&
331 !$.isEmptyObject( namespaces )
332 ) {
333 // Namespaces group
334 this.views.namespaces = { name: 'namespaces', label: mw.msg( 'namespaces' ), trigger: ':' };
335 $.each( namespaces, function ( namespaceID, label ) {
336 // Build and clean up the definition
337 namespaceDefinition.push( {
338 name: namespaceID,
339 label: label || mw.msg( 'blanknamespace' ),
340 description: '',
341 identifiers: [
342 ( namespaceID < 0 || namespaceID % 2 === 0 ) ?
343 'subject' : 'talk'
344 ],
345 cssClass: 'mw-changeslist-ns-' + namespaceID
346 } );
347 } );
348
349 // Add the group
350 model.groups.namespace = new mw.rcfilters.dm.FilterGroup(
351 'namespace', // Parameter name is singular
352 {
353 type: 'string_options',
354 view: 'namespaces',
355 title: 'namespaces', // Message key
356 separator: ';',
357 labelPrefixKey: { 'default': 'rcfilters-tag-prefix-namespace', inverted: 'rcfilters-tag-prefix-namespace-inverted' },
358 fullCoverage: true
359 }
360 );
361 // Add namespace items to group
362 model.groups.namespace.initializeFilters( namespaceDefinition );
363 items = items.concat( model.groups.namespace.getItems() );
364 }
365
366 // Add item references to the model, for lookup
367 this.addItems( items );
368 // Expand conflicts
369 groupConflictResult = expandConflictDefinitions( groupConflictMap );
370 filterConflictResult = expandConflictDefinitions( filterConflictMap );
371
372 // Set conflicts for groups
373 $.each( groupConflictResult, function ( group, conflicts ) {
374 model.groups[ group ].setConflicts( conflicts );
375 } );
376
377 // Set conflicts for items
378 $.each( filterConflictResult, function ( filterName, conflicts ) {
379 var filterItem = model.getItemByName( filterName );
380 // set conflicts for items in the group
381 filterItem.setConflicts( conflicts );
382 } );
383
384 // Create a map between known parameters and their models
385 $.each( this.groups, function ( group, groupModel ) {
386 if ( groupModel.getType() === 'send_unselected_if_any' ) {
387 // Individual filters
388 groupModel.getItems().forEach( function ( filterItem ) {
389 model.parameterMap[ filterItem.getParamName() ] = filterItem;
390 } );
391 } else if ( groupModel.getType() === 'string_options' ) {
392 // Group
393 model.parameterMap[ groupModel.getName() ] = groupModel;
394 }
395 } );
396
397 this.currentView = 'default';
398
399 // Finish initialization
400 this.emit( 'initialize' );
401 };
402
403 /**
404 * Get the names of all available filters
405 *
406 * @return {string[]} An array of filter names
407 */
408 mw.rcfilters.dm.FiltersViewModel.prototype.getFilterNames = function () {
409 return this.getItems().map( function ( item ) { return item.getName(); } );
410 };
411
412 /**
413 * Get the object that defines groups by their name.
414 *
415 * @return {Object} Filter groups
416 */
417 mw.rcfilters.dm.FiltersViewModel.prototype.getFilterGroups = function () {
418 return this.groups;
419 };
420
421 /**
422 * Get the object that defines groups that match a certain view by their name.
423 *
424 * @param {string} [view] Requested view. If not given, uses current view
425 * @return {Object} Filter groups matching a display group
426 */
427 mw.rcfilters.dm.FiltersViewModel.prototype.getFilterGroupsByView = function ( view ) {
428 var result = {};
429
430 view = view || this.getCurrentView();
431
432 $.each( this.groups, function ( groupName, groupModel ) {
433 if ( groupModel.getView() === view ) {
434 result[ groupName ] = groupModel;
435 }
436 } );
437
438 return result;
439 };
440
441 /**
442 * Get an array of filters matching the given display group.
443 *
444 * @param {string} [view] Requested view. If not given, uses current view
445 * @return {mw.rcfilters.dm.FilterItem} Filter items matching the group
446 */
447 mw.rcfilters.dm.FiltersViewModel.prototype.getFiltersByView = function ( view ) {
448 var groups,
449 result = [];
450
451 view = view || this.getCurrentView();
452
453 groups = this.getFilterGroupsByView( view );
454
455 $.each( groups, function ( groupName, groupModel ) {
456 result = result.concat( groupModel.getItems() );
457 } );
458
459 return result;
460 };
461
462 /**
463 * Get the trigger for the requested view.
464 *
465 * @param {string} view View name
466 * @return {string} View trigger, if exists
467 */
468 mw.rcfilters.dm.FiltersViewModel.prototype.getViewTrigger = function ( view ) {
469 return this.views[ view ] && this.views[ view ].trigger;
470 };
471 /**
472 * Get the value of a specific parameter
473 *
474 * @param {string} name Parameter name
475 * @return {number|string} Parameter value
476 */
477 mw.rcfilters.dm.FiltersViewModel.prototype.getParamValue = function ( name ) {
478 return this.parameters[ name ];
479 };
480
481 /**
482 * Get the current selected state of the filters
483 *
484 * @return {Object} Filters selected state
485 */
486 mw.rcfilters.dm.FiltersViewModel.prototype.getSelectedState = function () {
487 var i,
488 items = this.getItems(),
489 result = {};
490
491 for ( i = 0; i < items.length; i++ ) {
492 result[ items[ i ].getName() ] = items[ i ].isSelected();
493 }
494
495 return result;
496 };
497
498 /**
499 * Get the current full state of the filters
500 *
501 * @return {Object} Filters full state
502 */
503 mw.rcfilters.dm.FiltersViewModel.prototype.getFullState = function () {
504 var i,
505 items = this.getItems(),
506 result = {};
507
508 for ( i = 0; i < items.length; i++ ) {
509 result[ items[ i ].getName() ] = {
510 selected: items[ i ].isSelected(),
511 conflicted: items[ i ].isConflicted(),
512 included: items[ i ].isIncluded()
513 };
514 }
515
516 return result;
517 };
518
519 /**
520 * Get an object representing default parameters state
521 *
522 * @return {Object} Default parameter values
523 */
524 mw.rcfilters.dm.FiltersViewModel.prototype.getDefaultParams = function () {
525 var result = {};
526
527 // Get default filter state
528 $.each( this.groups, function ( name, model ) {
529 $.extend( true, result, model.getDefaultParams() );
530 } );
531
532 return result;
533 };
534
535 /**
536 * Analyze the groups and their filters and output an object representing
537 * the state of the parameters they represent.
538 *
539 * @param {Object} [filterDefinition] An object defining the filter values,
540 * keyed by filter names.
541 * @return {Object} Parameter state object
542 */
543 mw.rcfilters.dm.FiltersViewModel.prototype.getParametersFromFilters = function ( filterDefinition ) {
544 var groupItemDefinition,
545 result = {},
546 groupItems = this.getFilterGroups();
547
548 if ( filterDefinition ) {
549 groupItemDefinition = {};
550 // Filter definition is "flat", but in effect
551 // each group needs to tell us its result based
552 // on the values in it. We need to split this list
553 // back into groupings so we can "feed" it to the
554 // loop below, and we need to expand it so it includes
555 // all filters (set to false)
556 this.getItems().forEach( function ( filterItem ) {
557 groupItemDefinition[ filterItem.getGroupName() ] = groupItemDefinition[ filterItem.getGroupName() ] || {};
558 groupItemDefinition[ filterItem.getGroupName() ][ filterItem.getName() ] = !!filterDefinition[ filterItem.getName() ];
559 } );
560 }
561
562 $.each( groupItems, function ( group, model ) {
563 $.extend(
564 result,
565 model.getParamRepresentation(
566 groupItemDefinition ?
567 groupItemDefinition[ group ] : null
568 )
569 );
570 } );
571
572 return result;
573 };
574
575 /**
576 * This is the opposite of the #getParametersFromFilters method; this goes over
577 * the given parameters and translates into a selected/unselected value in the filters.
578 *
579 * @param {Object} params Parameters query object
580 * @return {Object} Filter state object
581 */
582 mw.rcfilters.dm.FiltersViewModel.prototype.getFiltersFromParameters = function ( params ) {
583 var groupMap = {},
584 model = this,
585 result = {};
586
587 // Go over the given parameters, break apart to groupings
588 // The resulting object represents the group with its parameter
589 // values. For example:
590 // {
591 // group1: {
592 // param1: "1",
593 // param2: "0",
594 // param3: "1"
595 // },
596 // group2: "param4|param5"
597 // }
598 $.each( params, function ( paramName, paramValue ) {
599 var itemOrGroup = model.parameterMap[ paramName ];
600
601 if ( itemOrGroup instanceof mw.rcfilters.dm.FilterItem ) {
602 groupMap[ itemOrGroup.getGroupName() ] = groupMap[ itemOrGroup.getGroupName() ] || {};
603 groupMap[ itemOrGroup.getGroupName() ][ itemOrGroup.getParamName() ] = paramValue;
604 } else if ( itemOrGroup instanceof mw.rcfilters.dm.FilterGroup ) {
605 // This parameter represents a group (values are the filters)
606 // this is equivalent to checking if the group is 'string_options'
607 groupMap[ itemOrGroup.getName() ] = groupMap[ itemOrGroup.getName() ] || {};
608 groupMap[ itemOrGroup.getName() ] = paramValue;
609 }
610 } );
611
612 // Go over all groups, so we make sure we get the complete output
613 // even if the parameters don't include a certain group
614 $.each( this.groups, function ( groupName, groupModel ) {
615 result = $.extend( true, {}, result, groupModel.getFilterRepresentation( groupMap[ groupName ] ) );
616 } );
617
618 return result;
619 };
620
621 /**
622 * Get the highlight parameters based on current filter configuration
623 *
624 * @return {Object} Object where keys are "<filter name>_color" and values
625 * are the selected highlight colors.
626 */
627 mw.rcfilters.dm.FiltersViewModel.prototype.getHighlightParameters = function () {
628 var result = {};
629
630 this.getItems().forEach( function ( filterItem ) {
631 result[ filterItem.getName() + '_color' ] = filterItem.getHighlightColor() || null;
632 } );
633 result.highlight = String( Number( this.isHighlightEnabled() ) );
634
635 return result;
636 };
637
638 /**
639 * Extract the highlight values from given object. Since highlights are
640 * the same for filter and parameters, it doesn't matter which one is
641 * given; values will be returned with a full list of the highlights
642 * with colors or null values.
643 *
644 * @param {Object} representation Object containing representation of
645 * some or all highlight values
646 * @return {Object} Object where keys are "<filter name>_color" and values
647 * are the selected highlight colors. The returned object
648 * contains all available filters either with a color value
649 * or with null.
650 */
651 mw.rcfilters.dm.FiltersViewModel.prototype.extractHighlightValues = function ( representation ) {
652 var result = {};
653
654 this.getItems().forEach( function ( filterItem ) {
655 var highlightName = filterItem.getName() + '_color';
656 result[ highlightName ] = representation[ highlightName ] || null;
657 } );
658
659 return result;
660 };
661
662 /**
663 * Sanitize value group of a string_option groups type
664 * Remove duplicates and make sure to only use valid
665 * values.
666 *
667 * @private
668 * @param {string} groupName Group name
669 * @param {string[]} valueArray Array of values
670 * @return {string[]} Array of valid values
671 */
672 mw.rcfilters.dm.FiltersViewModel.prototype.sanitizeStringOptionGroup = function ( groupName, valueArray ) {
673 var validNames = this.getGroupFilters( groupName ).map( function ( filterItem ) {
674 return filterItem.getParamName();
675 } );
676
677 return mw.rcfilters.utils.normalizeParamOptions( valueArray, validNames );
678 };
679
680 /**
681 * Check whether the current filter state is set to all false.
682 *
683 * @return {boolean} Current filters are all empty
684 */
685 mw.rcfilters.dm.FiltersViewModel.prototype.areCurrentFiltersEmpty = function () {
686 // Check if there are either any selected items or any items
687 // that have highlight enabled
688 return !this.getItems().some( function ( filterItem ) {
689 return filterItem.isSelected() || filterItem.isHighlighted();
690 } );
691 };
692
693 /**
694 * Check whether the default values of the filters are all false.
695 *
696 * @return {boolean} Default filters are all false
697 */
698 mw.rcfilters.dm.FiltersViewModel.prototype.areDefaultFiltersEmpty = function () {
699 var defaultFilters;
700
701 if ( this.defaultFiltersEmpty !== null ) {
702 // We only need to do this test once,
703 // because defaults are set once per session
704 defaultFilters = this.getFiltersFromParameters( this.getDefaultParams() );
705 this.defaultFiltersEmpty = Object.keys( defaultFilters ).every( function ( filterName ) {
706 return !defaultFilters[ filterName ];
707 } );
708 }
709
710 return this.defaultFiltersEmpty;
711 };
712
713 /**
714 * Get the item that matches the given name
715 *
716 * @param {string} name Filter name
717 * @return {mw.rcfilters.dm.FilterItem} Filter item
718 */
719 mw.rcfilters.dm.FiltersViewModel.prototype.getItemByName = function ( name ) {
720 return this.getItems().filter( function ( item ) {
721 return name === item.getName();
722 } )[ 0 ];
723 };
724
725 /**
726 * Set all filters to false or empty/all
727 * This is equivalent to display all.
728 */
729 mw.rcfilters.dm.FiltersViewModel.prototype.emptyAllFilters = function () {
730 this.getItems().forEach( function ( filterItem ) {
731 this.toggleFilterSelected( filterItem.getName(), false );
732 }.bind( this ) );
733 };
734
735 /**
736 * Toggle selected state of one item
737 *
738 * @param {string} name Name of the filter item
739 * @param {boolean} [isSelected] Filter selected state
740 */
741 mw.rcfilters.dm.FiltersViewModel.prototype.toggleFilterSelected = function ( name, isSelected ) {
742 var item = this.getItemByName( name );
743
744 if ( item ) {
745 item.toggleSelected( isSelected );
746 }
747 };
748
749 /**
750 * Toggle selected state of items by their names
751 *
752 * @param {Object} filterDef Filter definitions
753 */
754 mw.rcfilters.dm.FiltersViewModel.prototype.toggleFiltersSelected = function ( filterDef ) {
755 Object.keys( filterDef ).forEach( function ( name ) {
756 this.toggleFilterSelected( name, filterDef[ name ] );
757 }.bind( this ) );
758 };
759
760 /**
761 * Get a group model from its name
762 *
763 * @param {string} groupName Group name
764 * @return {mw.rcfilters.dm.FilterGroup} Group model
765 */
766 mw.rcfilters.dm.FiltersViewModel.prototype.getGroup = function ( groupName ) {
767 return this.groups[ groupName ];
768 };
769
770 /**
771 * Get all filters within a specified group by its name
772 *
773 * @param {string} groupName Group name
774 * @return {mw.rcfilters.dm.FilterItem[]} Filters belonging to this group
775 */
776 mw.rcfilters.dm.FiltersViewModel.prototype.getGroupFilters = function ( groupName ) {
777 return ( this.getGroup( groupName ) && this.getGroup( groupName ).getItems() ) || [];
778 };
779
780 /**
781 * Find items whose labels match the given string
782 *
783 * @param {string} query Search string
784 * @param {boolean} [returnFlat] Return a flat array. If false, the result
785 * is an object whose keys are the group names and values are an array of
786 * filters per group. If set to true, returns an array of filters regardless
787 * of their groups.
788 * @return {Object} An object of items to show
789 * arranged by their group names
790 */
791 mw.rcfilters.dm.FiltersViewModel.prototype.findMatches = function ( query, returnFlat ) {
792 var i, searchIsEmpty,
793 groupTitle,
794 result = {},
795 flatResult = [],
796 view = query.indexOf( this.getViewTrigger( 'namespaces' ) ) === 0 ? 'namespaces' : 'default',
797 items = this.getFiltersByView( view );
798
799 // Normalize so we can search strings regardless of case and view
800 query = query.toLowerCase();
801 if ( view === 'namespaces' ) {
802 query = query.substr( 1 );
803 }
804
805 // Check if the search if actually empty; this can be a problem when
806 // we use prefixes to denote different views
807 searchIsEmpty = query.length === 0;
808
809 // item label starting with the query string
810 for ( i = 0; i < items.length; i++ ) {
811 if (
812 searchIsEmpty ||
813 items[ i ].getLabel().toLowerCase().indexOf( query ) === 0
814 ) {
815 result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
816 result[ items[ i ].getGroupName() ].push( items[ i ] );
817 flatResult.push( items[ i ] );
818 }
819 }
820
821 if ( $.isEmptyObject( result ) ) {
822 // item containing the query string in their label, description, or group title
823 for ( i = 0; i < items.length; i++ ) {
824 groupTitle = items[ i ].getGroupModel().getTitle();
825 if (
826 searchIsEmpty ||
827 items[ i ].getLabel().toLowerCase().indexOf( query ) > -1 ||
828 items[ i ].getDescription().toLowerCase().indexOf( query ) > -1 ||
829 groupTitle.toLowerCase().indexOf( query ) > -1
830 ) {
831 result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
832 result[ items[ i ].getGroupName() ].push( items[ i ] );
833 flatResult.push( items[ i ] );
834 }
835 }
836 }
837
838 return returnFlat ? flatResult : result;
839 };
840
841 /**
842 * Get items that are highlighted
843 *
844 * @return {mw.rcfilters.dm.FilterItem[]} Highlighted items
845 */
846 mw.rcfilters.dm.FiltersViewModel.prototype.getHighlightedItems = function () {
847 return this.getItems().filter( function ( filterItem ) {
848 return filterItem.isHighlightSupported() &&
849 filterItem.getHighlightColor();
850 } );
851 };
852
853 /**
854 * Get items that allow highlights even if they're not currently highlighted
855 *
856 * @return {mw.rcfilters.dm.FilterItem[]} Items supporting highlights
857 */
858 mw.rcfilters.dm.FiltersViewModel.prototype.getItemsSupportingHighlights = function () {
859 return this.getItems().filter( function ( filterItem ) {
860 return filterItem.isHighlightSupported();
861 } );
862 };
863
864 /**
865 * Switch the current view
866 *
867 * @param {string} view View name
868 * @fires update
869 */
870 mw.rcfilters.dm.FiltersViewModel.prototype.switchView = function ( view ) {
871 if ( this.views[ view ] && this.currentView !== view ) {
872 this.currentView = view;
873 this.emit( 'update' );
874 }
875 };
876
877 /**
878 * Get the current view
879 *
880 * @return {string} Current view
881 */
882 mw.rcfilters.dm.FiltersViewModel.prototype.getCurrentView = function () {
883 return this.currentView;
884 };
885
886 /**
887 * Get the label for the current view
888 *
889 * @return {string} Label for the current view
890 */
891 mw.rcfilters.dm.FiltersViewModel.prototype.getCurrentViewLabel = function () {
892 return this.views[ this.getCurrentView() ].label;
893 };
894
895 /**
896 * Toggle the highlight feature on and off.
897 * Propagate the change to filter items.
898 *
899 * @param {boolean} enable Highlight should be enabled
900 * @fires highlightChange
901 */
902 mw.rcfilters.dm.FiltersViewModel.prototype.toggleHighlight = function ( enable ) {
903 enable = enable === undefined ? !this.highlightEnabled : enable;
904
905 if ( this.highlightEnabled !== enable ) {
906 this.highlightEnabled = enable;
907
908 this.getItems().forEach( function ( filterItem ) {
909 filterItem.toggleHighlight( this.highlightEnabled );
910 }.bind( this ) );
911
912 this.emit( 'highlightChange', this.highlightEnabled );
913 }
914 };
915
916 /**
917 * Check if the highlight feature is enabled
918 * @return {boolean}
919 */
920 mw.rcfilters.dm.FiltersViewModel.prototype.isHighlightEnabled = function () {
921 return !!this.highlightEnabled;
922 };
923
924 /**
925 * Toggle the inverted namespaces property on and off.
926 * Propagate the change to namespace filter items.
927 *
928 * @param {boolean} enable Inverted property is enabled
929 * @fires invertChange
930 */
931 mw.rcfilters.dm.FiltersViewModel.prototype.toggleInvertedNamespaces = function ( enable ) {
932 enable = enable === undefined ? !this.invertedNamespaces : enable;
933
934 if ( this.invertedNamespaces !== enable ) {
935 this.invertedNamespaces = enable;
936
937 this.getFiltersByView( 'namespaces' ).forEach( function ( filterItem ) {
938 filterItem.toggleInverted( this.invertedNamespaces );
939 }.bind( this ) );
940
941 this.emit( 'invertChange', this.invertedNamespaces );
942 }
943 };
944
945 /**
946 * Check if the namespaces selection is set to be inverted
947 * @return {boolean}
948 */
949 mw.rcfilters.dm.FiltersViewModel.prototype.areNamespacesInverted = function () {
950 return !!this.invertedNamespaces;
951 };
952
953 /**
954 * Set highlight color for a specific filter item
955 *
956 * @param {string} filterName Name of the filter item
957 * @param {string} color Selected color
958 */
959 mw.rcfilters.dm.FiltersViewModel.prototype.setHighlightColor = function ( filterName, color ) {
960 this.getItemByName( filterName ).setHighlightColor( color );
961 };
962
963 /**
964 * Clear highlight for a specific filter item
965 *
966 * @param {string} filterName Name of the filter item
967 */
968 mw.rcfilters.dm.FiltersViewModel.prototype.clearHighlightColor = function ( filterName ) {
969 this.getItemByName( filterName ).clearHighlightColor();
970 };
971
972 /**
973 * Clear highlight for all filter items
974 */
975 mw.rcfilters.dm.FiltersViewModel.prototype.clearAllHighlightColors = function () {
976 this.getItems().forEach( function ( filterItem ) {
977 filterItem.clearHighlightColor();
978 } );
979 };
980 }( mediaWiki, jQuery ) );