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} [title] Group title
13 * @cfg {string} [separator='|'] Value separator for 'string_options' groups
14 * @cfg {boolean} [active] Group is active
15 * @cfg {boolean} [fullCoverage] This filters in this group collectively cover all results
16 * @cfg {Object} [conflicts] Defines the conflicts for this filter group
17 * @cfg {Object} [whatsThis] Defines the messages that should appear for the 'what's this' popup
18 * @cfg {string} [whatsThis.header] The header of the whatsThis popup message
19 * @cfg {string} [whatsThis.body] The body of the whatsThis popup message
20 * @cfg {string} [whatsThis.url] The url for the link in the whatsThis popup message
21 * @cfg {string} [whatsThis.linkMessage] The text for the link in the whatsThis popup message
23 mw
.rcfilters
.dm
.FilterGroup
= function MwRcfiltersDmFilterGroup( name
, config
) {
24 config
= config
|| {};
27 OO
.EventEmitter
.call( this );
28 OO
.EmitterList
.call( this );
31 this.type
= config
.type
|| 'send_unselected_if_any';
32 this.title
= config
.title
;
33 this.separator
= config
.separator
|| '|';
35 this.active
= !!config
.active
;
36 this.fullCoverage
= !!config
.fullCoverage
;
38 this.whatsThis
= config
.whatsThis
|| {};
40 this.conflicts
= config
.conflicts
|| {};
41 this.defaultParams
= {};
43 this.aggregate( { update
: 'filterItemUpdate' } );
44 this.connect( this, { filterItemUpdate
: 'onFilterItemUpdate' } );
48 OO
.initClass( mw
.rcfilters
.dm
.FilterGroup
);
49 OO
.mixinClass( mw
.rcfilters
.dm
.FilterGroup
, OO
.EventEmitter
);
50 OO
.mixinClass( mw
.rcfilters
.dm
.FilterGroup
, OO
.EmitterList
);
57 * Group state has been updated
63 * Initialize the group and create its filter items
65 * @param {Object} filterDefinition Filter definition for this group
66 * @param {string|Object} [groupDefault] Definition of the group default
68 mw
.rcfilters
.dm
.FilterGroup
.prototype.initializeFilters = function ( filterDefinition
, groupDefault
) {
73 filterDefinition
.forEach( function ( filter
) {
74 // Instantiate an item
76 filterItem
= new mw
.rcfilters
.dm
.FilterItem( filter
.name
, model
, {
77 group
: model
.getName(),
78 label
: mw
.msg( filter
.label
),
79 description
: mw
.msg( filter
.description
),
80 cssClass
: filter
.cssClass
83 filter
.subset
= filter
.subset
|| [];
84 filter
.subset
= filter
.subset
.map( function ( el
) {
88 if ( filter
.subset
) {
90 filter
.subset
.forEach( function ( subsetFilterName
) { // eslint-disable-line no-loop-func
91 // Subsets (unlike conflicts) are always inside the same group
92 // We can re-map the names of the filters we are getting from
93 // the subsets with the group prefix
94 var subsetName
= model
.getPrefixedName( subsetFilterName
);
95 // For convenience, we should store each filter's "supersets" -- these are
96 // the filters that have that item in their subset list. This will just
97 // make it easier to go through whether the item has any other items
98 // that affect it (and are selected) at any given time
99 supersetMap
[ subsetName
] = supersetMap
[ subsetName
] || [];
100 mw
.rcfilters
.utils
.addArrayElementsUnique(
101 supersetMap
[ subsetName
],
105 // Translate subset param name to add the group name, so we
106 // get consistent naming. We know that subsets are only within
108 subsetNames
.push( subsetName
);
111 // Set translated subset
112 filterItem
.setSubset( subsetNames
);
115 items
.push( filterItem
);
117 // Store default parameter state; in this case, default is defined per filter
118 if ( model
.getType() === 'send_unselected_if_any' ) {
119 // Store the default parameter state
120 // For this group type, parameter values are direct
121 model
.defaultParams
[ filter
.name
] = Number( !!filter
.default );
126 this.addItems( items
);
128 // Now that we have all items, we can apply the superset map
129 this.getItems().forEach( function ( filterItem
) {
130 filterItem
.setSuperset( supersetMap
[ filterItem
.getName() ] );
133 // Store default parameter state; in this case, default is defined per the
134 // entire group, given by groupDefault method parameter
135 if ( this.getType() === 'string_options' ) {
136 // Store the default parameter group state
137 // For this group, the parameter is group name and value is the names
139 this.defaultParams
[ this.getName() ] = mw
.rcfilters
.utils
.normalizeParamOptions(
142 groupDefault
.split( this.getSeparator() ) :
145 this.getItems().map( function ( item
) {
146 return item
.getParamName();
148 ).join( this.getSeparator() );
153 * Respond to filterItem update event
157 mw
.rcfilters
.dm
.FilterGroup
.prototype.onFilterItemUpdate = function () {
159 var active
= this.areAnySelected();
161 if ( this.active
!== active
) {
162 this.active
= active
;
163 this.emit( 'update' );
168 * Get group active state
170 * @return {boolean} Active state
172 mw
.rcfilters
.dm
.FilterGroup
.prototype.isActive = function () {
179 * @return {string} Group name
181 mw
.rcfilters
.dm
.FilterGroup
.prototype.getName = function () {
186 * Get the default param state of this group
188 * @return {Object} Default param state
190 mw
.rcfilters
.dm
.FilterGroup
.prototype.getDefaultParams = function () {
191 return this.defaultParams
;
195 * Get the messags defining the 'whats this' popup for this group
197 * @return {Object} What's this messages
199 mw
.rcfilters
.dm
.FilterGroup
.prototype.getWhatsThis = function () {
200 return this.whatsThis
;
204 * Check whether this group has a 'what's this' message
206 * @return {boolean} This group has a what's this message
208 mw
.rcfilters
.dm
.FilterGroup
.prototype.hasWhatsThis = function () {
209 return !!this.whatsThis
.body
;
213 * Get the conflicts associated with the entire group.
214 * Conflict object is set up by filter name keys and conflict
215 * definition. For example:
219 * filter: filterName,
225 * filter: filterName2,
230 * @return {Object} Conflict definition
232 mw
.rcfilters
.dm
.FilterGroup
.prototype.getConflicts = function () {
233 return this.conflicts
;
237 * Set conflicts for this group. See #getConflicts for the expected
238 * structure of the definition.
240 * @param {Object} conflicts Conflicts for this group
242 mw
.rcfilters
.dm
.FilterGroup
.prototype.setConflicts = function ( conflicts
) {
243 this.conflicts
= conflicts
;
247 * Set conflicts for each filter item in the group based on the
250 * @param {Object} conflicts Object representing the conflict map,
251 * keyed by the item name, where its value is an object for all its conflicts
253 mw
.rcfilters
.dm
.FilterGroup
.prototype.setFilterConflicts = function ( conflicts
) {
254 this.getItems().forEach( function ( filterItem
) {
255 if ( conflicts
[ filterItem
.getName() ] ) {
256 filterItem
.setConflicts( conflicts
[ filterItem
.getName() ] );
262 * Check whether this item has a potential conflict with the given item
264 * This checks whether the given item is in the list of conflicts of
265 * the current item, but makes no judgment about whether the conflict
266 * is currently at play (either one of the items may not be selected)
268 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item
269 * @return {boolean} This item has a conflict with the given item
271 mw
.rcfilters
.dm
.FilterGroup
.prototype.existsInConflicts = function ( filterItem
) {
272 return Object
.prototype.hasOwnProperty
.call( this.getConflicts(), filterItem
.getName() );
276 * Check whether there are any items selected
278 * @return {boolean} Any items in the group are selected
280 mw
.rcfilters
.dm
.FilterGroup
.prototype.areAnySelected = function () {
281 return this.getItems().some( function ( filterItem
) {
282 return filterItem
.isSelected();
287 * Check whether all items selected
289 * @return {boolean} All items are selected
291 mw
.rcfilters
.dm
.FilterGroup
.prototype.areAllSelected = function () {
295 this.getItems().forEach( function ( filterItem
) {
296 if ( filterItem
.isSelected() ) {
297 selected
.push( filterItem
);
299 unselected
.push( filterItem
);
303 if ( unselected
.length
=== 0 ) {
307 // check if every unselected is a subset of a selected
308 return unselected
.every( function ( unselectedFilterItem
) {
309 return selected
.some( function ( selectedFilterItem
) {
310 return selectedFilterItem
.existsInSubset( unselectedFilterItem
.getName() );
316 * Get all selected items in this group
318 * @param {mw.rcfilters.dm.FilterItem} [excludeItem] Item to exclude from the list
319 * @return {mw.rcfilters.dm.FilterItem[]} Selected items
321 mw
.rcfilters
.dm
.FilterGroup
.prototype.getSelectedItems = function ( excludeItem
) {
322 var excludeName
= ( excludeItem
&& excludeItem
.getName() ) || '';
324 return this.getItems().filter( function ( item
) {
325 return item
.getName() !== excludeName
&& item
.isSelected();
330 * Check whether all selected items are in conflict with the given item
332 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
333 * @return {boolean} All selected items are in conflict with this item
335 mw
.rcfilters
.dm
.FilterGroup
.prototype.areAllSelectedInConflictWith = function ( filterItem
) {
336 var selectedItems
= this.getSelectedItems( filterItem
);
338 return selectedItems
.length
> 0 &&
340 // The group as a whole is in conflict with this item
341 this.existsInConflicts( filterItem
) ||
342 // All selected items are in conflict individually
343 selectedItems
.every( function ( selectedFilter
) {
344 return selectedFilter
.existsInConflicts( filterItem
);
350 * Check whether any of the selected items are in conflict with the given item
352 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
353 * @return {boolean} Any of the selected items are in conflict with this item
355 mw
.rcfilters
.dm
.FilterGroup
.prototype.areAnySelectedInConflictWith = function ( filterItem
) {
356 var selectedItems
= this.getSelectedItems( filterItem
);
358 return selectedItems
.length
> 0 && (
359 // The group as a whole is in conflict with this item
360 this.existsInConflicts( filterItem
) ||
361 // Any selected items are in conflict individually
362 selectedItems
.some( function ( selectedFilter
) {
363 return selectedFilter
.existsInConflicts( filterItem
);
369 * Get the parameter representation from this group
371 * @param {Object} [filterRepresentation] An object defining the state
372 * of the filters in this group, keyed by their name and current selected
374 * @return {Object} Parameter representation
376 mw
.rcfilters
.dm
.FilterGroup
.prototype.getParamRepresentation = function ( filterRepresentation
) {
378 areAnySelected
= false,
379 buildFromCurrentState
= !filterRepresentation
,
381 filterParamNames
= {};
383 filterRepresentation
= filterRepresentation
|| {};
385 // Create or complete the filterRepresentation definition
386 this.getItems().forEach( function ( item
) {
387 // Map filter names to their parameter names
388 filterParamNames
[ item
.getName() ] = item
.getParamName();
390 if ( buildFromCurrentState
) {
391 // This means we have not been given a filter representation
392 // so we are building one based on current state
393 filterRepresentation
[ item
.getName() ] = item
.isSelected();
394 } else if ( !filterRepresentation
[ item
.getName() ] ) {
395 // We are given a filter representation, but we have to make
396 // sure that we fill in the missing filters if there are any
397 // we will assume they are all falsey
398 filterRepresentation
[ item
.getName() ] = false;
401 if ( filterRepresentation
[ item
.getName() ] ) {
402 areAnySelected
= true;
407 if ( this.getType() === 'send_unselected_if_any' ) {
408 // First, check if any of the items are selected at all.
409 // If none is selected, we're treating it as if they are
412 // Go over the items and define the correct values
413 $.each( filterRepresentation
, function ( name
, value
) {
414 result
[ filterParamNames
[ name
] ] = areAnySelected
?
415 Number( !value
) : 0;
417 } else if ( this.getType() === 'string_options' ) {
420 $.each( filterRepresentation
, function ( name
, value
) {
423 values
.push( filterParamNames
[ name
] );
427 result
[ this.getName() ] = ( values
.length
=== Object
.keys( filterRepresentation
).length
) ?
428 'all' : values
.join( this.getSeparator() );
435 * Get the filter representation this group would provide
436 * based on given parameter states.
438 * @param {Object|string} [paramRepresentation] An object defining a parameter
439 * state to translate the filter state from. If not given, an object
440 * representing all filters as falsey is returned; same as if the parameter
441 * given were an empty object, or had some of the filters missing.
442 * @return {Object} Filter representation
444 mw
.rcfilters
.dm
.FilterGroup
.prototype.getFilterRepresentation = function ( paramRepresentation
) {
445 var areAnySelected
, paramValues
,
447 paramToFilterMap
= {},
450 if ( this.getType() === 'send_unselected_if_any' ) {
451 paramRepresentation
= paramRepresentation
|| {};
452 // Expand param representation to include all filters in the group
453 this.getItems().forEach( function ( filterItem
) {
454 paramRepresentation
[ filterItem
.getParamName() ] = !!paramRepresentation
[ filterItem
.getParamName() ];
455 paramToFilterMap
[ filterItem
.getParamName() ] = filterItem
;
457 if ( paramRepresentation
[ filterItem
.getParamName() ] ) {
458 areAnySelected
= true;
462 $.each( paramRepresentation
, function ( paramName
, paramValue
) {
463 var filterItem
= paramToFilterMap
[ paramName
];
465 result
[ filterItem
.getName() ] = areAnySelected
?
466 // Flip the definition between the parameter
467 // state and the filter state
468 // This is what the 'toggleSelected' value of the filter is
469 !Number( paramValue
) :
470 // Otherwise, there are no selected items in the
471 // group, which means the state is false
474 } else if ( this.getType() === 'string_options' ) {
475 paramRepresentation
= paramRepresentation
|| '';
477 // Normalize the given parameter values
478 paramValues
= mw
.rcfilters
.utils
.normalizeParamOptions(
480 paramRepresentation
.split(
484 this.getItems().map( function ( filterItem
) {
485 return filterItem
.getParamName();
488 // Translate the parameter values into a filter selection state
489 this.getItems().forEach( function ( filterItem
) {
490 result
[ filterItem
.getName() ] = (
491 // If it is the word 'all'
492 paramValues
.length
=== 1 && paramValues
[ 0 ] === 'all' ||
493 // All values are written
494 paramValues
.length
=== model
.getItemCount()
496 // All true (either because all values are written or the term 'all' is written)
497 // is the same as all filters set to true
499 // Otherwise, the filter is selected only if it appears in the parameter values
500 paramValues
.indexOf( filterItem
.getParamName() ) > -1;
504 // Go over result and make sure all filters are represented.
505 // If any filters are missing, they will get a falsey value
506 this.getItems().forEach( function ( filterItem
) {
507 result
[ filterItem
.getName() ] = !!result
[ filterItem
.getName() ];
514 * Get item by its parameter name
516 * @param {string} paramName Parameter name
517 * @return {mw.rcfilters.dm.FilterItem} Filter item
519 mw
.rcfilters
.dm
.FilterGroup
.prototype.getItemByParamName = function ( paramName
) {
520 return this.getItems().filter( function ( item
) {
521 return item
.getParamName() === paramName
;
528 * @return {string} Group type
530 mw
.rcfilters
.dm
.FilterGroup
.prototype.getType = function () {
535 * Get the prefix used for the filter names inside this group.
537 * @param {string} [name] Filter name to prefix
538 * @return {string} Group prefix
540 mw
.rcfilters
.dm
.FilterGroup
.prototype.getNamePrefix = function () {
541 return this.getName() + '__';
545 * Get a filter name with the prefix used for the filter names inside this group.
547 * @param {string} name Filter name to prefix
548 * @return {string} Group prefix
550 mw
.rcfilters
.dm
.FilterGroup
.prototype.getPrefixedName = function ( name
) {
551 return this.getNamePrefix() + name
;
557 * @return {string} Title
559 mw
.rcfilters
.dm
.FilterGroup
.prototype.getTitle = function () {
564 * Get group's values separator
566 * @return {string} Values separator
568 mw
.rcfilters
.dm
.FilterGroup
.prototype.getSeparator = function () {
569 return this.separator
;
573 * Check whether the group is defined as full coverage
575 * @return {boolean} Group is full coverage
577 mw
.rcfilters
.dm
.FilterGroup
.prototype.isFullCoverage = function () {
578 return this.fullCoverage
;