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