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