Localisation updates from https://translatewiki.net.
[mediawiki.git] / resources / src / mediawiki.rcfilters / dm / FilterGroup.js
blob7e41aeff3c2a7f4952abc7f476ac52d0cc916a44
1 const FilterItem = require( './FilterItem.js' ),
2         utils = require( '../utils.js' );
4 /**
5  * View model for a filter group.
6  *
7  * @class mw.rcfilters.dm.FilterGroup
8  * @ignore
9  * @mixes OO.EventEmitter
10  * @mixes OO.EmitterList
11  *
12  * @param {string} name Group name
13  * @param {Object} [config] Configuration options
14  * @param {string} [config.type='send_unselected_if_any'] Group type
15  * @param {string} [config.view='default'] Name of the display group this group
16  *  is a part of.
17  * @param {boolean} [config.sticky] This group is 'sticky'. It is synchronized
18  *  with a preference, does not participate in Saved Queries, and is
19  *  not shown in the active filters area.
20  * @param {string} [config.title] Group title
21  * @param {boolean} [config.hidden] This group is hidden from the regular menu views
22  *  and the active filters area.
23  * @param {boolean} [config.allowArbitrary] Allows for an arbitrary value to be added to the
24  *  group from the URL, even if it wasn't initially set up.
25  * @param {number} [config.range] An object defining minimum and maximum values for numeric
26  *  groups. { min: x, max: y }
27  * @param {number} [config.minValue] Minimum value for numeric groups
28  * @param {string} [config.separator='|'] Value separator for 'string_options' groups
29  * @param {boolean} [config.supportsAll=true] For 'string_options' groups, whether the magic 'all' value
30  *  is understood to mean all options are selected.
31  * @param {boolean} [config.active] Group is active
32  * @param {boolean} [config.fullCoverage] This filters in this group collectively cover all results
33  * @param {Object} [config.conflicts] Defines the conflicts for this filter group
34  * @param {string|Object} [config.labelPrefixKey] An i18n key defining the prefix label for this
35  *  group. If the prefix has 'invert' state, the parameter is expected to be an object
36  *  with 'default' and 'inverted' as keys.
37  * @param {Object} [config.whatsThis] Defines the messages that should appear for the 'what's this' popup
38  * @param {string} [config.whatsThis.header] The header of the whatsThis popup message
39  * @param {string} [config.whatsThis.body] The body of the whatsThis popup message
40  * @param {string} [config.whatsThis.url] The url for the link in the whatsThis popup message
41  * @param {string} [config.whatsThis.linkMessage] The text for the link in the whatsThis popup message
42  * @param {boolean} [config.visible=true] The visibility of the group
43  */
44 const FilterGroup = function MwRcfiltersDmFilterGroup( name, config ) {
45         config = config || {};
47         // Mixin constructor
48         OO.EventEmitter.call( this );
49         OO.EmitterList.call( this );
51         this.name = name;
52         this.type = config.type || 'send_unselected_if_any';
53         this.view = config.view || 'default';
54         this.sticky = !!config.sticky;
55         this.title = config.title || name;
56         this.hidden = !!config.hidden;
57         this.allowArbitrary = !!config.allowArbitrary;
58         this.numericRange = config.range;
59         this.separator = config.separator || '|';
60         this.supportsAll = config.supportsAll === undefined ? true : !!config.supportsAll;
61         this.labelPrefixKey = config.labelPrefixKey;
62         this.visible = config.visible === undefined ? true : !!config.visible;
64         this.currSelected = null;
65         this.active = !!config.active;
66         this.fullCoverage = !!config.fullCoverage;
68         this.whatsThis = config.whatsThis || {};
70         this.conflicts = config.conflicts || {};
71         this.defaultParams = {};
72         this.defaultFilters = {};
74         this.aggregate( { update: 'filterItemUpdate' } );
75         this.connect( this, { filterItemUpdate: 'onFilterItemUpdate' } );
78 /* Initialization */
79 OO.initClass( FilterGroup );
80 OO.mixinClass( FilterGroup, OO.EventEmitter );
81 OO.mixinClass( FilterGroup, OO.EmitterList );
83 /* Events */
85 /**
86  * Group state has been updated.
87  *
88  * @event update
89  * @ignore
90  */
92 /* Methods */
94 /**
95  * Initialize the group and create its filter items
96  *
97  * @param {Object} filterDefinition Filter definition for this group
98  * @param {string|Object} [groupDefault] Definition of the group default
99  */
100 FilterGroup.prototype.initializeFilters = function ( filterDefinition, groupDefault ) {
101         let defaultParam;
102         const supersetMap = {},
103                 items = [];
105         filterDefinition.forEach( ( filter ) => {
106                 // Instantiate an item
107                 const filterItem = new FilterItem( filter.name, this, {
108                         group: this.getName(),
109                         label: filter.label || filter.name,
110                         description: filter.description || '',
111                         labelPrefixKey: this.labelPrefixKey,
112                         cssClass: filter.cssClass,
113                         helpLink: filter.helpLink,
114                         identifiers: filter.identifiers,
115                         defaultHighlightColor: filter.defaultHighlightColor
116                 } );
118                 if ( filter.subset ) {
119                         filter.subset = filter.subset.map( ( el ) => el.filter );
121                         const subsetNames = [];
123                         filter.subset.forEach( ( subsetFilterName ) => {
124                                 // Subsets (unlike conflicts) are always inside the same group
125                                 // We can re-map the names of the filters we are getting from
126                                 // the subsets with the group prefix
127                                 const subsetName = this.getPrefixedName( subsetFilterName );
128                                 // For convenience, we should store each filter's "supersets" -- these are
129                                 // the filters that have that item in their subset list. This will just
130                                 // make it easier to go through whether the item has any other items
131                                 // that affect it (and are selected) at any given time
132                                 supersetMap[ subsetName ] = supersetMap[ subsetName ] || [];
133                                 utils.addArrayElementsUnique(
134                                         supersetMap[ subsetName ],
135                                         filterItem.getName()
136                                 );
138                                 // Translate subset param name to add the group name, so we
139                                 // get consistent naming. We know that subsets are only within
140                                 // the same group
141                                 subsetNames.push( subsetName );
142                         } );
144                         // Set translated subset
145                         filterItem.setSubset( subsetNames );
146                 }
148                 items.push( filterItem );
150                 // Store default parameter state; in this case, default is defined per filter
151                 if (
152                         this.getType() === 'send_unselected_if_any' ||
153                         this.getType() === 'boolean'
154                 ) {
155                         // Store the default parameter state
156                         // For this group type, parameter values are direct
157                         // We need to convert from a boolean to a string ('1' and '0')
158                         this.defaultParams[ filter.name ] = String( Number( filter.default || 0 ) );
159                 } else if ( this.getType() === 'any_value' ) {
160                         this.defaultParams[ filter.name ] = filter.default;
161                 }
162         } );
164         // Add items
165         this.addItems( items );
167         // Now that we have all items, we can apply the superset map
168         this.getItems().forEach( ( filterItem ) => {
169                 filterItem.setSuperset( supersetMap[ filterItem.getName() ] );
170         } );
172         // Store default parameter state; in this case, default is defined per the
173         // entire group, given by groupDefault method parameter
174         if ( this.getType() === 'string_options' ) {
175                 // Store the default parameter group state
176                 // For this group, the parameter is group name and value is the names
177                 // of selected items
178                 this.defaultParams[ this.getName() ] = utils.normalizeParamOptions(
179                         // Current values
180                         groupDefault ?
181                                 groupDefault.split( this.getSeparator() ) :
182                                 [],
183                         // Legal values
184                         this.getItems().map( ( item ) => item.getParamName() )
185                 ).join( this.getSeparator() );
186         } else if ( this.getType() === 'single_option' ) {
187                 defaultParam = groupDefault !== undefined ?
188                         groupDefault : this.getItems()[ 0 ].getParamName();
190                 // For this group, the parameter is the group name,
191                 // and a single item can be selected: default or first item
192                 this.defaultParams[ this.getName() ] = defaultParam;
193         }
195         // add highlights to defaultParams
196         this.getItems().forEach( ( filterItem ) => {
197                 if ( filterItem.isHighlighted() ) {
198                         this.defaultParams[ filterItem.getName() + '_color' ] = filterItem.getHighlightColor();
199                 }
200         } );
202         // Store default filter state based on default params
203         this.defaultFilters = this.getFilterRepresentation( this.getDefaultParams() );
205         // Check for filters that should be initially selected by their default value
206         if ( this.isSticky() ) {
207                 const defaultFilters = this.defaultFilters;
208                 for ( const filterName in defaultFilters ) {
209                         const filterValue = defaultFilters[ filterName ];
210                         this.getItemByName( filterName ).toggleSelected( filterValue );
211                 }
212         }
214         // Verify that single_option group has at least one item selected
215         if (
216                 this.getType() === 'single_option' &&
217                 this.findSelectedItems().length === 0
218         ) {
219                 defaultParam = groupDefault !== undefined ?
220                         groupDefault : this.getItems()[ 0 ].getParamName();
222                 // Single option means there must be a single option
223                 // selected, so we have to either select the default
224                 // or select the first option
225                 this.selectItemByParamName( defaultParam );
226         }
230  * Respond to filterItem update event
232  * @param {mw.rcfilters.dm.FilterItem} item Updated filter item
233  * @fires update
234  */
235 FilterGroup.prototype.onFilterItemUpdate = function ( item ) {
236         // Update state
237         let changed = false;
238         const active = this.areAnySelected();
240         if ( this.getType() === 'single_option' ) {
241                 // This group must have one item selected always
242                 // and must never have more than one item selected at a time
243                 if ( this.findSelectedItems().length === 0 ) {
244                         // Nothing is selected anymore
245                         // Select the default or the first item
246                         this.currSelected = this.getItemByParamName( this.defaultParams[ this.getName() ] ) ||
247                                 this.getItems()[ 0 ];
248                         this.currSelected.toggleSelected( true );
249                         changed = true;
250                 } else if ( this.findSelectedItems().length > 1 ) {
251                         // There is more than one item selected
252                         // This should only happen if the item given
253                         // is the one that is selected, so unselect
254                         // all items that is not it
255                         this.findSelectedItems().forEach( ( itemModel ) => {
256                                 // Note that in case the given item is actually
257                                 // not selected, this loop will end up unselecting
258                                 // all items, which would trigger the case above
259                                 // when the last item is unselected anyways
260                                 const selected = itemModel.getName() === item.getName() &&
261                                         item.isSelected();
263                                 itemModel.toggleSelected( selected );
264                                 if ( selected ) {
265                                         this.currSelected = itemModel;
266                                 }
267                         } );
268                         changed = true;
269                 }
270         }
272         if ( this.isSticky() ) {
273                 // If this group is sticky, then change the default according to the
274                 // current selection.
275                 this.defaultParams = this.getParamRepresentation( this.getSelectedState() );
276         }
278         if (
279                 changed ||
280                 this.active !== active ||
281                 this.currSelected !== item
282         ) {
283                 this.active = active;
284                 this.currSelected = item;
286                 this.emit( 'update' );
287         }
291  * Get group active state
293  * @return {boolean} Active state
294  */
295 FilterGroup.prototype.isActive = function () {
296         return this.active;
300  * Get group hidden state
302  * @return {boolean} Hidden state
303  */
304 FilterGroup.prototype.isHidden = function () {
305         return this.hidden;
309  * Get group allow arbitrary state
311  * @return {boolean} Group allows an arbitrary value from the URL
312  */
313 FilterGroup.prototype.isAllowArbitrary = function () {
314         return this.allowArbitrary;
318  * Get group maximum value for numeric groups
320  * @return {number|null} Group max value
321  */
322 FilterGroup.prototype.getMaxValue = function () {
323         return this.numericRange && this.numericRange.max !== undefined ?
324                 this.numericRange.max : null;
328  * Get group minimum value for numeric groups
330  * @return {number|null} Group max value
331  */
332 FilterGroup.prototype.getMinValue = function () {
333         return this.numericRange && this.numericRange.min !== undefined ?
334                 this.numericRange.min : null;
338  * Get group name
340  * @return {string} Group name
341  */
342 FilterGroup.prototype.getName = function () {
343         return this.name;
347  * Get the default param state of this group
349  * @return {Object} Default param state
350  */
351 FilterGroup.prototype.getDefaultParams = function () {
352         return this.defaultParams;
356  * Get the default filter state of this group
358  * @return {Object} Default filter state
359  */
360 FilterGroup.prototype.getDefaultFilters = function () {
361         return this.defaultFilters;
365  * Get the messags defining the 'whats this' popup for this group
367  * @return {Object} What's this messages
368  */
369 FilterGroup.prototype.getWhatsThis = function () {
370         return this.whatsThis;
374  * Check whether this group has a 'what's this' message
376  * @return {boolean} This group has a what's this message
377  */
378 FilterGroup.prototype.hasWhatsThis = function () {
379         return !!this.whatsThis.body;
383  * Get the conflicts associated with the entire group.
385  * Conflict object is set up by filter name keys and conflict
386  * definition.
388  * @example
389  * [
390  *     {
391  *         filterName: {
392  *             filter: filterName,
393  *             group: group1
394  *         }
395  *     },
396  *     {
397  *         filterName2: {
398  *             filter: filterName2,
399  *             group: group2
400  *         }
401  *     }
402  * ]
404  * @return {Object} Conflict definition
405  */
406 FilterGroup.prototype.getConflicts = function () {
407         return this.conflicts;
411  * Set conflicts for this group. See #getConflicts for the expected
412  * structure of the definition.
414  * @param {Object} conflicts Conflicts for this group
415  */
416 FilterGroup.prototype.setConflicts = function ( conflicts ) {
417         this.conflicts = conflicts;
421  * Check whether this item has a potential conflict with the given item
423  * This checks whether the given item is in the list of conflicts of
424  * the current item, but makes no judgment about whether the conflict
425  * is currently at play (either one of the items may not be selected)
427  * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item
428  * @return {boolean} This item has a conflict with the given item
429  */
430 FilterGroup.prototype.existsInConflicts = function ( filterItem ) {
431         return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() );
435  * Check whether there are any items selected
437  * @return {boolean} Any items in the group are selected
438  */
439 FilterGroup.prototype.areAnySelected = function () {
440         return this.getItems().some( ( filterItem ) => filterItem.isSelected() );
444  * Check whether all items selected
446  * @return {boolean} All items are selected
447  */
448 FilterGroup.prototype.areAllSelected = function () {
449         const selected = [],
450                 unselected = [];
452         this.getItems().forEach( ( filterItem ) => {
453                 if ( filterItem.isSelected() ) {
454                         selected.push( filterItem );
455                 } else {
456                         unselected.push( filterItem );
457                 }
458         } );
460         if ( unselected.length === 0 ) {
461                 return true;
462         }
464         // check if every unselected is a subset of a selected
465         return unselected.every( ( unselectedFilterItem ) => selected.some( ( selectedFilterItem ) => selectedFilterItem.existsInSubset( unselectedFilterItem.getName() ) ) );
469  * Get all selected items in this group
471  * @ignore
472  * @param {mw.rcfilters.dm.FilterItem} [excludeItem] Item to exclude from the list
473  * @return {mw.rcfilters.dm.FilterItem[]} Selected items
474  */
475 FilterGroup.prototype.findSelectedItems = function ( excludeItem ) {
476         const excludeName = ( excludeItem && excludeItem.getName() ) || '';
478         return this.getItems().filter( ( item ) => item.getName() !== excludeName && item.isSelected() );
482  * Check whether all selected items are in conflict with the given item
484  * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
485  * @return {boolean} All selected items are in conflict with this item
486  */
487 FilterGroup.prototype.areAllSelectedInConflictWith = function ( filterItem ) {
488         const selectedItems = this.findSelectedItems( filterItem );
490         return selectedItems.length > 0 &&
491                 (
492                         // The group as a whole is in conflict with this item
493                         this.existsInConflicts( filterItem ) ||
494                         // All selected items are in conflict individually
495                         selectedItems.every( ( selectedFilter ) => selectedFilter.existsInConflicts( filterItem ) )
496                 );
500  * Check whether any of the selected items are in conflict with the given item
502  * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
503  * @return {boolean} Any of the selected items are in conflict with this item
504  */
505 FilterGroup.prototype.areAnySelectedInConflictWith = function ( filterItem ) {
506         const selectedItems = this.findSelectedItems( filterItem );
508         return selectedItems.length > 0 && (
509                 // The group as a whole is in conflict with this item
510                 this.existsInConflicts( filterItem ) ||
511                 // Any selected items are in conflict individually
512                 selectedItems.some( ( selectedFilter ) => selectedFilter.existsInConflicts( filterItem ) )
513         );
517  * Get the parameter representation from this group
519  * @param {Object} [filterRepresentation] An object defining the state
520  *  of the filters in this group, keyed by their name and current selected
521  *  state value.
522  * @return {Object} Parameter representation
523  */
524 FilterGroup.prototype.getParamRepresentation = function ( filterRepresentation ) {
525         let areAnySelected = false;
526         const buildFromCurrentState = !filterRepresentation,
527                 defaultFilters = this.getDefaultFilters(),
528                 result = {},
529                 filterParamNames = {},
530                 getSelectedParameter = ( filters ) => {
531                         const selected = [];
533                         // Find if any are selected
534                         // eslint-disable-next-line no-jquery/no-each-util
535                         $.each( filters, ( name, value ) => {
536                                 if ( value ) {
537                                         selected.push( name );
538                                 }
539                         } );
541                         const item = this.getItemByName( selected[ 0 ] );
542                         return ( item && item.getParamName() ) || '';
543                 };
545         filterRepresentation = filterRepresentation || {};
547         // Create or complete the filterRepresentation definition
548         this.getItems().forEach( ( item ) => {
549                 // Map filter names to their parameter names
550                 filterParamNames[ item.getName() ] = item.getParamName();
552                 if ( buildFromCurrentState ) {
553                         // This means we have not been given a filter representation
554                         // so we are building one based on current state
555                         filterRepresentation[ item.getName() ] = item.getValue();
556                 } else if ( filterRepresentation[ item.getName() ] === undefined ) {
557                         // We are given a filter representation, but we have to make
558                         // sure that we fill in the missing filters if there are any
559                         // we will assume they are all falsey
560                         if ( this.isSticky() ) {
561                                 filterRepresentation[ item.getName() ] = !!defaultFilters[ item.getName() ];
562                         } else {
563                                 filterRepresentation[ item.getName() ] = false;
564                         }
565                 }
567                 if ( filterRepresentation[ item.getName() ] ) {
568                         areAnySelected = true;
569                 }
570         } );
572         // Build result
573         if (
574                 this.getType() === 'send_unselected_if_any' ||
575                 this.getType() === 'boolean' ||
576                 this.getType() === 'any_value'
577         ) {
578                 // First, check if any of the items are selected at all.
579                 // If none is selected, we're treating it as if they are
580                 // all false
582                 // Go over the items and define the correct values
583                 // eslint-disable-next-line no-jquery/no-each-util
584                 $.each( filterRepresentation, ( name, value ) => {
585                         // We must store all parameter values as strings '0' or '1'
586                         if ( this.getType() === 'send_unselected_if_any' ) {
587                                 result[ filterParamNames[ name ] ] = areAnySelected ?
588                                         String( Number( !value ) ) :
589                                         '0';
590                         } else if ( this.getType() === 'boolean' ) {
591                                 // Representation is straight-forward and direct from
592                                 // the parameter value to the filter state
593                                 result[ filterParamNames[ name ] ] = String( Number( !!value ) );
594                         } else if ( this.getType() === 'any_value' ) {
595                                 result[ filterParamNames[ name ] ] = value;
596                         }
597                 } );
598         } else if ( this.getType() === 'string_options' ) {
599                 const values = [];
601                 // eslint-disable-next-line no-jquery/no-each-util
602                 $.each( filterRepresentation, ( name, value ) => {
603                         // Collect values
604                         if ( value ) {
605                                 values.push( filterParamNames[ name ] );
606                         }
607                 } );
609                 result[ this.getName() ] = this.getSupportsAll() &&
610                                 values.length === Object.keys( filterRepresentation ).length ?
611                         'all' : values.join( this.getSeparator() );
612         } else if ( this.getType() === 'single_option' ) {
613                 result[ this.getName() ] = getSelectedParameter( filterRepresentation );
614         }
616         return result;
620  * Get the filter representation this group would provide
621  * based on given parameter states.
623  * @param {Object} [paramRepresentation] An object defining a parameter
624  *  state to translate the filter state from. If not given, an object
625  *  representing all filters as falsey is returned; same as if the parameter
626  *  given were an empty object, or had some of the filters missing.
627  * @return {Object} Filter representation
628  */
629 FilterGroup.prototype.getFilterRepresentation = function ( paramRepresentation ) {
630         let areAnySelected,
631                 oneWasSelected = false;
632         const defaultParams = this.getDefaultParams(),
633                 expandedParams = $.extend( true, {}, paramRepresentation ),
634                 paramToFilterMap = {},
635                 result = {};
637         if ( this.isSticky() ) {
638                 // If the group is sticky, check if all parameters are represented
639                 // and for those that aren't represented, add them with their default
640                 // values
641                 paramRepresentation = $.extend( true, {}, this.getDefaultParams(), paramRepresentation );
642         }
644         paramRepresentation = paramRepresentation || {};
645         if (
646                 this.getType() === 'send_unselected_if_any' ||
647                 this.getType() === 'boolean' ||
648                 this.getType() === 'any_value'
649         ) {
650                 // Go over param representation; map and check for selections
651                 this.getItems().forEach( ( filterItem ) => {
652                         const paramName = filterItem.getParamName();
654                         expandedParams[ paramName ] = paramRepresentation[ paramName ] || '0';
655                         paramToFilterMap[ paramName ] = filterItem;
657                         if ( Number( paramRepresentation[ filterItem.getParamName() ] ) ) {
658                                 areAnySelected = true;
659                         }
660                 } );
662                 // eslint-disable-next-line no-jquery/no-each-util
663                 $.each( expandedParams, ( paramName, paramValue ) => {
664                         const filterItem = paramToFilterMap[ paramName ];
666                         if ( this.getType() === 'send_unselected_if_any' ) {
667                                 // Flip the definition between the parameter
668                                 // state and the filter state
669                                 // This is what the 'toggleSelected' value of the filter is
670                                 result[ filterItem.getName() ] = areAnySelected ?
671                                         !Number( paramValue ) :
672                                         // Otherwise, there are no selected items in the
673                                         // group, which means the state is false
674                                         false;
675                         } else if ( this.getType() === 'boolean' ) {
676                                 // Straight-forward definition of state
677                                 result[ filterItem.getName() ] = !!Number( paramRepresentation[ filterItem.getParamName() ] );
678                         } else if ( this.getType() === 'any_value' ) {
679                                 result[ filterItem.getName() ] = paramRepresentation[ filterItem.getParamName() ];
680                         }
681                 } );
682         } else if ( this.getType() === 'string_options' ) {
683                 const currentValue = paramRepresentation[ this.getName() ] || '';
685                 // Normalize the given parameter values
686                 const paramValues = utils.normalizeParamOptions(
687                         // Given
688                         currentValue.split(
689                                 this.getSeparator()
690                         ),
691                         // Allowed values
692                         this.getItems().map( ( filterItem ) => filterItem.getParamName() ),
693                         this.getSupportsAll()
694                 );
695                 // Translate the parameter values into a filter selection state
696                 this.getItems().forEach( ( filterItem ) => {
697                         // If the parameter is set to 'all', set all filters to true
698                         result[ filterItem.getName() ] = (
699                                 this.getSupportsAll() && paramValues.length === 1 && paramValues[ 0 ] === 'all'
700                         ) ?
701                                 true :
702                                 // Otherwise, the filter is selected only if it appears in the parameter values
703                                 paramValues.indexOf( filterItem.getParamName() ) > -1;
704                 } );
705         } else if ( this.getType() === 'single_option' ) {
706                 // There is parameter that fits a single filter and if not, get the default
707                 this.getItems().forEach( ( filterItem ) => {
708                         const selected = filterItem.getParamName() === paramRepresentation[ this.getName() ];
710                         result[ filterItem.getName() ] = selected;
711                         oneWasSelected = oneWasSelected || selected;
712                 } );
713         }
715         // Go over result and make sure all filters are represented.
716         // If any filters are missing, they will get a falsey value
717         this.getItems().forEach( ( filterItem ) => {
718                 if ( result[ filterItem.getName() ] === undefined ) {
719                         result[ filterItem.getName() ] = this.getFalsyValue();
720                 }
721         } );
723         // Make sure that at least one option is selected in
724         // single_option groups, no matter what path was taken
725         // If none was selected by the given definition, then
726         // we need to select the one in the base state -- either
727         // the default given, or the first item
728         if (
729                 this.getType() === 'single_option' &&
730                 !oneWasSelected
731         ) {
732                 let item = this.getItems()[ 0 ];
733                 if ( defaultParams[ this.getName() ] ) {
734                         item = this.getItemByParamName( defaultParams[ this.getName() ] );
735                 }
737                 result[ item.getName() ] = true;
738         }
740         return result;
744  * @return {any} The appropriate falsy value for this group type
745  */
746 FilterGroup.prototype.getFalsyValue = function () {
747         return this.getType() === 'any_value' ? '' : false;
751  * Get current selected state of all filter items in this group
753  * @return {Object} Selected state
754  */
755 FilterGroup.prototype.getSelectedState = function () {
756         const state = {};
758         this.getItems().forEach( ( filterItem ) => {
759                 state[ filterItem.getName() ] = filterItem.getValue();
760         } );
762         return state;
766  * Get item by its filter name
768  * @ignore
769  * @param {string} filterName Filter name
770  * @return {mw.rcfilters.dm.FilterItem} Filter item
771  */
772 FilterGroup.prototype.getItemByName = function ( filterName ) {
773         return this.getItems().filter( ( item ) => item.getName() === filterName )[ 0 ];
777  * Select an item by its parameter name
779  * @param {string} paramName Filter parameter name
780  */
781 FilterGroup.prototype.selectItemByParamName = function ( paramName ) {
782         this.getItems().forEach( ( item ) => {
783                 item.toggleSelected( item.getParamName() === String( paramName ) );
784         } );
788  * Get item by its parameter name
790  * @ignore
791  * @param {string} paramName Parameter name
792  * @return {mw.rcfilters.dm.FilterItem} Filter item
793  */
794 FilterGroup.prototype.getItemByParamName = function ( paramName ) {
795         return this.getItems().filter( ( item ) => item.getParamName() === String( paramName ) )[ 0 ];
799  * Get group type
801  * @return {string} Group type
802  */
803 FilterGroup.prototype.getType = function () {
804         return this.type;
808  * Check whether this group is represented by a single parameter
809  * or whether each item is its own parameter
811  * @return {boolean} This group is a single parameter
812  */
813 FilterGroup.prototype.isPerGroupRequestParameter = function () {
814         return (
815                 this.getType() === 'string_options' ||
816                 this.getType() === 'single_option'
817         );
821  * Get display group
823  * @return {string} Display group
824  */
825 FilterGroup.prototype.getView = function () {
826         return this.view;
830  * Get the prefix used for the filter names inside this group.
832  * @return {string} Group prefix
833  */
834 FilterGroup.prototype.getNamePrefix = function () {
835         return this.getName() + '__';
839  * Get a filter name with the prefix used for the filter names inside this group.
841  * @param {string} name Filter name to prefix
842  * @return {string} Group prefix
843  */
844 FilterGroup.prototype.getPrefixedName = function ( name ) {
845         return this.getNamePrefix() + name;
849  * Get group's title
851  * @return {string} Title
852  */
853 FilterGroup.prototype.getTitle = function () {
854         return this.title;
858  * Get group's values separator
860  * @return {string} Values separator
861  */
862 FilterGroup.prototype.getSeparator = function () {
863         return this.separator;
867  * Check whether the group supports the magic 'all' value to indicate that all values are selected.
869  * @return {boolean} Group supports the magic 'all' value
870  */
871 FilterGroup.prototype.getSupportsAll = function () {
872         return this.supportsAll;
876  * Check whether the group is defined as full coverage
878  * @return {boolean} Group is full coverage
879  */
880 FilterGroup.prototype.isFullCoverage = function () {
881         return this.fullCoverage;
885  * Check whether the group is defined as sticky default
887  * @return {boolean} Group is sticky default
888  */
889 FilterGroup.prototype.isSticky = function () {
890         return this.sticky;
894  * Normalize a value given to this group. This is mostly for correcting
895  * arbitrary values for 'single option' groups, given by the user settings
896  * or the URL that can go outside the limits that are allowed.
898  * @param  {string} value Given value
899  * @return {string} Corrected value
900  */
901 FilterGroup.prototype.normalizeArbitraryValue = function ( value ) {
902         if (
903                 this.getType() === 'single_option' &&
904                 this.isAllowArbitrary()
905         ) {
906                 if (
907                         this.getMaxValue() !== null &&
908                         value > this.getMaxValue()
909                 ) {
910                         // Change the value to the actual max value
911                         return String( this.getMaxValue() );
912                 } else if (
913                         this.getMinValue() !== null &&
914                         value < this.getMinValue()
915                 ) {
916                         // Change the value to the actual min value
917                         return String( this.getMinValue() );
918                 }
919         }
921         return value;
925  * Toggle the visibility of this group
927  * @param {boolean} [isVisible] Item is visible
928  */
929 FilterGroup.prototype.toggleVisible = function ( isVisible ) {
930         isVisible = isVisible === undefined ? !this.visible : isVisible;
932         if ( this.visible !== isVisible ) {
933                 this.visible = isVisible;
934                 this.emit( 'update' );
935         }
939  * Check whether the group is visible
941  * @return {boolean} Group is visible
942  */
943 FilterGroup.prototype.isVisible = function () {
944         return this.visible;
948  * Set the visibility of the items under this group by the given items array
950  * @param {mw.rcfilters.dm.ItemModel[]} visibleItems An array of visible items
951  */
952 FilterGroup.prototype.setVisibleItems = function ( visibleItems ) {
953         this.getItems().forEach( ( itemModel ) => {
954                 itemModel.toggleVisible( visibleItems.indexOf( itemModel ) !== -1 );
955         } );
958 module.exports = FilterGroup;