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