Match Parsoid's attribute sanitization for video elements
[lhc/web/wiklou.git] / resources / src / mediawiki.rcfilters / dm / mw.rcfilters.dm.FilterItem.js
1 ( function ( mw ) {
2 /**
3 * Filter item model
4 *
5 * @mixins OO.EventEmitter
6 *
7 * @constructor
8 * @param {string} param Filter param name
9 * @param {mw.rcfilters.dm.FilterGroup} groupModel Filter group model
10 * @param {Object} config Configuration object
11 * @cfg {string} [group] The group this item belongs to
12 * @cfg {string} [label] The label for the filter
13 * @cfg {string} [description] The description of the filter
14 * @cfg {boolean} [active=true] The filter is active and affecting the result
15 * @cfg {string[]} [excludes=[]] A list of filter names this filter, if
16 * selected, makes inactive.
17 * @cfg {boolean} [selected] The item is selected
18 * @cfg {string[]} [subset] Defining the names of filters that are a subset of this filter
19 * @cfg {Object} [conflicts] Defines the conflicts for this filter
20 * @cfg {string} [cssClass] The class identifying the results that match this filter
21 */
22 mw.rcfilters.dm.FilterItem = function MwRcfiltersDmFilterItem( param, groupModel, config ) {
23 config = config || {};
24
25 // Mixin constructor
26 OO.EventEmitter.call( this );
27
28 this.param = param;
29 this.groupModel = groupModel;
30 this.name = this.groupModel.getNamePrefix() + param;
31
32 this.label = config.label || this.name;
33 this.description = config.description;
34 this.selected = !!config.selected;
35
36 // Interaction definitions
37 this.subset = config.subset || [];
38 this.conflicts = config.conflicts || {};
39 this.superset = [];
40
41 // Interaction states
42 this.included = false;
43 this.conflicted = false;
44 this.fullyCovered = false;
45
46 // Highlight
47 this.cssClass = config.cssClass;
48 this.highlightColor = null;
49 this.highlightEnabled = false;
50 };
51
52 /* Initialization */
53
54 OO.initClass( mw.rcfilters.dm.FilterItem );
55 OO.mixinClass( mw.rcfilters.dm.FilterItem, OO.EventEmitter );
56
57 /* Events */
58
59 /**
60 * @event update
61 *
62 * The state of this filter has changed
63 */
64
65 /* Methods */
66
67 /**
68 * Return the representation of the state of this item.
69 *
70 * @return {Object} State of the object
71 */
72 mw.rcfilters.dm.FilterItem.prototype.getState = function () {
73 return {
74 selected: this.isSelected(),
75 included: this.isIncluded(),
76 conflicted: this.isConflicted(),
77 fullyCovered: this.isFullyCovered()
78 };
79 };
80
81 /**
82 * Get the name of this filter
83 *
84 * @return {string} Filter name
85 */
86 mw.rcfilters.dm.FilterItem.prototype.getName = function () {
87 return this.name;
88 };
89
90 /**
91 * Get the param name or value of this filter
92 *
93 * @return {string} Filter param name
94 */
95 mw.rcfilters.dm.FilterItem.prototype.getParamName = function () {
96 return this.param;
97 };
98
99 /**
100 * Get the message for the display area for the currently active conflict
101 *
102 * @return {string} Conflict result message key
103 */
104 mw.rcfilters.dm.FilterItem.prototype.getCurrentConflictResultMessage = function () {
105 var details = {};
106
107 // First look in filter's own conflicts
108 details = this.getConflictDetails( this.getOwnConflicts(), 'globalDescription' );
109 if ( !details.message ) {
110 // Fall back onto conflicts in the group
111 details = this.getConflictDetails( this.getGroupModel().getConflicts(), 'globalDescription' );
112 }
113
114 return details.message;
115 };
116
117 /**
118 * Get the details of the active conflict on this filter
119 *
120 * @param {Object} conflicts Conflicts to examine
121 * @param {string} [key='contextDescription'] Message key
122 * @return {Object} Object with conflict message and conflict items
123 * @return {string} return.message Conflict message
124 * @return {string[]} return.names Conflicting item labels
125 */
126 mw.rcfilters.dm.FilterItem.prototype.getConflictDetails = function ( conflicts, key ) {
127 var group,
128 conflictMessage = '',
129 itemLabels = [];
130
131 key = key || 'contextDescription';
132
133 $.each( conflicts, function ( filterName, conflict ) {
134 if ( !conflict.item.isSelected() ) {
135 return;
136 }
137
138 if ( !conflictMessage ) {
139 conflictMessage = conflict[ key ];
140 group = conflict.group;
141 }
142
143 if ( group === conflict.group ) {
144 itemLabels.push( mw.msg( 'quotation-marks', conflict.item.getLabel() ) );
145 }
146 } );
147
148 return {
149 message: conflictMessage,
150 names: itemLabels
151 };
152
153 };
154
155 /**
156 * Get the message representing the state of this model.
157 *
158 * @return {string} State message
159 */
160 mw.rcfilters.dm.FilterItem.prototype.getStateMessage = function () {
161 var messageKey, details, superset,
162 affectingItems = [];
163
164 if ( this.isSelected() ) {
165 if ( this.isConflicted() ) {
166 // First look in filter's own conflicts
167 details = this.getConflictDetails( this.getOwnConflicts() );
168 if ( !details.message ) {
169 // Fall back onto conflicts in the group
170 details = this.getConflictDetails( this.getGroupModel().getConflicts() );
171 }
172
173 messageKey = details.message;
174 affectingItems = details.names;
175 } else if ( this.isIncluded() && !this.isHighlighted() ) {
176 // We only show the 'no effect' full-coverage message
177 // if the item is also not highlighted. See T161273
178 superset = this.getSuperset();
179 // For this message we need to collect the affecting superset
180 affectingItems = this.getGroupModel().getSelectedItems( this )
181 .filter( function ( item ) {
182 return superset.indexOf( item.getName() ) !== -1;
183 } )
184 .map( function ( item ) {
185 return mw.msg( 'quotation-marks', item.getLabel() );
186 } );
187
188 messageKey = 'rcfilters-state-message-subset';
189 } else if ( this.isFullyCovered() && !this.isHighlighted() ) {
190 affectingItems = this.getGroupModel().getSelectedItems( this )
191 .map( function ( item ) {
192 return mw.msg( 'quotation-marks', item.getLabel() );
193 } );
194
195 messageKey = 'rcfilters-state-message-fullcoverage';
196 }
197 }
198
199 if ( messageKey ) {
200 // Build message
201 return mw.msg(
202 messageKey,
203 mw.language.listToText( affectingItems ),
204 affectingItems.length
205 );
206 }
207
208 // Display description
209 return this.getDescription();
210 };
211
212 /**
213 * Get the model of the group this filter belongs to
214 *
215 * @return {mw.rcfilters.dm.FilterGroup} Filter group model
216 */
217 mw.rcfilters.dm.FilterItem.prototype.getGroupModel = function () {
218 return this.groupModel;
219 };
220
221 /**
222 * Get the group name this filter belongs to
223 *
224 * @return {string} Filter group name
225 */
226 mw.rcfilters.dm.FilterItem.prototype.getGroupName = function () {
227 return this.groupModel.getName();
228 };
229
230 /**
231 * Get the label of this filter
232 *
233 * @return {string} Filter label
234 */
235 mw.rcfilters.dm.FilterItem.prototype.getLabel = function () {
236 return this.label;
237 };
238
239 /**
240 * Get the description of this filter
241 *
242 * @return {string} Filter description
243 */
244 mw.rcfilters.dm.FilterItem.prototype.getDescription = function () {
245 return this.description;
246 };
247
248 /**
249 * Get the default value of this filter
250 *
251 * @return {boolean} Filter default
252 */
253 mw.rcfilters.dm.FilterItem.prototype.getDefault = function () {
254 return this.default;
255 };
256
257 /**
258 * Get filter subset
259 * This is a list of filter names that are defined to be included
260 * when this filter is selected.
261 *
262 * @return {string[]} Filter subset
263 */
264 mw.rcfilters.dm.FilterItem.prototype.getSubset = function () {
265 return this.subset;
266 };
267
268 /**
269 * Get filter superset
270 * This is a generated list of filters that define this filter
271 * to be included when either of them is selected.
272 *
273 * @return {string[]} Filter superset
274 */
275 mw.rcfilters.dm.FilterItem.prototype.getSuperset = function () {
276 return this.superset;
277 };
278
279 /**
280 * Get the selected state of this filter
281 *
282 * @return {boolean} Filter is selected
283 */
284 mw.rcfilters.dm.FilterItem.prototype.isSelected = function () {
285 return this.selected;
286 };
287
288 /**
289 * Check whether the filter is currently in a conflict state
290 *
291 * @return {boolean} Filter is in conflict state
292 */
293 mw.rcfilters.dm.FilterItem.prototype.isConflicted = function () {
294 return this.conflicted;
295 };
296
297 /**
298 * Check whether the filter is currently in an already included subset
299 *
300 * @return {boolean} Filter is in an already-included subset
301 */
302 mw.rcfilters.dm.FilterItem.prototype.isIncluded = function () {
303 return this.included;
304 };
305
306 /**
307 * Check whether the filter is currently fully covered
308 *
309 * @return {boolean} Filter is in fully-covered state
310 */
311 mw.rcfilters.dm.FilterItem.prototype.isFullyCovered = function () {
312 return this.fullyCovered;
313 };
314
315 /**
316 * Get all conflicts associated with this filter or its group
317 *
318 * Conflict object is set up by filter name keys and conflict
319 * definition. For example:
320 * {
321 * filterName: {
322 * filter: filterName,
323 * group: group1,
324 * label: itemLabel,
325 * item: itemModel
326 * }
327 * filterName2: {
328 * filter: filterName2,
329 * group: group2
330 * label: itemLabel2,
331 * item: itemModel2
332 * }
333 * }
334 *
335 * @return {Object} Filter conflicts
336 */
337 mw.rcfilters.dm.FilterItem.prototype.getConflicts = function () {
338 return $.extend( {}, this.conflicts, this.getGroupModel().getConflicts() );
339 };
340
341 /**
342 * Get the conflicts associated with this filter
343 *
344 * @return {Object} Filter conflicts
345 */
346 mw.rcfilters.dm.FilterItem.prototype.getOwnConflicts = function () {
347 return this.conflicts;
348 };
349
350 /**
351 * Set conflicts for this filter. See #getConflicts for the expected
352 * structure of the definition.
353 *
354 * @param {Object} conflicts Conflicts for this filter
355 */
356 mw.rcfilters.dm.FilterItem.prototype.setConflicts = function ( conflicts ) {
357 this.conflicts = conflicts || {};
358 };
359
360 /**
361 * Set filter superset
362 *
363 * @param {string[]} superset Filter superset
364 */
365 mw.rcfilters.dm.FilterItem.prototype.setSuperset = function ( superset ) {
366 this.superset = superset || [];
367 };
368
369 /**
370 * Set filter subset
371 *
372 * @param {string[]} subset Filter subset
373 */
374 mw.rcfilters.dm.FilterItem.prototype.setSubset = function ( subset ) {
375 this.subset = subset || [];
376 };
377
378 /**
379 * Check whether a filter exists in the subset list for this filter
380 *
381 * @param {string} filterName Filter name
382 * @return {boolean} Filter name is in the subset list
383 */
384 mw.rcfilters.dm.FilterItem.prototype.existsInSubset = function ( filterName ) {
385 return this.subset.indexOf( filterName ) > -1;
386 };
387
388 /**
389 * Check whether this item has a potential conflict with the given item
390 *
391 * This checks whether the given item is in the list of conflicts of
392 * the current item, but makes no judgment about whether the conflict
393 * is currently at play (either one of the items may not be selected)
394 *
395 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item
396 * @return {boolean} This item has a conflict with the given item
397 */
398 mw.rcfilters.dm.FilterItem.prototype.existsInConflicts = function ( filterItem ) {
399 return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() );
400 };
401
402 /**
403 * Set the state of this filter as being conflicted
404 * (This means any filters in its conflicts are selected)
405 *
406 * @param {boolean} [conflicted] Filter is in conflict state
407 * @fires update
408 */
409 mw.rcfilters.dm.FilterItem.prototype.toggleConflicted = function ( conflicted ) {
410 conflicted = conflicted === undefined ? !this.conflicted : conflicted;
411
412 if ( this.conflicted !== conflicted ) {
413 this.conflicted = conflicted;
414 this.emit( 'update' );
415 }
416 };
417
418 /**
419 * Set the state of this filter as being already included
420 * (This means any filters in its superset are selected)
421 *
422 * @param {boolean} [included] Filter is included as part of a subset
423 * @fires update
424 */
425 mw.rcfilters.dm.FilterItem.prototype.toggleIncluded = function ( included ) {
426 included = included === undefined ? !this.included : included;
427
428 if ( this.included !== included ) {
429 this.included = included;
430 this.emit( 'update' );
431 }
432 };
433
434 /**
435 * Toggle the selected state of the item
436 *
437 * @param {boolean} [isSelected] Filter is selected
438 * @fires update
439 */
440 mw.rcfilters.dm.FilterItem.prototype.toggleSelected = function ( isSelected ) {
441 isSelected = isSelected === undefined ? !this.selected : isSelected;
442
443 if ( this.selected !== isSelected ) {
444 this.selected = isSelected;
445 this.emit( 'update' );
446 }
447 };
448
449 /**
450 * Toggle the fully covered state of the item
451 *
452 * @param {boolean} [isFullyCovered] Filter is fully covered
453 * @fires update
454 */
455 mw.rcfilters.dm.FilterItem.prototype.toggleFullyCovered = function ( isFullyCovered ) {
456 isFullyCovered = isFullyCovered === undefined ? !this.fullycovered : isFullyCovered;
457
458 if ( this.fullyCovered !== isFullyCovered ) {
459 this.fullyCovered = isFullyCovered;
460 this.emit( 'update' );
461 }
462 };
463
464 /**
465 * Set the highlight color
466 *
467 * @param {string|null} highlightColor
468 */
469 mw.rcfilters.dm.FilterItem.prototype.setHighlightColor = function ( highlightColor ) {
470 if ( this.highlightColor !== highlightColor ) {
471 this.highlightColor = highlightColor;
472 this.emit( 'update' );
473 }
474 };
475
476 /**
477 * Clear the highlight color
478 */
479 mw.rcfilters.dm.FilterItem.prototype.clearHighlightColor = function () {
480 this.setHighlightColor( null );
481 };
482
483 /**
484 * Get the highlight color, or null if none is configured
485 *
486 * @return {string|null}
487 */
488 mw.rcfilters.dm.FilterItem.prototype.getHighlightColor = function () {
489 return this.highlightColor;
490 };
491
492 /**
493 * Get the CSS class that matches changes that fit this filter
494 * or null if none is configured
495 *
496 * @return {string|null}
497 */
498 mw.rcfilters.dm.FilterItem.prototype.getCssClass = function () {
499 return this.cssClass;
500 };
501
502 /**
503 * Toggle the highlight feature on and off for this filter.
504 * It only works if highlight is supported for this filter.
505 *
506 * @param {boolean} enable Highlight should be enabled
507 */
508 mw.rcfilters.dm.FilterItem.prototype.toggleHighlight = function ( enable ) {
509 enable = enable === undefined ? !this.highlightEnabled : enable;
510
511 if ( !this.isHighlightSupported() ) {
512 return;
513 }
514
515 if ( enable === this.highlightEnabled ) {
516 return;
517 }
518
519 this.highlightEnabled = enable;
520 this.emit( 'update' );
521 };
522
523 /**
524 * Check if the highlight feature is currently enabled for this filter
525 *
526 * @return {boolean}
527 */
528 mw.rcfilters.dm.FilterItem.prototype.isHighlightEnabled = function () {
529 return !!this.highlightEnabled;
530 };
531
532 /**
533 * Check if the highlight feature is supported for this filter
534 *
535 * @return {boolean}
536 */
537 mw.rcfilters.dm.FilterItem.prototype.isHighlightSupported = function () {
538 return !!this.getCssClass();
539 };
540
541 /**
542 * Check if the filter is currently highlighted
543 *
544 * @return {boolean}
545 */
546 mw.rcfilters.dm.FilterItem.prototype.isHighlighted = function () {
547 return this.isHighlightEnabled() && !!this.getHighlightColor();
548 };
549 }( mediaWiki ) );