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