1 const FilterItem = require( './FilterItem.js' ),
2 utils = require( '../utils.js' );
5 * View model for a filter group.
7 * @class mw.rcfilters.dm.FilterGroup
9 * @mixes OO.EventEmitter
10 * @mixes OO.EmitterList
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
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
44 const FilterGroup = function MwRcfiltersDmFilterGroup( name, config ) {
45 config = config || {};
48 OO.EventEmitter.call( this );
49 OO.EmitterList.call( this );
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' } );
79 OO.initClass( FilterGroup );
80 OO.mixinClass( FilterGroup, OO.EventEmitter );
81 OO.mixinClass( FilterGroup, OO.EmitterList );
86 * Group state has been updated.
95 * Initialize the group and create its filter items
97 * @param {Object} filterDefinition Filter definition for this group
98 * @param {string|Object} [groupDefault] Definition of the group default
100 FilterGroup.prototype.initializeFilters = function ( filterDefinition, groupDefault ) {
102 const supersetMap = {},
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
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 ],
138 // Translate subset param name to add the group name, so we
139 // get consistent naming. We know that subsets are only within
141 subsetNames.push( subsetName );
144 // Set translated subset
145 filterItem.setSubset( subsetNames );
148 items.push( filterItem );
150 // Store default parameter state; in this case, default is defined per filter
152 this.getType() === 'send_unselected_if_any' ||
153 this.getType() === 'boolean'
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;
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() ] );
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
178 this.defaultParams[ this.getName() ] = utils.normalizeParamOptions(
181 groupDefault.split( this.getSeparator() ) :
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;
195 // add highlights to defaultParams
196 this.getItems().forEach( ( filterItem ) => {
197 if ( filterItem.isHighlighted() ) {
198 this.defaultParams[ filterItem.getName() + '_color' ] = filterItem.getHighlightColor();
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 );
214 // Verify that single_option group has at least one item selected
216 this.getType() === 'single_option' &&
217 this.findSelectedItems().length === 0
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 );
230 * Respond to filterItem update event
232 * @param {mw.rcfilters.dm.FilterItem} item Updated filter item
235 FilterGroup.prototype.onFilterItemUpdate = function ( item ) {
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 );
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() &&
263 itemModel.toggleSelected( selected );
265 this.currSelected = itemModel;
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() );
280 this.active !== active ||
281 this.currSelected !== item
283 this.active = active;
284 this.currSelected = item;
286 this.emit( 'update' );
291 * Get group active state
293 * @return {boolean} Active state
295 FilterGroup.prototype.isActive = function () {
300 * Get group hidden state
302 * @return {boolean} Hidden state
304 FilterGroup.prototype.isHidden = function () {
309 * Get group allow arbitrary state
311 * @return {boolean} Group allows an arbitrary value from the URL
313 FilterGroup.prototype.isAllowArbitrary = function () {
314 return this.allowArbitrary;
318 * Get group maximum value for numeric groups
320 * @return {number|null} Group max value
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
332 FilterGroup.prototype.getMinValue = function () {
333 return this.numericRange && this.numericRange.min !== undefined ?
334 this.numericRange.min : null;
340 * @return {string} Group name
342 FilterGroup.prototype.getName = function () {
347 * Get the default param state of this group
349 * @return {Object} Default param state
351 FilterGroup.prototype.getDefaultParams = function () {
352 return this.defaultParams;
356 * Get the default filter state of this group
358 * @return {Object} Default filter state
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
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
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
392 * filter: filterName,
398 * filter: filterName2,
404 * @return {Object} Conflict definition
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
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
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
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
448 FilterGroup.prototype.areAllSelected = function () {
452 this.getItems().forEach( ( filterItem ) => {
453 if ( filterItem.isSelected() ) {
454 selected.push( filterItem );
456 unselected.push( filterItem );
460 if ( unselected.length === 0 ) {
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
472 * @param {mw.rcfilters.dm.FilterItem} [excludeItem] Item to exclude from the list
473 * @return {mw.rcfilters.dm.FilterItem[]} Selected items
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
487 FilterGroup.prototype.areAllSelectedInConflictWith = function ( filterItem ) {
488 const selectedItems = this.findSelectedItems( filterItem );
490 return selectedItems.length > 0 &&
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 ) )
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
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 ) )
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
522 * @return {Object} Parameter representation
524 FilterGroup.prototype.getParamRepresentation = function ( filterRepresentation ) {
525 let areAnySelected = false;
526 const buildFromCurrentState = !filterRepresentation,
527 defaultFilters = this.getDefaultFilters(),
529 filterParamNames = {},
530 getSelectedParameter = ( filters ) => {
533 // Find if any are selected
534 // eslint-disable-next-line no-jquery/no-each-util
535 $.each( filters, ( name, value ) => {
537 selected.push( name );
541 const item = this.getItemByName( selected[ 0 ] );
542 return ( item && item.getParamName() ) || '';
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() ];
563 filterRepresentation[ item.getName() ] = false;
567 if ( filterRepresentation[ item.getName() ] ) {
568 areAnySelected = true;
574 this.getType() === 'send_unselected_if_any' ||
575 this.getType() === 'boolean' ||
576 this.getType() === 'any_value'
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
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 ) ) :
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;
598 } else if ( this.getType() === 'string_options' ) {
601 // eslint-disable-next-line no-jquery/no-each-util
602 $.each( filterRepresentation, ( name, value ) => {
605 values.push( filterParamNames[ name ] );
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 );
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
629 FilterGroup.prototype.getFilterRepresentation = function ( paramRepresentation ) {
631 oneWasSelected = false;
632 const defaultParams = this.getDefaultParams(),
633 expandedParams = $.extend( true, {}, paramRepresentation ),
634 paramToFilterMap = {},
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
641 paramRepresentation = $.extend( true, {}, this.getDefaultParams(), paramRepresentation );
644 paramRepresentation = paramRepresentation || {};
646 this.getType() === 'send_unselected_if_any' ||
647 this.getType() === 'boolean' ||
648 this.getType() === 'any_value'
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;
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
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() ];
682 } else if ( this.getType() === 'string_options' ) {
683 const currentValue = paramRepresentation[ this.getName() ] || '';
685 // Normalize the given parameter values
686 const paramValues = utils.normalizeParamOptions(
692 this.getItems().map( ( filterItem ) => filterItem.getParamName() ),
693 this.getSupportsAll()
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'
702 // Otherwise, the filter is selected only if it appears in the parameter values
703 paramValues.indexOf( filterItem.getParamName() ) > -1;
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;
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();
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
729 this.getType() === 'single_option' &&
732 let item = this.getItems()[ 0 ];
733 if ( defaultParams[ this.getName() ] ) {
734 item = this.getItemByParamName( defaultParams[ this.getName() ] );
737 result[ item.getName() ] = true;
744 * @return {any} The appropriate falsy value for this group type
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
755 FilterGroup.prototype.getSelectedState = function () {
758 this.getItems().forEach( ( filterItem ) => {
759 state[ filterItem.getName() ] = filterItem.getValue();
766 * Get item by its filter name
769 * @param {string} filterName Filter name
770 * @return {mw.rcfilters.dm.FilterItem} Filter item
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
781 FilterGroup.prototype.selectItemByParamName = function ( paramName ) {
782 this.getItems().forEach( ( item ) => {
783 item.toggleSelected( item.getParamName() === String( paramName ) );
788 * Get item by its parameter name
791 * @param {string} paramName Parameter name
792 * @return {mw.rcfilters.dm.FilterItem} Filter item
794 FilterGroup.prototype.getItemByParamName = function ( paramName ) {
795 return this.getItems().filter( ( item ) => item.getParamName() === String( paramName ) )[ 0 ];
801 * @return {string} Group type
803 FilterGroup.prototype.getType = function () {
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
813 FilterGroup.prototype.isPerGroupRequestParameter = function () {
815 this.getType() === 'string_options' ||
816 this.getType() === 'single_option'
823 * @return {string} Display group
825 FilterGroup.prototype.getView = function () {
830 * Get the prefix used for the filter names inside this group.
832 * @return {string} Group prefix
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
844 FilterGroup.prototype.getPrefixedName = function ( name ) {
845 return this.getNamePrefix() + name;
851 * @return {string} Title
853 FilterGroup.prototype.getTitle = function () {
858 * Get group's values separator
860 * @return {string} Values separator
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
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
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
889 FilterGroup.prototype.isSticky = function () {
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
901 FilterGroup.prototype.normalizeArbitraryValue = function ( value ) {
903 this.getType() === 'single_option' &&
904 this.isAllowArbitrary()
907 this.getMaxValue() !== null &&
908 value > this.getMaxValue()
910 // Change the value to the actual max value
911 return String( this.getMaxValue() );
913 this.getMinValue() !== null &&
914 value < this.getMinValue()
916 // Change the value to the actual min value
917 return String( this.getMinValue() );
925 * Toggle the visibility of this group
927 * @param {boolean} [isVisible] Item is visible
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' );
939 * Check whether the group is visible
941 * @return {boolean} Group is visible
943 FilterGroup.prototype.isVisible = function () {
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
952 FilterGroup.prototype.setVisibleItems = function ( visibleItems ) {
953 this.getItems().forEach( ( itemModel ) => {
954 itemModel.toggleVisible( visibleItems.indexOf( itemModel ) !== -1 );
958 module.exports = FilterGroup;