3 * View model for a filter group
5 * @mixins OO.EventEmitter
6 * @mixins OO.EmitterList
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
14 * @cfg {string} [title] Group title
15 * @cfg {boolean} [hidden] This group is hidden from the regular menu views
16 * @cfg {string} [separator='|'] Value separator for 'string_options' groups
17 * @cfg {boolean} [active] Group is active
18 * @cfg {boolean} [fullCoverage] This filters in this group collectively cover all results
19 * @cfg {Object} [conflicts] Defines the conflicts for this filter group
20 * @cfg {string|Object} [labelPrefixKey] An i18n key defining the prefix label for this
21 * group. If the prefix has 'invert' state, the parameter is expected to be an object
22 * with 'default' and 'inverted' as keys.
23 * @cfg {Object} [whatsThis] Defines the messages that should appear for the 'what's this' popup
24 * @cfg {string} [whatsThis.header] The header of the whatsThis popup message
25 * @cfg {string} [whatsThis.body] The body of the whatsThis popup message
26 * @cfg {string} [whatsThis.url] The url for the link in the whatsThis popup message
27 * @cfg {string} [whatsThis.linkMessage] The text for the link in the whatsThis popup message
29 mw
.rcfilters
.dm
.FilterGroup
= function MwRcfiltersDmFilterGroup( name
, config
) {
30 config
= config
|| {};
33 OO
.EventEmitter
.call( this );
34 OO
.EmitterList
.call( this );
37 this.type
= config
.type
|| 'send_unselected_if_any';
38 this.view
= config
.view
|| 'default';
39 this.title
= config
.title
|| name
;
40 this.hidden
= !!config
.hidden
;
41 this.separator
= config
.separator
|| '|';
42 this.labelPrefixKey
= config
.labelPrefixKey
;
44 this.active
= !!config
.active
;
45 this.fullCoverage
= !!config
.fullCoverage
;
47 this.whatsThis
= config
.whatsThis
|| {};
49 this.conflicts
= config
.conflicts
|| {};
50 this.defaultParams
= {};
52 this.aggregate( { update
: 'filterItemUpdate' } );
53 this.connect( this, { filterItemUpdate
: 'onFilterItemUpdate' } );
57 OO
.initClass( mw
.rcfilters
.dm
.FilterGroup
);
58 OO
.mixinClass( mw
.rcfilters
.dm
.FilterGroup
, OO
.EventEmitter
);
59 OO
.mixinClass( mw
.rcfilters
.dm
.FilterGroup
, OO
.EmitterList
);
66 * Group state has been updated
72 * Initialize the group and create its filter items
74 * @param {Object} filterDefinition Filter definition for this group
75 * @param {string|Object} [groupDefault] Definition of the group default
77 mw
.rcfilters
.dm
.FilterGroup
.prototype.initializeFilters = function ( filterDefinition
, groupDefault
) {
82 filterDefinition
.forEach( function ( filter
) {
83 // Instantiate an item
85 filterItem
= new mw
.rcfilters
.dm
.FilterItem( filter
.name
, model
, {
86 group
: model
.getName(),
87 label
: filter
.label
|| filter
.name
,
88 description
: filter
.description
|| '',
89 labelPrefixKey
: model
.labelPrefixKey
,
90 cssClass
: filter
.cssClass
,
91 identifiers
: filter
.identifiers
94 if ( filter
.subset
) {
95 filter
.subset
= filter
.subset
.map( function ( el
) {
101 filter
.subset
.forEach( function ( subsetFilterName
) { // eslint-disable-line no-loop-func
102 // Subsets (unlike conflicts) are always inside the same group
103 // We can re-map the names of the filters we are getting from
104 // the subsets with the group prefix
105 var subsetName
= model
.getPrefixedName( subsetFilterName
);
106 // For convenience, we should store each filter's "supersets" -- these are
107 // the filters that have that item in their subset list. This will just
108 // make it easier to go through whether the item has any other items
109 // that affect it (and are selected) at any given time
110 supersetMap
[ subsetName
] = supersetMap
[ subsetName
] || [];
111 mw
.rcfilters
.utils
.addArrayElementsUnique(
112 supersetMap
[ subsetName
],
116 // Translate subset param name to add the group name, so we
117 // get consistent naming. We know that subsets are only within
119 subsetNames
.push( subsetName
);
122 // Set translated subset
123 filterItem
.setSubset( subsetNames
);
126 items
.push( filterItem
);
128 // Store default parameter state; in this case, default is defined per filter
129 if ( model
.getType() === 'send_unselected_if_any' ) {
130 // Store the default parameter state
131 // For this group type, parameter values are direct
132 // We need to convert from a boolean to a string ('1' and '0')
133 model
.defaultParams
[ filter
.name
] = String( Number( !!filter
.default ) );
138 this.addItems( items
);
140 // Now that we have all items, we can apply the superset map
141 this.getItems().forEach( function ( filterItem
) {
142 filterItem
.setSuperset( supersetMap
[ filterItem
.getName() ] );
145 // Store default parameter state; in this case, default is defined per the
146 // entire group, given by groupDefault method parameter
147 if ( this.getType() === 'string_options' ) {
148 // Store the default parameter group state
149 // For this group, the parameter is group name and value is the names
151 this.defaultParams
[ this.getName() ] = mw
.rcfilters
.utils
.normalizeParamOptions(
154 groupDefault
.split( this.getSeparator() ) :
157 this.getItems().map( function ( item
) {
158 return item
.getParamName();
160 ).join( this.getSeparator() );
161 } else if ( this.getType() === 'single_option' ) {
162 // For this group, the parameter is the group name,
163 // and a single item can be selected, or none at all
164 // The item also must be recognized or none is set as
166 model
.defaultParams
[ this.getName() ] = this.getItemByParamName( groupDefault
) ? groupDefault
: '';
171 * Respond to filterItem update event
173 * @param {mw.rcfilters.dm.FilterItem} item Updated filter item
176 mw
.rcfilters
.dm
.FilterGroup
.prototype.onFilterItemUpdate = function ( item
) {
178 var active
= this.areAnySelected(),
179 itemName
= item
&& item
.getName();
181 if ( item
.isSelected() && this.getType() === 'single_option' ) {
182 // Change the selection to only be the newly selected item
183 this.getItems().forEach( function ( filterItem
) {
184 if ( filterItem
.getName() !== itemName
) {
185 filterItem
.toggleSelected( false );
190 if ( this.active
!== active
) {
191 this.active
= active
;
192 this.emit( 'update' );
197 * Get group active state
199 * @return {boolean} Active state
201 mw
.rcfilters
.dm
.FilterGroup
.prototype.isActive = function () {
206 * Get group hidden state
208 * @return {boolean} Hidden state
210 mw
.rcfilters
.dm
.FilterGroup
.prototype.isHidden = function () {
217 * @return {string} Group name
219 mw
.rcfilters
.dm
.FilterGroup
.prototype.getName = function () {
224 * Get the default param state of this group
226 * @return {Object} Default param state
228 mw
.rcfilters
.dm
.FilterGroup
.prototype.getDefaultParams = function () {
229 return this.defaultParams
;
233 * Get the messags defining the 'whats this' popup for this group
235 * @return {Object} What's this messages
237 mw
.rcfilters
.dm
.FilterGroup
.prototype.getWhatsThis = function () {
238 return this.whatsThis
;
242 * Check whether this group has a 'what's this' message
244 * @return {boolean} This group has a what's this message
246 mw
.rcfilters
.dm
.FilterGroup
.prototype.hasWhatsThis = function () {
247 return !!this.whatsThis
.body
;
251 * Get the conflicts associated with the entire group.
252 * Conflict object is set up by filter name keys and conflict
253 * definition. For example:
257 * filter: filterName,
263 * filter: filterName2,
268 * @return {Object} Conflict definition
270 mw
.rcfilters
.dm
.FilterGroup
.prototype.getConflicts = function () {
271 return this.conflicts
;
275 * Set conflicts for this group. See #getConflicts for the expected
276 * structure of the definition.
278 * @param {Object} conflicts Conflicts for this group
280 mw
.rcfilters
.dm
.FilterGroup
.prototype.setConflicts = function ( conflicts
) {
281 this.conflicts
= conflicts
;
285 * Set conflicts for each filter item in the group based on the
288 * @param {Object} conflicts Object representing the conflict map,
289 * keyed by the item name, where its value is an object for all its conflicts
291 mw
.rcfilters
.dm
.FilterGroup
.prototype.setFilterConflicts = function ( conflicts
) {
292 this.getItems().forEach( function ( filterItem
) {
293 if ( conflicts
[ filterItem
.getName() ] ) {
294 filterItem
.setConflicts( conflicts
[ filterItem
.getName() ] );
300 * Check whether this item has a potential conflict with the given item
302 * This checks whether the given item is in the list of conflicts of
303 * the current item, but makes no judgment about whether the conflict
304 * is currently at play (either one of the items may not be selected)
306 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item
307 * @return {boolean} This item has a conflict with the given item
309 mw
.rcfilters
.dm
.FilterGroup
.prototype.existsInConflicts = function ( filterItem
) {
310 return Object
.prototype.hasOwnProperty
.call( this.getConflicts(), filterItem
.getName() );
314 * Check whether there are any items selected
316 * @return {boolean} Any items in the group are selected
318 mw
.rcfilters
.dm
.FilterGroup
.prototype.areAnySelected = function () {
319 return this.getItems().some( function ( filterItem
) {
320 return filterItem
.isSelected();
325 * Check whether all items selected
327 * @return {boolean} All items are selected
329 mw
.rcfilters
.dm
.FilterGroup
.prototype.areAllSelected = function () {
333 this.getItems().forEach( function ( filterItem
) {
334 if ( filterItem
.isSelected() ) {
335 selected
.push( filterItem
);
337 unselected
.push( filterItem
);
341 if ( unselected
.length
=== 0 ) {
345 // check if every unselected is a subset of a selected
346 return unselected
.every( function ( unselectedFilterItem
) {
347 return selected
.some( function ( selectedFilterItem
) {
348 return selectedFilterItem
.existsInSubset( unselectedFilterItem
.getName() );
354 * Get all selected items in this group
356 * @param {mw.rcfilters.dm.FilterItem} [excludeItem] Item to exclude from the list
357 * @return {mw.rcfilters.dm.FilterItem[]} Selected items
359 mw
.rcfilters
.dm
.FilterGroup
.prototype.getSelectedItems = function ( excludeItem
) {
360 var excludeName
= ( excludeItem
&& excludeItem
.getName() ) || '';
362 return this.getItems().filter( function ( item
) {
363 return item
.getName() !== excludeName
&& item
.isSelected();
368 * Check whether all selected items are in conflict with the given item
370 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
371 * @return {boolean} All selected items are in conflict with this item
373 mw
.rcfilters
.dm
.FilterGroup
.prototype.areAllSelectedInConflictWith = function ( filterItem
) {
374 var selectedItems
= this.getSelectedItems( filterItem
);
376 return selectedItems
.length
> 0 &&
378 // The group as a whole is in conflict with this item
379 this.existsInConflicts( filterItem
) ||
380 // All selected items are in conflict individually
381 selectedItems
.every( function ( selectedFilter
) {
382 return selectedFilter
.existsInConflicts( filterItem
);
388 * Check whether any of the selected items are in conflict with the given item
390 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
391 * @return {boolean} Any of the selected items are in conflict with this item
393 mw
.rcfilters
.dm
.FilterGroup
.prototype.areAnySelectedInConflictWith = function ( filterItem
) {
394 var selectedItems
= this.getSelectedItems( filterItem
);
396 return selectedItems
.length
> 0 && (
397 // The group as a whole is in conflict with this item
398 this.existsInConflicts( filterItem
) ||
399 // Any selected items are in conflict individually
400 selectedItems
.some( function ( selectedFilter
) {
401 return selectedFilter
.existsInConflicts( filterItem
);
407 * Get the parameter representation from this group
409 * @param {Object} [filterRepresentation] An object defining the state
410 * of the filters in this group, keyed by their name and current selected
412 * @return {Object} Parameter representation
414 mw
.rcfilters
.dm
.FilterGroup
.prototype.getParamRepresentation = function ( filterRepresentation
) {
416 areAnySelected
= false,
417 buildFromCurrentState
= !filterRepresentation
,
420 filterParamNames
= {},
421 getSelectedParameter = function ( filters
) {
425 // Find if any are selected
426 $.each( filters
, function ( name
, value
) {
428 selected
.push( name
);
432 item
= model
.getItemByName( selected
[ 0 ] );
433 return ( item
&& item
.getParamName() ) || '';
436 filterRepresentation
= filterRepresentation
|| {};
438 // Create or complete the filterRepresentation definition
439 this.getItems().forEach( function ( item
) {
440 // Map filter names to their parameter names
441 filterParamNames
[ item
.getName() ] = item
.getParamName();
443 if ( buildFromCurrentState
) {
444 // This means we have not been given a filter representation
445 // so we are building one based on current state
446 filterRepresentation
[ item
.getName() ] = item
.isSelected();
447 } else if ( !filterRepresentation
[ item
.getName() ] ) {
448 // We are given a filter representation, but we have to make
449 // sure that we fill in the missing filters if there are any
450 // we will assume they are all falsey
451 filterRepresentation
[ item
.getName() ] = false;
454 if ( filterRepresentation
[ item
.getName() ] ) {
455 areAnySelected
= true;
460 if ( this.getType() === 'send_unselected_if_any' ) {
461 // First, check if any of the items are selected at all.
462 // If none is selected, we're treating it as if they are
465 // Go over the items and define the correct values
466 $.each( filterRepresentation
, function ( name
, value
) {
467 result
[ filterParamNames
[ name
] ] = areAnySelected
?
468 // We must store all parameter values as strings '0' or '1'
469 String( Number( !value
) ) :
472 } else if ( this.getType() === 'string_options' ) {
475 $.each( filterRepresentation
, function ( name
, value
) {
478 values
.push( filterParamNames
[ name
] );
482 result
[ this.getName() ] = ( values
.length
=== Object
.keys( filterRepresentation
).length
) ?
483 'all' : values
.join( this.getSeparator() );
484 } else if ( this.getType() === 'single_option' ) {
485 result
[ this.getName() ] = getSelectedParameter( filterRepresentation
);
492 * Get the filter representation this group would provide
493 * based on given parameter states.
495 * @param {Object|string} [paramRepresentation] An object defining a parameter
496 * state to translate the filter state from. If not given, an object
497 * representing all filters as falsey is returned; same as if the parameter
498 * given were an empty object, or had some of the filters missing.
499 * @return {Object} Filter representation
501 mw
.rcfilters
.dm
.FilterGroup
.prototype.getFilterRepresentation = function ( paramRepresentation
) {
502 var areAnySelected
, paramValues
,
504 paramToFilterMap
= {},
507 if ( this.getType() === 'send_unselected_if_any' ) {
508 paramRepresentation
= paramRepresentation
|| {};
509 // Expand param representation to include all filters in the group
510 this.getItems().forEach( function ( filterItem
) {
511 var paramName
= filterItem
.getParamName();
513 paramRepresentation
[ paramName
] = paramRepresentation
[ paramName
] || '0';
514 paramToFilterMap
[ paramName
] = filterItem
;
516 if ( Number( paramRepresentation
[ filterItem
.getParamName() ] ) ) {
517 areAnySelected
= true;
521 $.each( paramRepresentation
, function ( paramName
, paramValue
) {
522 var filterItem
= paramToFilterMap
[ paramName
];
524 result
[ filterItem
.getName() ] = areAnySelected
?
525 // Flip the definition between the parameter
526 // state and the filter state
527 // This is what the 'toggleSelected' value of the filter is
528 !Number( paramValue
) :
529 // Otherwise, there are no selected items in the
530 // group, which means the state is false
533 } else if ( this.getType() === 'string_options' ) {
534 paramRepresentation
= paramRepresentation
|| '';
536 // Normalize the given parameter values
537 paramValues
= mw
.rcfilters
.utils
.normalizeParamOptions(
539 paramRepresentation
.split(
543 this.getItems().map( function ( filterItem
) {
544 return filterItem
.getParamName();
547 // Translate the parameter values into a filter selection state
548 this.getItems().forEach( function ( filterItem
) {
549 result
[ filterItem
.getName() ] = (
550 // If it is the word 'all'
551 paramValues
.length
=== 1 && paramValues
[ 0 ] === 'all' ||
552 // All values are written
553 paramValues
.length
=== model
.getItemCount()
555 // All true (either because all values are written or the term 'all' is written)
556 // is the same as all filters set to true
558 // Otherwise, the filter is selected only if it appears in the parameter values
559 paramValues
.indexOf( filterItem
.getParamName() ) > -1;
561 } else if ( this.getType() === 'single_option' ) {
562 // There is parameter that fits a single filter, or none at all
563 this.getItems().forEach( function ( filterItem
) {
564 result
[ filterItem
.getName() ] = filterItem
.getParamName() === paramRepresentation
;
568 // Go over result and make sure all filters are represented.
569 // If any filters are missing, they will get a falsey value
570 this.getItems().forEach( function ( filterItem
) {
571 result
[ filterItem
.getName() ] = !!result
[ filterItem
.getName() ];
578 * Get item by its filter name
580 * @param {string} filterName Filter name
581 * @return {mw.rcfilters.dm.FilterItem} Filter item
583 mw
.rcfilters
.dm
.FilterGroup
.prototype.getItemByName = function ( filterName
) {
584 return this.getItems().filter( function ( item
) {
585 return item
.getName() === filterName
;
590 * Get item by its parameter name
592 * @param {string} paramName Parameter name
593 * @return {mw.rcfilters.dm.FilterItem} Filter item
595 mw
.rcfilters
.dm
.FilterGroup
.prototype.getItemByParamName = function ( paramName
) {
596 return this.getItems().filter( function ( item
) {
597 return item
.getParamName() === paramName
;
604 * @return {string} Group type
606 mw
.rcfilters
.dm
.FilterGroup
.prototype.getType = function () {
613 * @return {string} Display group
615 mw
.rcfilters
.dm
.FilterGroup
.prototype.getView = function () {
620 * Get the prefix used for the filter names inside this group.
622 * @param {string} [name] Filter name to prefix
623 * @return {string} Group prefix
625 mw
.rcfilters
.dm
.FilterGroup
.prototype.getNamePrefix = function () {
626 return this.getName() + '__';
630 * Get a filter name with the prefix used for the filter names inside this group.
632 * @param {string} name Filter name to prefix
633 * @return {string} Group prefix
635 mw
.rcfilters
.dm
.FilterGroup
.prototype.getPrefixedName = function ( name
) {
636 return this.getNamePrefix() + name
;
642 * @return {string} Title
644 mw
.rcfilters
.dm
.FilterGroup
.prototype.getTitle = function () {
649 * Get group's values separator
651 * @return {string} Values separator
653 mw
.rcfilters
.dm
.FilterGroup
.prototype.getSeparator = function () {
654 return this.separator
;
658 * Check whether the group is defined as full coverage
660 * @return {boolean} Group is full coverage
662 mw
.rcfilters
.dm
.FilterGroup
.prototype.isFullCoverage = function () {
663 return this.fullCoverage
;