Merge "Skin: Make skins aware of their registered skin name"
[lhc/web/wiklou.git] / resources / src / mediawiki.rcfilters / dm / mw.rcfilters.dm.SavedQueriesModel.js
1 ( function ( mw, $ ) {
2 /**
3 * View model for saved queries
4 *
5 * @class
6 * @mixins OO.EventEmitter
7 * @mixins OO.EmitterList
8 *
9 * @constructor
10 * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters model
11 * @param {Object} [config] Configuration options
12 * @cfg {string} [default] Default query ID
13 */
14 mw.rcfilters.dm.SavedQueriesModel = function MwRcfiltersDmSavedQueriesModel( filtersModel, config ) {
15 config = config || {};
16
17 // Mixin constructor
18 OO.EventEmitter.call( this );
19 OO.EmitterList.call( this );
20
21 this.default = config.default;
22 this.filtersModel = filtersModel;
23 this.converted = false;
24
25 // Events
26 this.aggregate( { update: 'itemUpdate' } );
27 };
28
29 /* Initialization */
30
31 OO.initClass( mw.rcfilters.dm.SavedQueriesModel );
32 OO.mixinClass( mw.rcfilters.dm.SavedQueriesModel, OO.EventEmitter );
33 OO.mixinClass( mw.rcfilters.dm.SavedQueriesModel, OO.EmitterList );
34
35 /* Events */
36
37 /**
38 * @event initialize
39 *
40 * Model is initialized
41 */
42
43 /**
44 * @event itemUpdate
45 * @param {mw.rcfilters.dm.SavedQueryItemModel} Changed item
46 *
47 * An item has changed
48 */
49
50 /**
51 * @event default
52 * @param {string} New default ID
53 *
54 * The default has changed
55 */
56
57 /* Methods */
58
59 /**
60 * Initialize the saved queries model by reading it from the user's settings.
61 * The structure of the saved queries is:
62 * {
63 * version: (string) Version number; if version 2, the query represents
64 * parameters. Otherwise, the older version represented filters
65 * and needs to be readjusted,
66 * default: (string) Query ID
67 * queries:{
68 * query_id_1: {
69 * data:{
70 * filters: (Object) Minimal definition of the filters
71 * highlights: (Object) Definition of the highlights
72 * },
73 * label: (optional) Name of this query
74 * }
75 * }
76 * }
77 *
78 * @param {Object} [savedQueries] An object with the saved queries with
79 * the above structure.
80 * @fires initialize
81 */
82 mw.rcfilters.dm.SavedQueriesModel.prototype.initialize = function ( savedQueries ) {
83 var model = this,
84 excludedParams = this.filtersModel.getExcludedParams();
85
86 savedQueries = savedQueries || {};
87
88 this.clearItems();
89 this.default = null;
90 this.converted = false;
91
92 if ( savedQueries.version !== '2' ) {
93 // Old version dealt with filter names. We need to migrate to the new structure
94 // The new structure:
95 // {
96 // version: (string) '2',
97 // default: (string) Query ID,
98 // queries: {
99 // query_id: {
100 // label: (string) Name of the query
101 // data: {
102 // params: (object) Representing all the parameter states
103 // highlights: (object) Representing all the filter highlight states
104 // }
105 // }
106 // }
107 $.each( savedQueries.queries || {}, function ( id, obj ) {
108 if ( obj.data && obj.data.filters ) {
109 obj.data = model.convertToParameters( obj.data );
110 }
111 } );
112
113 this.converted = true;
114 savedQueries.version = '2';
115 }
116
117 // Initialize the query items
118 $.each( savedQueries.queries || {}, function ( id, obj ) {
119 var normalizedData = obj.data,
120 isDefault = String( savedQueries.default ) === String( id );
121
122 if ( normalizedData && normalizedData.params ) {
123 // Backwards-compat fix: Remove excluded parameters from
124 // the given data, if they exist
125 excludedParams.forEach( function ( name ) {
126 delete normalizedData.params[ name ];
127 } );
128
129 id = String( id );
130 model.addNewQuery( obj.label, normalizedData, isDefault, id );
131
132 if ( isDefault ) {
133 model.default = id;
134 }
135 }
136 } );
137
138 this.emit( 'initialize' );
139 };
140
141 /**
142 * Convert from representation of filters to representation of parameters
143 *
144 * @param {Object} data Query data
145 * @return {Object} New converted query data
146 */
147 mw.rcfilters.dm.SavedQueriesModel.prototype.convertToParameters = function ( data ) {
148 var newData = {},
149 defaultFilters = this.filtersModel.getFiltersFromParameters( this.filtersModel.getDefaultParams() ),
150 fullFilterRepresentation = $.extend( true, {}, defaultFilters, data.filters ),
151 highlightEnabled = data.highlights.highlight;
152
153 delete data.highlights.highlight;
154
155 // Filters
156 newData.params = this.filtersModel.getParametersFromFilters( fullFilterRepresentation );
157
158 // Highlights (taking out 'highlight' itself, appending _color to keys)
159 newData.highlights = {};
160 Object.keys( data.highlights ).forEach( function ( highlightedFilterName ) {
161 newData.highlights[ highlightedFilterName + '_color' ] = data.highlights[ highlightedFilterName ];
162 } );
163
164 // Add highlight
165 newData.params.highlight = String( Number( highlightEnabled || 0 ) );
166
167 return newData;
168 };
169
170 /**
171 * Get an object representing the base state of parameters
172 * and highlights.
173 *
174 * This is meant to make sure that the saved queries that are
175 * in memory are always the same structure as what we would get
176 * by calling the current model's "getSelectedState" and by checking
177 * highlight items.
178 *
179 * In cases where a user saved a query when the system had a certain
180 * set of params, and then a filter was added to the system, we want
181 * to make sure that the stored queries can still be comparable to
182 * the current state, which means that we need the base state for
183 * two operations:
184 *
185 * - Saved queries are stored in "minimal" view (only changed params
186 * are stored); When we initialize the system, we merge each minimal
187 * query with the base state (using 'getMinimalParamList') so all
188 * saved queries have the exact same structure as what we would get
189 * by checking the getSelectedState of the filter.
190 * - When we save the queries, we minimize the object to only represent
191 * whatever has actually changed, rather than store the entire
192 * object. To check what actually is different so we can store it,
193 * we need to obtain a base state to compare against, this is
194 * what #getMinimalParamList does
195 *
196 * @return {Object} Base parameter state
197 */
198 mw.rcfilters.dm.SavedQueriesModel.prototype.getBaseParamState = function () {
199 var allParams,
200 highlightedItems = {};
201
202 if ( !this.baseParamState ) {
203 allParams = this.filtersModel.getParametersFromFilters( {} );
204
205 // Prepare highlights
206 this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) {
207 highlightedItems[ item.getName() + '_color' ] = null;
208 } );
209
210 this.baseParamState = {
211 params: $.extend( true, { highlight: '0' }, allParams ),
212 highlights: highlightedItems
213 };
214 }
215
216 return this.baseParamState;
217 };
218
219 /**
220 * Get an object that holds only the parameters and highlights that have
221 * values different than the base value.
222 *
223 * This is the reverse of the normalization we do initially on loading and
224 * initializing the saved queries model.
225 *
226 * @param {Object} valuesObject Object representing the state of both
227 * filters and highlights in its normalized version, to be minimized.
228 * @return {Object} Minimal filters and highlights list
229 */
230 mw.rcfilters.dm.SavedQueriesModel.prototype.getMinimalParamList = function ( valuesObject ) {
231 var result = { params: {}, highlights: {} },
232 baseState = this.getBaseParamState();
233
234 // XOR results
235 $.each( valuesObject.params, function ( name, value ) {
236 if ( baseState.params !== undefined && baseState.params[ name ] !== value ) {
237 result.params[ name ] = value;
238 }
239 } );
240
241 $.each( valuesObject.highlights, function ( name, value ) {
242 if ( baseState.highlights !== undefined && baseState.highlights[ name ] !== value ) {
243 result.highlights[ name ] = value;
244 }
245 } );
246
247 return result;
248 };
249
250 /**
251 * Add a query item
252 *
253 * @param {string} label Label for the new query
254 * @param {Object} data Data for the new query
255 * @param {boolean} isDefault Item is default
256 * @param {string} [id] Query ID, if exists. If this isn't given, a random
257 * new ID will be created.
258 * @return {string} ID of the newly added query
259 */
260 mw.rcfilters.dm.SavedQueriesModel.prototype.addNewQuery = function ( label, data, isDefault, id ) {
261 var randomID = String( id || ( new Date() ).getTime() ),
262 normalizedData = this.getMinimalParamList( data );
263
264 // Add item
265 this.addItems( [
266 new mw.rcfilters.dm.SavedQueryItemModel(
267 randomID,
268 label,
269 normalizedData,
270 { 'default': isDefault }
271 )
272 ] );
273
274 if ( isDefault ) {
275 this.setDefault( randomID );
276 }
277
278 return randomID;
279 };
280
281 /**
282 * Remove query from model
283 *
284 * @param {string} queryID Query ID
285 */
286 mw.rcfilters.dm.SavedQueriesModel.prototype.removeQuery = function ( queryID ) {
287 var query = this.getItemByID( queryID );
288
289 if ( query ) {
290 // Check if this item was the default
291 if ( String( this.getDefault() ) === String( queryID ) ) {
292 // Nulify the default
293 this.setDefault( null );
294 }
295
296 this.removeItems( [ query ] );
297 }
298 };
299
300 /**
301 * Get an item that matches the requested query
302 *
303 * @param {Object} fullQueryComparison Object representing all filters and highlights to compare
304 * @return {mw.rcfilters.dm.SavedQueryItemModel} Matching item model
305 */
306 mw.rcfilters.dm.SavedQueriesModel.prototype.findMatchingQuery = function ( fullQueryComparison ) {
307 // Minimize before comparison
308 fullQueryComparison = this.getMinimalParamList( fullQueryComparison );
309
310 return this.getItems().filter( function ( item ) {
311 return OO.compare(
312 item.getData(),
313 fullQueryComparison
314 );
315 } )[ 0 ];
316 };
317
318 /**
319 * Get query by its identifier
320 *
321 * @param {string} queryID Query identifier
322 * @return {mw.rcfilters.dm.SavedQueryItemModel|undefined} Item matching
323 * the search. Undefined if not found.
324 */
325 mw.rcfilters.dm.SavedQueriesModel.prototype.getItemByID = function ( queryID ) {
326 return this.getItems().filter( function ( item ) {
327 return item.getID() === queryID;
328 } )[ 0 ];
329 };
330
331 /**
332 * Get an item's full data
333 *
334 * @param {string} queryID Query identifier
335 * @return {Object} Item's full data
336 */
337 mw.rcfilters.dm.SavedQueriesModel.prototype.getItemFullData = function ( queryID ) {
338 var item = this.getItemByID( queryID );
339
340 // Fill in the base params
341 return item ? $.extend( true, {}, this.getBaseParamState(), item.getData() ) : {};
342 };
343
344 /**
345 * Get the object representing the state of the entire model and items
346 *
347 * @return {Object} Object representing the state of the model and items
348 */
349 mw.rcfilters.dm.SavedQueriesModel.prototype.getState = function () {
350 var model = this,
351 obj = { queries: {}, version: '2' };
352
353 // Translate the items to the saved object
354 this.getItems().forEach( function ( item ) {
355 var itemState = item.getState();
356
357 itemState.data = model.getMinimalParamList( itemState.data );
358
359 obj.queries[ item.getID() ] = itemState;
360 } );
361
362 if ( this.getDefault() ) {
363 obj.default = this.getDefault();
364 }
365
366 return obj;
367 };
368
369 /**
370 * Set a default query. Null to unset default.
371 *
372 * @param {string} itemID Query identifier
373 * @fires default
374 */
375 mw.rcfilters.dm.SavedQueriesModel.prototype.setDefault = function ( itemID ) {
376 if ( this.default !== itemID ) {
377 this.default = itemID;
378
379 // Set for individual itens
380 this.getItems().forEach( function ( item ) {
381 item.toggleDefault( item.getID() === itemID );
382 } );
383
384 this.emit( 'default', itemID );
385 }
386 };
387
388 /**
389 * Get the default query ID
390 *
391 * @return {string} Default query identifier
392 */
393 mw.rcfilters.dm.SavedQueriesModel.prototype.getDefault = function () {
394 return this.default;
395 };
396
397 /**
398 * Check if the saved queries were converted
399 *
400 * @return {boolean} Saved queries were converted from the previous
401 * version to the new version
402 */
403 mw.rcfilters.dm.SavedQueriesModel.prototype.isConverted = function () {
404 return this.converted;
405 };
406 }( mediaWiki, jQuery ) );