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