6825fa456cc479288d9be93c046414193e918e91
[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 * @param {Object[]} [tags] Tag array definition
212 */
213 mw.rcfilters.dm.FiltersViewModel.prototype.initializeFilters = function ( filters, namespaces, tags ) {
214 var filterItem, filterConflictResult, groupConflictResult,
215 model = this,
216 items = [],
217 namespaceDefinition = [],
218 groupConflictMap = {},
219 filterConflictMap = {},
220 /*!
221 * Expand a conflict definition from group name to
222 * the list of all included filters in that group.
223 * We do this so that the direct relationship in the
224 * models are consistently item->items rather than
225 * mixing item->group with item->item.
226 *
227 * @param {Object} obj Conflict definition
228 * @return {Object} Expanded conflict definition
229 */
230 expandConflictDefinitions = function ( obj ) {
231 var result = {};
232
233 $.each( obj, function ( key, conflicts ) {
234 var filterName,
235 adjustedConflicts = {};
236
237 conflicts.forEach( function ( conflict ) {
238 var filter;
239
240 if ( conflict.filter ) {
241 filterName = model.groups[ conflict.group ].getPrefixedName( conflict.filter );
242 filter = model.getItemByName( filterName );
243
244 // Rename
245 adjustedConflicts[ filterName ] = $.extend(
246 {},
247 conflict,
248 {
249 filter: filterName,
250 item: filter
251 }
252 );
253 } else {
254 // This conflict is for an entire group. Split it up to
255 // represent each filter
256
257 // Get the relevant group items
258 model.groups[ conflict.group ].getItems().forEach( function ( groupItem ) {
259 // Rebuild the conflict
260 adjustedConflicts[ groupItem.getName() ] = $.extend(
261 {},
262 conflict,
263 {
264 filter: groupItem.getName(),
265 item: groupItem
266 }
267 );
268 } );
269 }
270 } );
271
272 result[ key ] = adjustedConflicts;
273 } );
274
275 return result;
276 };
277
278 // Reset
279 this.clearItems();
280 this.groups = {};
281 this.views = {};
282
283 // Filters
284 this.views.default = { name: 'default', label: mw.msg( 'rcfilters-filterlist-title' ) };
285 filters.forEach( function ( data ) {
286 var i,
287 group = data.name;
288
289 if ( !model.groups[ group ] ) {
290 model.groups[ group ] = new mw.rcfilters.dm.FilterGroup( group, {
291 type: data.type,
292 title: data.title ? mw.msg( data.title ) : group,
293 separator: data.separator,
294 fullCoverage: !!data.fullCoverage,
295 whatsThis: {
296 body: data.whatsThisBody,
297 header: data.whatsThisHeader,
298 linkText: data.whatsThisLinkText,
299 url: data.whatsThisUrl
300 }
301 } );
302 }
303
304 // Filters are given to us with msg-keys, we need
305 // to translate those before we hand them off
306 for ( i = 0; i < data.filters.length; i++ ) {
307 data.filters[ i ].label = data.filters[ i ].label ? mw.msg( data.filters[ i ].label ) : data.filters[ i ].name;
308 data.filters[ i ].description = data.filters[ i ].description ? mw.msg( data.filters[ i ].description ) : '';
309 }
310
311 model.groups[ group ].initializeFilters( data.filters, data.default );
312 items = items.concat( model.groups[ group ].getItems() );
313
314 // Prepare conflicts
315 if ( data.conflicts ) {
316 // Group conflicts
317 groupConflictMap[ group ] = data.conflicts;
318 }
319
320 for ( i = 0; i < data.filters.length; i++ ) {
321 // Filter conflicts
322 if ( data.filters[ i ].conflicts ) {
323 filterItem = model.groups[ group ].getItemByParamName( data.filters[ i ].name );
324 filterConflictMap[ filterItem.getName() ] = data.filters[ i ].conflicts;
325 }
326 }
327 } );
328
329 namespaces = namespaces || {};
330 if (
331 mw.config.get( 'wgStructuredChangeFiltersEnableExperimentalViews' ) &&
332 !$.isEmptyObject( namespaces )
333 ) {
334 // Namespaces group
335 this.views.namespaces = { name: 'namespaces', label: mw.msg( 'namespaces' ), trigger: ':' };
336 $.each( namespaces, function ( namespaceID, label ) {
337 // Build and clean up the definition
338 namespaceDefinition.push( {
339 name: namespaceID,
340 label: label || mw.msg( 'blanknamespace' ),
341 description: '',
342 identifiers: [
343 ( namespaceID < 0 || namespaceID % 2 === 0 ) ?
344 'subject' : 'talk'
345 ],
346 cssClass: 'mw-changeslist-ns-' + namespaceID
347 } );
348 } );
349
350 // Add the group
351 model.groups.namespace = new mw.rcfilters.dm.FilterGroup(
352 'namespace', // Parameter name is singular
353 {
354 type: 'string_options',
355 view: 'namespaces',
356 title: 'namespaces', // Message key
357 separator: ';',
358 labelPrefixKey: { 'default': 'rcfilters-tag-prefix-namespace', inverted: 'rcfilters-tag-prefix-namespace-inverted' },
359 fullCoverage: true
360 }
361 );
362 // Add namespace items to group
363 model.groups.namespace.initializeFilters( namespaceDefinition );
364 items = items.concat( model.groups.namespace.getItems() );
365 }
366
367 tags = tags || [];
368 if (
369 mw.config.get( 'wgStructuredChangeFiltersEnableExperimentalViews' ) &&
370 tags.length > 0
371 ) {
372 // Define view
373 this.views.tags = { name: 'tags', label: mw.msg( 'rcfilters-view-tags' ), trigger: '#' };
374
375 // Add the group
376 model.groups.tagfilter = new mw.rcfilters.dm.FilterGroup(
377 'tagfilter',
378 {
379 type: 'string_options',
380 view: 'tags',
381 title: 'rcfilters-view-tags', // Message key
382 labelPrefixKey: 'rcfilters-tag-prefix-tags',
383 separator: '|',
384 fullCoverage: false
385 }
386 );
387
388 // Add tag items to group
389 model.groups.tagfilter.initializeFilters( tags );
390
391 // Add item references to the model, for lookup
392 items = items.concat( model.groups.tagfilter.getItems() );
393 }
394
395 // Add item references to the model, for lookup
396 this.addItems( items );
397
398 // Expand conflicts
399 groupConflictResult = expandConflictDefinitions( groupConflictMap );
400 filterConflictResult = expandConflictDefinitions( filterConflictMap );
401
402 // Set conflicts for groups
403 $.each( groupConflictResult, function ( group, conflicts ) {
404 model.groups[ group ].setConflicts( conflicts );
405 } );
406
407 // Set conflicts for items
408 $.each( filterConflictResult, function ( filterName, conflicts ) {
409 var filterItem = model.getItemByName( filterName );
410 // set conflicts for items in the group
411 filterItem.setConflicts( conflicts );
412 } );
413
414 // Create a map between known parameters and their models
415 $.each( this.groups, function ( group, groupModel ) {
416 if ( groupModel.getType() === 'send_unselected_if_any' ) {
417 // Individual filters
418 groupModel.getItems().forEach( function ( filterItem ) {
419 model.parameterMap[ filterItem.getParamName() ] = filterItem;
420 } );
421 } else if ( groupModel.getType() === 'string_options' ) {
422 // Group
423 model.parameterMap[ groupModel.getName() ] = groupModel;
424 }
425 } );
426
427 this.currentView = 'default';
428
429 // Finish initialization
430 this.emit( 'initialize' );
431 };
432
433 /**
434 * Get the names of all available filters
435 *
436 * @return {string[]} An array of filter names
437 */
438 mw.rcfilters.dm.FiltersViewModel.prototype.getFilterNames = function () {
439 return this.getItems().map( function ( item ) { return item.getName(); } );
440 };
441
442 /**
443 * Get the object that defines groups by their name.
444 *
445 * @return {Object} Filter groups
446 */
447 mw.rcfilters.dm.FiltersViewModel.prototype.getFilterGroups = function () {
448 return this.groups;
449 };
450
451 /**
452 * Get the object that defines groups that match a certain view by their name.
453 *
454 * @param {string} [view] Requested view. If not given, uses current view
455 * @return {Object} Filter groups matching a display group
456 */
457 mw.rcfilters.dm.FiltersViewModel.prototype.getFilterGroupsByView = function ( view ) {
458 var result = {};
459
460 view = view || this.getCurrentView();
461
462 $.each( this.groups, function ( groupName, groupModel ) {
463 if ( groupModel.getView() === view ) {
464 result[ groupName ] = groupModel;
465 }
466 } );
467
468 return result;
469 };
470
471 /**
472 * Get an array of filters matching the given display group.
473 *
474 * @param {string} [view] Requested view. If not given, uses current view
475 * @return {mw.rcfilters.dm.FilterItem} Filter items matching the group
476 */
477 mw.rcfilters.dm.FiltersViewModel.prototype.getFiltersByView = function ( view ) {
478 var groups,
479 result = [];
480
481 view = view || this.getCurrentView();
482
483 groups = this.getFilterGroupsByView( view );
484
485 $.each( groups, function ( groupName, groupModel ) {
486 result = result.concat( groupModel.getItems() );
487 } );
488
489 return result;
490 };
491
492 /**
493 * Get the trigger for the requested view.
494 *
495 * @param {string} view View name
496 * @return {string} View trigger, if exists
497 */
498 mw.rcfilters.dm.FiltersViewModel.prototype.getViewTrigger = function ( view ) {
499 return this.views[ view ] && this.views[ view ].trigger;
500 };
501 /**
502 * Get the value of a specific parameter
503 *
504 * @param {string} name Parameter name
505 * @return {number|string} Parameter value
506 */
507 mw.rcfilters.dm.FiltersViewModel.prototype.getParamValue = function ( name ) {
508 return this.parameters[ name ];
509 };
510
511 /**
512 * Get the current selected state of the filters
513 *
514 * @return {Object} Filters selected state
515 */
516 mw.rcfilters.dm.FiltersViewModel.prototype.getSelectedState = function () {
517 var i,
518 items = this.getItems(),
519 result = {};
520
521 for ( i = 0; i < items.length; i++ ) {
522 result[ items[ i ].getName() ] = items[ i ].isSelected();
523 }
524
525 return result;
526 };
527
528 /**
529 * Get the current full state of the filters
530 *
531 * @return {Object} Filters full state
532 */
533 mw.rcfilters.dm.FiltersViewModel.prototype.getFullState = function () {
534 var i,
535 items = this.getItems(),
536 result = {};
537
538 for ( i = 0; i < items.length; i++ ) {
539 result[ items[ i ].getName() ] = {
540 selected: items[ i ].isSelected(),
541 conflicted: items[ i ].isConflicted(),
542 included: items[ i ].isIncluded()
543 };
544 }
545
546 return result;
547 };
548
549 /**
550 * Get an object representing default parameters state
551 *
552 * @return {Object} Default parameter values
553 */
554 mw.rcfilters.dm.FiltersViewModel.prototype.getDefaultParams = function () {
555 var result = {};
556
557 // Get default filter state
558 $.each( this.groups, function ( name, model ) {
559 $.extend( true, result, model.getDefaultParams() );
560 } );
561
562 return result;
563 };
564
565 /**
566 * Analyze the groups and their filters and output an object representing
567 * the state of the parameters they represent.
568 *
569 * @param {Object} [filterDefinition] An object defining the filter values,
570 * keyed by filter names.
571 * @return {Object} Parameter state object
572 */
573 mw.rcfilters.dm.FiltersViewModel.prototype.getParametersFromFilters = function ( filterDefinition ) {
574 var groupItemDefinition,
575 result = {},
576 groupItems = this.getFilterGroups();
577
578 if ( filterDefinition ) {
579 groupItemDefinition = {};
580 // Filter definition is "flat", but in effect
581 // each group needs to tell us its result based
582 // on the values in it. We need to split this list
583 // back into groupings so we can "feed" it to the
584 // loop below, and we need to expand it so it includes
585 // all filters (set to false)
586 this.getItems().forEach( function ( filterItem ) {
587 groupItemDefinition[ filterItem.getGroupName() ] = groupItemDefinition[ filterItem.getGroupName() ] || {};
588 groupItemDefinition[ filterItem.getGroupName() ][ filterItem.getName() ] = !!filterDefinition[ filterItem.getName() ];
589 } );
590 }
591
592 $.each( groupItems, function ( group, model ) {
593 $.extend(
594 result,
595 model.getParamRepresentation(
596 groupItemDefinition ?
597 groupItemDefinition[ group ] : null
598 )
599 );
600 } );
601
602 return result;
603 };
604
605 /**
606 * This is the opposite of the #getParametersFromFilters method; this goes over
607 * the given parameters and translates into a selected/unselected value in the filters.
608 *
609 * @param {Object} params Parameters query object
610 * @return {Object} Filter state object
611 */
612 mw.rcfilters.dm.FiltersViewModel.prototype.getFiltersFromParameters = function ( params ) {
613 var groupMap = {},
614 model = this,
615 result = {};
616
617 // Go over the given parameters, break apart to groupings
618 // The resulting object represents the group with its parameter
619 // values. For example:
620 // {
621 // group1: {
622 // param1: "1",
623 // param2: "0",
624 // param3: "1"
625 // },
626 // group2: "param4|param5"
627 // }
628 $.each( params, function ( paramName, paramValue ) {
629 var itemOrGroup = model.parameterMap[ paramName ];
630
631 if ( itemOrGroup instanceof mw.rcfilters.dm.FilterItem ) {
632 groupMap[ itemOrGroup.getGroupName() ] = groupMap[ itemOrGroup.getGroupName() ] || {};
633 groupMap[ itemOrGroup.getGroupName() ][ itemOrGroup.getParamName() ] = paramValue;
634 } else if ( itemOrGroup instanceof mw.rcfilters.dm.FilterGroup ) {
635 // This parameter represents a group (values are the filters)
636 // this is equivalent to checking if the group is 'string_options'
637 groupMap[ itemOrGroup.getName() ] = groupMap[ itemOrGroup.getName() ] || {};
638 groupMap[ itemOrGroup.getName() ] = paramValue;
639 }
640 } );
641
642 // Go over all groups, so we make sure we get the complete output
643 // even if the parameters don't include a certain group
644 $.each( this.groups, function ( groupName, groupModel ) {
645 result = $.extend( true, {}, result, groupModel.getFilterRepresentation( groupMap[ groupName ] ) );
646 } );
647
648 return result;
649 };
650
651 /**
652 * Get the highlight parameters based on current filter configuration
653 *
654 * @return {Object} Object where keys are "<filter name>_color" and values
655 * are the selected highlight colors.
656 */
657 mw.rcfilters.dm.FiltersViewModel.prototype.getHighlightParameters = function () {
658 var result = {};
659
660 this.getItems().forEach( function ( filterItem ) {
661 result[ filterItem.getName() + '_color' ] = filterItem.getHighlightColor() || null;
662 } );
663 result.highlight = String( Number( this.isHighlightEnabled() ) );
664
665 return result;
666 };
667
668 /**
669 * Extract the highlight values from given object. Since highlights are
670 * the same for filter and parameters, it doesn't matter which one is
671 * given; values will be returned with a full list of the highlights
672 * with colors or null values.
673 *
674 * @param {Object} representation Object containing representation of
675 * some or all highlight values
676 * @return {Object} Object where keys are "<filter name>_color" and values
677 * are the selected highlight colors. The returned object
678 * contains all available filters either with a color value
679 * or with null.
680 */
681 mw.rcfilters.dm.FiltersViewModel.prototype.extractHighlightValues = function ( representation ) {
682 var result = {};
683
684 this.getItems().forEach( function ( filterItem ) {
685 var highlightName = filterItem.getName() + '_color';
686 result[ highlightName ] = representation[ highlightName ] || null;
687 } );
688
689 return result;
690 };
691
692 /**
693 * Sanitize value group of a string_option groups type
694 * Remove duplicates and make sure to only use valid
695 * values.
696 *
697 * @private
698 * @param {string} groupName Group name
699 * @param {string[]} valueArray Array of values
700 * @return {string[]} Array of valid values
701 */
702 mw.rcfilters.dm.FiltersViewModel.prototype.sanitizeStringOptionGroup = function ( groupName, valueArray ) {
703 var validNames = this.getGroupFilters( groupName ).map( function ( filterItem ) {
704 return filterItem.getParamName();
705 } );
706
707 return mw.rcfilters.utils.normalizeParamOptions( valueArray, validNames );
708 };
709
710 /**
711 * Check whether the current filter state is set to all false.
712 *
713 * @return {boolean} Current filters are all empty
714 */
715 mw.rcfilters.dm.FiltersViewModel.prototype.areCurrentFiltersEmpty = function () {
716 // Check if there are either any selected items or any items
717 // that have highlight enabled
718 return !this.getItems().some( function ( filterItem ) {
719 return filterItem.isSelected() || filterItem.isHighlighted();
720 } );
721 };
722
723 /**
724 * Check whether the default values of the filters are all false.
725 *
726 * @return {boolean} Default filters are all false
727 */
728 mw.rcfilters.dm.FiltersViewModel.prototype.areDefaultFiltersEmpty = function () {
729 var defaultFilters;
730
731 if ( this.defaultFiltersEmpty !== null ) {
732 // We only need to do this test once,
733 // because defaults are set once per session
734 defaultFilters = this.getFiltersFromParameters( this.getDefaultParams() );
735 this.defaultFiltersEmpty = Object.keys( defaultFilters ).every( function ( filterName ) {
736 return !defaultFilters[ filterName ];
737 } );
738 }
739
740 return this.defaultFiltersEmpty;
741 };
742
743 /**
744 * Get the item that matches the given name
745 *
746 * @param {string} name Filter name
747 * @return {mw.rcfilters.dm.FilterItem} Filter item
748 */
749 mw.rcfilters.dm.FiltersViewModel.prototype.getItemByName = function ( name ) {
750 return this.getItems().filter( function ( item ) {
751 return name === item.getName();
752 } )[ 0 ];
753 };
754
755 /**
756 * Set all filters to false or empty/all
757 * This is equivalent to display all.
758 */
759 mw.rcfilters.dm.FiltersViewModel.prototype.emptyAllFilters = function () {
760 this.getItems().forEach( function ( filterItem ) {
761 this.toggleFilterSelected( filterItem.getName(), false );
762 }.bind( this ) );
763 };
764
765 /**
766 * Toggle selected state of one item
767 *
768 * @param {string} name Name of the filter item
769 * @param {boolean} [isSelected] Filter selected state
770 */
771 mw.rcfilters.dm.FiltersViewModel.prototype.toggleFilterSelected = function ( name, isSelected ) {
772 var item = this.getItemByName( name );
773
774 if ( item ) {
775 item.toggleSelected( isSelected );
776 }
777 };
778
779 /**
780 * Toggle selected state of items by their names
781 *
782 * @param {Object} filterDef Filter definitions
783 */
784 mw.rcfilters.dm.FiltersViewModel.prototype.toggleFiltersSelected = function ( filterDef ) {
785 Object.keys( filterDef ).forEach( function ( name ) {
786 this.toggleFilterSelected( name, filterDef[ name ] );
787 }.bind( this ) );
788 };
789
790 /**
791 * Get a group model from its name
792 *
793 * @param {string} groupName Group name
794 * @return {mw.rcfilters.dm.FilterGroup} Group model
795 */
796 mw.rcfilters.dm.FiltersViewModel.prototype.getGroup = function ( groupName ) {
797 return this.groups[ groupName ];
798 };
799
800 /**
801 * Get all filters within a specified group by its name
802 *
803 * @param {string} groupName Group name
804 * @return {mw.rcfilters.dm.FilterItem[]} Filters belonging to this group
805 */
806 mw.rcfilters.dm.FiltersViewModel.prototype.getGroupFilters = function ( groupName ) {
807 return ( this.getGroup( groupName ) && this.getGroup( groupName ).getItems() ) || [];
808 };
809
810 /**
811 * Find items whose labels match the given string
812 *
813 * @param {string} query Search string
814 * @param {boolean} [returnFlat] Return a flat array. If false, the result
815 * is an object whose keys are the group names and values are an array of
816 * filters per group. If set to true, returns an array of filters regardless
817 * of their groups.
818 * @return {Object} An object of items to show
819 * arranged by their group names
820 */
821 mw.rcfilters.dm.FiltersViewModel.prototype.findMatches = function ( query, returnFlat ) {
822 var i, searchIsEmpty,
823 groupTitle,
824 result = {},
825 flatResult = [],
826 view = this.getViewByTrigger( query.substr( 0, 1 ) ),
827 items = this.getFiltersByView( view );
828
829 // Normalize so we can search strings regardless of case and view
830 query = query.toLowerCase();
831 if ( view !== 'default' ) {
832 query = query.substr( 1 );
833 }
834
835 // Check if the search if actually empty; this can be a problem when
836 // we use prefixes to denote different views
837 searchIsEmpty = query.length === 0;
838
839 // item label starting with the query string
840 for ( i = 0; i < items.length; i++ ) {
841 if (
842 searchIsEmpty ||
843 items[ i ].getLabel().toLowerCase().indexOf( query ) === 0 ||
844 (
845 // For tags, we want the parameter name to be included in the search
846 view === 'tags' &&
847 items[ i ].getParamName().toLowerCase().indexOf( query ) > -1
848 )
849 ) {
850 result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
851 result[ items[ i ].getGroupName() ].push( items[ i ] );
852 flatResult.push( items[ i ] );
853 }
854 }
855
856 if ( $.isEmptyObject( result ) ) {
857 // item containing the query string in their label, description, or group title
858 for ( i = 0; i < items.length; i++ ) {
859 groupTitle = items[ i ].getGroupModel().getTitle();
860 if (
861 searchIsEmpty ||
862 items[ i ].getLabel().toLowerCase().indexOf( query ) > -1 ||
863 items[ i ].getDescription().toLowerCase().indexOf( query ) > -1 ||
864 groupTitle.toLowerCase().indexOf( query ) > -1 ||
865 (
866 // For tags, we want the parameter name to be included in the search
867 view === 'tags' &&
868 items[ i ].getParamName().toLowerCase().indexOf( query ) > -1
869 )
870 ) {
871 result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
872 result[ items[ i ].getGroupName() ].push( items[ i ] );
873 flatResult.push( items[ i ] );
874 }
875 }
876 }
877
878 return returnFlat ? flatResult : result;
879 };
880
881 /**
882 * Get items that are highlighted
883 *
884 * @return {mw.rcfilters.dm.FilterItem[]} Highlighted items
885 */
886 mw.rcfilters.dm.FiltersViewModel.prototype.getHighlightedItems = function () {
887 return this.getItems().filter( function ( filterItem ) {
888 return filterItem.isHighlightSupported() &&
889 filterItem.getHighlightColor();
890 } );
891 };
892
893 /**
894 * Get items that allow highlights even if they're not currently highlighted
895 *
896 * @return {mw.rcfilters.dm.FilterItem[]} Items supporting highlights
897 */
898 mw.rcfilters.dm.FiltersViewModel.prototype.getItemsSupportingHighlights = function () {
899 return this.getItems().filter( function ( filterItem ) {
900 return filterItem.isHighlightSupported();
901 } );
902 };
903
904 /**
905 * Switch the current view
906 *
907 * @param {string} view View name
908 * @fires update
909 */
910 mw.rcfilters.dm.FiltersViewModel.prototype.switchView = function ( view ) {
911 if ( this.views[ view ] && this.currentView !== view ) {
912 this.currentView = view;
913 this.emit( 'update' );
914 }
915 };
916
917 /**
918 * Get the current view
919 *
920 * @return {string} Current view
921 */
922 mw.rcfilters.dm.FiltersViewModel.prototype.getCurrentView = function () {
923 return this.currentView;
924 };
925
926 /**
927 * Get the label for the current view
928 *
929 * @return {string} Label for the current view
930 */
931 mw.rcfilters.dm.FiltersViewModel.prototype.getCurrentViewLabel = function () {
932 return this.views[ this.getCurrentView() ].label;
933 };
934
935 /**
936 * Get an array of all available view names
937 *
938 * @return {string} Available view names
939 */
940 mw.rcfilters.dm.FiltersViewModel.prototype.getAvailableViews = function () {
941 return Object.keys( this.views );
942 };
943
944 /**
945 * Get the view that fits the given trigger
946 *
947 * @param {string} trigger Trigger
948 * @return {string} Name of view
949 */
950 mw.rcfilters.dm.FiltersViewModel.prototype.getViewByTrigger = function ( trigger ) {
951 var result = 'default';
952
953 $.each( this.views, function ( name, data ) {
954 if ( data.trigger === trigger ) {
955 result = name;
956 }
957 } );
958
959 return result;
960 };
961
962 /**
963 * Toggle the highlight feature on and off.
964 * Propagate the change to filter items.
965 *
966 * @param {boolean} enable Highlight should be enabled
967 * @fires highlightChange
968 */
969 mw.rcfilters.dm.FiltersViewModel.prototype.toggleHighlight = function ( enable ) {
970 enable = enable === undefined ? !this.highlightEnabled : enable;
971
972 if ( this.highlightEnabled !== enable ) {
973 this.highlightEnabled = enable;
974
975 this.getItems().forEach( function ( filterItem ) {
976 filterItem.toggleHighlight( this.highlightEnabled );
977 }.bind( this ) );
978
979 this.emit( 'highlightChange', this.highlightEnabled );
980 }
981 };
982
983 /**
984 * Check if the highlight feature is enabled
985 * @return {boolean}
986 */
987 mw.rcfilters.dm.FiltersViewModel.prototype.isHighlightEnabled = function () {
988 return !!this.highlightEnabled;
989 };
990
991 /**
992 * Toggle the inverted namespaces property on and off.
993 * Propagate the change to namespace filter items.
994 *
995 * @param {boolean} enable Inverted property is enabled
996 * @fires invertChange
997 */
998 mw.rcfilters.dm.FiltersViewModel.prototype.toggleInvertedNamespaces = function ( enable ) {
999 enable = enable === undefined ? !this.invertedNamespaces : enable;
1000
1001 if ( this.invertedNamespaces !== enable ) {
1002 this.invertedNamespaces = enable;
1003
1004 this.getFiltersByView( 'namespaces' ).forEach( function ( filterItem ) {
1005 filterItem.toggleInverted( this.invertedNamespaces );
1006 }.bind( this ) );
1007
1008 this.emit( 'invertChange', this.invertedNamespaces );
1009 }
1010 };
1011
1012 /**
1013 * Check if the namespaces selection is set to be inverted
1014 * @return {boolean}
1015 */
1016 mw.rcfilters.dm.FiltersViewModel.prototype.areNamespacesInverted = function () {
1017 return !!this.invertedNamespaces;
1018 };
1019
1020 /**
1021 * Set highlight color for a specific filter item
1022 *
1023 * @param {string} filterName Name of the filter item
1024 * @param {string} color Selected color
1025 */
1026 mw.rcfilters.dm.FiltersViewModel.prototype.setHighlightColor = function ( filterName, color ) {
1027 this.getItemByName( filterName ).setHighlightColor( color );
1028 };
1029
1030 /**
1031 * Clear highlight for a specific filter item
1032 *
1033 * @param {string} filterName Name of the filter item
1034 */
1035 mw.rcfilters.dm.FiltersViewModel.prototype.clearHighlightColor = function ( filterName ) {
1036 this.getItemByName( filterName ).clearHighlightColor();
1037 };
1038
1039 /**
1040 * Clear highlight for all filter items
1041 */
1042 mw.rcfilters.dm.FiltersViewModel.prototype.clearAllHighlightColors = function () {
1043 this.getItems().forEach( function ( filterItem ) {
1044 filterItem.clearHighlightColor();
1045 } );
1046 };
1047 }( mediaWiki, jQuery ) );