Merge "Add 3D filetype for STL files"
[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 {string} [title] Group title
15 * @cfg {string} [separator='|'] Value separator for 'string_options' groups
16 * @cfg {boolean} [active] Group is active
17 * @cfg {boolean} [fullCoverage] This filters in this group collectively cover all results
18 * @cfg {Object} [conflicts] Defines the conflicts for this filter group
19 * @cfg {string|Object} [labelPrefixKey] An i18n key defining the prefix label for this
20 * group. If the prefix has 'invert' state, the parameter is expected to be an object
21 * with 'default' and 'inverted' as keys.
22 * @cfg {Object} [whatsThis] Defines the messages that should appear for the 'what's this' popup
23 * @cfg {string} [whatsThis.header] The header of the whatsThis popup message
24 * @cfg {string} [whatsThis.body] The body of the whatsThis popup message
25 * @cfg {string} [whatsThis.url] The url for the link in the whatsThis popup message
26 * @cfg {string} [whatsThis.linkMessage] The text for the link in the whatsThis popup message
27 */
28 mw.rcfilters.dm.FilterGroup = function MwRcfiltersDmFilterGroup( name, config ) {
29 config = config || {};
30
31 // Mixin constructor
32 OO.EventEmitter.call( this );
33 OO.EmitterList.call( this );
34
35 this.name = name;
36 this.type = config.type || 'send_unselected_if_any';
37 this.view = config.view || 'default';
38 this.title = config.title;
39 this.separator = config.separator || '|';
40 this.labelPrefixKey = config.labelPrefixKey;
41
42 this.active = !!config.active;
43 this.fullCoverage = !!config.fullCoverage;
44
45 this.whatsThis = config.whatsThis || {};
46
47 this.conflicts = config.conflicts || {};
48 this.defaultParams = {};
49
50 this.aggregate( { update: 'filterItemUpdate' } );
51 this.connect( this, { filterItemUpdate: 'onFilterItemUpdate' } );
52 };
53
54 /* Initialization */
55 OO.initClass( mw.rcfilters.dm.FilterGroup );
56 OO.mixinClass( mw.rcfilters.dm.FilterGroup, OO.EventEmitter );
57 OO.mixinClass( mw.rcfilters.dm.FilterGroup, OO.EmitterList );
58
59 /* Events */
60
61 /**
62 * @event update
63 *
64 * Group state has been updated
65 */
66
67 /* Methods */
68
69 /**
70 * Initialize the group and create its filter items
71 *
72 * @param {Object} filterDefinition Filter definition for this group
73 * @param {string|Object} [groupDefault] Definition of the group default
74 */
75 mw.rcfilters.dm.FilterGroup.prototype.initializeFilters = function ( filterDefinition, groupDefault ) {
76 var supersetMap = {},
77 model = this,
78 items = [];
79
80 filterDefinition.forEach( function ( filter ) {
81 // Instantiate an item
82 var subsetNames = [],
83 filterItem = new mw.rcfilters.dm.FilterItem( filter.name, model, {
84 group: model.getName(),
85 label: filter.label || filter.name,
86 description: filter.description || '',
87 labelPrefixKey: model.labelPrefixKey,
88 cssClass: filter.cssClass,
89 identifiers: filter.identifiers
90 } );
91
92 filter.subset = filter.subset || [];
93 filter.subset = filter.subset.map( function ( el ) {
94 return el.filter;
95 } );
96
97 if ( filter.subset ) {
98 subsetNames = [];
99 filter.subset.forEach( function ( subsetFilterName ) { // eslint-disable-line no-loop-func
100 // Subsets (unlike conflicts) are always inside the same group
101 // We can re-map the names of the filters we are getting from
102 // the subsets with the group prefix
103 var subsetName = model.getPrefixedName( subsetFilterName );
104 // For convenience, we should store each filter's "supersets" -- these are
105 // the filters that have that item in their subset list. This will just
106 // make it easier to go through whether the item has any other items
107 // that affect it (and are selected) at any given time
108 supersetMap[ subsetName ] = supersetMap[ subsetName ] || [];
109 mw.rcfilters.utils.addArrayElementsUnique(
110 supersetMap[ subsetName ],
111 filterItem.getName()
112 );
113
114 // Translate subset param name to add the group name, so we
115 // get consistent naming. We know that subsets are only within
116 // the same group
117 subsetNames.push( subsetName );
118 } );
119
120 // Set translated subset
121 filterItem.setSubset( subsetNames );
122 }
123
124 items.push( filterItem );
125
126 // Store default parameter state; in this case, default is defined per filter
127 if ( model.getType() === 'send_unselected_if_any' ) {
128 // Store the default parameter state
129 // For this group type, parameter values are direct
130 // We need to convert from a boolean to a string ('1' and '0')
131 model.defaultParams[ filter.name ] = String( Number( !!filter.default ) );
132 }
133 } );
134
135 // Add items
136 this.addItems( items );
137
138 // Now that we have all items, we can apply the superset map
139 this.getItems().forEach( function ( filterItem ) {
140 filterItem.setSuperset( supersetMap[ filterItem.getName() ] );
141 } );
142
143 // Store default parameter state; in this case, default is defined per the
144 // entire group, given by groupDefault method parameter
145 if ( this.getType() === 'string_options' ) {
146 // Store the default parameter group state
147 // For this group, the parameter is group name and value is the names
148 // of selected items
149 this.defaultParams[ this.getName() ] = mw.rcfilters.utils.normalizeParamOptions(
150 // Current values
151 groupDefault ?
152 groupDefault.split( this.getSeparator() ) :
153 [],
154 // Legal values
155 this.getItems().map( function ( item ) {
156 return item.getParamName();
157 } )
158 ).join( this.getSeparator() );
159 }
160 };
161
162 /**
163 * Respond to filterItem update event
164 *
165 * @fires update
166 */
167 mw.rcfilters.dm.FilterGroup.prototype.onFilterItemUpdate = function () {
168 // Update state
169 var active = this.areAnySelected();
170
171 if ( this.active !== active ) {
172 this.active = active;
173 this.emit( 'update' );
174 }
175 };
176
177 /**
178 * Get group active state
179 *
180 * @return {boolean} Active state
181 */
182 mw.rcfilters.dm.FilterGroup.prototype.isActive = function () {
183 return this.active;
184 };
185
186 /**
187 * Get group name
188 *
189 * @return {string} Group name
190 */
191 mw.rcfilters.dm.FilterGroup.prototype.getName = function () {
192 return this.name;
193 };
194
195 /**
196 * Get the default param state of this group
197 *
198 * @return {Object} Default param state
199 */
200 mw.rcfilters.dm.FilterGroup.prototype.getDefaultParams = function () {
201 return this.defaultParams;
202 };
203
204 /**
205 * Get the messags defining the 'whats this' popup for this group
206 *
207 * @return {Object} What's this messages
208 */
209 mw.rcfilters.dm.FilterGroup.prototype.getWhatsThis = function () {
210 return this.whatsThis;
211 };
212
213 /**
214 * Check whether this group has a 'what's this' message
215 *
216 * @return {boolean} This group has a what's this message
217 */
218 mw.rcfilters.dm.FilterGroup.prototype.hasWhatsThis = function () {
219 return !!this.whatsThis.body;
220 };
221
222 /**
223 * Get the conflicts associated with the entire group.
224 * Conflict object is set up by filter name keys and conflict
225 * definition. For example:
226 * [
227 * {
228 * filterName: {
229 * filter: filterName,
230 * group: group1
231 * }
232 * },
233 * {
234 * filterName2: {
235 * filter: filterName2,
236 * group: group2
237 * }
238 * }
239 * ]
240 * @return {Object} Conflict definition
241 */
242 mw.rcfilters.dm.FilterGroup.prototype.getConflicts = function () {
243 return this.conflicts;
244 };
245
246 /**
247 * Set conflicts for this group. See #getConflicts for the expected
248 * structure of the definition.
249 *
250 * @param {Object} conflicts Conflicts for this group
251 */
252 mw.rcfilters.dm.FilterGroup.prototype.setConflicts = function ( conflicts ) {
253 this.conflicts = conflicts;
254 };
255
256 /**
257 * Set conflicts for each filter item in the group based on the
258 * given conflict map
259 *
260 * @param {Object} conflicts Object representing the conflict map,
261 * keyed by the item name, where its value is an object for all its conflicts
262 */
263 mw.rcfilters.dm.FilterGroup.prototype.setFilterConflicts = function ( conflicts ) {
264 this.getItems().forEach( function ( filterItem ) {
265 if ( conflicts[ filterItem.getName() ] ) {
266 filterItem.setConflicts( conflicts[ filterItem.getName() ] );
267 }
268 } );
269 };
270
271 /**
272 * Check whether this item has a potential conflict with the given item
273 *
274 * This checks whether the given item is in the list of conflicts of
275 * the current item, but makes no judgment about whether the conflict
276 * is currently at play (either one of the items may not be selected)
277 *
278 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item
279 * @return {boolean} This item has a conflict with the given item
280 */
281 mw.rcfilters.dm.FilterGroup.prototype.existsInConflicts = function ( filterItem ) {
282 return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() );
283 };
284
285 /**
286 * Check whether there are any items selected
287 *
288 * @return {boolean} Any items in the group are selected
289 */
290 mw.rcfilters.dm.FilterGroup.prototype.areAnySelected = function () {
291 return this.getItems().some( function ( filterItem ) {
292 return filterItem.isSelected();
293 } );
294 };
295
296 /**
297 * Check whether all items selected
298 *
299 * @return {boolean} All items are selected
300 */
301 mw.rcfilters.dm.FilterGroup.prototype.areAllSelected = function () {
302 var selected = [],
303 unselected = [];
304
305 this.getItems().forEach( function ( filterItem ) {
306 if ( filterItem.isSelected() ) {
307 selected.push( filterItem );
308 } else {
309 unselected.push( filterItem );
310 }
311 } );
312
313 if ( unselected.length === 0 ) {
314 return true;
315 }
316
317 // check if every unselected is a subset of a selected
318 return unselected.every( function ( unselectedFilterItem ) {
319 return selected.some( function ( selectedFilterItem ) {
320 return selectedFilterItem.existsInSubset( unselectedFilterItem.getName() );
321 } );
322 } );
323 };
324
325 /**
326 * Get all selected items in this group
327 *
328 * @param {mw.rcfilters.dm.FilterItem} [excludeItem] Item to exclude from the list
329 * @return {mw.rcfilters.dm.FilterItem[]} Selected items
330 */
331 mw.rcfilters.dm.FilterGroup.prototype.getSelectedItems = function ( excludeItem ) {
332 var excludeName = ( excludeItem && excludeItem.getName() ) || '';
333
334 return this.getItems().filter( function ( item ) {
335 return item.getName() !== excludeName && item.isSelected();
336 } );
337 };
338
339 /**
340 * Check whether all selected items are in conflict with the given item
341 *
342 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
343 * @return {boolean} All selected items are in conflict with this item
344 */
345 mw.rcfilters.dm.FilterGroup.prototype.areAllSelectedInConflictWith = function ( filterItem ) {
346 var selectedItems = this.getSelectedItems( filterItem );
347
348 return selectedItems.length > 0 &&
349 (
350 // The group as a whole is in conflict with this item
351 this.existsInConflicts( filterItem ) ||
352 // All selected items are in conflict individually
353 selectedItems.every( function ( selectedFilter ) {
354 return selectedFilter.existsInConflicts( filterItem );
355 } )
356 );
357 };
358
359 /**
360 * Check whether any of the selected items are in conflict with the given item
361 *
362 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
363 * @return {boolean} Any of the selected items are in conflict with this item
364 */
365 mw.rcfilters.dm.FilterGroup.prototype.areAnySelectedInConflictWith = function ( filterItem ) {
366 var selectedItems = this.getSelectedItems( filterItem );
367
368 return selectedItems.length > 0 && (
369 // The group as a whole is in conflict with this item
370 this.existsInConflicts( filterItem ) ||
371 // Any selected items are in conflict individually
372 selectedItems.some( function ( selectedFilter ) {
373 return selectedFilter.existsInConflicts( filterItem );
374 } )
375 );
376 };
377
378 /**
379 * Get the parameter representation from this group
380 *
381 * @param {Object} [filterRepresentation] An object defining the state
382 * of the filters in this group, keyed by their name and current selected
383 * state value.
384 * @return {Object} Parameter representation
385 */
386 mw.rcfilters.dm.FilterGroup.prototype.getParamRepresentation = function ( filterRepresentation ) {
387 var values,
388 areAnySelected = false,
389 buildFromCurrentState = !filterRepresentation,
390 result = {},
391 filterParamNames = {};
392
393 filterRepresentation = filterRepresentation || {};
394
395 // Create or complete the filterRepresentation definition
396 this.getItems().forEach( function ( item ) {
397 // Map filter names to their parameter names
398 filterParamNames[ item.getName() ] = item.getParamName();
399
400 if ( buildFromCurrentState ) {
401 // This means we have not been given a filter representation
402 // so we are building one based on current state
403 filterRepresentation[ item.getName() ] = item.isSelected();
404 } else if ( !filterRepresentation[ item.getName() ] ) {
405 // We are given a filter representation, but we have to make
406 // sure that we fill in the missing filters if there are any
407 // we will assume they are all falsey
408 filterRepresentation[ item.getName() ] = false;
409 }
410
411 if ( filterRepresentation[ item.getName() ] ) {
412 areAnySelected = true;
413 }
414 } );
415
416 // Build result
417 if ( this.getType() === 'send_unselected_if_any' ) {
418 // First, check if any of the items are selected at all.
419 // If none is selected, we're treating it as if they are
420 // all false
421
422 // Go over the items and define the correct values
423 $.each( filterRepresentation, function ( name, value ) {
424 result[ filterParamNames[ name ] ] = areAnySelected ?
425 // We must store all parameter values as strings '0' or '1'
426 String( Number( !value ) ) :
427 '0';
428 } );
429 } else if ( this.getType() === 'string_options' ) {
430 values = [];
431
432 $.each( filterRepresentation, function ( name, value ) {
433 // Collect values
434 if ( value ) {
435 values.push( filterParamNames[ name ] );
436 }
437 } );
438
439 result[ this.getName() ] = ( values.length === Object.keys( filterRepresentation ).length ) ?
440 'all' : values.join( this.getSeparator() );
441 }
442
443 return result;
444 };
445
446 /**
447 * Get the filter representation this group would provide
448 * based on given parameter states.
449 *
450 * @param {Object|string} [paramRepresentation] An object defining a parameter
451 * state to translate the filter state from. If not given, an object
452 * representing all filters as falsey is returned; same as if the parameter
453 * given were an empty object, or had some of the filters missing.
454 * @return {Object} Filter representation
455 */
456 mw.rcfilters.dm.FilterGroup.prototype.getFilterRepresentation = function ( paramRepresentation ) {
457 var areAnySelected, paramValues,
458 model = this,
459 paramToFilterMap = {},
460 result = {};
461
462 if ( this.getType() === 'send_unselected_if_any' ) {
463 paramRepresentation = paramRepresentation || {};
464 // Expand param representation to include all filters in the group
465 this.getItems().forEach( function ( filterItem ) {
466 var paramName = filterItem.getParamName();
467
468 paramRepresentation[ paramName ] = paramRepresentation[ paramName ] || '0';
469 paramToFilterMap[ paramName ] = filterItem;
470
471 if ( Number( paramRepresentation[ filterItem.getParamName() ] ) ) {
472 areAnySelected = true;
473 }
474 } );
475
476 $.each( paramRepresentation, function ( paramName, paramValue ) {
477 var filterItem = paramToFilterMap[ paramName ];
478
479 result[ filterItem.getName() ] = areAnySelected ?
480 // Flip the definition between the parameter
481 // state and the filter state
482 // This is what the 'toggleSelected' value of the filter is
483 !Number( paramValue ) :
484 // Otherwise, there are no selected items in the
485 // group, which means the state is false
486 false;
487 } );
488 } else if ( this.getType() === 'string_options' ) {
489 paramRepresentation = paramRepresentation || '';
490
491 // Normalize the given parameter values
492 paramValues = mw.rcfilters.utils.normalizeParamOptions(
493 // Given
494 paramRepresentation.split(
495 this.getSeparator()
496 ),
497 // Allowed values
498 this.getItems().map( function ( filterItem ) {
499 return filterItem.getParamName();
500 } )
501 );
502 // Translate the parameter values into a filter selection state
503 this.getItems().forEach( function ( filterItem ) {
504 result[ filterItem.getName() ] = (
505 // If it is the word 'all'
506 paramValues.length === 1 && paramValues[ 0 ] === 'all' ||
507 // All values are written
508 paramValues.length === model.getItemCount()
509 ) ?
510 // All true (either because all values are written or the term 'all' is written)
511 // is the same as all filters set to true
512 true :
513 // Otherwise, the filter is selected only if it appears in the parameter values
514 paramValues.indexOf( filterItem.getParamName() ) > -1;
515 } );
516 }
517
518 // Go over result and make sure all filters are represented.
519 // If any filters are missing, they will get a falsey value
520 this.getItems().forEach( function ( filterItem ) {
521 result[ filterItem.getName() ] = !!result[ filterItem.getName() ];
522 } );
523
524 return result;
525 };
526
527 /**
528 * Get item by its parameter name
529 *
530 * @param {string} paramName Parameter name
531 * @return {mw.rcfilters.dm.FilterItem} Filter item
532 */
533 mw.rcfilters.dm.FilterGroup.prototype.getItemByParamName = function ( paramName ) {
534 return this.getItems().filter( function ( item ) {
535 return item.getParamName() === paramName;
536 } )[ 0 ];
537 };
538
539 /**
540 * Get group type
541 *
542 * @return {string} Group type
543 */
544 mw.rcfilters.dm.FilterGroup.prototype.getType = function () {
545 return this.type;
546 };
547
548 /**
549 * Get display group
550 *
551 * @return {string} Display group
552 */
553 mw.rcfilters.dm.FilterGroup.prototype.getView = function () {
554 return this.view;
555 };
556
557 /**
558 * Get the prefix used for the filter names inside this group.
559 *
560 * @param {string} [name] Filter name to prefix
561 * @return {string} Group prefix
562 */
563 mw.rcfilters.dm.FilterGroup.prototype.getNamePrefix = function () {
564 return this.getName() + '__';
565 };
566
567 /**
568 * Get a filter name with the prefix used for the filter names inside this group.
569 *
570 * @param {string} name Filter name to prefix
571 * @return {string} Group prefix
572 */
573 mw.rcfilters.dm.FilterGroup.prototype.getPrefixedName = function ( name ) {
574 return this.getNamePrefix() + name;
575 };
576
577 /**
578 * Get group's title
579 *
580 * @return {string} Title
581 */
582 mw.rcfilters.dm.FilterGroup.prototype.getTitle = function () {
583 return this.title;
584 };
585
586 /**
587 * Get group's values separator
588 *
589 * @return {string} Values separator
590 */
591 mw.rcfilters.dm.FilterGroup.prototype.getSeparator = function () {
592 return this.separator;
593 };
594
595 /**
596 * Check whether the group is defined as full coverage
597 *
598 * @return {boolean} Group is full coverage
599 */
600 mw.rcfilters.dm.FilterGroup.prototype.isFullCoverage = function () {
601 return this.fullCoverage;
602 };
603 }( mediaWiki ) );