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