669420caf93ef5b61da699d4bada87d94ce15afa
[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 if ( !filterItem ) {
115 // If no filter was found, break
116 return;
117 }
118
119 isSelected = isSelected === undefined ? !filterItem.isSelected() : isSelected;
120
121 if ( filterItem.isSelected() !== isSelected ) {
122 this.filtersModel.toggleFilterSelected( filterName, isSelected );
123
124 this.updateChangesList();
125
126 // Check filter interactions
127 this.filtersModel.reassessFilterInteractions( filterItem );
128 }
129 };
130
131 /**
132 * Update the URL of the page to reflect current filters
133 *
134 * This should not be called directly from outside the controller.
135 * If an action requires changing the URL, it should either use the
136 * highlighting actions below, or call #updateChangesList which does
137 * the uri corrections already.
138 *
139 * @private
140 * @param {Object} [params] Extra parameters to add to the API call
141 */
142 mw.rcfilters.Controller.prototype.updateURL = function ( params ) {
143 var updatedUri,
144 notEquivalent = function ( obj1, obj2 ) {
145 var keys = Object.keys( obj1 ).concat( Object.keys( obj2 ) );
146 return keys.some( function ( key ) {
147 return obj1[ key ] != obj2[ key ]; // eslint-disable-line eqeqeq
148 } );
149 };
150
151 params = params || {};
152
153 updatedUri = this.getUpdatedUri();
154 updatedUri.extend( params );
155
156 if ( notEquivalent( updatedUri.query, new mw.Uri().query ) ) {
157 window.history.pushState( { tag: 'rcfilters' }, document.title, updatedUri.toString() );
158 }
159 };
160
161 /**
162 * Get an updated mw.Uri object based on the model state
163 *
164 * @return {mw.Uri} Updated Uri
165 */
166 mw.rcfilters.Controller.prototype.getUpdatedUri = function () {
167 var uri = new mw.Uri(),
168 highlightParams = this.filtersModel.getHighlightParameters();
169
170 // Add to existing queries in URL
171 // TODO: Clean up the list of filters; perhaps 'falsy' filters
172 // shouldn't appear at all? Or compare to existing query string
173 // and see if current state of a specific filter is needed?
174 uri.extend( this.filtersModel.getParametersFromFilters() );
175
176 // highlight params
177 Object.keys( highlightParams ).forEach( function ( paramName ) {
178 if ( highlightParams[ paramName ] ) {
179 uri.query[ paramName ] = highlightParams[ paramName ];
180 } else {
181 delete uri.query[ paramName ];
182 }
183 } );
184
185 return uri;
186 };
187
188 /**
189 * Fetch the list of changes from the server for the current filters
190 *
191 * @return {jQuery.Promise} Promise object that will resolve with the changes list
192 * or with a string denoting no results.
193 */
194 mw.rcfilters.Controller.prototype.fetchChangesList = function () {
195 var uri = this.getUpdatedUri(),
196 requestId = ++this.requestCounter,
197 latestRequest = function () {
198 return requestId === this.requestCounter;
199 }.bind( this );
200
201 return $.ajax( uri.toString(), { contentType: 'html' } )
202 .then(
203 // Success
204 function ( html ) {
205 var $parsed;
206 if ( !latestRequest() ) {
207 return $.Deferred().reject();
208 }
209
210 $parsed = $( $.parseHTML( html ) );
211
212 return {
213 // Changes list
214 changes: $parsed.find( '.mw-changeslist' ).first().contents(),
215 // Fieldset
216 fieldset: $parsed.find( 'fieldset.rcoptions' ).first()
217 };
218 },
219 // Failure
220 function ( responseObj ) {
221 var $parsed;
222
223 if ( !latestRequest() ) {
224 return $.Deferred().reject();
225 }
226
227 $parsed = $( $.parseHTML( responseObj.responseText ) );
228
229 // Force a resolve state to this promise
230 return $.Deferred().resolve( {
231 changes: 'NO_RESULTS',
232 fieldset: $parsed.find( 'fieldset.rcoptions' ).first()
233 } ).promise();
234 }
235 );
236 };
237
238 /**
239 * Update the list of changes and notify the model
240 *
241 * @param {Object} [params] Extra parameters to add to the API call
242 */
243 mw.rcfilters.Controller.prototype.updateChangesList = function ( params ) {
244 this.updateURL( params );
245 this.changesListModel.invalidate();
246 this.fetchChangesList()
247 .then(
248 // Success
249 function ( pieces ) {
250 var $changesListContent = pieces.changes,
251 $fieldset = pieces.fieldset;
252 this.changesListModel.update( $changesListContent, $fieldset );
253 }.bind( this )
254 // Do nothing for failure
255 );
256 };
257
258 /**
259 * Toggle the highlight feature on and off
260 */
261 mw.rcfilters.Controller.prototype.toggleHighlight = function () {
262 this.filtersModel.toggleHighlight();
263 this.updateURL();
264
265 if ( this.filtersModel.isHighlightEnabled() ) {
266 mw.hook( 'RcFilters.highlight.enable' ).fire();
267 }
268 };
269
270 /**
271 * Set the highlight color for a filter item
272 *
273 * @param {string} filterName Name of the filter item
274 * @param {string} color Selected color
275 */
276 mw.rcfilters.Controller.prototype.setHighlightColor = function ( filterName, color ) {
277 this.filtersModel.setHighlightColor( filterName, color );
278 this.updateURL();
279 this.trackHighlight( 'set', { name: filterName, color: color } );
280 };
281
282 /**
283 * Clear highlight for a filter item
284 *
285 * @param {string} filterName Name of the filter item
286 */
287 mw.rcfilters.Controller.prototype.clearHighlightColor = function ( filterName ) {
288 this.filtersModel.clearHighlightColor( filterName );
289 this.updateURL();
290 this.trackHighlight( 'clear', filterName );
291 };
292
293 /**
294 * Clear both highlight and selection of a filter
295 *
296 * @param {string} filterName Name of the filter item
297 */
298 mw.rcfilters.Controller.prototype.clearFilter = function ( filterName ) {
299 var filterItem = this.filtersModel.getItemByName( filterName ),
300 isHighlighted = filterItem.isHighlighted();
301
302 if ( filterItem.isSelected() || isHighlighted ) {
303 this.filtersModel.clearHighlightColor( filterName );
304 this.filtersModel.toggleFilterSelected( filterName, false );
305 this.updateChangesList();
306 this.filtersModel.reassessFilterInteractions( filterItem );
307 }
308
309 if ( isHighlighted ) {
310 this.trackHighlight( 'clear', filterName );
311 }
312 };
313
314 /**
315 * Synchronize the URL with the current state of the filters
316 * without adding an history entry.
317 */
318 mw.rcfilters.Controller.prototype.replaceUrl = function () {
319 window.history.replaceState(
320 { tag: 'rcfilters' },
321 document.title,
322 this.getUpdatedUri().toString()
323 );
324 };
325
326 /**
327 * Track usage of highlight feature
328 *
329 * @param {string} action
330 * @param {array|object|string} filters
331 */
332 mw.rcfilters.Controller.prototype.trackHighlight = function ( action, filters ) {
333 filters = typeof filters === 'string' ? { name: filters } : filters;
334 filters = !Array.isArray( filters ) ? [ filters ] : filters;
335 mw.track(
336 'event.ChangesListHighlights',
337 {
338 action: action,
339 filters: filters,
340 userId: mw.user.getId()
341 }
342 );
343 };
344 }( mediaWiki, jQuery ) );