this.savedQueriesModel = savedQueriesModel;
this.requestCounter = 0;
this.baseFilterState = {};
- this.emptyParameterState = {};
+ this.uriProcessor = null;
this.initializing = false;
};
* Initialize the filter and parameter states
*
* @param {Array} filterStructure Filter definition and structure for the model
+ * @param {Object} [namespaceStructure] Namespace definition
+ * @param {Object} [tagList] Tag definition
*/
- mw.rcfilters.Controller.prototype.initialize = function ( filterStructure ) {
- var parsedSavedQueries, validParameterNames,
+ mw.rcfilters.Controller.prototype.initialize = function ( filterStructure, namespaceStructure, tagList ) {
+ var parsedSavedQueries,
+ views = {},
+ items = [],
uri = new mw.Uri(),
$changesList = $( '.mw-changeslist' ).first().contents();
+ // Prepare views
+ if ( namespaceStructure ) {
+ items = [];
+ $.each( namespaceStructure, function ( namespaceID, label ) {
+ // Build and clean up the individual namespace items definition
+ items.push( {
+ name: namespaceID,
+ label: label || mw.msg( 'blanknamespace' ),
+ description: '',
+ identifiers: [
+ ( namespaceID < 0 || namespaceID % 2 === 0 ) ?
+ 'subject' : 'talk'
+ ],
+ cssClass: 'mw-changeslist-ns-' + namespaceID
+ } );
+ } );
+
+ views.namespaces = {
+ title: mw.msg( 'namespaces' ),
+ trigger: ':',
+ groups: [ {
+ // Group definition (single group)
+ name: 'namespace', // parameter name is singular
+ type: 'string_options',
+ title: mw.msg( 'namespaces' ),
+ labelPrefixKey: { 'default': 'rcfilters-tag-prefix-namespace', inverted: 'rcfilters-tag-prefix-namespace-inverted' },
+ separator: ';',
+ fullCoverage: true,
+ filters: items
+ } ]
+ };
+ }
+ if ( tagList ) {
+ views.tags = {
+ title: mw.msg( 'rcfilters-view-tags' ),
+ trigger: '#',
+ groups: [ {
+ // Group definition (single group)
+ name: 'tagfilter', // Parameter name
+ type: 'string_options',
+ title: 'rcfilters-view-tags', // Message key
+ labelPrefixKey: 'rcfilters-tag-prefix-tags',
+ separator: '|',
+ fullCoverage: false,
+ filters: tagList
+ } ]
+ };
+ }
+
// Initialize the model
- this.filtersModel.initializeFilters( filterStructure );
+ this.filtersModel.initializeFilters( filterStructure, views );
this._buildBaseFilterState();
- this._buildEmptyParameterState();
- validParameterNames = Object.keys( this._getEmptyParameterState() )
- .filter( function ( param ) {
- // Remove 'highlight' parameter from this check;
- // if it's the only parameter in the URL we still
- // want to consider the URL 'empty' for defaults to load
- return param !== 'highlight';
- } );
+
+ this.uriProcessor = new mw.rcfilters.UriProcessor(
+ this.filtersModel
+ );
try {
parsedSavedQueries = JSON.parse( mw.user.options.get( 'rcfilters-saved-queries' ) || '{}' );
// the user loads the base-page and we load defaults.
// Defaults should only be applied on load (if necessary)
// or on request
+ this.initializing = true;
if (
- Object.keys( uri.query ).some( function ( parameter ) {
- return validParameterNames.indexOf( parameter ) > -1;
- } )
+ this.savedQueriesModel.getDefault() &&
+ !this.uriProcessor.doesQueryContainRecognizedParams( uri.query )
) {
- // There are parameters in the url, update model state
- this.updateStateBasedOnUrl();
+ // 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
+ this.applySavedQuery( this.savedQueriesModel.getDefault() );
} else {
- this.initializing = true;
- // No valid parameters are given, load defaults
- this._updateModelState(
- $.extend(
- true,
- // We've ignored the highlight parameter for the sake
- // of seeing whether we need to apply defaults - but
- // while we do load the defaults, we still want to retain
- // the actual value given in the URL for it on top of the
- // defaults
- { highlight: String( Number( uri.query.highlight ) ) },
- this._getDefaultParams()
- )
+ // There are either recognized parameters in the URL
+ // or there are none, but there is also no default
+ // saved query (so defaults are from the backend)
+ // We want to update the state but not fetch results
+ // again
+ this.updateStateFromUrl( false );
+
+ // Update the changes list with the existing data
+ // so it gets processed
+ this.changesListModel.update(
+ $changesList.length ? $changesList : 'NO_RESULTS',
+ $( 'fieldset.rcoptions' ).first()
);
- this.updateChangesList();
- this.initializing = false;
}
- // Update the changes list with the existing data
- // so it gets processed
- this.changesListModel.update(
- $changesList.length ? $changesList : 'NO_RESULTS',
- $( 'fieldset.rcoptions' ).first()
- );
+ this.initializing = false;
+ this.switchView( 'default' );
+ };
+
+ /**
+ * Switch the view of the filters model
+ *
+ * @param {string} view Requested view
+ */
+ mw.rcfilters.Controller.prototype.switchView = function ( view ) {
+ this.filtersModel.switchView( view );
};
/**
* Reset to default filters
*/
mw.rcfilters.Controller.prototype.resetToDefaults = function () {
- this._updateModelState( $.extend( true, { highlight: '0' }, this._getDefaultParams() ) );
+ this.uriProcessor.updateModelBasedOnQuery( this._getDefaultParams() );
this.updateChangesList();
};
}
};
+ /**
+ * Toggle the namespaces inverted feature on and off
+ */
+ mw.rcfilters.Controller.prototype.toggleInvertedNamespaces = function () {
+ this.filtersModel.toggleInvertedNamespaces();
+ this.updateChangesList();
+ };
+
/**
* Set the highlight color for a filter item
*
label || mw.msg( 'rcfilters-savedqueries-defaultlabel' ),
{
filters: this.filtersModel.getSelectedState(),
- highlights: highlightedItems
+ highlights: highlightedItems,
+ invert: this.filtersModel.areNamespacesInverted()
}
);
* @param {string} queryID Query id
*/
mw.rcfilters.Controller.prototype.removeSavedQuery = function ( queryID ) {
- var query = this.savedQueriesModel.getItemByID( queryID );
-
- this.savedQueriesModel.removeItems( [ query ] );
+ this.savedQueriesModel.removeQuery( queryID );
- // Check if this item was the default
- if ( this.savedQueriesModel.getDefault() === queryID ) {
- // Nulify the default
- this.savedQueriesModel.setDefault( null );
- }
this._saveSavedQueries();
};
// Update model state from filters
this.filtersModel.toggleFiltersSelected( data.filters );
+ // 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 ) {
return this.savedQueriesModel.findMatchingQuery(
{
filters: this.filtersModel.getSelectedState(),
- highlights: highlightedItems
+ highlights: highlightedItems,
+ invert: this.filtersModel.areNamespacesInverted()
}
);
};
this.baseFilterState = {
filters: this.filtersModel.getFiltersFromParameters( defaultParams ),
- highlights: highlightedItems
+ highlights: highlightedItems,
+ invert: false
};
};
- /**
- * Build an empty representation of the parameters, where all parameters
- * are either set to '0' or '' depending on their type.
- * This must run during initialization, before highlights are set.
- */
- mw.rcfilters.Controller.prototype._buildEmptyParameterState = function () {
- var emptyParams = this.filtersModel.getParametersFromFilters( {} ),
- emptyHighlights = this.filtersModel.getHighlightParameters();
-
- this.emptyParameterState = $.extend(
- true,
- {},
- emptyParams,
- emptyHighlights,
- { highlight: '0' }
- );
- };
-
/**
* Get an object representing the base filter state of both
* filters and highlights. The structure is similar to what we use
return this.baseFilterState;
};
- /**
- * Get an object representing the base state of parameters
- * and highlights. The structure is similar to what we use
- * to store each query in the saved queries object:
- * {
- * param1: "value",
- * param2: "value1|value2"
- * }
- *
- * @return {Object} Object representing the base state of
- * parameters and highlights
- */
- mw.rcfilters.Controller.prototype._getEmptyParameterState = function () {
- return this.emptyParameterState;
- };
-
/**
* Get an object that holds only the parameters and highlights that have
* values different than the base default value.
* without adding an history entry.
*/
mw.rcfilters.Controller.prototype.replaceUrl = function () {
- window.history.replaceState(
- { tag: 'rcfilters' },
- document.title,
- this._getUpdatedUri().toString()
- );
+ mw.rcfilters.UriProcessor.static.replaceState( this._getUpdatedUri() );
};
/**
* Update filter state (selection and highlighting) based
* on current URL values.
+ *
+ * @param {boolean} [fetchChangesList=true] Fetch new results into the changes
+ * list based on the updated model.
*/
- mw.rcfilters.Controller.prototype.updateStateBasedOnUrl = function () {
- var uri = new mw.Uri();
+ mw.rcfilters.Controller.prototype.updateStateFromUrl = function ( fetchChangesList ) {
+ fetchChangesList = fetchChangesList === undefined ? true : !!fetchChangesList;
- this._updateModelState( uri.query );
- this.updateChangesList();
+ this.uriProcessor.updateModelBasedOnQuery( new mw.Uri().query );
+
+ // Only update and fetch new results if it is requested
+ if ( fetchChangesList ) {
+ this.updateChangesList();
+ }
};
/**
};
/**
- * Update the model state from given the given parameters.
- *
- * This is an internal method, and should only be used from inside
- * the controller.
+ * Get an object representing the default parameter state, whether
+ * it is from the model defaults or from the saved queries.
*
- * @param {Object} parameters Object representing the parameters for
- * filters and highlights
+ * @return {Object} Default parameters
*/
- mw.rcfilters.Controller.prototype._updateModelState = function ( parameters ) {
- // Update filter states
- this.filtersModel.toggleFiltersSelected(
- this.filtersModel.getFiltersFromParameters(
- parameters
- )
- );
+ mw.rcfilters.Controller.prototype._getDefaultParams = function () {
+ var data, queryHighlights,
+ savedParams = {},
+ savedHighlights = {},
+ defaultSavedQueryItem = this.savedQueriesModel.getItemByID( this.savedQueriesModel.getDefault() );
- // Update highlight state
- this.filtersModel.toggleHighlight( !!Number( parameters.highlight ) );
- this.filtersModel.getItems().forEach( function ( filterItem ) {
- var color = parameters[ filterItem.getName() + '_color' ];
- if ( color ) {
- filterItem.setHighlightColor( color );
- } else {
- filterItem.clearHighlightColor();
- }
- } );
+ if ( mw.config.get( 'wgStructuredChangeFiltersEnableSaving' ) &&
+ defaultSavedQueryItem ) {
- // Check all filter interactions
- this.filtersModel.reassessFilterInteractions();
+ data = defaultSavedQueryItem.getData();
+
+ queryHighlights = data.highlights || {};
+ savedParams = this.filtersModel.getParametersFromFilters( data.filters || {} );
+
+ // 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: data.invert } );
+ }
+
+ return $.extend(
+ { highlight: '0' },
+ this.filtersModel.getDefaultParams()
+ );
};
/**
* @param {Object} [params] Extra parameters to add to the API call
*/
mw.rcfilters.Controller.prototype._updateURL = function ( params ) {
- var currentFilterState, updatedFilterState, updatedUri,
- uri = new mw.Uri(),
- notEquivalent = function ( obj1, obj2 ) {
- var keys = Object.keys( obj1 ).concat( Object.keys( obj2 ) );
- return keys.some( function ( key ) {
- return obj1[ key ] != obj2[ key ]; // eslint-disable-line eqeqeq
- } );
- };
+ var currentUri = new mw.Uri(),
+ updatedUri = this._getUpdatedUri();
- params = params || {};
-
- updatedUri = this._getUpdatedUri();
- updatedUri.extend( params );
-
- // Compare states instead of parameters
- // This will allow us to always have a proper check of whether
- // the requested new url is one to change or not, regardless of
- // actual parameter visibility/representation in the URL
- currentFilterState = this.filtersModel.getFiltersFromParameters( uri.query );
- updatedFilterState = this.filtersModel.getFiltersFromParameters( updatedUri.query );
- // HACK: Re-merge extra parameters in
- // This is a hack and a quickfix; a better, more sustainable
- // fix is being worked on with a UriProcessor, but for now
- // we need to make sure the **comparison** of whether currentFilterState
- // and updatedFilterState differ **includes** the extra parameters in the URL
- currentFilterState = $.extend( true, {}, uri.query, currentFilterState );
- updatedFilterState = $.extend( true, {}, updatedUri.query, updatedFilterState );
-
- // Include highlight states
- $.extend( true,
- currentFilterState,
- this.filtersModel.extractHighlightValues( uri.query ),
- { highlight: !!Number( uri.query.highlight ) }
- );
- $.extend( true,
- updatedFilterState,
- this.filtersModel.extractHighlightValues( updatedUri.query ),
- { highlight: !!Number( updatedUri.query.highlight ) }
- );
+ updatedUri.extend( params || {} );
- if ( notEquivalent( currentFilterState, updatedFilterState ) ) {
- if ( this.initializing ) {
- // Initially, when we just build the first page load
- // out of defaults, we want to replace the history
- window.history.replaceState( { tag: 'rcfilters' }, document.title, updatedUri.toString() );
- } else {
- window.history.pushState( { tag: 'rcfilters' }, document.title, updatedUri.toString() );
- }
+ if (
+ this.uriProcessor.getVersion( currentUri.query ) !== 2 ||
+ this.uriProcessor.isNewState( currentUri.query, updatedUri.query )
+ ) {
+ mw.rcfilters.UriProcessor.static.replaceState( updatedUri );
}
};
* @return {mw.Uri} Updated Uri
*/
mw.rcfilters.Controller.prototype._getUpdatedUri = function () {
- var uri = new mw.Uri(),
- highlightParams = this.filtersModel.getHighlightParameters(),
- modelParameters = this.filtersModel.getParametersFromFilters(),
- baseParams = this._getEmptyParameterState();
-
- // Minimize values of the model parameters; show only the values that
- // are non-zero. We assume that all parameters that are not literally
- // showing in the URL are set to zero or empty
- $.each( modelParameters, function ( paramName, value ) {
- if ( baseParams[ paramName ] !== value ) {
- uri.query[ paramName ] = value;
- } else {
- // We need to remove this value from the url
- delete uri.query[ paramName ];
- }
- } );
+ var uri = new mw.Uri();
- // highlight params
- if ( this.filtersModel.isHighlightEnabled() ) {
- uri.query.highlight = Number( this.filtersModel.isHighlightEnabled() );
- } else {
- delete uri.query.highlight;
- }
- $.each( highlightParams, function ( paramName, value ) {
- // Only output if it is different than the base parameters
- if ( baseParams[ paramName ] !== value ) {
- uri.query[ paramName ] = value;
- } else {
- delete uri.query[ paramName ];
- }
- } );
+ // 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;
};