Merge "ResourceLoaderOOUIImageModule: Actually load non-default themes' images"
[lhc/web/wiklou.git] / resources / src / mediawiki.rcfilters / mw.rcfilters.Controller.js
1 ( function ( mw, $ ) {
2 /**
3 * Controller for the filters in Recent Changes
4 *
5 * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters view model
6 * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel Changes list view model
7 */
8 mw.rcfilters.Controller = function MwRcfiltersController( filtersModel, changesListModel ) {
9 this.filtersModel = filtersModel;
10 this.changesListModel = changesListModel;
11 this.requestCounter = 0;
12 };
13
14 /* Initialization */
15 OO.initClass( mw.rcfilters.Controller );
16
17 /**
18 * Initialize the filter and parameter states
19 *
20 * @param {Array} filterStructure Filter definition and structure for the model
21 */
22 mw.rcfilters.Controller.prototype.initialize = function ( filterStructure ) {
23 var $changesList = $( '.mw-changeslist' ).first().contents();
24 // Initialize the model
25 this.filtersModel.initializeFilters( filterStructure );
26 this.updateStateBasedOnUrl();
27
28 // Update the changes list with the existing data
29 // so it gets processed
30 this.changesListModel.update(
31 $changesList.length ? $changesList : 'NO_RESULTS',
32 $( 'fieldset.rcoptions' ).first()
33 );
34
35 };
36
37 /**
38 * Update filter state (selection and highlighting) based
39 * on current URL and default values.
40 */
41 mw.rcfilters.Controller.prototype.updateStateBasedOnUrl = function () {
42 var uri = new mw.Uri();
43
44 // Set filter states based on defaults and URL params
45 this.filtersModel.toggleFiltersSelected(
46 this.filtersModel.getFiltersFromParameters(
47 // Merge defaults with URL params for initialization
48 $.extend(
49 true,
50 {},
51 this.filtersModel.getDefaultParams(),
52 // URI query overrides defaults
53 uri.query
54 )
55 )
56 );
57
58 // Initialize highlights
59 this.filtersModel.toggleHighlight( !!uri.query.highlight );
60 this.filtersModel.getItems().forEach( function ( filterItem ) {
61 var color = uri.query[ filterItem.getName() + '_color' ];
62 if ( color ) {
63 filterItem.setHighlightColor( color );
64 } else {
65 filterItem.clearHighlightColor();
66 }
67 } );
68
69 // Check all filter interactions
70 this.filtersModel.reassessFilterInteractions();
71 };
72
73 /**
74 * Reset to default filters
75 */
76 mw.rcfilters.Controller.prototype.resetToDefaults = function () {
77 this.filtersModel.setFiltersToDefaults();
78 this.filtersModel.clearAllHighlightColors();
79 // Check all filter interactions
80 this.filtersModel.reassessFilterInteractions();
81
82 this.updateChangesList();
83 };
84
85 /**
86 * Empty all selected filters
87 */
88 mw.rcfilters.Controller.prototype.emptyFilters = function () {
89 var highlightedFilterNames = this.filtersModel
90 .getHighlightedItems()
91 .map( function ( filterItem ) { return { name: filterItem.getName() }; } );
92
93 this.filtersModel.emptyAllFilters();
94 this.filtersModel.clearAllHighlightColors();
95 // Check all filter interactions
96 this.filtersModel.reassessFilterInteractions();
97
98 this.updateChangesList();
99
100 if ( highlightedFilterNames ) {
101 this.trackHighlight( 'clearAll', highlightedFilterNames );
102 }
103 };
104
105 /**
106 * Update the selected state of a filter
107 *
108 * @param {string} filterName Filter name
109 * @param {boolean} [isSelected] Filter selected state
110 */
111 mw.rcfilters.Controller.prototype.toggleFilterSelect = function ( filterName, isSelected ) {
112 var filterItem = this.filtersModel.getItemByName( filterName );
113
114 isSelected = isSelected === undefined ? !filterItem.isSelected() : isSelected;
115
116 if ( filterItem.isSelected() !== isSelected ) {
117 this.filtersModel.toggleFilterSelected( filterName, isSelected );
118
119 this.updateChangesList();
120
121 // Check filter interactions
122 this.filtersModel.reassessFilterInteractions( filterItem );
123 }
124 };
125
126 /**
127 * Update the URL of the page to reflect current filters
128 *
129 * This should not be called directly from outside the controller.
130 * If an action requires changing the URL, it should either use the
131 * highlighting actions below, or call #updateChangesList which does
132 * the uri corrections already.
133 *
134 * @private
135 * @param {Object} [params] Extra parameters to add to the API call
136 */
137 mw.rcfilters.Controller.prototype.updateURL = function ( params ) {
138 var updatedUri,
139 notEquivalent = function ( obj1, obj2 ) {
140 var keys = Object.keys( obj1 ).concat( Object.keys( obj2 ) );
141 return keys.some( function ( key ) {
142 return obj1[ key ] != obj2[ key ]; // eslint-disable-line eqeqeq
143 } );
144 };
145
146 params = params || {};
147
148 updatedUri = this.getUpdatedUri();
149 updatedUri.extend( params );
150
151 if ( notEquivalent( updatedUri.query, new mw.Uri().query ) ) {
152 window.history.pushState( { tag: 'rcfilters' }, document.title, updatedUri.toString() );
153 }
154 };
155
156 /**
157 * Get an updated mw.Uri object based on the model state
158 *
159 * @return {mw.Uri} Updated Uri
160 */
161 mw.rcfilters.Controller.prototype.getUpdatedUri = function () {
162 var uri = new mw.Uri(),
163 highlightParams = this.filtersModel.getHighlightParameters();
164
165 // Add to existing queries in URL
166 // TODO: Clean up the list of filters; perhaps 'falsy' filters
167 // shouldn't appear at all? Or compare to existing query string
168 // and see if current state of a specific filter is needed?
169 uri.extend( this.filtersModel.getParametersFromFilters() );
170
171 // highlight params
172 Object.keys( highlightParams ).forEach( function ( paramName ) {
173 if ( highlightParams[ paramName ] ) {
174 uri.query[ paramName ] = highlightParams[ paramName ];
175 } else {
176 delete uri.query[ paramName ];
177 }
178 } );
179
180 return uri;
181 };
182
183 /**
184 * Fetch the list of changes from the server for the current filters
185 *
186 * @return {jQuery.Promise} Promise object that will resolve with the changes list
187 * or with a string denoting no results.
188 */
189 mw.rcfilters.Controller.prototype.fetchChangesList = function () {
190 var uri = this.getUpdatedUri(),
191 requestId = ++this.requestCounter,
192 latestRequest = function () {
193 return requestId === this.requestCounter;
194 }.bind( this );
195
196 return $.ajax( uri.toString(), { contentType: 'html' } )
197 .then(
198 // Success
199 function ( html ) {
200 var $parsed;
201 if ( !latestRequest() ) {
202 return $.Deferred().reject();
203 }
204
205 $parsed = $( $.parseHTML( html ) );
206
207 return {
208 // Changes list
209 changes: $parsed.find( '.mw-changeslist' ).first().contents(),
210 // Fieldset
211 fieldset: $parsed.find( 'fieldset.rcoptions' ).first()
212 };
213 },
214 // Failure
215 function ( responseObj ) {
216 var $parsed;
217
218 if ( !latestRequest() ) {
219 return $.Deferred().reject();
220 }
221
222 $parsed = $( $.parseHTML( responseObj.responseText ) );
223
224 // Force a resolve state to this promise
225 return $.Deferred().resolve( {
226 changes: 'NO_RESULTS',
227 fieldset: $parsed.find( 'fieldset.rcoptions' ).first()
228 } ).promise();
229 }
230 );
231 };
232
233 /**
234 * Update the list of changes and notify the model
235 *
236 * @param {Object} [params] Extra parameters to add to the API call
237 */
238 mw.rcfilters.Controller.prototype.updateChangesList = function ( params ) {
239 this.updateURL( params );
240 this.changesListModel.invalidate();
241 this.fetchChangesList()
242 .then(
243 // Success
244 function ( pieces ) {
245 var $changesListContent = pieces.changes,
246 $fieldset = pieces.fieldset;
247 this.changesListModel.update( $changesListContent, $fieldset );
248 }.bind( this )
249 // Do nothing for failure
250 );
251 };
252
253 /**
254 * Toggle the highlight feature on and off
255 */
256 mw.rcfilters.Controller.prototype.toggleHighlight = function () {
257 this.filtersModel.toggleHighlight();
258 this.updateURL();
259 };
260
261 /**
262 * Set the highlight color for a filter item
263 *
264 * @param {string} filterName Name of the filter item
265 * @param {string} color Selected color
266 */
267 mw.rcfilters.Controller.prototype.setHighlightColor = function ( filterName, color ) {
268 this.filtersModel.setHighlightColor( filterName, color );
269 this.updateURL();
270 this.trackHighlight( 'set', { name: filterName, color: color } );
271 };
272
273 /**
274 * Clear highlight for a filter item
275 *
276 * @param {string} filterName Name of the filter item
277 */
278 mw.rcfilters.Controller.prototype.clearHighlightColor = function ( filterName ) {
279 this.filtersModel.clearHighlightColor( filterName );
280 this.updateURL();
281 this.trackHighlight( 'clear', filterName );
282 };
283
284 /**
285 * Clear both highlight and selection of a filter
286 *
287 * @param {string} filterName Name of the filter item
288 */
289 mw.rcfilters.Controller.prototype.clearFilter = function ( filterName ) {
290 var filterItem = this.filtersModel.getItemByName( filterName ),
291 isHighlighted = filterItem.isHighlighted();
292
293 if ( filterItem.isSelected() || isHighlighted ) {
294 this.filtersModel.clearHighlightColor( filterName );
295 this.filtersModel.toggleFilterSelected( filterName, false );
296 this.updateChangesList();
297 this.filtersModel.reassessFilterInteractions( filterItem );
298 }
299
300 if ( isHighlighted ) {
301 this.trackHighlight( 'clear', filterName );
302 }
303 };
304
305 /**
306 * Synchronize the URL with the current state of the filters
307 * without adding an history entry.
308 */
309 mw.rcfilters.Controller.prototype.replaceUrl = function () {
310 window.history.replaceState(
311 { tag: 'rcfilters' },
312 document.title,
313 this.getUpdatedUri().toString()
314 );
315 };
316
317 /**
318 * Track usage of highlight feature
319 *
320 * @param {string} action
321 * @param {array|object|string} filters
322 */
323 mw.rcfilters.Controller.prototype.trackHighlight = function ( action, filters ) {
324 filters = $.type( filters ) === 'string' ? { name: filters } : filters;
325 filters = $.type( filters ) === 'object' ? [ filters ] : filters;
326 mw.track(
327 'event.ChangesListHighlights',
328 {
329 action: action,
330 filters: filters,
331 userId: mw.user.getId()
332 }
333 );
334 };
335 }( mediaWiki, jQuery ) );