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