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