Merge "Added release notes for 'ContentHandler::runLegacyHooks' removal"
[mediawiki.git] / resources / src / mediawiki.rcfilters / dm / mw.rcfilters.dm.FiltersViewModel.js
blob3217d0d7d416fbd8e4164b93414a339cf7b3dda1
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 );
15                 this.groups = {};
17                 // Events
18                 this.aggregate( { update: 'itemUpdate' } );
19         };
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 );
26         /* Events */
28         /**
29          * @event initialize
30          *
31          * Filter list is initialized
32          */
34         /**
35          * @event itemUpdate
36          * @param {mw.rcfilters.dm.FilterItem} item Filter item updated
37          *
38          * Filter item has changed
39          */
41         /* Methods */
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 = [];
54                 // Reset
55                 this.clearItems();
56                 this.groups = {};
58                 $.each( filters, function ( group, data ) {
59                         model.groups[ group ] = model.groups[ group ] || {};
60                         model.groups[ group ].filters = model.groups[ group ].filters || [];
62                         model.groups[ group ].title = data.title;
63                         model.groups[ group ].type = data.type;
64                         model.groups[ group ].separator = data.separator || '|';
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                                 } );
74                                 model.groups[ group ].filters.push( filterItem );
75                                 items.push( filterItem );
76                         }
77                 } );
79                 this.addItems( items );
80                 this.emit( 'initialize' );
81         };
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         };
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         };
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 = {};
119                 for ( i = 0; i < items.length; i++ ) {
120                         result[ items[ i ].getName() ] = items[ i ].isSelected();
121                 }
123                 return result;
124         };
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();
137                 $.each( groupItems, function ( group, data ) {
138                         filterItems = data.filters;
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                                 } );
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                                 }
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                 } );
169                 return result;
170         };
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                         } );
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                 }
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                 } );
208                 return result;
209         };
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();
226                 params = $.extend( {}, base, params );
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() ] || {};
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                                 );
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                 } );
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;
257                         if ( model.groups[ group ].type === 'send_unselected_if_any' ) {
258                                 for ( i = 0; i < allItemsInGroup.length; i++ ) {
259                                         filterItem = allItemsInGroup[ i ];
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 ) );
273                                 for ( i = 0; i < allItemsInGroup.length; i++ ) {
274                                         filterItem = allItemsInGroup[ i ];
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         };
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         };
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;
313                 for ( name in filterDef ) {
314                         filterItem = this.getItemByName( name );
315                         filterItem.toggleSelected( filterDef[ name ] );
316                 }
317         };
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();
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         };
342 }( mediaWiki, jQuery ) );