this.baseFilterState = {};
this.uriProcessor = null;
this.initializing = false;
+ this.wereSavedQueriesSaved = false;
this.prevLoggedItems = [];
* @param {Object} [tagList] Tag definition
*/
mw.rcfilters.Controller.prototype.initialize = function ( filterStructure, namespaceStructure, tagList ) {
- var parsedSavedQueries,
+ var parsedSavedQueries, pieces,
displayConfig = mw.config.get( 'StructuredChangeFiltersDisplayConfig' ),
+ defaultSavedQueryExists = mw.config.get( 'wgStructuredChangeFiltersDefaultSavedQueryExists' ),
controller = this,
views = {},
items = [],
- uri = new mw.Uri(),
- $changesList = $( '.mw-changeslist' ).first().contents();
+ uri = new mw.Uri();
// Prepare views
if ( namespaceStructure ) {
separator: ';',
fullCoverage: true,
filters: items
+ },
+ {
+ name: 'invertGroup',
+ type: 'boolean',
+ hidden: true,
+ filters: [ {
+ name: 'invert',
+ 'default': '0'
+ } ]
} ]
};
}
// Initialize the model
this.filtersModel.initializeFilters( filterStructure, views );
- this._buildBaseFilterState();
-
this.uriProcessor = new mw.rcfilters.UriProcessor(
this.filtersModel
);
parsedSavedQueries = {};
}
- // The queries are saved in a minimized state, so we need
- // to send over the base state so the saved queries model
- // can normalize them per each query item
- this.savedQueriesModel.initialize(
- parsedSavedQueries,
- this._getBaseFilterState(),
- // This is for backwards compatibility - delete all excluded filter states
- Object.keys( this.filtersModel.getExcludedFiltersState() )
- );
+ // Initialize saved queries
+ this.savedQueriesModel.initialize( parsedSavedQueries );
+ if ( this.savedQueriesModel.isConverted() ) {
+ // Since we know we converted, we're going to re-save
+ // the queries so they are now migrated to the new format
+ this._saveSavedQueries();
+ }
}
// Check whether we need to load defaults.
// Defaults should only be applied on load (if necessary)
// or on request
this.initializing = true;
- if (
- !mw.user.isAnon() && this.savedQueriesModel.getDefault() &&
- !this.uriProcessor.doesQueryContainRecognizedParams( uri.query )
- ) {
- // We have defaults from a saved query.
- // We will load them straight-forward (as if
- // they were clicked in the menu) so we trigger
- // a full ajax request and change of URL
+
+ if ( defaultSavedQueryExists ) {
+ // This came from the server, meaning that we have a default
+ // saved query, but the server could not load it, probably because
+ // it was pre-conversion to the new format.
+ // We need to load this query again
this.applySavedQuery( this.savedQueriesModel.getDefault() );
} else {
// There are either recognized parameters in the URL
// again
this.updateStateFromUrl( false );
+ pieces = this._extractChangesListInfo( $( '#mw-content-text' ) );
+
// Update the changes list with the existing data
// so it gets processed
this.changesListModel.update(
- $changesList.length ? $changesList : 'NO_RESULTS',
- $( 'fieldset.cloptions' ).first(),
+ pieces.changes,
+ pieces.fieldset,
+ pieces.noResultsDetails,
true // We're using existing DOM elements
);
}
}
};
+ /**
+ * Extracts information from the changes list DOM
+ *
+ * @param {jQuery} $root Root DOM to find children from
+ * @return {Object} Information about changes list
+ * @return {Object|string} return.changes Changes list, or 'NO_RESULTS' if there are no results
+ * (either normally or as an error)
+ * @return {string} [return.noResultsDetails] 'NO_RESULTS_NORMAL' for a normal 0-result set,
+ * 'NO_RESULTS_TIMEOUT' for no results due to a timeout, or omitted for more than 0 results
+ * @return {jQuery} return.fieldset Fieldset
+ */
+ mw.rcfilters.Controller.prototype._extractChangesListInfo = function ( $root ) {
+ var info, isTimeout,
+ $changesListContents = $root.find( '.mw-changeslist' ).first().contents(),
+ areResults = !!$changesListContents.length;
+
+ info = {
+ changes: $changesListContents.length ? $changesListContents : 'NO_RESULTS',
+ fieldset: $root.find( 'fieldset.cloptions' ).first()
+ };
+
+ if ( !areResults ) {
+ isTimeout = !!$root.find( '.mw-changeslist-timeout' ).length;
+ info.noResultsDetails = isTimeout ? 'NO_RESULTS_TIMEOUT' : 'NO_RESULTS_NORMAL';
+ }
+
+ return info;
+ };
+
/**
* Create filter data from a number, for the filters that are numerical value
*
* Reset to default filters
*/
mw.rcfilters.Controller.prototype.resetToDefaults = function () {
- this.uriProcessor.updateModelBasedOnQuery( this._getDefaultParams() );
-
- this.updateChangesList();
+ if ( this.applyParamChange( this._getDefaultParams() ) ) {
+ // Only update the changes list if there was a change to actual filters
+ this.updateChangesList();
+ }
};
/**
* @return {boolean} Defaults are all false
*/
mw.rcfilters.Controller.prototype.areDefaultsEmpty = function () {
- var defaultFilters = this.filtersModel.getFiltersFromParameters( this._getDefaultParams() );
-
- this._deleteExcludedValuesFromFilterState( defaultFilters );
-
- // Defaults can change in a session, so we need to do this every time
- return Object.keys( defaultFilters ).every( function ( filterName ) {
- return !defaultFilters[ filterName ];
- } );
+ return $.isEmptyObject( this._getDefaultParams( true ) );
};
/**
* Empty all selected filters
*/
mw.rcfilters.Controller.prototype.emptyFilters = function () {
- var highlightedFilterNames = this.filtersModel
- .getHighlightedItems()
+ var highlightedFilterNames = this.filtersModel.getHighlightedItems()
.map( function ( filterItem ) { return { name: filterItem.getName() }; } );
- this.filtersModel.emptyAllFilters();
- this.filtersModel.clearAllHighlightColors();
- // Check all filter interactions
- this.filtersModel.reassessFilterInteractions();
-
- this.updateChangesList();
+ if ( this.applyParamChange( {} ) ) {
+ // Only update the changes list if there was a change to actual filters
+ this.updateChangesList();
+ }
if ( highlightedFilterNames ) {
this._trackHighlight( 'clearAll', highlightedFilterNames );
*/
mw.rcfilters.Controller.prototype.clearFilter = function ( filterName ) {
var filterItem = this.filtersModel.getItemByName( filterName ),
- isHighlighted = filterItem.isHighlighted();
+ isHighlighted = filterItem.isHighlighted(),
+ isSelected = filterItem.isSelected();
- if ( filterItem.isSelected() || isHighlighted ) {
+ if ( isSelected || isHighlighted ) {
this.filtersModel.clearHighlightColor( filterName );
this.filtersModel.toggleFilterSelected( filterName, false );
- this.updateChangesList();
+
+ if ( isSelected ) {
+ // Only update the changes list if the filter changed
+ // its selection state. If it only changed its highlight
+ // then don't reload
+ this.updateChangesList();
+ }
+
this.filtersModel.reassessFilterInteractions( filterItem );
// Log filter grouping
*/
mw.rcfilters.Controller.prototype.toggleHighlight = function () {
this.filtersModel.toggleHighlight();
- this._updateURL();
+ this.uriProcessor.updateURL();
if ( this.filtersModel.isHighlightEnabled() ) {
mw.hook( 'RcFilters.highlight.enable' ).fire();
*/
mw.rcfilters.Controller.prototype.setHighlightColor = function ( filterName, color ) {
this.filtersModel.setHighlightColor( filterName, color );
- this._updateURL();
+ this.uriProcessor.updateURL();
this._trackHighlight( 'set', { name: filterName, color: color } );
};
*/
mw.rcfilters.Controller.prototype.clearHighlightColor = function ( filterName ) {
this.filtersModel.clearHighlightColor( filterName );
- this._updateURL();
+ this.uriProcessor.updateURL();
this._trackHighlight( 'clear', filterName );
};
* @param {boolean} [setAsDefault=false] This query should be set as the default
*/
mw.rcfilters.Controller.prototype.saveCurrentQuery = function ( label, setAsDefault ) {
- var queryID,
- highlightedItems = {},
- highlightEnabled = this.filtersModel.isHighlightEnabled(),
- selectedState = this.filtersModel.getSelectedState();
-
- // Prepare highlights
- this.filtersModel.getHighlightedItems().forEach( function ( item ) {
- highlightedItems[ item.getName() ] = highlightEnabled ?
- item.getHighlightColor() : null;
- } );
- // These are filter states; highlight is stored as boolean
- highlightedItems.highlight = this.filtersModel.isHighlightEnabled();
-
- // Delete all excluded filters
- this._deleteExcludedValuesFromFilterState( selectedState );
-
// Add item
- queryID = this.savedQueriesModel.addNewQuery(
+ this.savedQueriesModel.addNewQuery(
label || mw.msg( 'rcfilters-savedqueries-defaultlabel' ),
- {
- filters: selectedState,
- highlights: highlightedItems,
- invert: this.filtersModel.areNamespacesInverted()
- }
+ this.filtersModel.getCurrentParameterState( true ),
+ setAsDefault
);
- if ( setAsDefault ) {
- this.savedQueriesModel.setDefault( queryID );
- }
-
// Save item
this._saveSavedQueries();
};
* @param {string} queryID Query id
*/
mw.rcfilters.Controller.prototype.applySavedQuery = function ( queryID ) {
- var data, highlights,
- queryItem = this.savedQueriesModel.getItemByID( queryID ),
- currentMatchingQuery = this.findQueryMatchingCurrentState();
+ var currentMatchingQuery,
+ params = this.savedQueriesModel.getItemParams( queryID );
+
+ currentMatchingQuery = this.findQueryMatchingCurrentState();
if (
- queryItem &&
- (
- // If there's already a query, don't reload it
- // if it's the same as the one that already exists
- !currentMatchingQuery ||
- currentMatchingQuery.getID() !== queryItem.getID()
- )
+ currentMatchingQuery &&
+ currentMatchingQuery.getID() === queryID
) {
- data = queryItem.getData();
- highlights = data.highlights;
-
- // Backwards compatibility; initial version mispelled 'highlight' with 'highlights'
- highlights.highlight = highlights.highlights || highlights.highlight;
-
- // Update model state from filters
- this.filtersModel.toggleFiltersSelected(
- // Merge filters with excluded values
- $.extend( true, {}, data.filters, this.filtersModel.getExcludedFiltersState() )
- );
-
- // Update namespace inverted property
- this.filtersModel.toggleInvertedNamespaces( !!Number( data.invert ) );
-
- // Update highlight state
- this.filtersModel.toggleHighlight( !!Number( highlights.highlight ) );
- this.filtersModel.getItems().forEach( function ( filterItem ) {
- var color = highlights[ filterItem.getName() ];
- if ( color ) {
- filterItem.setHighlightColor( color );
- } else {
- filterItem.clearHighlightColor();
- }
- } );
-
- // Check all filter interactions
- this.filtersModel.reassessFilterInteractions();
+ // If the query we want to load is the one that is already
+ // loaded, don't reload it
+ return;
+ }
+ if ( this.applyParamChange( params ) ) {
+ // Update changes list only if there was a difference in filter selection
this.updateChangesList();
-
- // Log filter grouping
- this.trackFilterGroupings( 'savedfilters' );
}
+
+ // Log filter grouping
+ this.trackFilterGroupings( 'savedfilters' );
};
/**
* Check whether the current filter and highlight state exists
* in the saved queries model.
*
- * @return {boolean} Query exists
+ * @return {mw.rcfilters.dm.SavedQueryItemModel} Matching item model
*/
mw.rcfilters.Controller.prototype.findQueryMatchingCurrentState = function () {
- var highlightedItems = {},
- selectedState = this.filtersModel.getSelectedState();
-
- // Prepare highlights of the current query
- this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) {
- highlightedItems[ item.getName() ] = item.getHighlightColor();
- } );
- highlightedItems.highlight = this.filtersModel.isHighlightEnabled();
-
- // Remove anything that should be excluded from the saved query
- // this includes sticky filters and filters marked with 'excludedFromSavedQueries'
- this._deleteExcludedValuesFromFilterState( selectedState );
-
return this.savedQueriesModel.findMatchingQuery(
- {
- filters: selectedState,
- highlights: highlightedItems,
- invert: this.filtersModel.areNamespacesInverted()
- }
+ this.filtersModel.getCurrentParameterState( true )
);
};
- /**
- * Delete sticky filters from given object
- *
- * @param {Object} filterState Filter state
- */
- mw.rcfilters.Controller.prototype._deleteExcludedValuesFromFilterState = function ( filterState ) {
- // Remove excluded filters
- $.each( this.filtersModel.getExcludedFiltersState(), function ( filterName ) {
- delete filterState[ filterName ];
- } );
- };
-
- /**
- * Get an object representing the base state of parameters
- * and highlights.
- *
- * This is meant to make sure that the saved queries that are
- * in memory are always the same structure as what we would get
- * by calling the current model's "getSelectedState" and by checking
- * highlight items.
- *
- * In cases where a user saved a query when the system had a certain
- * set of filters, and then a filter was added to the system, we want
- * to make sure that the stored queries can still be comparable to
- * the current state, which means that we need the base state for
- * two operations:
- *
- * - Saved queries are stored in "minimal" view (only changed filters
- * are stored); When we initialize the system, we merge each minimal
- * query with the base state (using 'getNormalizedFilters') so all
- * saved queries have the exact same structure as what we would get
- * by checking the getSelectedState of the filter.
- * - When we save the queries, we minimize the object to only represent
- * whatever has actually changed, rather than store the entire
- * object. To check what actually is different so we can store it,
- * we need to obtain a base state to compare against, this is
- * what #_getMinimalFilterList does
- */
- mw.rcfilters.Controller.prototype._buildBaseFilterState = function () {
- var defaultParams = this.filtersModel.getDefaultParams(),
- highlightedItems = {};
-
- // Prepare highlights
- this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) {
- highlightedItems[ item.getName() ] = null;
- } );
- highlightedItems.highlight = false;
-
- this.baseFilterState = {
- filters: this.filtersModel.getFiltersFromParameters( defaultParams ),
- highlights: highlightedItems,
- invert: false
- };
- };
-
- /**
- * Get an object representing the base filter state of both
- * filters and highlights. The structure is similar to what we use
- * to store each query in the saved queries object:
- * {
- * filters: {
- * filterName: (bool)
- * },
- * highlights: {
- * filterName: (string|null)
- * }
- * }
- *
- * @return {Object} Object representing the base state of
- * parameters and highlights
- */
- mw.rcfilters.Controller.prototype._getBaseFilterState = function () {
- return this.baseFilterState;
- };
-
- /**
- * Get an object that holds only the parameters and highlights that have
- * values different than the base default value.
- *
- * This is the reverse of the normalization we do initially on loading and
- * initializing the saved queries model.
- *
- * @param {Object} valuesObject Object representing the state of both
- * filters and highlights in its normalized version, to be minimized.
- * @return {Object} Minimal filters and highlights list
- */
- mw.rcfilters.Controller.prototype._getMinimalFilterList = function ( valuesObject ) {
- var result = { filters: {}, highlights: {}, invert: valuesObject.invert },
- baseState = this._getBaseFilterState();
-
- // XOR results
- $.each( valuesObject.filters, function ( name, value ) {
- if ( baseState.filters !== undefined && baseState.filters[ name ] !== value ) {
- result.filters[ name ] = value;
- }
- } );
-
- $.each( valuesObject.highlights, function ( name, value ) {
- if ( baseState.highlights !== undefined && baseState.highlights[ name ] !== value ) {
- result.highlights[ name ] = value;
- }
- } );
-
- return result;
- };
-
/**
* Save the current state of the saved queries model with all
* query item representation in the user settings.
*/
mw.rcfilters.Controller.prototype._saveSavedQueries = function () {
- var stringified,
- state = this.savedQueriesModel.getState(),
- controller = this;
-
- // Minimize before save
- $.each( state.queries, function ( queryID, info ) {
- state.queries[ queryID ].data = controller._getMinimalFilterList( info.data );
- } );
+ var stringified, oldPrefValue,
+ backupPrefName = this.savedQueriesPreferenceName + '-versionbackup',
+ state = this.savedQueriesModel.getState();
// Stringify state
stringified = JSON.stringify( state );
return;
}
+ if ( !this.wereSavedQueriesSaved && this.savedQueriesModel.isConverted() ) {
+ // The queries were converted from the previous version
+ // Keep the old string in the [prefname]-versionbackup
+ oldPrefValue = mw.user.options.get( this.savedQueriesPreferenceName );
+
+ // Save the old preference in the backup preference
+ new mw.Api().saveOption( backupPrefName, oldPrefValue );
+ // Update the preference for this session
+ mw.user.options.set( backupPrefName, oldPrefValue );
+ }
+
// Save the preference
new mw.Api().saveOption( this.savedQueriesPreferenceName, stringified );
// Update the preference for this session
mw.user.options.set( this.savedQueriesPreferenceName, stringified );
+
+ // Tag as already saved so we don't do this again
+ this.wereSavedQueriesSaved = true;
};
/**
* without adding an history entry.
*/
mw.rcfilters.Controller.prototype.replaceUrl = function () {
- mw.rcfilters.UriProcessor.static.replaceState( this._getUpdatedUri() );
+ this.uriProcessor.updateURL();
};
/**
updateMode = updateMode === undefined ? this.FILTER_CHANGE : updateMode;
if ( updateMode === this.FILTER_CHANGE ) {
- this._updateURL( params );
+ this.uriProcessor.updateURL( params );
}
if ( updateMode === this.FILTER_CHANGE || updateMode === this.SHOW_NEW_CHANGES ) {
this.changesListModel.invalidate();
this.changesListModel.update(
$changesListContent,
$fieldset,
+ pieces.noResultsDetails,
false,
// separator between old and new changes
updateMode === this.SHOW_NEW_CHANGES || updateMode === this.LIVE_UPDATE
* Get an object representing the default parameter state, whether
* it is from the model defaults or from the saved queries.
*
+ * @param {boolean} [excludeHiddenParams] Exclude hidden and sticky params
* @return {Object} Default parameters
*/
- mw.rcfilters.Controller.prototype._getDefaultParams = function () {
- var data, queryHighlights,
- savedParams = {},
- savedHighlights = {},
- defaultSavedQueryItem = !mw.user.isAnon() && this.savedQueriesModel.getItemByID( this.savedQueriesModel.getDefault() );
-
- if ( defaultSavedQueryItem ) {
- data = defaultSavedQueryItem.getData();
-
- queryHighlights = data.highlights || {};
- savedParams = this.filtersModel.getParametersFromFilters(
- $.extend( true, {}, data.filters, this.filtersModel.getStickyFiltersState() )
- );
-
- // Translate highlights to parameters
- savedHighlights.highlight = String( Number( queryHighlights.highlight ) );
- $.each( queryHighlights, function ( filterName, color ) {
- if ( filterName !== 'highlights' ) {
- savedHighlights[ filterName + '_color' ] = color;
- }
- } );
-
- return $.extend( true, {}, savedParams, savedHighlights, { invert: String( Number( data.invert || 0 ) ) } );
- }
-
- return this.filtersModel.getDefaultParams();
- };
-
- /**
- * Update the URL of the page to reflect current filters
- *
- * This should not be called directly from outside the controller.
- * If an action requires changing the URL, it should either use the
- * highlighting actions below, or call #updateChangesList which does
- * the uri corrections already.
- *
- * @param {Object} [params] Extra parameters to add to the API call
- */
- mw.rcfilters.Controller.prototype._updateURL = function ( params ) {
- var currentUri = new mw.Uri(),
- updatedUri = this._getUpdatedUri();
-
- updatedUri.extend( params || {} );
-
- if (
- this.uriProcessor.getVersion( currentUri.query ) !== 2 ||
- this.uriProcessor.isNewState( currentUri.query, updatedUri.query )
- ) {
- mw.rcfilters.UriProcessor.static.replaceState( updatedUri );
+ mw.rcfilters.Controller.prototype._getDefaultParams = function ( excludeHiddenParams ) {
+ if ( this.savedQueriesModel.getDefault() ) {
+ return this.savedQueriesModel.getDefaultParams( excludeHiddenParams );
+ } else {
+ return this.filtersModel.getDefaultParams( excludeHiddenParams );
}
};
- /**
- * Get an updated mw.Uri object based on the model state
- *
- * @return {mw.Uri} Updated Uri
- */
- mw.rcfilters.Controller.prototype._getUpdatedUri = function () {
- var uri = new mw.Uri();
-
- // Minimize url
- uri.query = this.uriProcessor.minimizeQuery(
- $.extend(
- true,
- {},
- // We want to retain unrecognized params
- // The uri params from model will override
- // any recognized value in the current uri
- // query, retain unrecognized params, and
- // the result will then be minimized
- uri.query,
- this.uriProcessor.getUriParametersFromModel(),
- { urlversion: '2' }
- )
- );
-
- return uri;
- };
-
/**
* Query the list of changes from the server for the current filters
*
* @return {jQuery.Promise} Promise object resolved with { content, status }
*/
mw.rcfilters.Controller.prototype._queryChangesList = function ( counterId, params ) {
- var uri = this._getUpdatedUri(),
- stickyParams = this.filtersModel.getStickyParams(),
+ var uri = this.uriProcessor.getUpdatedUri(),
+ stickyParams = this.filtersModel.getStickyParamsValues(),
requestId,
latestRequest;
return this._queryChangesList( 'updateChangesList' )
.then(
function ( data ) {
- var $parsed = $( '<div>' ).append( $( $.parseHTML( data.content ) ) ),
- pieces = {
- // Changes list
- changes: $parsed.find( '.mw-changeslist' ).first().contents(),
- // Fieldset
- fieldset: $parsed.find( 'fieldset.cloptions' ).first()
+ var $parsed;
+
+ // Status code 0 is not HTTP status code,
+ // but is valid value of XMLHttpRequest status.
+ // It is used for variety of network errors, for example
+ // when an AJAX call was cancelled before getting the response
+ if ( data && data.status === 0 ) {
+ return {
+ changes: 'NO_RESULTS',
+ // We need empty result set, to avoid exceptions because of undefined value
+ fieldset: $( [] ),
+ noResultsDetails: 'NO_RESULTS_NETWORK_ERROR'
};
-
- if ( pieces.changes.length === 0 ) {
- pieces.changes = 'NO_RESULTS';
}
- return pieces;
- }
+ $parsed = $( '<div>' ).append( $( $.parseHTML( data.content ) ) );
+
+ return this._extractChangesListInfo( $parsed );
+
+ }.bind( this )
);
};
}
};
+ /**
+ * Apply a change of parameters to the model state, and check whether
+ * the new state is different than the old state.
+ *
+ * @param {Object} newParamState New parameter state to apply
+ * @return {boolean} New applied model state is different than the previous state
+ */
+ mw.rcfilters.Controller.prototype.applyParamChange = function ( newParamState ) {
+ var after,
+ before = this.filtersModel.getSelectedState();
+
+ this.filtersModel.updateStateFromParams( newParamState );
+
+ after = this.filtersModel.getSelectedState();
+
+ return !OO.compare( before, after );
+ };
+
/**
* Mark all changes as seen on Watchlist
*/