375b68b034c67049e1643a382b2591af77ec5dcb
[lhc/web/wiklou.git] / resources / src / mediawiki.rcfilters / mw.rcfilters.Controller.js
1 ( function ( mw, $ ) {
2 /* eslint no-underscore-dangle: "off" */
3 /**
4 * Controller for the filters in Recent Changes
5 *
6 * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters view model
7 * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel Changes list view model
8 * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
9 */
10 mw.rcfilters.Controller = function MwRcfiltersController( filtersModel, changesListModel, savedQueriesModel ) {
11 this.filtersModel = filtersModel;
12 this.changesListModel = changesListModel;
13 this.savedQueriesModel = savedQueriesModel;
14 this.requestCounter = 0;
15 this.baseFilterState = {};
16 this.emptyParameterState = {};
17 this.initializing = false;
18 };
19
20 /* Initialization */
21 OO.initClass( mw.rcfilters.Controller );
22
23 /**
24 * Initialize the filter and parameter states
25 *
26 * @param {Array} filterStructure Filter definition and structure for the model
27 */
28 mw.rcfilters.Controller.prototype.initialize = function ( filterStructure ) {
29 var parsedSavedQueries, validParameterNames,
30 uri = new mw.Uri(),
31 $changesList = $( '.mw-changeslist' ).first().contents();
32
33 // Initialize the model
34 this.filtersModel.initializeFilters( filterStructure );
35
36 this._buildBaseFilterState();
37 this._buildEmptyParameterState();
38 validParameterNames = Object.keys( this._getEmptyParameterState() )
39 .filter( function ( param ) {
40 // Remove 'highlight' parameter from this check;
41 // if it's the only parameter in the URL we still
42 // want to consider the URL 'empty' for defaults to load
43 return param !== 'highlight';
44 } );
45
46 try {
47 parsedSavedQueries = JSON.parse( mw.user.options.get( 'rcfilters-saved-queries' ) || '{}' );
48 } catch ( err ) {
49 parsedSavedQueries = {};
50 }
51
52 // The queries are saved in a minimized state, so we need
53 // to send over the base state so the saved queries model
54 // can normalize them per each query item
55 this.savedQueriesModel.initialize(
56 parsedSavedQueries,
57 this._getBaseFilterState()
58 );
59
60 // Check whether we need to load defaults.
61 // We do this by checking whether the current URI query
62 // contains any parameters recognized by the system.
63 // If it does, we load the given state.
64 // If it doesn't, we have no values at all, and we assume
65 // the user loads the base-page and we load defaults.
66 // Defaults should only be applied on load (if necessary)
67 // or on request
68 if (
69 Object.keys( uri.query ).some( function ( parameter ) {
70 return validParameterNames.indexOf( parameter ) > -1;
71 } )
72 ) {
73 // There are parameters in the url, update model state
74 this.updateStateBasedOnUrl();
75 } else {
76 this.initializing = true;
77 // No valid parameters are given, load defaults
78 this._updateModelState(
79 $.extend(
80 true,
81 // We've ignored the highlight parameter for the sake
82 // of seeing whether we need to apply defaults - but
83 // while we do load the defaults, we still want to retain
84 // the actual value given in the URL for it on top of the
85 // defaults
86 { highlight: String( Number( uri.query.highlight ) ) },
87 this._getDefaultParams()
88 )
89 );
90 this.updateChangesList();
91 this.initializing = false;
92 }
93
94 // Update the changes list with the existing data
95 // so it gets processed
96 this.changesListModel.update(
97 $changesList.length ? $changesList : 'NO_RESULTS',
98 $( 'fieldset.rcoptions' ).first()
99 );
100 };
101
102 /**
103 * Reset to default filters
104 */
105 mw.rcfilters.Controller.prototype.resetToDefaults = function () {
106 this._updateModelState( $.extend( true, { highlight: '0' }, this._getDefaultParams() ) );
107 this.updateChangesList();
108 };
109
110 /**
111 * Empty all selected filters
112 */
113 mw.rcfilters.Controller.prototype.emptyFilters = function () {
114 var highlightedFilterNames = this.filtersModel
115 .getHighlightedItems()
116 .map( function ( filterItem ) { return { name: filterItem.getName() }; } );
117
118 this.filtersModel.emptyAllFilters();
119 this.filtersModel.clearAllHighlightColors();
120 // Check all filter interactions
121 this.filtersModel.reassessFilterInteractions();
122
123 this.updateChangesList();
124
125 if ( highlightedFilterNames ) {
126 this._trackHighlight( 'clearAll', highlightedFilterNames );
127 }
128 };
129
130 /**
131 * Update the selected state of a filter
132 *
133 * @param {string} filterName Filter name
134 * @param {boolean} [isSelected] Filter selected state
135 */
136 mw.rcfilters.Controller.prototype.toggleFilterSelect = function ( filterName, isSelected ) {
137 var filterItem = this.filtersModel.getItemByName( filterName );
138
139 if ( !filterItem ) {
140 // If no filter was found, break
141 return;
142 }
143
144 isSelected = isSelected === undefined ? !filterItem.isSelected() : isSelected;
145
146 if ( filterItem.isSelected() !== isSelected ) {
147 this.filtersModel.toggleFilterSelected( filterName, isSelected );
148
149 this.updateChangesList();
150
151 // Check filter interactions
152 this.filtersModel.reassessFilterInteractions( filterItem );
153 }
154 };
155
156 /**
157 * Clear both highlight and selection of a filter
158 *
159 * @param {string} filterName Name of the filter item
160 */
161 mw.rcfilters.Controller.prototype.clearFilter = function ( filterName ) {
162 var filterItem = this.filtersModel.getItemByName( filterName ),
163 isHighlighted = filterItem.isHighlighted();
164
165 if ( filterItem.isSelected() || isHighlighted ) {
166 this.filtersModel.clearHighlightColor( filterName );
167 this.filtersModel.toggleFilterSelected( filterName, false );
168 this.updateChangesList();
169 this.filtersModel.reassessFilterInteractions( filterItem );
170 }
171
172 if ( isHighlighted ) {
173 this._trackHighlight( 'clear', filterName );
174 }
175 };
176
177 /**
178 * Toggle the highlight feature on and off
179 */
180 mw.rcfilters.Controller.prototype.toggleHighlight = function () {
181 this.filtersModel.toggleHighlight();
182 this._updateURL();
183
184 if ( this.filtersModel.isHighlightEnabled() ) {
185 mw.hook( 'RcFilters.highlight.enable' ).fire();
186 }
187 };
188
189 /**
190 * Set the highlight color for a filter item
191 *
192 * @param {string} filterName Name of the filter item
193 * @param {string} color Selected color
194 */
195 mw.rcfilters.Controller.prototype.setHighlightColor = function ( filterName, color ) {
196 this.filtersModel.setHighlightColor( filterName, color );
197 this._updateURL();
198 this._trackHighlight( 'set', { name: filterName, color: color } );
199 };
200
201 /**
202 * Clear highlight for a filter item
203 *
204 * @param {string} filterName Name of the filter item
205 */
206 mw.rcfilters.Controller.prototype.clearHighlightColor = function ( filterName ) {
207 this.filtersModel.clearHighlightColor( filterName );
208 this._updateURL();
209 this._trackHighlight( 'clear', filterName );
210 };
211
212 /**
213 * Save the current model state as a saved query
214 *
215 * @param {string} [label] Label of the saved query
216 */
217 mw.rcfilters.Controller.prototype.saveCurrentQuery = function ( label ) {
218 var highlightedItems = {},
219 highlightEnabled = this.filtersModel.isHighlightEnabled();
220
221 // Prepare highlights
222 this.filtersModel.getHighlightedItems().forEach( function ( item ) {
223 highlightedItems[ item.getName() ] = highlightEnabled ?
224 item.getHighlightColor() : null;
225 } );
226 // These are filter states; highlight is stored as boolean
227 highlightedItems.highlight = this.filtersModel.isHighlightEnabled();
228
229 // Add item
230 this.savedQueriesModel.addNewQuery(
231 label || mw.msg( 'rcfilters-savedqueries-defaultlabel' ),
232 {
233 filters: this.filtersModel.getSelectedState(),
234 highlights: highlightedItems
235 }
236 );
237
238 // Save item
239 this._saveSavedQueries();
240 };
241
242 /**
243 * Remove a saved query
244 *
245 * @param {string} queryID Query id
246 */
247 mw.rcfilters.Controller.prototype.removeSavedQuery = function ( queryID ) {
248 var query = this.savedQueriesModel.getItemByID( queryID );
249
250 this.savedQueriesModel.removeItems( [ query ] );
251
252 // Check if this item was the default
253 if ( this.savedQueriesModel.getDefault() === queryID ) {
254 // Nulify the default
255 this.savedQueriesModel.setDefault( null );
256 }
257 this._saveSavedQueries();
258 };
259
260 /**
261 * Rename a saved query
262 *
263 * @param {string} queryID Query id
264 * @param {string} newLabel New label for the query
265 */
266 mw.rcfilters.Controller.prototype.renameSavedQuery = function ( queryID, newLabel ) {
267 var queryItem = this.savedQueriesModel.getItemByID( queryID );
268
269 if ( queryItem ) {
270 queryItem.updateLabel( newLabel );
271 }
272 this._saveSavedQueries();
273 };
274
275 /**
276 * Set a saved query as default
277 *
278 * @param {string} queryID Query Id. If null is given, default
279 * query is reset.
280 */
281 mw.rcfilters.Controller.prototype.setDefaultSavedQuery = function ( queryID ) {
282 this.savedQueriesModel.setDefault( queryID );
283 this._saveSavedQueries();
284 };
285
286 /**
287 * Load a saved query
288 *
289 * @param {string} queryID Query id
290 */
291 mw.rcfilters.Controller.prototype.applySavedQuery = function ( queryID ) {
292 var data, highlights,
293 queryItem = this.savedQueriesModel.getItemByID( queryID );
294
295 if ( queryItem ) {
296 data = queryItem.getData();
297 highlights = data.highlights;
298
299 // Backwards compatibility; initial version mispelled 'highlight' with 'highlights'
300 highlights.highlight = highlights.highlights || highlights.highlight;
301
302 // Update model state from filters
303 this.filtersModel.toggleFiltersSelected( data.filters );
304
305 // Update highlight state
306 this.filtersModel.toggleHighlight( !!Number( highlights.highlight ) );
307 this.filtersModel.getItems().forEach( function ( filterItem ) {
308 var color = highlights[ filterItem.getName() ];
309 if ( color ) {
310 filterItem.setHighlightColor( color );
311 } else {
312 filterItem.clearHighlightColor();
313 }
314 } );
315
316 // Check all filter interactions
317 this.filtersModel.reassessFilterInteractions();
318
319 this.updateChangesList();
320 }
321 };
322
323 /**
324 * Check whether the current filter and highlight state exists
325 * in the saved queries model.
326 *
327 * @return {boolean} Query exists
328 */
329 mw.rcfilters.Controller.prototype.findQueryMatchingCurrentState = function () {
330 var highlightedItems = {};
331
332 // Prepare highlights of the current query
333 this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) {
334 highlightedItems[ item.getName() ] = item.getHighlightColor();
335 } );
336 highlightedItems.highlight = this.filtersModel.isHighlightEnabled();
337
338 return this.savedQueriesModel.findMatchingQuery(
339 {
340 filters: this.filtersModel.getSelectedState(),
341 highlights: highlightedItems
342 }
343 );
344 };
345
346 /**
347 * Get an object representing the base state of parameters
348 * and highlights.
349 *
350 * This is meant to make sure that the saved queries that are
351 * in memory are always the same structure as what we would get
352 * by calling the current model's "getSelectedState" and by checking
353 * highlight items.
354 *
355 * In cases where a user saved a query when the system had a certain
356 * set of filters, and then a filter was added to the system, we want
357 * to make sure that the stored queries can still be comparable to
358 * the current state, which means that we need the base state for
359 * two operations:
360 *
361 * - Saved queries are stored in "minimal" view (only changed filters
362 * are stored); When we initialize the system, we merge each minimal
363 * query with the base state (using 'getNormalizedFilters') so all
364 * saved queries have the exact same structure as what we would get
365 * by checking the getSelectedState of the filter.
366 * - When we save the queries, we minimize the object to only represent
367 * whatever has actually changed, rather than store the entire
368 * object. To check what actually is different so we can store it,
369 * we need to obtain a base state to compare against, this is
370 * what #_getMinimalFilterList does
371 */
372 mw.rcfilters.Controller.prototype._buildBaseFilterState = function () {
373 var defaultParams = this.filtersModel.getDefaultParams(),
374 highlightedItems = {};
375
376 // Prepare highlights
377 this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) {
378 highlightedItems[ item.getName() ] = null;
379 } );
380 highlightedItems.highlight = false;
381
382 this.baseFilterState = {
383 filters: this.filtersModel.getFiltersFromParameters( defaultParams ),
384 highlights: highlightedItems
385 };
386 };
387
388 /**
389 * Build an empty representation of the parameters, where all parameters
390 * are either set to '0' or '' depending on their type.
391 * This must run during initialization, before highlights are set.
392 */
393 mw.rcfilters.Controller.prototype._buildEmptyParameterState = function () {
394 var emptyParams = this.filtersModel.getParametersFromFilters( {} ),
395 emptyHighlights = this.filtersModel.getHighlightParameters();
396
397 this.emptyParameterState = $.extend(
398 true,
399 {},
400 emptyParams,
401 emptyHighlights,
402 { highlight: '0' }
403 );
404 };
405
406 /**
407 * Get an object representing the base filter state of both
408 * filters and highlights. The structure is similar to what we use
409 * to store each query in the saved queries object:
410 * {
411 * filters: {
412 * filterName: (bool)
413 * },
414 * highlights: {
415 * filterName: (string|null)
416 * }
417 * }
418 *
419 * @return {Object} Object representing the base state of
420 * parameters and highlights
421 */
422 mw.rcfilters.Controller.prototype._getBaseFilterState = function () {
423 return this.baseFilterState;
424 };
425
426 /**
427 * Get an object representing the base state of parameters
428 * and highlights. The structure is similar to what we use
429 * to store each query in the saved queries object:
430 * {
431 * param1: "value",
432 * param2: "value1|value2"
433 * }
434 *
435 * @return {Object} Object representing the base state of
436 * parameters and highlights
437 */
438 mw.rcfilters.Controller.prototype._getEmptyParameterState = function () {
439 return this.emptyParameterState;
440 };
441
442 /**
443 * Get an object that holds only the parameters and highlights that have
444 * values different than the base default value.
445 *
446 * This is the reverse of the normalization we do initially on loading and
447 * initializing the saved queries model.
448 *
449 * @param {Object} valuesObject Object representing the state of both
450 * filters and highlights in its normalized version, to be minimized.
451 * @return {Object} Minimal filters and highlights list
452 */
453 mw.rcfilters.Controller.prototype._getMinimalFilterList = function ( valuesObject ) {
454 var result = { filters: {}, highlights: {} },
455 baseState = this._getBaseFilterState();
456
457 // XOR results
458 $.each( valuesObject.filters, function ( name, value ) {
459 if ( baseState.filters !== undefined && baseState.filters[ name ] !== value ) {
460 result.filters[ name ] = value;
461 }
462 } );
463
464 $.each( valuesObject.highlights, function ( name, value ) {
465 if ( baseState.highlights !== undefined && baseState.highlights[ name ] !== value ) {
466 result.highlights[ name ] = value;
467 }
468 } );
469
470 return result;
471 };
472
473 /**
474 * Save the current state of the saved queries model with all
475 * query item representation in the user settings.
476 */
477 mw.rcfilters.Controller.prototype._saveSavedQueries = function () {
478 var stringified,
479 state = this.savedQueriesModel.getState(),
480 controller = this;
481
482 // Minimize before save
483 $.each( state.queries, function ( queryID, info ) {
484 state.queries[ queryID ].data = controller._getMinimalFilterList( info.data );
485 } );
486
487 // Stringify state
488 stringified = JSON.stringify( state );
489
490 if ( stringified.length > 65535 ) {
491 // Sanity check, since the preference can only hold that.
492 return;
493 }
494
495 // Save the preference
496 new mw.Api().saveOption( 'rcfilters-saved-queries', stringified );
497 // Update the preference for this session
498 mw.user.options.set( 'rcfilters-saved-queries', stringified );
499 };
500
501 /**
502 * Synchronize the URL with the current state of the filters
503 * without adding an history entry.
504 */
505 mw.rcfilters.Controller.prototype.replaceUrl = function () {
506 window.history.replaceState(
507 { tag: 'rcfilters' },
508 document.title,
509 this._getUpdatedUri().toString()
510 );
511 };
512
513 /**
514 * Update filter state (selection and highlighting) based
515 * on current URL values.
516 */
517 mw.rcfilters.Controller.prototype.updateStateBasedOnUrl = function () {
518 var uri = new mw.Uri();
519
520 this._updateModelState( uri.query );
521 this.updateChangesList();
522 };
523
524 /**
525 * Update the list of changes and notify the model
526 *
527 * @param {Object} [params] Extra parameters to add to the API call
528 */
529 mw.rcfilters.Controller.prototype.updateChangesList = function ( params ) {
530 this._updateURL( params );
531 this.changesListModel.invalidate();
532 this._fetchChangesList()
533 .then(
534 // Success
535 function ( pieces ) {
536 var $changesListContent = pieces.changes,
537 $fieldset = pieces.fieldset;
538 this.changesListModel.update( $changesListContent, $fieldset );
539 }.bind( this )
540 // Do nothing for failure
541 );
542 };
543
544 /**
545 * Update the model state from given the given parameters.
546 *
547 * This is an internal method, and should only be used from inside
548 * the controller.
549 *
550 * @param {Object} parameters Object representing the parameters for
551 * filters and highlights
552 */
553 mw.rcfilters.Controller.prototype._updateModelState = function ( parameters ) {
554 // Update filter states
555 this.filtersModel.toggleFiltersSelected(
556 this.filtersModel.getFiltersFromParameters(
557 parameters
558 )
559 );
560
561 // Update highlight state
562 this.filtersModel.toggleHighlight( !!Number( parameters.highlight ) );
563 this.filtersModel.getItems().forEach( function ( filterItem ) {
564 var color = parameters[ filterItem.getName() + '_color' ];
565 if ( color ) {
566 filterItem.setHighlightColor( color );
567 } else {
568 filterItem.clearHighlightColor();
569 }
570 } );
571
572 // Check all filter interactions
573 this.filtersModel.reassessFilterInteractions();
574 };
575
576 /**
577 * Get an object representing the default parameter state, whether
578 * it is from the model defaults or from the saved queries.
579 *
580 * @return {Object} Default parameters
581 */
582 mw.rcfilters.Controller.prototype._getDefaultParams = function () {
583 var data, queryHighlights,
584 savedParams = {},
585 savedHighlights = {},
586 defaultSavedQueryItem = this.savedQueriesModel.getItemByID( this.savedQueriesModel.getDefault() );
587
588 if ( mw.config.get( 'wgStructuredChangeFiltersEnableSaving' ) &&
589 defaultSavedQueryItem ) {
590
591 data = defaultSavedQueryItem.getData();
592
593 queryHighlights = data.highlights || {};
594 savedParams = this.filtersModel.getParametersFromFilters( data.filters || {} );
595
596 // Translate highlights to parameters
597 savedHighlights.highlight = String( Number( queryHighlights.highlight ) );
598 $.each( queryHighlights, function ( filterName, color ) {
599 if ( filterName !== 'highlights' ) {
600 savedHighlights[ filterName + '_color' ] = color;
601 }
602 } );
603
604 return $.extend( true, {}, savedParams, savedHighlights );
605 }
606
607 return this.filtersModel.getDefaultParams();
608 };
609
610 /**
611 * Update the URL of the page to reflect current filters
612 *
613 * This should not be called directly from outside the controller.
614 * If an action requires changing the URL, it should either use the
615 * highlighting actions below, or call #updateChangesList which does
616 * the uri corrections already.
617 *
618 * @param {Object} [params] Extra parameters to add to the API call
619 */
620 mw.rcfilters.Controller.prototype._updateURL = function ( params ) {
621 var currentFilterState, updatedFilterState, updatedUri,
622 uri = new mw.Uri(),
623 notEquivalent = function ( obj1, obj2 ) {
624 var keys = Object.keys( obj1 ).concat( Object.keys( obj2 ) );
625 return keys.some( function ( key ) {
626 return obj1[ key ] != obj2[ key ]; // eslint-disable-line eqeqeq
627 } );
628 };
629
630 params = params || {};
631
632 updatedUri = this._getUpdatedUri();
633 updatedUri.extend( params );
634
635 // Compare states instead of parameters
636 // This will allow us to always have a proper check of whether
637 // the requested new url is one to change or not, regardless of
638 // actual parameter visibility/representation in the URL
639 currentFilterState = this.filtersModel.getFiltersFromParameters( uri.query );
640 updatedFilterState = this.filtersModel.getFiltersFromParameters( updatedUri.query );
641 // HACK: Re-merge extra parameters in
642 // This is a hack and a quickfix; a better, more sustainable
643 // fix is being worked on with a UriProcessor, but for now
644 // we need to make sure the **comparison** of whether currentFilterState
645 // and updatedFilterState differ **includes** the extra parameters in the URL
646 currentFilterState = $.extend( true, {}, uri.query, currentFilterState );
647 updatedFilterState = $.extend( true, {}, updatedUri.query, updatedFilterState );
648
649 // Include highlight states
650 $.extend( true,
651 currentFilterState,
652 this.filtersModel.extractHighlightValues( uri.query ),
653 { highlight: !!Number( uri.query.highlight ) }
654 );
655 $.extend( true,
656 updatedFilterState,
657 this.filtersModel.extractHighlightValues( updatedUri.query ),
658 { highlight: !!Number( updatedUri.query.highlight ) }
659 );
660
661 if ( notEquivalent( currentFilterState, updatedFilterState ) ) {
662 if ( this.initializing ) {
663 // Initially, when we just build the first page load
664 // out of defaults, we want to replace the history
665 window.history.replaceState( { tag: 'rcfilters' }, document.title, updatedUri.toString() );
666 } else {
667 window.history.pushState( { tag: 'rcfilters' }, document.title, updatedUri.toString() );
668 }
669 }
670 };
671
672 /**
673 * Get an updated mw.Uri object based on the model state
674 *
675 * @return {mw.Uri} Updated Uri
676 */
677 mw.rcfilters.Controller.prototype._getUpdatedUri = function () {
678 var uri = new mw.Uri(),
679 highlightParams = this.filtersModel.getHighlightParameters(),
680 modelParameters = this.filtersModel.getParametersFromFilters(),
681 baseParams = this._getEmptyParameterState();
682
683 // Minimize values of the model parameters; show only the values that
684 // are non-zero. We assume that all parameters that are not literally
685 // showing in the URL are set to zero or empty
686 $.each( modelParameters, function ( paramName, value ) {
687 if ( baseParams[ paramName ] !== value ) {
688 uri.query[ paramName ] = value;
689 } else {
690 // We need to remove this value from the url
691 delete uri.query[ paramName ];
692 }
693 } );
694
695 // highlight params
696 if ( this.filtersModel.isHighlightEnabled() ) {
697 uri.query.highlight = Number( this.filtersModel.isHighlightEnabled() );
698 } else {
699 delete uri.query.highlight;
700 }
701 $.each( highlightParams, function ( paramName, value ) {
702 // Only output if it is different than the base parameters
703 if ( baseParams[ paramName ] !== value ) {
704 uri.query[ paramName ] = value;
705 } else {
706 delete uri.query[ paramName ];
707 }
708 } );
709
710 return uri;
711 };
712
713 /**
714 * Fetch the list of changes from the server for the current filters
715 *
716 * @return {jQuery.Promise} Promise object that will resolve with the changes list
717 * or with a string denoting no results.
718 */
719 mw.rcfilters.Controller.prototype._fetchChangesList = function () {
720 var uri = this._getUpdatedUri(),
721 requestId = ++this.requestCounter,
722 latestRequest = function () {
723 return requestId === this.requestCounter;
724 }.bind( this );
725
726 return $.ajax( uri.toString(), { contentType: 'html' } )
727 .then(
728 // Success
729 function ( html ) {
730 var $parsed;
731 if ( !latestRequest() ) {
732 return $.Deferred().reject();
733 }
734
735 $parsed = $( $.parseHTML( html ) );
736
737 return {
738 // Changes list
739 changes: $parsed.find( '.mw-changeslist' ).first().contents(),
740 // Fieldset
741 fieldset: $parsed.find( 'fieldset.rcoptions' ).first()
742 };
743 },
744 // Failure
745 function ( responseObj ) {
746 var $parsed;
747
748 if ( !latestRequest() ) {
749 return $.Deferred().reject();
750 }
751
752 $parsed = $( $.parseHTML( responseObj.responseText ) );
753
754 // Force a resolve state to this promise
755 return $.Deferred().resolve( {
756 changes: 'NO_RESULTS',
757 fieldset: $parsed.find( 'fieldset.rcoptions' ).first()
758 } ).promise();
759 }
760 );
761 };
762
763 /**
764 * Track usage of highlight feature
765 *
766 * @param {string} action
767 * @param {array|object|string} filters
768 */
769 mw.rcfilters.Controller.prototype._trackHighlight = function ( action, filters ) {
770 filters = typeof filters === 'string' ? { name: filters } : filters;
771 filters = !Array.isArray( filters ) ? [ filters ] : filters;
772 mw.track(
773 'event.ChangesListHighlights',
774 {
775 action: action,
776 filters: filters,
777 userId: mw.user.getId()
778 }
779 );
780 };
781
782 }( mediaWiki, jQuery ) );