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