RCFilters: refactor highlight state
[lhc/web/wiklou.git] / resources / src / mediawiki.rcfilters / dm / mw.rcfilters.dm.FilterGroup.js
1 ( function ( mw ) {
2 /**
3 * View model for a filter group
4 *
5 * @mixins OO.EventEmitter
6 * @mixins OO.EmitterList
7 *
8 * @constructor
9 * @param {string} name Group name
10 * @param {Object} [config] Configuration options
11 * @cfg {string} [type='send_unselected_if_any'] Group type
12 * @cfg {string} [view='default'] Name of the display group this group
13 * is a part of.
14 * @cfg {boolean} [isSticky] This group is using a 'sticky' default; meaning
15 * that every time a value is changed, it becomes the new default
16 * @cfg {boolean} [excludedFromSavedQueries] A specific requirement to exclude
17 * this filter from saved queries. This is always true if the filter is 'sticky'
18 * but can be used for non-sticky filters as an additional requirement. Similarly
19 * to 'sticky' it works for the entire group as a whole.
20 * @cfg {string} [title] Group title
21 * @cfg {boolean} [hidden] This group is hidden from the regular menu views
22 * @cfg {boolean} [allowArbitrary] Allows for an arbitrary value to be added to the
23 * group from the URL, even if it wasn't initially set up.
24 * @cfg {number} [range] An object defining minimum and maximum values for numeric
25 * groups. { min: x, max: y }
26 * @cfg {number} [minValue] Minimum value for numeric groups
27 * @cfg {string} [separator='|'] Value separator for 'string_options' groups
28 * @cfg {boolean} [active] Group is active
29 * @cfg {boolean} [fullCoverage] This filters in this group collectively cover all results
30 * @cfg {Object} [conflicts] Defines the conflicts for this filter group
31 * @cfg {string|Object} [labelPrefixKey] An i18n key defining the prefix label for this
32 * group. If the prefix has 'invert' state, the parameter is expected to be an object
33 * with 'default' and 'inverted' as keys.
34 * @cfg {Object} [whatsThis] Defines the messages that should appear for the 'what's this' popup
35 * @cfg {string} [whatsThis.header] The header of the whatsThis popup message
36 * @cfg {string} [whatsThis.body] The body of the whatsThis popup message
37 * @cfg {string} [whatsThis.url] The url for the link in the whatsThis popup message
38 * @cfg {string} [whatsThis.linkMessage] The text for the link in the whatsThis popup message
39 */
40 mw.rcfilters.dm.FilterGroup = function MwRcfiltersDmFilterGroup( name, config ) {
41 config = config || {};
42
43 // Mixin constructor
44 OO.EventEmitter.call( this );
45 OO.EmitterList.call( this );
46
47 this.name = name;
48 this.type = config.type || 'send_unselected_if_any';
49 this.view = config.view || 'default';
50 this.sticky = !!config.isSticky;
51 this.excludedFromSavedQueries = this.sticky || !!config.excludedFromSavedQueries;
52 this.title = config.title || name;
53 this.hidden = !!config.hidden;
54 this.allowArbitrary = !!config.allowArbitrary;
55 this.numericRange = config.range;
56 this.separator = config.separator || '|';
57 this.labelPrefixKey = config.labelPrefixKey;
58
59 this.currSelected = null;
60 this.active = !!config.active;
61 this.fullCoverage = !!config.fullCoverage;
62
63 this.whatsThis = config.whatsThis || {};
64
65 this.conflicts = config.conflicts || {};
66 this.defaultParams = {};
67 this.defaultFilters = {};
68
69 this.aggregate( { update: 'filterItemUpdate' } );
70 this.connect( this, { filterItemUpdate: 'onFilterItemUpdate' } );
71 };
72
73 /* Initialization */
74 OO.initClass( mw.rcfilters.dm.FilterGroup );
75 OO.mixinClass( mw.rcfilters.dm.FilterGroup, OO.EventEmitter );
76 OO.mixinClass( mw.rcfilters.dm.FilterGroup, OO.EmitterList );
77
78 /* Events */
79
80 /**
81 * @event update
82 *
83 * Group state has been updated
84 */
85
86 /* Methods */
87
88 /**
89 * Initialize the group and create its filter items
90 *
91 * @param {Object} filterDefinition Filter definition for this group
92 * @param {string|Object} [groupDefault] Definition of the group default
93 */
94 mw.rcfilters.dm.FilterGroup.prototype.initializeFilters = function ( filterDefinition, groupDefault ) {
95 var defaultParam,
96 supersetMap = {},
97 model = this,
98 items = [];
99
100 filterDefinition.forEach( function ( filter ) {
101 // Instantiate an item
102 var subsetNames = [],
103 filterItem = new mw.rcfilters.dm.FilterItem( filter.name, model, {
104 group: model.getName(),
105 label: filter.label || filter.name,
106 description: filter.description || '',
107 labelPrefixKey: model.labelPrefixKey,
108 cssClass: filter.cssClass,
109 identifiers: filter.identifiers,
110 defaultHighlightColor: filter.defaultHighlightColor
111 } );
112
113 if ( filter.subset ) {
114 filter.subset = filter.subset.map( function ( el ) {
115 return el.filter;
116 } );
117
118 subsetNames = [];
119
120 filter.subset.forEach( function ( subsetFilterName ) { // eslint-disable-line no-loop-func
121 // Subsets (unlike conflicts) are always inside the same group
122 // We can re-map the names of the filters we are getting from
123 // the subsets with the group prefix
124 var subsetName = model.getPrefixedName( subsetFilterName );
125 // For convenience, we should store each filter's "supersets" -- these are
126 // the filters that have that item in their subset list. This will just
127 // make it easier to go through whether the item has any other items
128 // that affect it (and are selected) at any given time
129 supersetMap[ subsetName ] = supersetMap[ subsetName ] || [];
130 mw.rcfilters.utils.addArrayElementsUnique(
131 supersetMap[ subsetName ],
132 filterItem.getName()
133 );
134
135 // Translate subset param name to add the group name, so we
136 // get consistent naming. We know that subsets are only within
137 // the same group
138 subsetNames.push( subsetName );
139 } );
140
141 // Set translated subset
142 filterItem.setSubset( subsetNames );
143 }
144
145 items.push( filterItem );
146
147 // Store default parameter state; in this case, default is defined per filter
148 if (
149 model.getType() === 'send_unselected_if_any' ||
150 model.getType() === 'boolean'
151 ) {
152 // Store the default parameter state
153 // For this group type, parameter values are direct
154 // We need to convert from a boolean to a string ('1' and '0')
155 model.defaultParams[ filter.name ] = String( Number( filter.default || 0 ) );
156 }
157 } );
158
159 // Add items
160 this.addItems( items );
161
162 // Now that we have all items, we can apply the superset map
163 this.getItems().forEach( function ( filterItem ) {
164 filterItem.setSuperset( supersetMap[ filterItem.getName() ] );
165 } );
166
167 // Store default parameter state; in this case, default is defined per the
168 // entire group, given by groupDefault method parameter
169 if ( this.getType() === 'string_options' ) {
170 // Store the default parameter group state
171 // For this group, the parameter is group name and value is the names
172 // of selected items
173 this.defaultParams[ this.getName() ] = mw.rcfilters.utils.normalizeParamOptions(
174 // Current values
175 groupDefault ?
176 groupDefault.split( this.getSeparator() ) :
177 [],
178 // Legal values
179 this.getItems().map( function ( item ) {
180 return item.getParamName();
181 } )
182 ).join( this.getSeparator() );
183 } else if ( this.getType() === 'single_option' ) {
184 defaultParam = groupDefault !== undefined ?
185 groupDefault : this.getItems()[ 0 ].getParamName();
186
187 // For this group, the parameter is the group name,
188 // and a single item can be selected: default or first item
189 this.defaultParams[ this.getName() ] = defaultParam;
190 }
191
192 // add highlights to defaultParams
193 this.getItems().forEach( function ( filterItem ) {
194 if ( filterItem.isHighlighted() ) {
195 this.defaultParams[ filterItem.getName() + '_color' ] = filterItem.getHighlightColor();
196 }
197 }.bind( this ) );
198
199 // Store default filter state based on default params
200 this.defaultFilters = this.getFilterRepresentation( this.getDefaultParams() );
201
202 // Check for filters that should be initially selected by their default value
203 if ( this.isSticky() ) {
204 $.each( this.defaultFilters, function ( filterName, filterValue ) {
205 model.getItemByName( filterName ).toggleSelected( filterValue );
206 } );
207 }
208
209 // Verify that single_option group has at least one item selected
210 if (
211 this.getType() === 'single_option' &&
212 this.getSelectedItems().length === 0
213 ) {
214 defaultParam = groupDefault !== undefined ?
215 groupDefault : this.getItems()[ 0 ].getParamName();
216
217 // Single option means there must be a single option
218 // selected, so we have to either select the default
219 // or select the first option
220 this.selectItemByParamName( defaultParam );
221 }
222 };
223
224 /**
225 * Respond to filterItem update event
226 *
227 * @param {mw.rcfilters.dm.FilterItem} item Updated filter item
228 * @fires update
229 */
230 mw.rcfilters.dm.FilterGroup.prototype.onFilterItemUpdate = function ( item ) {
231 // Update state
232 var changed = false,
233 active = this.areAnySelected(),
234 model = this;
235
236 if ( this.getType() === 'single_option' ) {
237 // This group must have one item selected always
238 // and must never have more than one item selected at a time
239 if ( this.getSelectedItems().length === 0 ) {
240 // Nothing is selected anymore
241 // Select the default or the first item
242 this.currSelected = this.getItemByParamName( this.defaultParams[ this.getName() ] ) ||
243 this.getItems()[ 0 ];
244 this.currSelected.toggleSelected( true );
245 changed = true;
246 } else if ( this.getSelectedItems().length > 1 ) {
247 // There is more than one item selected
248 // This should only happen if the item given
249 // is the one that is selected, so unselect
250 // all items that is not it
251 this.getSelectedItems().forEach( function ( itemModel ) {
252 // Note that in case the given item is actually
253 // not selected, this loop will end up unselecting
254 // all items, which would trigger the case above
255 // when the last item is unselected anyways
256 var selected = itemModel.getName() === item.getName() &&
257 item.isSelected();
258
259 itemModel.toggleSelected( selected );
260 if ( selected ) {
261 model.currSelected = itemModel;
262 }
263 } );
264 changed = true;
265 }
266 }
267
268 if (
269 changed ||
270 this.active !== active ||
271 this.currSelected !== item
272 ) {
273 if ( this.isSticky() ) {
274 // If this group is sticky, then change the default according to the
275 // current selection.
276 this.defaultParams = this.getParamRepresentation( this.getSelectedState() );
277 }
278
279 this.active = active;
280 this.currSelected = item;
281
282 this.emit( 'update' );
283 }
284 };
285
286 /**
287 * Get group active state
288 *
289 * @return {boolean} Active state
290 */
291 mw.rcfilters.dm.FilterGroup.prototype.isActive = function () {
292 return this.active;
293 };
294
295 /**
296 * Get group hidden state
297 *
298 * @return {boolean} Hidden state
299 */
300 mw.rcfilters.dm.FilterGroup.prototype.isHidden = function () {
301 return this.hidden;
302 };
303
304 /**
305 * Get group allow arbitrary state
306 *
307 * @return {boolean} Group allows an arbitrary value from the URL
308 */
309 mw.rcfilters.dm.FilterGroup.prototype.isAllowArbitrary = function () {
310 return this.allowArbitrary;
311 };
312
313 /**
314 * Get group maximum value for numeric groups
315 *
316 * @return {number|null} Group max value
317 */
318 mw.rcfilters.dm.FilterGroup.prototype.getMaxValue = function () {
319 return this.numericRange && this.numericRange.max !== undefined ?
320 this.numericRange.max : null;
321 };
322
323 /**
324 * Get group minimum value for numeric groups
325 *
326 * @return {number|null} Group max value
327 */
328 mw.rcfilters.dm.FilterGroup.prototype.getMinValue = function () {
329 return this.numericRange && this.numericRange.min !== undefined ?
330 this.numericRange.min : null;
331 };
332
333 /**
334 * Get group name
335 *
336 * @return {string} Group name
337 */
338 mw.rcfilters.dm.FilterGroup.prototype.getName = function () {
339 return this.name;
340 };
341
342 /**
343 * Get the default param state of this group
344 *
345 * @return {Object} Default param state
346 */
347 mw.rcfilters.dm.FilterGroup.prototype.getDefaultParams = function () {
348 return this.defaultParams;
349 };
350
351 /**
352 * Get the default filter state of this group
353 *
354 * @return {Object} Default filter state
355 */
356 mw.rcfilters.dm.FilterGroup.prototype.getDefaultFilters = function () {
357 return this.defaultFilters;
358 };
359
360 /**
361 * This is for a single_option and string_options group types
362 * it returns the value of the default
363 *
364 * @return {string} Value of the default
365 */
366 mw.rcfilters.dm.FilterGroup.prototype.getDefaulParamValue = function () {
367 return this.defaultParams[ this.getName() ];
368 };
369 /**
370 * Get the messags defining the 'whats this' popup for this group
371 *
372 * @return {Object} What's this messages
373 */
374 mw.rcfilters.dm.FilterGroup.prototype.getWhatsThis = function () {
375 return this.whatsThis;
376 };
377
378 /**
379 * Check whether this group has a 'what's this' message
380 *
381 * @return {boolean} This group has a what's this message
382 */
383 mw.rcfilters.dm.FilterGroup.prototype.hasWhatsThis = function () {
384 return !!this.whatsThis.body;
385 };
386
387 /**
388 * Get the conflicts associated with the entire group.
389 * Conflict object is set up by filter name keys and conflict
390 * definition. For example:
391 * [
392 * {
393 * filterName: {
394 * filter: filterName,
395 * group: group1
396 * }
397 * },
398 * {
399 * filterName2: {
400 * filter: filterName2,
401 * group: group2
402 * }
403 * }
404 * ]
405 * @return {Object} Conflict definition
406 */
407 mw.rcfilters.dm.FilterGroup.prototype.getConflicts = function () {
408 return this.conflicts;
409 };
410
411 /**
412 * Set conflicts for this group. See #getConflicts for the expected
413 * structure of the definition.
414 *
415 * @param {Object} conflicts Conflicts for this group
416 */
417 mw.rcfilters.dm.FilterGroup.prototype.setConflicts = function ( conflicts ) {
418 this.conflicts = conflicts;
419 };
420
421 /**
422 * Set conflicts for each filter item in the group based on the
423 * given conflict map
424 *
425 * @param {Object} conflicts Object representing the conflict map,
426 * keyed by the item name, where its value is an object for all its conflicts
427 */
428 mw.rcfilters.dm.FilterGroup.prototype.setFilterConflicts = function ( conflicts ) {
429 this.getItems().forEach( function ( filterItem ) {
430 if ( conflicts[ filterItem.getName() ] ) {
431 filterItem.setConflicts( conflicts[ filterItem.getName() ] );
432 }
433 } );
434 };
435
436 /**
437 * Check whether this item has a potential conflict with the given item
438 *
439 * This checks whether the given item is in the list of conflicts of
440 * the current item, but makes no judgment about whether the conflict
441 * is currently at play (either one of the items may not be selected)
442 *
443 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item
444 * @return {boolean} This item has a conflict with the given item
445 */
446 mw.rcfilters.dm.FilterGroup.prototype.existsInConflicts = function ( filterItem ) {
447 return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() );
448 };
449
450 /**
451 * Check whether there are any items selected
452 *
453 * @return {boolean} Any items in the group are selected
454 */
455 mw.rcfilters.dm.FilterGroup.prototype.areAnySelected = function () {
456 return this.getItems().some( function ( filterItem ) {
457 return filterItem.isSelected();
458 } );
459 };
460
461 /**
462 * Check whether all items selected
463 *
464 * @return {boolean} All items are selected
465 */
466 mw.rcfilters.dm.FilterGroup.prototype.areAllSelected = function () {
467 var selected = [],
468 unselected = [];
469
470 this.getItems().forEach( function ( filterItem ) {
471 if ( filterItem.isSelected() ) {
472 selected.push( filterItem );
473 } else {
474 unselected.push( filterItem );
475 }
476 } );
477
478 if ( unselected.length === 0 ) {
479 return true;
480 }
481
482 // check if every unselected is a subset of a selected
483 return unselected.every( function ( unselectedFilterItem ) {
484 return selected.some( function ( selectedFilterItem ) {
485 return selectedFilterItem.existsInSubset( unselectedFilterItem.getName() );
486 } );
487 } );
488 };
489
490 /**
491 * Get all selected items in this group
492 *
493 * @param {mw.rcfilters.dm.FilterItem} [excludeItem] Item to exclude from the list
494 * @return {mw.rcfilters.dm.FilterItem[]} Selected items
495 */
496 mw.rcfilters.dm.FilterGroup.prototype.getSelectedItems = function ( excludeItem ) {
497 var excludeName = ( excludeItem && excludeItem.getName() ) || '';
498
499 return this.getItems().filter( function ( item ) {
500 return item.getName() !== excludeName && item.isSelected();
501 } );
502 };
503
504 /**
505 * Check whether all selected items are in conflict with the given item
506 *
507 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
508 * @return {boolean} All selected items are in conflict with this item
509 */
510 mw.rcfilters.dm.FilterGroup.prototype.areAllSelectedInConflictWith = function ( filterItem ) {
511 var selectedItems = this.getSelectedItems( filterItem );
512
513 return selectedItems.length > 0 &&
514 (
515 // The group as a whole is in conflict with this item
516 this.existsInConflicts( filterItem ) ||
517 // All selected items are in conflict individually
518 selectedItems.every( function ( selectedFilter ) {
519 return selectedFilter.existsInConflicts( filterItem );
520 } )
521 );
522 };
523
524 /**
525 * Check whether any of the selected items are in conflict with the given item
526 *
527 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
528 * @return {boolean} Any of the selected items are in conflict with this item
529 */
530 mw.rcfilters.dm.FilterGroup.prototype.areAnySelectedInConflictWith = function ( filterItem ) {
531 var selectedItems = this.getSelectedItems( filterItem );
532
533 return selectedItems.length > 0 && (
534 // The group as a whole is in conflict with this item
535 this.existsInConflicts( filterItem ) ||
536 // Any selected items are in conflict individually
537 selectedItems.some( function ( selectedFilter ) {
538 return selectedFilter.existsInConflicts( filterItem );
539 } )
540 );
541 };
542
543 /**
544 * Get the parameter representation from this group
545 *
546 * @param {Object} [filterRepresentation] An object defining the state
547 * of the filters in this group, keyed by their name and current selected
548 * state value.
549 * @return {Object} Parameter representation
550 */
551 mw.rcfilters.dm.FilterGroup.prototype.getParamRepresentation = function ( filterRepresentation ) {
552 var values,
553 areAnySelected = false,
554 buildFromCurrentState = !filterRepresentation,
555 defaultFilters = this.getDefaultFilters(),
556 result = {},
557 model = this,
558 filterParamNames = {},
559 getSelectedParameter = function ( filters ) {
560 var item,
561 selected = [];
562
563 // Find if any are selected
564 $.each( filters, function ( name, value ) {
565 if ( value ) {
566 selected.push( name );
567 }
568 } );
569
570 item = model.getItemByName( selected[ 0 ] );
571 return ( item && item.getParamName() ) || '';
572 };
573
574 filterRepresentation = filterRepresentation || {};
575
576 // Create or complete the filterRepresentation definition
577 this.getItems().forEach( function ( item ) {
578 // Map filter names to their parameter names
579 filterParamNames[ item.getName() ] = item.getParamName();
580
581 if ( buildFromCurrentState ) {
582 // This means we have not been given a filter representation
583 // so we are building one based on current state
584 filterRepresentation[ item.getName() ] = item.isSelected();
585 } else if ( filterRepresentation[ item.getName() ] === undefined ) {
586 // We are given a filter representation, but we have to make
587 // sure that we fill in the missing filters if there are any
588 // we will assume they are all falsey
589 if ( model.isSticky() ) {
590 filterRepresentation[ item.getName() ] = !!defaultFilters[ item.getName() ];
591 } else {
592 filterRepresentation[ item.getName() ] = false;
593 }
594 }
595
596 if ( filterRepresentation[ item.getName() ] ) {
597 areAnySelected = true;
598 }
599 } );
600
601 // Build result
602 if (
603 this.getType() === 'send_unselected_if_any' ||
604 this.getType() === 'boolean'
605 ) {
606 // First, check if any of the items are selected at all.
607 // If none is selected, we're treating it as if they are
608 // all false
609
610 // Go over the items and define the correct values
611 $.each( filterRepresentation, function ( name, value ) {
612 // We must store all parameter values as strings '0' or '1'
613 if ( model.getType() === 'send_unselected_if_any' ) {
614 result[ filterParamNames[ name ] ] = areAnySelected ?
615 String( Number( !value ) ) :
616 '0';
617 } else if ( model.getType() === 'boolean' ) {
618 // Representation is straight-forward and direct from
619 // the parameter value to the filter state
620 result[ filterParamNames[ name ] ] = String( Number( !!value ) );
621 }
622 } );
623 } else if ( this.getType() === 'string_options' ) {
624 values = [];
625
626 $.each( filterRepresentation, function ( name, value ) {
627 // Collect values
628 if ( value ) {
629 values.push( filterParamNames[ name ] );
630 }
631 } );
632
633 result[ this.getName() ] = ( values.length === Object.keys( filterRepresentation ).length ) ?
634 'all' : values.join( this.getSeparator() );
635 } else if ( this.getType() === 'single_option' ) {
636 result[ this.getName() ] = getSelectedParameter( filterRepresentation );
637 }
638
639 return result;
640 };
641
642 /**
643 * Get the filter representation this group would provide
644 * based on given parameter states.
645 *
646 * @param {Object} [paramRepresentation] An object defining a parameter
647 * state to translate the filter state from. If not given, an object
648 * representing all filters as falsey is returned; same as if the parameter
649 * given were an empty object, or had some of the filters missing.
650 * @return {Object} Filter representation
651 */
652 mw.rcfilters.dm.FilterGroup.prototype.getFilterRepresentation = function ( paramRepresentation ) {
653 var areAnySelected, paramValues, item, currentValue,
654 oneWasSelected = false,
655 defaultParams = this.getDefaultParams(),
656 expandedParams = $.extend( true, {}, paramRepresentation ),
657 model = this,
658 paramToFilterMap = {},
659 result = {};
660
661 if ( this.isSticky() ) {
662 // If the group is sticky, check if all parameters are represented
663 // and for those that aren't represented, add them with their default
664 // values
665 paramRepresentation = $.extend( true, {}, this.getDefaultParams(), paramRepresentation );
666 }
667
668 paramRepresentation = paramRepresentation || {};
669 if (
670 this.getType() === 'send_unselected_if_any' ||
671 this.getType() === 'boolean'
672 ) {
673 // Go over param representation; map and check for selections
674 this.getItems().forEach( function ( filterItem ) {
675 var paramName = filterItem.getParamName();
676
677 expandedParams[ paramName ] = paramRepresentation[ paramName ] || '0';
678 paramToFilterMap[ paramName ] = filterItem;
679
680 if ( Number( paramRepresentation[ filterItem.getParamName() ] ) ) {
681 areAnySelected = true;
682 }
683 } );
684
685 $.each( expandedParams, function ( paramName, paramValue ) {
686 var filterItem = paramToFilterMap[ paramName ];
687
688 if ( model.getType() === 'send_unselected_if_any' ) {
689 // Flip the definition between the parameter
690 // state and the filter state
691 // This is what the 'toggleSelected' value of the filter is
692 result[ filterItem.getName() ] = areAnySelected ?
693 !Number( paramValue ) :
694 // Otherwise, there are no selected items in the
695 // group, which means the state is false
696 false;
697 } else if ( model.getType() === 'boolean' ) {
698 // Straight-forward definition of state
699 result[ filterItem.getName() ] = !!Number( paramRepresentation[ filterItem.getParamName() ] );
700 }
701 } );
702 } else if ( this.getType() === 'string_options' ) {
703 currentValue = paramRepresentation[ this.getName() ] || '';
704
705 // Normalize the given parameter values
706 paramValues = mw.rcfilters.utils.normalizeParamOptions(
707 // Given
708 currentValue.split(
709 this.getSeparator()
710 ),
711 // Allowed values
712 this.getItems().map( function ( filterItem ) {
713 return filterItem.getParamName();
714 } )
715 );
716 // Translate the parameter values into a filter selection state
717 this.getItems().forEach( function ( filterItem ) {
718 // All true (either because all values are written or the term 'all' is written)
719 // is the same as all filters set to true
720 result[ filterItem.getName() ] = (
721 // If it is the word 'all'
722 paramValues.length === 1 && paramValues[ 0 ] === 'all' ||
723 // All values are written
724 paramValues.length === model.getItemCount()
725 ) ?
726 true :
727 // Otherwise, the filter is selected only if it appears in the parameter values
728 paramValues.indexOf( filterItem.getParamName() ) > -1;
729 } );
730 } else if ( this.getType() === 'single_option' ) {
731 // There is parameter that fits a single filter and if not, get the default
732 this.getItems().forEach( function ( filterItem ) {
733 var selected = filterItem.getParamName() === paramRepresentation[ model.getName() ];
734
735 result[ filterItem.getName() ] = selected;
736 oneWasSelected = oneWasSelected || selected;
737 } );
738 }
739
740 // Go over result and make sure all filters are represented.
741 // If any filters are missing, they will get a falsey value
742 this.getItems().forEach( function ( filterItem ) {
743 if ( result[ filterItem.getName() ] === undefined ) {
744 result[ filterItem.getName() ] = false;
745 }
746 } );
747
748 // Make sure that at least one option is selected in
749 // single_option groups, no matter what path was taken
750 // If none was selected by the given definition, then
751 // we need to select the one in the base state -- either
752 // the default given, or the first item
753 if (
754 this.getType() === 'single_option' &&
755 !oneWasSelected
756 ) {
757 item = this.getItems()[ 0 ];
758 if ( defaultParams[ this.getName() ] ) {
759 item = this.getItemByParamName( defaultParams[ this.getName() ] );
760 }
761
762 result[ item.getName() ] = true;
763 }
764
765 return result;
766 };
767
768 /**
769 * Get current selected state of all filter items in this group
770 *
771 * @return {Object} Selected state
772 */
773 mw.rcfilters.dm.FilterGroup.prototype.getSelectedState = function () {
774 var state = {};
775
776 this.getItems().forEach( function ( filterItem ) {
777 state[ filterItem.getName() ] = filterItem.isSelected();
778 } );
779
780 return state;
781 };
782
783 /**
784 * Get item by its filter name
785 *
786 * @param {string} filterName Filter name
787 * @return {mw.rcfilters.dm.FilterItem} Filter item
788 */
789 mw.rcfilters.dm.FilterGroup.prototype.getItemByName = function ( filterName ) {
790 return this.getItems().filter( function ( item ) {
791 return item.getName() === filterName;
792 } )[ 0 ];
793 };
794
795 /**
796 * Select an item by its parameter name
797 *
798 * @param {string} paramName Filter parameter name
799 */
800 mw.rcfilters.dm.FilterGroup.prototype.selectItemByParamName = function ( paramName ) {
801 this.getItems().forEach( function ( item ) {
802 item.toggleSelected( item.getParamName() === String( paramName ) );
803 } );
804 };
805
806 /**
807 * Get item by its parameter name
808 *
809 * @param {string} paramName Parameter name
810 * @return {mw.rcfilters.dm.FilterItem} Filter item
811 */
812 mw.rcfilters.dm.FilterGroup.prototype.getItemByParamName = function ( paramName ) {
813 return this.getItems().filter( function ( item ) {
814 return item.getParamName() === String( paramName );
815 } )[ 0 ];
816 };
817
818 /**
819 * Get group type
820 *
821 * @return {string} Group type
822 */
823 mw.rcfilters.dm.FilterGroup.prototype.getType = function () {
824 return this.type;
825 };
826
827 /**
828 * Check whether this group is represented by a single parameter
829 * or whether each item is its own parameter
830 *
831 * @return {boolean} This group is a single parameter
832 */
833 mw.rcfilters.dm.FilterGroup.prototype.isPerGroupRequestParameter = function () {
834 return (
835 this.getType() === 'string_options' ||
836 this.getType() === 'single_option'
837 );
838 };
839
840 /**
841 * Get display group
842 *
843 * @return {string} Display group
844 */
845 mw.rcfilters.dm.FilterGroup.prototype.getView = function () {
846 return this.view;
847 };
848
849 /**
850 * Get the prefix used for the filter names inside this group.
851 *
852 * @param {string} [name] Filter name to prefix
853 * @return {string} Group prefix
854 */
855 mw.rcfilters.dm.FilterGroup.prototype.getNamePrefix = function () {
856 return this.getName() + '__';
857 };
858
859 /**
860 * Get a filter name with the prefix used for the filter names inside this group.
861 *
862 * @param {string} name Filter name to prefix
863 * @return {string} Group prefix
864 */
865 mw.rcfilters.dm.FilterGroup.prototype.getPrefixedName = function ( name ) {
866 return this.getNamePrefix() + name;
867 };
868
869 /**
870 * Get group's title
871 *
872 * @return {string} Title
873 */
874 mw.rcfilters.dm.FilterGroup.prototype.getTitle = function () {
875 return this.title;
876 };
877
878 /**
879 * Get group's values separator
880 *
881 * @return {string} Values separator
882 */
883 mw.rcfilters.dm.FilterGroup.prototype.getSeparator = function () {
884 return this.separator;
885 };
886
887 /**
888 * Check whether the group is defined as full coverage
889 *
890 * @return {boolean} Group is full coverage
891 */
892 mw.rcfilters.dm.FilterGroup.prototype.isFullCoverage = function () {
893 return this.fullCoverage;
894 };
895
896 /**
897 * Check whether the group is defined as sticky default
898 *
899 * @return {boolean} Group is sticky default
900 */
901 mw.rcfilters.dm.FilterGroup.prototype.isSticky = function () {
902 return this.sticky;
903 };
904
905 /**
906 * Check whether the group value is excluded from saved queries
907 *
908 * @return {boolean} Group value is excluded from saved queries
909 */
910 mw.rcfilters.dm.FilterGroup.prototype.isExcludedFromSavedQueries = function () {
911 return this.excludedFromSavedQueries;
912 };
913
914 /**
915 * Normalize a value given to this group. This is mostly for correcting
916 * arbitrary values for 'single option' groups, given by the user settings
917 * or the URL that can go outside the limits that are allowed.
918 *
919 * @param {string} value Given value
920 * @return {string} Corrected value
921 */
922 mw.rcfilters.dm.FilterGroup.prototype.normalizeArbitraryValue = function ( value ) {
923 if (
924 this.getType() === 'single_option' &&
925 this.isAllowArbitrary()
926 ) {
927 if (
928 this.getMaxValue() !== null &&
929 value > this.getMaxValue()
930 ) {
931 // Change the value to the actual max value
932 return String( this.getMaxValue() );
933 } else if (
934 this.getMinValue() !== null &&
935 value < this.getMinValue()
936 ) {
937 // Change the value to the actual min value
938 return String( this.getMinValue() );
939 }
940 }
941
942 return value;
943 };
944 }( mediaWiki ) );