Merge "ApiSandbox: Specify a $overlay for menu-using widgets"
[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} [title] Group title
13 * @cfg {string} [separator='|'] Value separator for 'string_options' groups
14 * @cfg {boolean} [active] Group is active
15 * @cfg {boolean} [fullCoverage] This filters in this group collectively cover all results
16 * @cfg {Object} [conflicts] Defines the conflicts for this filter group
17 * @cfg {Object} [whatsThis] Defines the messages that should appear for the 'what's this' popup
18 * @cfg {string} [whatsThis.header] The header of the whatsThis popup message
19 * @cfg {string} [whatsThis.body] The body of the whatsThis popup message
20 * @cfg {string} [whatsThis.url] The url for the link in the whatsThis popup message
21 * @cfg {string} [whatsThis.linkMessage] The text for the link in the whatsThis popup message
22 */
23 mw.rcfilters.dm.FilterGroup = function MwRcfiltersDmFilterGroup( name, config ) {
24 config = config || {};
25
26 // Mixin constructor
27 OO.EventEmitter.call( this );
28 OO.EmitterList.call( this );
29
30 this.name = name;
31 this.type = config.type || 'send_unselected_if_any';
32 this.title = config.title;
33 this.separator = config.separator || '|';
34
35 this.active = !!config.active;
36 this.fullCoverage = !!config.fullCoverage;
37
38 this.whatsThis = config.whatsThis || {};
39
40 this.conflicts = config.conflicts || {};
41
42 this.aggregate( { update: 'filterItemUpdate' } );
43 this.connect( this, { filterItemUpdate: 'onFilterItemUpdate' } );
44 };
45
46 /* Initialization */
47 OO.initClass( mw.rcfilters.dm.FilterGroup );
48 OO.mixinClass( mw.rcfilters.dm.FilterGroup, OO.EventEmitter );
49 OO.mixinClass( mw.rcfilters.dm.FilterGroup, OO.EmitterList );
50
51 /* Events */
52
53 /**
54 * @event update
55 *
56 * Group state has been updated
57 */
58
59 /* Methods */
60
61 /**
62 * Respond to filterItem update event
63 *
64 * @fires update
65 */
66 mw.rcfilters.dm.FilterGroup.prototype.onFilterItemUpdate = function () {
67 // Update state
68 var active = this.areAnySelected();
69
70 if ( this.active !== active ) {
71 this.active = active;
72 this.emit( 'update' );
73 }
74 };
75
76 /**
77 * Get group active state
78 *
79 * @return {boolean} Active state
80 */
81 mw.rcfilters.dm.FilterGroup.prototype.isActive = function () {
82 return this.active;
83 };
84
85 /**
86 * Get group name
87 *
88 * @return {string} Group name
89 */
90 mw.rcfilters.dm.FilterGroup.prototype.getName = function () {
91 return this.name;
92 };
93
94 /**
95 * Get the messags defining the 'whats this' popup for this group
96 *
97 * @return {Object} What's this messages
98 */
99 mw.rcfilters.dm.FilterGroup.prototype.getWhatsThis = function () {
100 return this.whatsThis;
101 };
102
103 /**
104 * Check whether this group has a 'what's this' message
105 *
106 * @return {boolean} This group has a what's this message
107 */
108 mw.rcfilters.dm.FilterGroup.prototype.hasWhatsThis = function () {
109 return !!this.whatsThis.body;
110 };
111
112 /**
113 * Get the conflicts associated with the entire group.
114 * Conflict object is set up by filter name keys and conflict
115 * definition. For example:
116 * [
117 * {
118 * filterName: {
119 * filter: filterName,
120 * group: group1
121 * }
122 * },
123 * {
124 * filterName2: {
125 * filter: filterName2,
126 * group: group2
127 * }
128 * }
129 * ]
130 * @return {Object} Conflict definition
131 */
132 mw.rcfilters.dm.FilterGroup.prototype.getConflicts = function () {
133 return this.conflicts;
134 };
135
136 /**
137 * Set conflicts for this group. See #getConflicts for the expected
138 * structure of the definition.
139 *
140 * @param {Object} conflicts Conflicts for this group
141 */
142 mw.rcfilters.dm.FilterGroup.prototype.setConflicts = function ( conflicts ) {
143 this.conflicts = conflicts;
144 };
145
146 /**
147 * Check whether this item has a potential conflict with the given item
148 *
149 * This checks whether the given item is in the list of conflicts of
150 * the current item, but makes no judgment about whether the conflict
151 * is currently at play (either one of the items may not be selected)
152 *
153 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item
154 * @return {boolean} This item has a conflict with the given item
155 */
156 mw.rcfilters.dm.FilterGroup.prototype.existsInConflicts = function ( filterItem ) {
157 return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() );
158 };
159
160 /**
161 * Check whether there are any items selected
162 *
163 * @return {boolean} Any items in the group are selected
164 */
165 mw.rcfilters.dm.FilterGroup.prototype.areAnySelected = function () {
166 return this.getItems().some( function ( filterItem ) {
167 return filterItem.isSelected();
168 } );
169 };
170
171 /**
172 * Check whether all items selected
173 *
174 * @return {boolean} All items are selected
175 */
176 mw.rcfilters.dm.FilterGroup.prototype.areAllSelected = function () {
177 var selected = [],
178 unselected = [];
179
180 this.getItems().forEach( function ( filterItem ) {
181 if ( filterItem.isSelected() ) {
182 selected.push( filterItem );
183 } else {
184 unselected.push( filterItem );
185 }
186 } );
187
188 if ( unselected.length === 0 ) {
189 return true;
190 }
191
192 // check if every unselected is a subset of a selected
193 return unselected.every( function ( unselectedFilterItem ) {
194 return selected.some( function ( selectedFilterItem ) {
195 return selectedFilterItem.existsInSubset( unselectedFilterItem.getName() );
196 } );
197 } );
198 };
199
200 /**
201 * Get all selected items in this group
202 *
203 * @param {mw.rcfilters.dm.FilterItem} [excludeItem] Item to exclude from the list
204 * @return {mw.rcfilters.dm.FilterItem[]} Selected items
205 */
206 mw.rcfilters.dm.FilterGroup.prototype.getSelectedItems = function ( excludeItem ) {
207 var excludeName = ( excludeItem && excludeItem.getName() ) || '';
208
209 return this.getItems().filter( function ( item ) {
210 return item.getName() !== excludeName && item.isSelected();
211 } );
212 };
213
214 /**
215 * Check whether all selected items are in conflict with the given item
216 *
217 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
218 * @return {boolean} All selected items are in conflict with this item
219 */
220 mw.rcfilters.dm.FilterGroup.prototype.areAllSelectedInConflictWith = function ( filterItem ) {
221 var selectedItems = this.getSelectedItems( filterItem );
222
223 return selectedItems.length > 0 &&
224 (
225 // The group as a whole is in conflict with this item
226 this.existsInConflicts( filterItem ) ||
227 // All selected items are in conflict individually
228 selectedItems.every( function ( selectedFilter ) {
229 return selectedFilter.existsInConflicts( filterItem );
230 } )
231 );
232 };
233
234 /**
235 * Check whether any of the selected items are in conflict with the given item
236 *
237 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
238 * @return {boolean} Any of the selected items are in conflict with this item
239 */
240 mw.rcfilters.dm.FilterGroup.prototype.areAnySelectedInConflictWith = function ( filterItem ) {
241 var selectedItems = this.getSelectedItems( filterItem );
242
243 return selectedItems.length > 0 && (
244 // The group as a whole is in conflict with this item
245 this.existsInConflicts( filterItem ) ||
246 // Any selected items are in conflict individually
247 selectedItems.some( function ( selectedFilter ) {
248 return selectedFilter.existsInConflicts( filterItem );
249 } )
250 );
251 };
252
253 /**
254 * Get the parameter representation from this group
255 *
256 * @return {Object} Parameter representation
257 */
258 mw.rcfilters.dm.FilterGroup.prototype.getParamRepresentation = function () {
259 var i, values,
260 result = {},
261 filterItems = this.getItems();
262
263 if ( this.getType() === 'send_unselected_if_any' ) {
264 // First, check if any of the items are selected at all.
265 // If none is selected, we're treating it as if they are
266 // all false
267
268 // Go over the items and define the correct values
269 for ( i = 0; i < filterItems.length; i++ ) {
270 result[ filterItems[ i ].getParamName() ] = this.areAnySelected() ?
271 Number( !filterItems[ i ].isSelected() ) : 0;
272 }
273
274 } else if ( this.getType() === 'string_options' ) {
275 values = [];
276 for ( i = 0; i < filterItems.length; i++ ) {
277 if ( filterItems[ i ].isSelected() ) {
278 values.push( filterItems[ i ].getParamName() );
279 }
280 }
281
282 result[ this.getName() ] = ( values.length === filterItems.length ) ?
283 'all' : values.join( this.getSeparator() );
284 }
285
286 return result;
287 };
288
289 /**
290 * Get group type
291 *
292 * @return {string} Group type
293 */
294 mw.rcfilters.dm.FilterGroup.prototype.getType = function () {
295 return this.type;
296 };
297
298 /**
299 * Get the prefix used for the filter names inside this group
300 *
301 * @return {string} Group prefix
302 */
303 mw.rcfilters.dm.FilterGroup.prototype.getNamePrefix = function () {
304 return this.getName() + '__';
305 };
306
307 /**
308 * Get group's title
309 *
310 * @return {string} Title
311 */
312 mw.rcfilters.dm.FilterGroup.prototype.getTitle = function () {
313 return this.title;
314 };
315
316 /**
317 * Get group's values separator
318 *
319 * @return {string} Values separator
320 */
321 mw.rcfilters.dm.FilterGroup.prototype.getSeparator = function () {
322 return this.separator;
323 };
324
325 /**
326 * Check whether the group is defined as full coverage
327 *
328 * @return {boolean} Group is full coverage
329 */
330 mw.rcfilters.dm.FilterGroup.prototype.isFullCoverage = function () {
331 return this.fullCoverage;
332 };
333 }( mediaWiki ) );