Add userExpLevel filter in the RCFilters UI
[lhc/web/wiklou.git] / resources / src / mediawiki.rcfilters / dm / mw.rcfilters.dm.FiltersViewModel.js
1 ( function ( mw, $ ) {
2 /**
3 * View model for the filters selection and display
4 *
5 * @mixins OO.EventEmitter
6 * @mixins OO.EmitterList
7 *
8 * @constructor
9 */
10 mw.rcfilters.dm.FiltersViewModel = function MwRcfiltersDmFiltersViewModel() {
11 // Mixin constructor
12 OO.EventEmitter.call( this );
13 OO.EmitterList.call( this );
14
15 this.groups = {};
16
17 // Events
18 this.aggregate( { update: 'itemUpdate' } );
19 };
20
21 /* Initialization */
22 OO.initClass( mw.rcfilters.dm.FiltersViewModel );
23 OO.mixinClass( mw.rcfilters.dm.FiltersViewModel, OO.EventEmitter );
24 OO.mixinClass( mw.rcfilters.dm.FiltersViewModel, OO.EmitterList );
25
26 /* Events */
27
28 /**
29 * @event initialize
30 *
31 * Filter list is initialized
32 */
33
34 /**
35 * @event itemUpdate
36 * @param {mw.rcfilters.dm.FilterItem} item Filter item updated
37 *
38 * Filter item has changed
39 */
40
41 /* Methods */
42
43 /**
44 * Set filters and preserve a group relationship based on
45 * the definition given by an object
46 *
47 * @param {Object} filters Filter group definition
48 */
49 mw.rcfilters.dm.FiltersViewModel.prototype.initializeFilters = function ( filters ) {
50 var i, filterItem,
51 model = this,
52 items = [];
53
54 // Reset
55 this.clearItems();
56 this.groups = {};
57
58 $.each( filters, function ( group, data ) {
59 model.groups[ group ] = model.groups[ group ] || {};
60 model.groups[ group ].filters = model.groups[ group ].filters || [];
61
62 model.groups[ group ].title = data.title;
63 model.groups[ group ].type = data.type;
64 model.groups[ group ].separator = data.separator || '|';
65
66 for ( i = 0; i < data.filters.length; i++ ) {
67 filterItem = new mw.rcfilters.dm.FilterItem( data.filters[ i ].name, {
68 group: group,
69 label: data.filters[ i ].label,
70 description: data.filters[ i ].description,
71 selected: data.filters[ i ].selected
72 } );
73
74 model.groups[ group ].filters.push( filterItem );
75 items.push( filterItem );
76 }
77 } );
78
79 this.addItems( items );
80 this.emit( 'initialize' );
81 };
82
83 /**
84 * Get the names of all available filters
85 *
86 * @return {string[]} An array of filter names
87 */
88 mw.rcfilters.dm.FiltersViewModel.prototype.getFilterNames = function () {
89 return this.getItems().map( function ( item ) { return item.getName(); } );
90 };
91
92 /**
93 * Get the object that defines groups and their filter items.
94 * The structure of this response:
95 * {
96 * groupName: {
97 * title: {string} Group title
98 * type: {string} Group type
99 * filters: {string[]} Filters in the group
100 * }
101 * }
102 *
103 * @return {Object} Filter groups
104 */
105 mw.rcfilters.dm.FiltersViewModel.prototype.getFilterGroups = function () {
106 return this.groups;
107 };
108
109 /**
110 * Get the current state of the filters
111 *
112 * @return {Object} Filters current state
113 */
114 mw.rcfilters.dm.FiltersViewModel.prototype.getState = function () {
115 var i,
116 items = this.getItems(),
117 result = {};
118
119 for ( i = 0; i < items.length; i++ ) {
120 result[ items[ i ].getName() ] = items[ i ].isSelected();
121 }
122
123 return result;
124 };
125
126 /**
127 * Analyze the groups and their filters and output an object representing
128 * the state of the parameters they represent.
129 *
130 * @return {Object} Parameter state object
131 */
132 mw.rcfilters.dm.FiltersViewModel.prototype.getParametersFromFilters = function () {
133 var i, filterItems, anySelected, values,
134 result = {},
135 groupItems = this.getFilterGroups();
136
137 $.each( groupItems, function ( group, data ) {
138 filterItems = data.filters;
139
140 if ( data.type === 'send_unselected_if_any' ) {
141 // First, check if any of the items are selected at all.
142 // If none is selected, we're treating it as if they are
143 // all false
144 anySelected = filterItems.some( function ( filterItem ) {
145 return filterItem.isSelected();
146 } );
147
148 // Go over the items and define the correct values
149 for ( i = 0; i < filterItems.length; i++ ) {
150 result[ filterItems[ i ].getName() ] = anySelected ?
151 Number( !filterItems[ i ].isSelected() ) : 0;
152 }
153 } else if ( data.type === 'string_options' ) {
154 values = [];
155 for ( i = 0; i < filterItems.length; i++ ) {
156 if ( filterItems[ i ].isSelected() ) {
157 values.push( filterItems[ i ].getName() );
158 }
159 }
160
161 if ( values.length === 0 || values.length === filterItems.length ) {
162 result[ group ] = 'all';
163 } else {
164 result[ group ] = values.join( data.separator );
165 }
166 }
167 } );
168
169 return result;
170 };
171
172 /**
173 * Sanitize value group of a string_option groups type
174 * Remove duplicates and make sure to only use valid
175 * values.
176 *
177 * @param {string} groupName Group name
178 * @param {string[]} valueArray Array of values
179 * @return {string[]} Array of valid values
180 */
181 mw.rcfilters.dm.FiltersViewModel.prototype.sanitizeStringOptionGroup = function( groupName, valueArray ) {
182 var result = [],
183 validNames = this.groups[ groupName ].filters.map( function ( filterItem ) {
184 return filterItem.getName();
185 } );
186
187 if ( valueArray.indexOf( 'all' ) > -1 ) {
188 // If anywhere in the values there's 'all', we
189 // treat it as if only 'all' was selected.
190 // Example: param=valid1,valid2,all
191 // Result: param=all
192 return [ 'all' ];
193 }
194
195 // Get rid of any dupe and invalid parameter, only output
196 // valid ones
197 // Example: param=valid1,valid2,invalid1,valid1
198 // Result: param=valid1,valid2
199 valueArray.forEach( function ( value ) {
200 if (
201 validNames.indexOf( value ) > -1 &&
202 result.indexOf( value ) === -1
203 ) {
204 result.push( value );
205 }
206 } );
207
208 return result;
209 };
210
211 /**
212 * This is the opposite of the #getParametersFromFilters method; this goes over
213 * the parameters and translates into a selected/unselected value in the filters.
214 *
215 * @param {Object} params Parameters query object
216 * @return {Object} Filter state object
217 */
218 mw.rcfilters.dm.FiltersViewModel.prototype.getFiltersFromParameters = function ( params ) {
219 var i, filterItem,
220 groupMap = {},
221 model = this,
222 base = this.getParametersFromFilters(),
223 // Start with current state
224 result = this.getState();
225
226 params = $.extend( {}, base, params );
227
228 $.each( params, function ( paramName, paramValue ) {
229 // Find the filter item
230 filterItem = model.getItemByName( paramName );
231 // Ignore if no filter item exists
232 if ( filterItem ) {
233 groupMap[ filterItem.getGroup() ] = groupMap[ filterItem.getGroup() ] || {};
234
235 // Mark the group if it has any items that are selected
236 groupMap[ filterItem.getGroup() ].hasSelected = (
237 groupMap[ filterItem.getGroup() ].hasSelected ||
238 !!Number( paramValue )
239 );
240
241 // Add the relevant filter into the group map
242 groupMap[ filterItem.getGroup() ].filters = groupMap[ filterItem.getGroup() ].filters || [];
243 groupMap[ filterItem.getGroup() ].filters.push( filterItem );
244 } else if ( model.groups.hasOwnProperty( paramName ) ) {
245 // This parameter represents a group (values are the filters)
246 // this is equivalent to checking if the group is 'string_options'
247 groupMap[ paramName ] = { filters: model.groups[ paramName ].filters };
248 }
249 } );
250
251 // Now that we know the groups' selection states, we need to go over
252 // the filters in the groups and mark their selected states appropriately
253 $.each( groupMap, function ( group, data ) {
254 var paramValues, filterItem,
255 allItemsInGroup = data.filters;
256
257 if ( model.groups[ group ].type === 'send_unselected_if_any' ) {
258 for ( i = 0; i < allItemsInGroup.length; i++ ) {
259 filterItem = allItemsInGroup[ i ];
260
261 result[ filterItem.getName() ] = data.hasSelected ?
262 // Flip the definition between the parameter
263 // state and the filter state
264 // This is what the 'toggleSelected' value of the filter is
265 !Number( params[ filterItem.getName() ] ) :
266 // Otherwise, there are no selected items in the
267 // group, which means the state is false
268 false;
269 }
270 } else if ( model.groups[ group ].type === 'string_options' ) {
271 paramValues = model.sanitizeStringOptionGroup( group, params[ group ].split( model.groups[ group ].separator ) );
272
273 for ( i = 0; i < allItemsInGroup.length; i++ ) {
274 filterItem = allItemsInGroup[ i ];
275
276 result[ filterItem.getName() ] = (
277 // If it is the word 'all'
278 paramValues.length === 1 && paramValues[ 0 ] === 'all' ||
279 // All values are written
280 paramValues.length === model.groups[ group ].filters.length
281 ) ?
282 // All true (either because all values are written or the term 'all' is written)
283 // is the same as all filters set to false
284 false :
285 // Otherwise, the filter is selected only if it appears in the parameter values
286 paramValues.indexOf( filterItem.getName() ) > -1;
287 }
288 }
289 } );
290 return result;
291 };
292
293 /**
294 * Get the item that matches the given name
295 *
296 * @param {string} name Filter name
297 * @return {mw.rcfilters.dm.FilterItem} Filter item
298 */
299 mw.rcfilters.dm.FiltersViewModel.prototype.getItemByName = function ( name ) {
300 return this.getItems().filter( function ( item ) {
301 return name === item.getName();
302 } )[ 0 ];
303 };
304
305 /**
306 * Toggle selected state of items by their names
307 *
308 * @param {Object} filterDef Filter definitions
309 */
310 mw.rcfilters.dm.FiltersViewModel.prototype.updateFilters = function ( filterDef ) {
311 var name, filterItem;
312
313 for ( name in filterDef ) {
314 filterItem = this.getItemByName( name );
315 filterItem.toggleSelected( filterDef[ name ] );
316 }
317 };
318
319 /**
320 * Find items whose labels match the given string
321 *
322 * @param {string} str Search string
323 * @return {Object} An object of items to show
324 * arranged by their group names
325 */
326 mw.rcfilters.dm.FiltersViewModel.prototype.findMatches = function ( str ) {
327 var i,
328 result = {},
329 items = this.getItems();
330
331 // Normalize so we can search strings regardless of case
332 str = str.toLowerCase();
333 for ( i = 0; i < items.length; i++ ) {
334 if ( items[ i ].getLabel().toLowerCase().indexOf( str ) > -1 ) {
335 result[ items[ i ].getGroup() ] = result[ items[ i ].getGroup() ] || [];
336 result[ items[ i ].getGroup() ].push( items[ i ] );
337 }
338 }
339 return result;
340 };
341
342 }( mediaWiki, jQuery ) );