Prepare for REL1_33 cut, labelling master as 1.34-alpha
[lhc/web/wiklou.git] / resources / src / mediawiki.rcfilters / dm / FilterItem.js
1 ( function () {
2 var ItemModel = require( './ItemModel.js' ),
3 FilterItem;
4
5 /**
6 * Filter item model
7 *
8 * @class mw.rcfilters.dm.FilterItem
9 * @extends mw.rcfilters.dm.ItemModel
10 *
11 * @constructor
12 * @param {string} param Filter param name
13 * @param {mw.rcfilters.dm.FilterGroup} groupModel Filter group model
14 * @param {Object} config Configuration object
15 * @cfg {string[]} [excludes=[]] A list of filter names this filter, if
16 * selected, makes inactive.
17 * @cfg {string[]} [subset] Defining the names of filters that are a subset of this filter
18 * @cfg {Object} [conflicts] Defines the conflicts for this filter
19 * @cfg {boolean} [visible=true] The visibility of the group
20 */
21 FilterItem = function MwRcfiltersDmFilterItem( param, groupModel, config ) {
22 config = config || {};
23
24 this.groupModel = groupModel;
25
26 // Parent
27 FilterItem.parent.call( this, param, $.extend( {
28 namePrefix: this.groupModel.getNamePrefix()
29 }, config ) );
30 // Mixin constructor
31 OO.EventEmitter.call( this );
32
33 // Interaction definitions
34 this.subset = config.subset || [];
35 this.conflicts = config.conflicts || {};
36 this.superset = [];
37 this.visible = config.visible === undefined ? true : !!config.visible;
38
39 // Interaction states
40 this.included = false;
41 this.conflicted = false;
42 this.fullyCovered = false;
43 };
44
45 /* Initialization */
46
47 OO.inheritClass( FilterItem, ItemModel );
48
49 /* Methods */
50
51 /**
52 * Return the representation of the state of this item.
53 *
54 * @return {Object} State of the object
55 */
56 FilterItem.prototype.getState = function () {
57 return {
58 selected: this.isSelected(),
59 included: this.isIncluded(),
60 conflicted: this.isConflicted(),
61 fullyCovered: this.isFullyCovered()
62 };
63 };
64
65 /**
66 * Get the message for the display area for the currently active conflict
67 *
68 * @private
69 * @return {string} Conflict result message key
70 */
71 FilterItem.prototype.getCurrentConflictResultMessage = function () {
72 var details = {};
73
74 // First look in filter's own conflicts
75 details = this.getConflictDetails( this.getOwnConflicts(), 'globalDescription' );
76 if ( !details.message ) {
77 // Fall back onto conflicts in the group
78 details = this.getConflictDetails( this.getGroupModel().getConflicts(), 'globalDescription' );
79 }
80
81 return details.message;
82 };
83
84 /**
85 * Get the details of the active conflict on this filter
86 *
87 * @private
88 * @param {Object} conflicts Conflicts to examine
89 * @param {string} [key='contextDescription'] Message key
90 * @return {Object} Object with conflict message and conflict items
91 * @return {string} return.message Conflict message
92 * @return {string[]} return.names Conflicting item labels
93 */
94 FilterItem.prototype.getConflictDetails = function ( conflicts, key ) {
95 var group,
96 conflictMessage = '',
97 itemLabels = [];
98
99 key = key || 'contextDescription';
100
101 // eslint-disable-next-line no-jquery/no-each-util
102 $.each( conflicts, function ( filterName, conflict ) {
103 if ( !conflict.item.isSelected() ) {
104 return;
105 }
106
107 if ( !conflictMessage ) {
108 conflictMessage = conflict[ key ];
109 group = conflict.group;
110 }
111
112 if ( group === conflict.group ) {
113 itemLabels.push( mw.msg( 'quotation-marks', conflict.item.getLabel() ) );
114 }
115 } );
116
117 return {
118 message: conflictMessage,
119 names: itemLabels
120 };
121
122 };
123
124 /**
125 * @inheritdoc
126 */
127 FilterItem.prototype.getStateMessage = function () {
128 var messageKey, details, superset,
129 affectingItems = [];
130
131 if ( this.isSelected() ) {
132 if ( this.isConflicted() ) {
133 // First look in filter's own conflicts
134 details = this.getConflictDetails( this.getOwnConflicts() );
135 if ( !details.message ) {
136 // Fall back onto conflicts in the group
137 details = this.getConflictDetails( this.getGroupModel().getConflicts() );
138 }
139
140 messageKey = details.message;
141 affectingItems = details.names;
142 } else if ( this.isIncluded() && !this.isHighlighted() ) {
143 // We only show the 'no effect' full-coverage message
144 // if the item is also not highlighted. See T161273
145 superset = this.getSuperset();
146 // For this message we need to collect the affecting superset
147 affectingItems = this.getGroupModel().findSelectedItems( this )
148 .filter( function ( item ) {
149 return superset.indexOf( item.getName() ) !== -1;
150 } )
151 .map( function ( item ) {
152 return mw.msg( 'quotation-marks', item.getLabel() );
153 } );
154
155 messageKey = 'rcfilters-state-message-subset';
156 } else if ( this.isFullyCovered() && !this.isHighlighted() ) {
157 affectingItems = this.getGroupModel().findSelectedItems( this )
158 .map( function ( item ) {
159 return mw.msg( 'quotation-marks', item.getLabel() );
160 } );
161
162 messageKey = 'rcfilters-state-message-fullcoverage';
163 }
164 }
165
166 if ( messageKey ) {
167 // Build message
168 return mw.msg(
169 messageKey,
170 mw.language.listToText( affectingItems ),
171 affectingItems.length
172 );
173 }
174
175 // Display description
176 return this.getDescription();
177 };
178
179 /**
180 * Get the model of the group this filter belongs to
181 *
182 * @return {mw.rcfilters.dm.FilterGroup} Filter group model
183 */
184 FilterItem.prototype.getGroupModel = function () {
185 return this.groupModel;
186 };
187
188 /**
189 * Get the group name this filter belongs to
190 *
191 * @return {string} Filter group name
192 */
193 FilterItem.prototype.getGroupName = function () {
194 return this.groupModel.getName();
195 };
196
197 /**
198 * Get filter subset
199 * This is a list of filter names that are defined to be included
200 * when this filter is selected.
201 *
202 * @return {string[]} Filter subset
203 */
204 FilterItem.prototype.getSubset = function () {
205 return this.subset;
206 };
207
208 /**
209 * Get filter superset
210 * This is a generated list of filters that define this filter
211 * to be included when either of them is selected.
212 *
213 * @return {string[]} Filter superset
214 */
215 FilterItem.prototype.getSuperset = function () {
216 return this.superset;
217 };
218
219 /**
220 * Check whether the filter is currently in a conflict state
221 *
222 * @return {boolean} Filter is in conflict state
223 */
224 FilterItem.prototype.isConflicted = function () {
225 return this.conflicted;
226 };
227
228 /**
229 * Check whether the filter is currently in an already included subset
230 *
231 * @return {boolean} Filter is in an already-included subset
232 */
233 FilterItem.prototype.isIncluded = function () {
234 return this.included;
235 };
236
237 /**
238 * Check whether the filter is currently fully covered
239 *
240 * @return {boolean} Filter is in fully-covered state
241 */
242 FilterItem.prototype.isFullyCovered = function () {
243 return this.fullyCovered;
244 };
245
246 /**
247 * Get all conflicts associated with this filter or its group
248 *
249 * Conflict object is set up by filter name keys and conflict
250 * definition. For example:
251 *
252 * {
253 * filterName: {
254 * filter: filterName,
255 * group: group1,
256 * label: itemLabel,
257 * item: itemModel
258 * }
259 * filterName2: {
260 * filter: filterName2,
261 * group: group2
262 * label: itemLabel2,
263 * item: itemModel2
264 * }
265 * }
266 *
267 * @return {Object} Filter conflicts
268 */
269 FilterItem.prototype.getConflicts = function () {
270 return $.extend( {}, this.conflicts, this.getGroupModel().getConflicts() );
271 };
272
273 /**
274 * Get the conflicts associated with this filter
275 *
276 * @return {Object} Filter conflicts
277 */
278 FilterItem.prototype.getOwnConflicts = function () {
279 return this.conflicts;
280 };
281
282 /**
283 * Set conflicts for this filter. See #getConflicts for the expected
284 * structure of the definition.
285 *
286 * @param {Object} conflicts Conflicts for this filter
287 */
288 FilterItem.prototype.setConflicts = function ( conflicts ) {
289 this.conflicts = conflicts || {};
290 };
291
292 /**
293 * Set filter superset
294 *
295 * @param {string[]} superset Filter superset
296 */
297 FilterItem.prototype.setSuperset = function ( superset ) {
298 this.superset = superset || [];
299 };
300
301 /**
302 * Set filter subset
303 *
304 * @param {string[]} subset Filter subset
305 */
306 FilterItem.prototype.setSubset = function ( subset ) {
307 this.subset = subset || [];
308 };
309
310 /**
311 * Check whether a filter exists in the subset list for this filter
312 *
313 * @param {string} filterName Filter name
314 * @return {boolean} Filter name is in the subset list
315 */
316 FilterItem.prototype.existsInSubset = function ( filterName ) {
317 return this.subset.indexOf( filterName ) > -1;
318 };
319
320 /**
321 * Check whether this item has a potential conflict with the given item
322 *
323 * This checks whether the given item is in the list of conflicts of
324 * the current item, but makes no judgment about whether the conflict
325 * is currently at play (either one of the items may not be selected)
326 *
327 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item
328 * @return {boolean} This item has a conflict with the given item
329 */
330 FilterItem.prototype.existsInConflicts = function ( filterItem ) {
331 return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() );
332 };
333
334 /**
335 * Set the state of this filter as being conflicted
336 * (This means any filters in its conflicts are selected)
337 *
338 * @param {boolean} [conflicted] Filter is in conflict state
339 * @fires update
340 */
341 FilterItem.prototype.toggleConflicted = function ( conflicted ) {
342 conflicted = conflicted === undefined ? !this.conflicted : conflicted;
343
344 if ( this.conflicted !== conflicted ) {
345 this.conflicted = conflicted;
346 this.emit( 'update' );
347 }
348 };
349
350 /**
351 * Set the state of this filter as being already included
352 * (This means any filters in its superset are selected)
353 *
354 * @param {boolean} [included] Filter is included as part of a subset
355 * @fires update
356 */
357 FilterItem.prototype.toggleIncluded = function ( included ) {
358 included = included === undefined ? !this.included : included;
359
360 if ( this.included !== included ) {
361 this.included = included;
362 this.emit( 'update' );
363 }
364 };
365
366 /**
367 * Toggle the fully covered state of the item
368 *
369 * @param {boolean} [isFullyCovered] Filter is fully covered
370 * @fires update
371 */
372 FilterItem.prototype.toggleFullyCovered = function ( isFullyCovered ) {
373 isFullyCovered = isFullyCovered === undefined ? !this.fullycovered : isFullyCovered;
374
375 if ( this.fullyCovered !== isFullyCovered ) {
376 this.fullyCovered = isFullyCovered;
377 this.emit( 'update' );
378 }
379 };
380
381 /**
382 * Toggle the visibility of this item
383 *
384 * @param {boolean} [isVisible] Item is visible
385 */
386 FilterItem.prototype.toggleVisible = function ( isVisible ) {
387 isVisible = isVisible === undefined ? !this.visible : !!isVisible;
388
389 if ( this.visible !== isVisible ) {
390 this.visible = isVisible;
391 this.emit( 'update' );
392 }
393 };
394
395 /**
396 * Check whether the item is visible
397 *
398 * @return {boolean} Item is visible
399 */
400 FilterItem.prototype.isVisible = function () {
401 return this.visible;
402 };
403
404 module.exports = FilterItem;
405
406 }() );