Merge "RCFilters UI: Create tooltips for filter states"
[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 details of the active conflict on this filter
101 *
102 * @param {Object} conflicts Conflicts to examine
103 * @param {string} [key='contextDescription'] Message key
104 * @return {Object} Object with conflict message and conflict items
105 * @return {string} return.message Conflict message
106 * @return {string[]} return.names Conflicting item labels
107 */
108 mw.rcfilters.dm.FilterItem.prototype.getConflictDetails = function ( conflicts, key ) {
109 var group,
110 conflictMessage = '',
111 itemLabels = [];
112
113 key = key || 'contextDescription';
114
115 $.each( conflicts, function ( filterName, conflict ) {
116 if ( !conflict.item.isSelected() ) {
117 return;
118 }
119
120 if ( !conflictMessage ) {
121 conflictMessage = conflict[ key ];
122 group = conflict.group;
123 }
124
125 if ( group === conflict.group ) {
126 itemLabels.push( mw.msg( 'quotation-marks', conflict.item.getLabel() ) );
127 }
128 } );
129
130 return {
131 message: conflictMessage,
132 names: itemLabels
133 };
134
135 };
136
137 /**
138 * Get the message representing the state of this model.
139 *
140 * @return {string} State message
141 */
142 mw.rcfilters.dm.FilterItem.prototype.getStateMessage = function () {
143 var messageKey, details, superset,
144 affectingItems = [];
145
146 if ( this.isConflicted() ) {
147 // First look in filter's own conflicts
148 details = this.getConflictDetails( this.getOwnConflicts() );
149 if ( !details.message ) {
150 // Fall back onto conflicts in the group
151 details = this.getConflictDetails( this.getGroupModel().getConflicts() );
152 }
153
154 messageKey = details.message;
155 affectingItems = details.names;
156 } else if ( this.isIncluded() ) {
157 superset = this.getSuperset();
158 // For this message we need to collect the affecting superset
159 affectingItems = this.getGroupModel().getSelectedItems( this )
160 .filter( function ( item ) {
161 return superset.indexOf( item.getName() ) !== -1;
162 } )
163 .map( function ( item ) {
164 return mw.msg( 'quotation-marks', item.getLabel() );
165 } );
166
167 messageKey = 'rcfilters-state-message-subset';
168 } else if ( this.isFullyCovered() ) {
169 affectingItems = this.getGroupModel().getSelectedItems( this )
170 .map( function ( item ) {
171 return mw.msg( 'quotation-marks', item.getLabel() );
172 } );
173
174 messageKey = 'rcfilters-state-message-fullcoverage';
175 }
176
177 if ( messageKey ) {
178 // Build message
179 return mw.msg(
180 messageKey,
181 mw.language.listToText( affectingItems ),
182 affectingItems.length
183 );
184 }
185
186 // Display description
187 return this.getDescription();
188 };
189
190 /**
191 * Get the model of the group this filter belongs to
192 *
193 * @return {mw.rcfilters.dm.FilterGroup} Filter group model
194 */
195 mw.rcfilters.dm.FilterItem.prototype.getGroupModel = function () {
196 return this.groupModel;
197 };
198
199 /**
200 * Get the group name this filter belongs to
201 *
202 * @return {string} Filter group name
203 */
204 mw.rcfilters.dm.FilterItem.prototype.getGroupName = function () {
205 return this.groupModel.getName();
206 };
207
208 /**
209 * Get the label of this filter
210 *
211 * @return {string} Filter label
212 */
213 mw.rcfilters.dm.FilterItem.prototype.getLabel = function () {
214 return this.label;
215 };
216
217 /**
218 * Get the description of this filter
219 *
220 * @return {string} Filter description
221 */
222 mw.rcfilters.dm.FilterItem.prototype.getDescription = function () {
223 return this.description;
224 };
225
226 /**
227 * Get the default value of this filter
228 *
229 * @return {boolean} Filter default
230 */
231 mw.rcfilters.dm.FilterItem.prototype.getDefault = function () {
232 return this.default;
233 };
234
235 /**
236 * Get filter subset
237 * This is a list of filter names that are defined to be included
238 * when this filter is selected.
239 *
240 * @return {string[]} Filter subset
241 */
242 mw.rcfilters.dm.FilterItem.prototype.getSubset = function () {
243 return this.subset;
244 };
245
246 /**
247 * Get filter superset
248 * This is a generated list of filters that define this filter
249 * to be included when either of them is selected.
250 *
251 * @return {string[]} Filter superset
252 */
253 mw.rcfilters.dm.FilterItem.prototype.getSuperset = function () {
254 return this.superset;
255 };
256
257 /**
258 * Get the selected state of this filter
259 *
260 * @return {boolean} Filter is selected
261 */
262 mw.rcfilters.dm.FilterItem.prototype.isSelected = function () {
263 return this.selected;
264 };
265
266 /**
267 * Check whether the filter is currently in a conflict state
268 *
269 * @return {boolean} Filter is in conflict state
270 */
271 mw.rcfilters.dm.FilterItem.prototype.isConflicted = function () {
272 return this.conflicted;
273 };
274
275 /**
276 * Check whether the filter is currently in an already included subset
277 *
278 * @return {boolean} Filter is in an already-included subset
279 */
280 mw.rcfilters.dm.FilterItem.prototype.isIncluded = function () {
281 return this.included;
282 };
283
284 /**
285 * Check whether the filter is currently fully covered
286 *
287 * @return {boolean} Filter is in fully-covered state
288 */
289 mw.rcfilters.dm.FilterItem.prototype.isFullyCovered = function () {
290 return this.fullyCovered;
291 };
292
293 /**
294 * Get all conflicts associated with this filter or its group
295 *
296 * Conflict object is set up by filter name keys and conflict
297 * definition. For example:
298 * {
299 * filterName: {
300 * filter: filterName,
301 * group: group1,
302 * label: itemLabel,
303 * item: itemModel
304 * }
305 * filterName2: {
306 * filter: filterName2,
307 * group: group2
308 * label: itemLabel2,
309 * item: itemModel2
310 * }
311 * }
312 *
313 * @return {Object} Filter conflicts
314 */
315 mw.rcfilters.dm.FilterItem.prototype.getConflicts = function () {
316 return $.extend( {}, this.conflicts, this.getGroupModel().getConflicts() );
317 };
318
319 /**
320 * Get the conflicts associated with this filter
321 *
322 * @return {Object} Filter conflicts
323 */
324 mw.rcfilters.dm.FilterItem.prototype.getOwnConflicts = function () {
325 return this.conflicts;
326 };
327
328 /**
329 * Set conflicts for this filter. See #getConflicts for the expected
330 * structure of the definition.
331 *
332 * @param {Object} conflicts Conflicts for this filter
333 */
334 mw.rcfilters.dm.FilterItem.prototype.setConflicts = function ( conflicts ) {
335 this.conflicts = conflicts || {};
336 };
337
338 /**
339 * Set filter superset
340 *
341 * @param {string[]} superset Filter superset
342 */
343 mw.rcfilters.dm.FilterItem.prototype.setSuperset = function ( superset ) {
344 this.superset = superset || [];
345 };
346
347 /**
348 * Set filter subset
349 *
350 * @param {string[]} subset Filter subset
351 */
352 mw.rcfilters.dm.FilterItem.prototype.setSubset = function ( subset ) {
353 this.subset = subset || [];
354 };
355
356 /**
357 * Check whether a filter exists in the subset list for this filter
358 *
359 * @param {string} filterName Filter name
360 * @return {boolean} Filter name is in the subset list
361 */
362 mw.rcfilters.dm.FilterItem.prototype.existsInSubset = function ( filterName ) {
363 return this.subset.indexOf( filterName ) > -1;
364 };
365
366 /**
367 * Check whether this item has a potential conflict with the given item
368 *
369 * This checks whether the given item is in the list of conflicts of
370 * the current item, but makes no judgment about whether the conflict
371 * is currently at play (either one of the items may not be selected)
372 *
373 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item
374 * @return {boolean} This item has a conflict with the given item
375 */
376 mw.rcfilters.dm.FilterItem.prototype.existsInConflicts = function ( filterItem ) {
377 return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() );
378 };
379
380 /**
381 * Set the state of this filter as being conflicted
382 * (This means any filters in its conflicts are selected)
383 *
384 * @param {boolean} [conflicted] Filter is in conflict state
385 * @fires update
386 */
387 mw.rcfilters.dm.FilterItem.prototype.toggleConflicted = function ( conflicted ) {
388 conflicted = conflicted === undefined ? !this.conflicted : conflicted;
389
390 if ( this.conflicted !== conflicted ) {
391 this.conflicted = conflicted;
392 this.emit( 'update' );
393 }
394 };
395
396 /**
397 * Set the state of this filter as being already included
398 * (This means any filters in its superset are selected)
399 *
400 * @param {boolean} [included] Filter is included as part of a subset
401 * @fires update
402 */
403 mw.rcfilters.dm.FilterItem.prototype.toggleIncluded = function ( included ) {
404 included = included === undefined ? !this.included : included;
405
406 if ( this.included !== included ) {
407 this.included = included;
408 this.emit( 'update' );
409 }
410 };
411
412 /**
413 * Toggle the selected state of the item
414 *
415 * @param {boolean} [isSelected] Filter is selected
416 * @fires update
417 */
418 mw.rcfilters.dm.FilterItem.prototype.toggleSelected = function ( isSelected ) {
419 isSelected = isSelected === undefined ? !this.selected : isSelected;
420
421 if ( this.selected !== isSelected ) {
422 this.selected = isSelected;
423 this.emit( 'update' );
424 }
425 };
426
427 /**
428 * Toggle the fully covered state of the item
429 *
430 * @param {boolean} [isFullyCovered] Filter is fully covered
431 * @fires update
432 */
433 mw.rcfilters.dm.FilterItem.prototype.toggleFullyCovered = function ( isFullyCovered ) {
434 isFullyCovered = isFullyCovered === undefined ? !this.fullycovered : isFullyCovered;
435
436 if ( this.fullyCovered !== isFullyCovered ) {
437 this.fullyCovered = isFullyCovered;
438 this.emit( 'update' );
439 }
440 };
441
442 /**
443 * Set the highlight color
444 *
445 * @param {string|null} highlightColor
446 */
447 mw.rcfilters.dm.FilterItem.prototype.setHighlightColor = function ( highlightColor ) {
448 if ( this.highlightColor !== highlightColor ) {
449 this.highlightColor = highlightColor;
450 this.emit( 'update' );
451 }
452 };
453
454 /**
455 * Clear the highlight color
456 */
457 mw.rcfilters.dm.FilterItem.prototype.clearHighlightColor = function () {
458 this.setHighlightColor( null );
459 };
460
461 /**
462 * Get the highlight color, or null if none is configured
463 *
464 * @return {string|null}
465 */
466 mw.rcfilters.dm.FilterItem.prototype.getHighlightColor = function () {
467 return this.highlightColor;
468 };
469
470 /**
471 * Get the CSS class that matches changes that fit this filter
472 * or null if none is configured
473 *
474 * @return {string|null}
475 */
476 mw.rcfilters.dm.FilterItem.prototype.getCssClass = function () {
477 return this.cssClass;
478 };
479
480 /**
481 * Toggle the highlight feature on and off for this filter.
482 * It only works if highlight is supported for this filter.
483 *
484 * @param {boolean} enable Highlight should be enabled
485 */
486 mw.rcfilters.dm.FilterItem.prototype.toggleHighlight = function ( enable ) {
487 enable = enable === undefined ? !this.highlightEnabled : enable;
488
489 if ( !this.isHighlightSupported() ) {
490 return;
491 }
492
493 if ( enable === this.highlightEnabled ) {
494 return;
495 }
496
497 this.highlightEnabled = enable;
498 this.emit( 'update' );
499 };
500
501 /**
502 * Check if the highlight feature is currently enabled for this filter
503 *
504 * @return {boolean}
505 */
506 mw.rcfilters.dm.FilterItem.prototype.isHighlightEnabled = function () {
507 return !!this.highlightEnabled;
508 };
509
510 /**
511 * Check if the highlight feature is supported for this filter
512 *
513 * @return {boolean}
514 */
515 mw.rcfilters.dm.FilterItem.prototype.isHighlightSupported = function () {
516 return !!this.getCssClass();
517 };
518
519 /**
520 * Check if the filter is currently highlighted
521 *
522 * @return {boolean}
523 */
524 mw.rcfilters.dm.FilterItem.prototype.isHighlighted = function () {
525 return this.isHighlightEnabled() && !!this.getHighlightColor();
526 };
527 }( mediaWiki ) );