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