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