Merge "resourceloader: Add filename to validateScriptFile cache key"
[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} [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
22 */
23 mw.rcfilters.dm.FilterGroup = function MwRcfiltersDmFilterGroup( name, config ) {
24 config = config || {};
25
26 // Mixin constructor
27 OO.EventEmitter.call( this );
28 OO.EmitterList.call( this );
29
30 this.name = name;
31 this.type = config.type || 'send_unselected_if_any';
32 this.title = config.title;
33 this.separator = config.separator || '|';
34
35 this.active = !!config.active;
36 this.fullCoverage = !!config.fullCoverage;
37
38 this.whatsThis = config.whatsThis || {};
39
40 this.conflicts = config.conflicts || {};
41 this.defaultParams = {};
42
43 this.aggregate( { update: 'filterItemUpdate' } );
44 this.connect( this, { filterItemUpdate: 'onFilterItemUpdate' } );
45 };
46
47 /* Initialization */
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 );
51
52 /* Events */
53
54 /**
55 * @event update
56 *
57 * Group state has been updated
58 */
59
60 /* Methods */
61
62 /**
63 * Initialize the group and create its filter items
64 *
65 * @param {Object} filterDefinition Filter definition for this group
66 * @param {string|Object} [groupDefault] Definition of the group default
67 */
68 mw.rcfilters.dm.FilterGroup.prototype.initializeFilters = function ( filterDefinition, groupDefault ) {
69 var supersetMap = {},
70 model = this,
71 items = [];
72
73 filterDefinition.forEach( function ( filter ) {
74 // Instantiate an item
75 var subsetNames = [],
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
81 } );
82
83 filter.subset = filter.subset || [];
84 filter.subset = filter.subset.map( function ( el ) {
85 return el.filter;
86 } );
87
88 if ( filter.subset ) {
89 subsetNames = [];
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 ],
102 filterItem.getName()
103 );
104
105 // Translate subset param name to add the group name, so we
106 // get consistent naming. We know that subsets are only within
107 // the same group
108 subsetNames.push( subsetName );
109 } );
110
111 // Set translated subset
112 filterItem.setSubset( subsetNames );
113 }
114
115 items.push( filterItem );
116
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 );
122 }
123 } );
124
125 // Add items
126 this.addItems( items );
127
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() ] );
131 } );
132
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
138 // of selected items
139 this.defaultParams[ this.getName() ] = mw.rcfilters.utils.normalizeParamOptions(
140 // Current values
141 groupDefault ?
142 groupDefault.split( this.getSeparator() ) :
143 [],
144 // Legal values
145 this.getItems().map( function ( item ) {
146 return item.getParamName();
147 } )
148 ).join( this.getSeparator() );
149 }
150 };
151
152 /**
153 * Respond to filterItem update event
154 *
155 * @fires update
156 */
157 mw.rcfilters.dm.FilterGroup.prototype.onFilterItemUpdate = function () {
158 // Update state
159 var active = this.areAnySelected();
160
161 if ( this.active !== active ) {
162 this.active = active;
163 this.emit( 'update' );
164 }
165 };
166
167 /**
168 * Get group active state
169 *
170 * @return {boolean} Active state
171 */
172 mw.rcfilters.dm.FilterGroup.prototype.isActive = function () {
173 return this.active;
174 };
175
176 /**
177 * Get group name
178 *
179 * @return {string} Group name
180 */
181 mw.rcfilters.dm.FilterGroup.prototype.getName = function () {
182 return this.name;
183 };
184
185 /**
186 * Get the default param state of this group
187 *
188 * @return {Object} Default param state
189 */
190 mw.rcfilters.dm.FilterGroup.prototype.getDefaultParams = function () {
191 return this.defaultParams;
192 };
193
194 /**
195 * Get the messags defining the 'whats this' popup for this group
196 *
197 * @return {Object} What's this messages
198 */
199 mw.rcfilters.dm.FilterGroup.prototype.getWhatsThis = function () {
200 return this.whatsThis;
201 };
202
203 /**
204 * Check whether this group has a 'what's this' message
205 *
206 * @return {boolean} This group has a what's this message
207 */
208 mw.rcfilters.dm.FilterGroup.prototype.hasWhatsThis = function () {
209 return !!this.whatsThis.body;
210 };
211
212 /**
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:
216 * [
217 * {
218 * filterName: {
219 * filter: filterName,
220 * group: group1
221 * }
222 * },
223 * {
224 * filterName2: {
225 * filter: filterName2,
226 * group: group2
227 * }
228 * }
229 * ]
230 * @return {Object} Conflict definition
231 */
232 mw.rcfilters.dm.FilterGroup.prototype.getConflicts = function () {
233 return this.conflicts;
234 };
235
236 /**
237 * Set conflicts for this group. See #getConflicts for the expected
238 * structure of the definition.
239 *
240 * @param {Object} conflicts Conflicts for this group
241 */
242 mw.rcfilters.dm.FilterGroup.prototype.setConflicts = function ( conflicts ) {
243 this.conflicts = conflicts;
244 };
245
246 /**
247 * Set conflicts for each filter item in the group based on the
248 * given conflict map
249 *
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
252 */
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() ] );
257 }
258 } );
259 };
260
261 /**
262 * Check whether this item has a potential conflict with the given item
263 *
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)
267 *
268 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item
269 * @return {boolean} This item has a conflict with the given item
270 */
271 mw.rcfilters.dm.FilterGroup.prototype.existsInConflicts = function ( filterItem ) {
272 return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() );
273 };
274
275 /**
276 * Check whether there are any items selected
277 *
278 * @return {boolean} Any items in the group are selected
279 */
280 mw.rcfilters.dm.FilterGroup.prototype.areAnySelected = function () {
281 return this.getItems().some( function ( filterItem ) {
282 return filterItem.isSelected();
283 } );
284 };
285
286 /**
287 * Check whether all items selected
288 *
289 * @return {boolean} All items are selected
290 */
291 mw.rcfilters.dm.FilterGroup.prototype.areAllSelected = function () {
292 var selected = [],
293 unselected = [];
294
295 this.getItems().forEach( function ( filterItem ) {
296 if ( filterItem.isSelected() ) {
297 selected.push( filterItem );
298 } else {
299 unselected.push( filterItem );
300 }
301 } );
302
303 if ( unselected.length === 0 ) {
304 return true;
305 }
306
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() );
311 } );
312 } );
313 };
314
315 /**
316 * Get all selected items in this group
317 *
318 * @param {mw.rcfilters.dm.FilterItem} [excludeItem] Item to exclude from the list
319 * @return {mw.rcfilters.dm.FilterItem[]} Selected items
320 */
321 mw.rcfilters.dm.FilterGroup.prototype.getSelectedItems = function ( excludeItem ) {
322 var excludeName = ( excludeItem && excludeItem.getName() ) || '';
323
324 return this.getItems().filter( function ( item ) {
325 return item.getName() !== excludeName && item.isSelected();
326 } );
327 };
328
329 /**
330 * Check whether all selected items are in conflict with the given item
331 *
332 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
333 * @return {boolean} All selected items are in conflict with this item
334 */
335 mw.rcfilters.dm.FilterGroup.prototype.areAllSelectedInConflictWith = function ( filterItem ) {
336 var selectedItems = this.getSelectedItems( filterItem );
337
338 return selectedItems.length > 0 &&
339 (
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 );
345 } )
346 );
347 };
348
349 /**
350 * Check whether any of the selected items are in conflict with the given item
351 *
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
354 */
355 mw.rcfilters.dm.FilterGroup.prototype.areAnySelectedInConflictWith = function ( filterItem ) {
356 var selectedItems = this.getSelectedItems( filterItem );
357
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 );
364 } )
365 );
366 };
367
368 /**
369 * Get the parameter representation from this group
370 *
371 * @param {Object} [filterRepresentation] An object defining the state
372 * of the filters in this group, keyed by their name and current selected
373 * state value.
374 * @return {Object} Parameter representation
375 */
376 mw.rcfilters.dm.FilterGroup.prototype.getParamRepresentation = function ( filterRepresentation ) {
377 var values,
378 areAnySelected = false,
379 buildFromCurrentState = !filterRepresentation,
380 result = {},
381 filterParamNames = {};
382
383 filterRepresentation = filterRepresentation || {};
384
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();
389
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;
399 }
400
401 if ( filterRepresentation[ item.getName() ] ) {
402 areAnySelected = true;
403 }
404 } );
405
406 // Build result
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
410 // all false
411
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;
416 } );
417 } else if ( this.getType() === 'string_options' ) {
418 values = [];
419
420 $.each( filterRepresentation, function ( name, value ) {
421 // Collect values
422 if ( value ) {
423 values.push( filterParamNames[ name ] );
424 }
425 } );
426
427 result[ this.getName() ] = ( values.length === Object.keys( filterRepresentation ).length ) ?
428 'all' : values.join( this.getSeparator() );
429 }
430
431 return result;
432 };
433
434 /**
435 * Get the filter representation this group would provide
436 * based on given parameter states.
437 *
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
443 */
444 mw.rcfilters.dm.FilterGroup.prototype.getFilterRepresentation = function ( paramRepresentation ) {
445 var areAnySelected, paramValues,
446 model = this,
447 paramToFilterMap = {},
448 result = {};
449
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;
456
457 if ( paramRepresentation[ filterItem.getParamName() ] ) {
458 areAnySelected = true;
459 }
460 } );
461
462 $.each( paramRepresentation, function ( paramName, paramValue ) {
463 var filterItem = paramToFilterMap[ paramName ];
464
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
472 false;
473 } );
474 } else if ( this.getType() === 'string_options' ) {
475 paramRepresentation = paramRepresentation || '';
476
477 // Normalize the given parameter values
478 paramValues = mw.rcfilters.utils.normalizeParamOptions(
479 // Given
480 paramRepresentation.split(
481 this.getSeparator()
482 ),
483 // Allowed values
484 this.getItems().map( function ( filterItem ) {
485 return filterItem.getParamName();
486 } )
487 );
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()
495 ) ?
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
498 true :
499 // Otherwise, the filter is selected only if it appears in the parameter values
500 paramValues.indexOf( filterItem.getParamName() ) > -1;
501 } );
502 }
503
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() ];
508 } );
509
510 return result;
511 };
512
513 /**
514 * Get item by its parameter name
515 *
516 * @param {string} paramName Parameter name
517 * @return {mw.rcfilters.dm.FilterItem} Filter item
518 */
519 mw.rcfilters.dm.FilterGroup.prototype.getItemByParamName = function ( paramName ) {
520 return this.getItems().filter( function ( item ) {
521 return item.getParamName() === paramName;
522 } )[ 0 ];
523 };
524
525 /**
526 * Get group type
527 *
528 * @return {string} Group type
529 */
530 mw.rcfilters.dm.FilterGroup.prototype.getType = function () {
531 return this.type;
532 };
533
534 /**
535 * Get the prefix used for the filter names inside this group.
536 *
537 * @param {string} [name] Filter name to prefix
538 * @return {string} Group prefix
539 */
540 mw.rcfilters.dm.FilterGroup.prototype.getNamePrefix = function () {
541 return this.getName() + '__';
542 };
543
544 /**
545 * Get a filter name with the prefix used for the filter names inside this group.
546 *
547 * @param {string} name Filter name to prefix
548 * @return {string} Group prefix
549 */
550 mw.rcfilters.dm.FilterGroup.prototype.getPrefixedName = function ( name ) {
551 return this.getNamePrefix() + name;
552 };
553
554 /**
555 * Get group's title
556 *
557 * @return {string} Title
558 */
559 mw.rcfilters.dm.FilterGroup.prototype.getTitle = function () {
560 return this.title;
561 };
562
563 /**
564 * Get group's values separator
565 *
566 * @return {string} Values separator
567 */
568 mw.rcfilters.dm.FilterGroup.prototype.getSeparator = function () {
569 return this.separator;
570 };
571
572 /**
573 * Check whether the group is defined as full coverage
574 *
575 * @return {boolean} Group is full coverage
576 */
577 mw.rcfilters.dm.FilterGroup.prototype.isFullCoverage = function () {
578 return this.fullCoverage;
579 };
580 }( mediaWiki ) );