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 // We need to convert from a boolean to a string ('1' and '0')
122 model
.defaultParams
[ filter
.name
] = String( Number( !!filter
.default ) );
127 this.addItems( items
);
129 // Now that we have all items, we can apply the superset map
130 this.getItems().forEach( function ( filterItem
) {
131 filterItem
.setSuperset( supersetMap
[ filterItem
.getName() ] );
134 // Store default parameter state; in this case, default is defined per the
135 // entire group, given by groupDefault method parameter
136 if ( this.getType() === 'string_options' ) {
137 // Store the default parameter group state
138 // For this group, the parameter is group name and value is the names
140 this.defaultParams
[ this.getName() ] = mw
.rcfilters
.utils
.normalizeParamOptions(
143 groupDefault
.split( this.getSeparator() ) :
146 this.getItems().map( function ( item
) {
147 return item
.getParamName();
149 ).join( this.getSeparator() );
154 * Respond to filterItem update event
158 mw
.rcfilters
.dm
.FilterGroup
.prototype.onFilterItemUpdate = function () {
160 var active
= this.areAnySelected();
162 if ( this.active
!== active
) {
163 this.active
= active
;
164 this.emit( 'update' );
169 * Get group active state
171 * @return {boolean} Active state
173 mw
.rcfilters
.dm
.FilterGroup
.prototype.isActive = function () {
180 * @return {string} Group name
182 mw
.rcfilters
.dm
.FilterGroup
.prototype.getName = function () {
187 * Get the default param state of this group
189 * @return {Object} Default param state
191 mw
.rcfilters
.dm
.FilterGroup
.prototype.getDefaultParams = function () {
192 return this.defaultParams
;
196 * Get the messags defining the 'whats this' popup for this group
198 * @return {Object} What's this messages
200 mw
.rcfilters
.dm
.FilterGroup
.prototype.getWhatsThis = function () {
201 return this.whatsThis
;
205 * Check whether this group has a 'what's this' message
207 * @return {boolean} This group has a what's this message
209 mw
.rcfilters
.dm
.FilterGroup
.prototype.hasWhatsThis = function () {
210 return !!this.whatsThis
.body
;
214 * Get the conflicts associated with the entire group.
215 * Conflict object is set up by filter name keys and conflict
216 * definition. For example:
220 * filter: filterName,
226 * filter: filterName2,
231 * @return {Object} Conflict definition
233 mw
.rcfilters
.dm
.FilterGroup
.prototype.getConflicts = function () {
234 return this.conflicts
;
238 * Set conflicts for this group. See #getConflicts for the expected
239 * structure of the definition.
241 * @param {Object} conflicts Conflicts for this group
243 mw
.rcfilters
.dm
.FilterGroup
.prototype.setConflicts = function ( conflicts
) {
244 this.conflicts
= conflicts
;
248 * Set conflicts for each filter item in the group based on the
251 * @param {Object} conflicts Object representing the conflict map,
252 * keyed by the item name, where its value is an object for all its conflicts
254 mw
.rcfilters
.dm
.FilterGroup
.prototype.setFilterConflicts = function ( conflicts
) {
255 this.getItems().forEach( function ( filterItem
) {
256 if ( conflicts
[ filterItem
.getName() ] ) {
257 filterItem
.setConflicts( conflicts
[ filterItem
.getName() ] );
263 * Check whether this item has a potential conflict with the given item
265 * This checks whether the given item is in the list of conflicts of
266 * the current item, but makes no judgment about whether the conflict
267 * is currently at play (either one of the items may not be selected)
269 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item
270 * @return {boolean} This item has a conflict with the given item
272 mw
.rcfilters
.dm
.FilterGroup
.prototype.existsInConflicts = function ( filterItem
) {
273 return Object
.prototype.hasOwnProperty
.call( this.getConflicts(), filterItem
.getName() );
277 * Check whether there are any items selected
279 * @return {boolean} Any items in the group are selected
281 mw
.rcfilters
.dm
.FilterGroup
.prototype.areAnySelected = function () {
282 return this.getItems().some( function ( filterItem
) {
283 return filterItem
.isSelected();
288 * Check whether all items selected
290 * @return {boolean} All items are selected
292 mw
.rcfilters
.dm
.FilterGroup
.prototype.areAllSelected = function () {
296 this.getItems().forEach( function ( filterItem
) {
297 if ( filterItem
.isSelected() ) {
298 selected
.push( filterItem
);
300 unselected
.push( filterItem
);
304 if ( unselected
.length
=== 0 ) {
308 // check if every unselected is a subset of a selected
309 return unselected
.every( function ( unselectedFilterItem
) {
310 return selected
.some( function ( selectedFilterItem
) {
311 return selectedFilterItem
.existsInSubset( unselectedFilterItem
.getName() );
317 * Get all selected items in this group
319 * @param {mw.rcfilters.dm.FilterItem} [excludeItem] Item to exclude from the list
320 * @return {mw.rcfilters.dm.FilterItem[]} Selected items
322 mw
.rcfilters
.dm
.FilterGroup
.prototype.getSelectedItems = function ( excludeItem
) {
323 var excludeName
= ( excludeItem
&& excludeItem
.getName() ) || '';
325 return this.getItems().filter( function ( item
) {
326 return item
.getName() !== excludeName
&& item
.isSelected();
331 * Check whether all selected items are in conflict with the given item
333 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
334 * @return {boolean} All selected items are in conflict with this item
336 mw
.rcfilters
.dm
.FilterGroup
.prototype.areAllSelectedInConflictWith = function ( filterItem
) {
337 var selectedItems
= this.getSelectedItems( filterItem
);
339 return selectedItems
.length
> 0 &&
341 // The group as a whole is in conflict with this item
342 this.existsInConflicts( filterItem
) ||
343 // All selected items are in conflict individually
344 selectedItems
.every( function ( selectedFilter
) {
345 return selectedFilter
.existsInConflicts( filterItem
);
351 * Check whether any of the selected items are in conflict with the given item
353 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
354 * @return {boolean} Any of the selected items are in conflict with this item
356 mw
.rcfilters
.dm
.FilterGroup
.prototype.areAnySelectedInConflictWith = function ( filterItem
) {
357 var selectedItems
= this.getSelectedItems( filterItem
);
359 return selectedItems
.length
> 0 && (
360 // The group as a whole is in conflict with this item
361 this.existsInConflicts( filterItem
) ||
362 // Any selected items are in conflict individually
363 selectedItems
.some( function ( selectedFilter
) {
364 return selectedFilter
.existsInConflicts( filterItem
);
370 * Get the parameter representation from this group
372 * @param {Object} [filterRepresentation] An object defining the state
373 * of the filters in this group, keyed by their name and current selected
375 * @return {Object} Parameter representation
377 mw
.rcfilters
.dm
.FilterGroup
.prototype.getParamRepresentation = function ( filterRepresentation
) {
379 areAnySelected
= false,
380 buildFromCurrentState
= !filterRepresentation
,
382 filterParamNames
= {};
384 filterRepresentation
= filterRepresentation
|| {};
386 // Create or complete the filterRepresentation definition
387 this.getItems().forEach( function ( item
) {
388 // Map filter names to their parameter names
389 filterParamNames
[ item
.getName() ] = item
.getParamName();
391 if ( buildFromCurrentState
) {
392 // This means we have not been given a filter representation
393 // so we are building one based on current state
394 filterRepresentation
[ item
.getName() ] = item
.isSelected();
395 } else if ( !filterRepresentation
[ item
.getName() ] ) {
396 // We are given a filter representation, but we have to make
397 // sure that we fill in the missing filters if there are any
398 // we will assume they are all falsey
399 filterRepresentation
[ item
.getName() ] = false;
402 if ( filterRepresentation
[ item
.getName() ] ) {
403 areAnySelected
= true;
408 if ( this.getType() === 'send_unselected_if_any' ) {
409 // First, check if any of the items are selected at all.
410 // If none is selected, we're treating it as if they are
413 // Go over the items and define the correct values
414 $.each( filterRepresentation
, function ( name
, value
) {
415 result
[ filterParamNames
[ name
] ] = areAnySelected
?
416 // We must store all parameter values as strings '0' or '1'
417 String( Number( !value
) ) :
420 } else if ( this.getType() === 'string_options' ) {
423 $.each( filterRepresentation
, function ( name
, value
) {
426 values
.push( filterParamNames
[ name
] );
430 result
[ this.getName() ] = ( values
.length
=== Object
.keys( filterRepresentation
).length
) ?
431 'all' : values
.join( this.getSeparator() );
438 * Get the filter representation this group would provide
439 * based on given parameter states.
441 * @param {Object|string} [paramRepresentation] An object defining a parameter
442 * state to translate the filter state from. If not given, an object
443 * representing all filters as falsey is returned; same as if the parameter
444 * given were an empty object, or had some of the filters missing.
445 * @return {Object} Filter representation
447 mw
.rcfilters
.dm
.FilterGroup
.prototype.getFilterRepresentation = function ( paramRepresentation
) {
448 var areAnySelected
, paramValues
,
450 paramToFilterMap
= {},
453 if ( this.getType() === 'send_unselected_if_any' ) {
454 paramRepresentation
= paramRepresentation
|| {};
455 // Expand param representation to include all filters in the group
456 this.getItems().forEach( function ( filterItem
) {
457 var paramName
= filterItem
.getParamName();
459 paramRepresentation
[ paramName
] = paramRepresentation
[ paramName
] || '0';
460 paramToFilterMap
[ paramName
] = filterItem
;
462 if ( Number( paramRepresentation
[ filterItem
.getParamName() ] ) ) {
463 areAnySelected
= true;
467 $.each( paramRepresentation
, function ( paramName
, paramValue
) {
468 var filterItem
= paramToFilterMap
[ paramName
];
470 result
[ filterItem
.getName() ] = areAnySelected
?
471 // Flip the definition between the parameter
472 // state and the filter state
473 // This is what the 'toggleSelected' value of the filter is
474 !Number( paramValue
) :
475 // Otherwise, there are no selected items in the
476 // group, which means the state is false
479 } else if ( this.getType() === 'string_options' ) {
480 paramRepresentation
= paramRepresentation
|| '';
482 // Normalize the given parameter values
483 paramValues
= mw
.rcfilters
.utils
.normalizeParamOptions(
485 paramRepresentation
.split(
489 this.getItems().map( function ( filterItem
) {
490 return filterItem
.getParamName();
493 // Translate the parameter values into a filter selection state
494 this.getItems().forEach( function ( filterItem
) {
495 result
[ filterItem
.getName() ] = (
496 // If it is the word 'all'
497 paramValues
.length
=== 1 && paramValues
[ 0 ] === 'all' ||
498 // All values are written
499 paramValues
.length
=== model
.getItemCount()
501 // All true (either because all values are written or the term 'all' is written)
502 // is the same as all filters set to true
504 // Otherwise, the filter is selected only if it appears in the parameter values
505 paramValues
.indexOf( filterItem
.getParamName() ) > -1;
509 // Go over result and make sure all filters are represented.
510 // If any filters are missing, they will get a falsey value
511 this.getItems().forEach( function ( filterItem
) {
512 result
[ filterItem
.getName() ] = !!result
[ filterItem
.getName() ];
519 * Get item by its parameter name
521 * @param {string} paramName Parameter name
522 * @return {mw.rcfilters.dm.FilterItem} Filter item
524 mw
.rcfilters
.dm
.FilterGroup
.prototype.getItemByParamName = function ( paramName
) {
525 return this.getItems().filter( function ( item
) {
526 return item
.getParamName() === paramName
;
533 * @return {string} Group type
535 mw
.rcfilters
.dm
.FilterGroup
.prototype.getType = function () {
540 * Get the prefix used for the filter names inside this group.
542 * @param {string} [name] Filter name to prefix
543 * @return {string} Group prefix
545 mw
.rcfilters
.dm
.FilterGroup
.prototype.getNamePrefix = function () {
546 return this.getName() + '__';
550 * Get a filter name with the prefix used for the filter names inside this group.
552 * @param {string} name Filter name to prefix
553 * @return {string} Group prefix
555 mw
.rcfilters
.dm
.FilterGroup
.prototype.getPrefixedName = function ( name
) {
556 return this.getNamePrefix() + name
;
562 * @return {string} Title
564 mw
.rcfilters
.dm
.FilterGroup
.prototype.getTitle = function () {
569 * Get group's values separator
571 * @return {string} Values separator
573 mw
.rcfilters
.dm
.FilterGroup
.prototype.getSeparator = function () {
574 return this.separator
;
578 * Check whether the group is defined as full coverage
580 * @return {boolean} Group is full coverage
582 mw
.rcfilters
.dm
.FilterGroup
.prototype.isFullCoverage = function () {
583 return this.fullCoverage
;