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