mw.rcfilters.dm.FilterGroup.prototype.isExcludedFromSavedQueries = function () {
return this.excludedFromSavedQueries;
};
+
+ /**
+ * Normalize a value given to this group. This is mostly for correcting
+ * arbitrary values for 'single option' groups, given by the user settings
+ * or the URL that can go outside the limits that are allowed.
+ *
+ * @param {string} value Given value
+ * @return {string} Corrected value
+ */
+ mw.rcfilters.dm.FilterGroup.prototype.normalizeArbitraryValue = function ( value ) {
+ if (
+ this.getType() === 'single_option' &&
+ this.isAllowArbitrary()
+ ) {
+ if (
+ this.getMaxValue() !== null &&
+ value > this.getMaxValue()
+ ) {
+ // Change the value to the actual max value
+ return String( this.getMaxValue() );
+ } else if (
+ this.getMinValue() !== null &&
+ value < this.getMinValue()
+ ) {
+ // Change the value to the actual min value
+ return String( this.getMinValue() );
+ }
+ }
+
+ return value;
+ };
}( mediaWiki ) );
this.defaultFiltersEmpty = null;
this.highlightEnabled = false;
this.parameterMap = {};
+ this.emptyParameterState = null;
this.views = {};
this.currentView = 'default';
this.emit( 'initialize' );
};
+ /**
+ * Update filter view model state based on a parameter object
+ *
+ * @param {Object} params Parameters object
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.updateStateFromParams = function ( params ) {
+ // For arbitrary numeric single_option values make sure the values
+ // are normalized to fit within the limits
+ $.each( this.getFilterGroups(), function ( groupName, groupModel ) {
+ params[ groupName ] = groupModel.normalizeArbitraryValue( params[ groupName ] );
+ } );
+
+ // Update filter states
+ this.toggleFiltersSelected(
+ this.getFiltersFromParameters(
+ params
+ )
+ );
+
+ // Update highlight state
+ this.getItemsSupportingHighlights().forEach( function ( filterItem ) {
+ var color = params[ filterItem.getName() + '_color' ];
+ if ( color ) {
+ filterItem.setHighlightColor( color );
+ } else {
+ filterItem.clearHighlightColor();
+ }
+ } );
+ this.toggleHighlight( !!Number( params.highlight ) );
+
+ // Check all filter interactions
+ this.reassessFilterInteractions();
+ };
+
+ /**
+ * Get a representation of an empty (falsey) parameter state
+ *
+ * @return {Object} Empty parameter state
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getEmptyParameterState = function () {
+ if ( !this.emptyParameterState ) {
+ this.emptyParameterState = $.extend(
+ true,
+ {},
+ this.getParametersFromFilters( {} ),
+ this.getEmptyHighlightParameters(),
+ { highlight: '0' }
+ );
+ }
+ return this.emptyParameterState;
+ };
+
+ /**
+ * Get a representation of only the non-falsey parameters
+ *
+ * @param {Object} [parameters] A given parameter state to minimize. If not given the current
+ * state of the system will be used.
+ * @return {Object} Empty parameter state
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getMinimizedParamRepresentation = function ( parameters ) {
+ var result = {};
+
+ parameters = parameters ? $.extend( true, {}, parameters ) : this.getCurrentParameterState();
+
+ // Params
+ $.each( this.getEmptyParameterState(), function ( param, value ) {
+ if ( parameters[ param ] !== undefined && parameters[ param ] !== value ) {
+ result[ param ] = parameters[ param ];
+ }
+ } );
+
+ // Highlights
+ Object.keys( this.getEmptyHighlightParameters() ).forEach( function ( param ) {
+ if ( param !== 'highlight' && parameters[ param ] ) {
+ // If a highlight parameter is not undefined and not null
+ // add it to the result
+ // Ignore "highlight" parameter because that, we checked already with
+ // the empty parameter state (and this soon changes to an implicit value)
+ result[ param ] = parameters[ param ];
+ }
+ } );
+
+ return result;
+ };
+
+ /**
+ * Get a representation of the full parameter list, including all base values
+ *
+ * @param {Object} [parameters] A given parameter state to minimize. If not given the current
+ * state of the system will be used.
+ * @param {boolean} [removeExcluded] Remove excluded and sticky parameters
+ * @return {Object} Full parameter representation
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getExpandedParamRepresentation = function ( parameters, removeExcluded ) {
+ var result = {};
+
+ parameters = parameters ? $.extend( true, {}, parameters ) : this.getCurrentParameterState();
+
+ result = $.extend(
+ true,
+ {},
+ this.getEmptyParameterState(),
+ parameters
+ );
+
+ if ( removeExcluded ) {
+ result = this.removeExcludedParams( result );
+ }
+
+ return result;
+ };
+
+ /**
+ * Get a parameter representation of the current state of the model
+ *
+ * @param {boolean} [removeExcludedParams] Remove excluded filters from final result
+ * @return {Object} Parameter representation of the current state of the model
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getCurrentParameterState = function ( removeExcludedParams ) {
+ var excludedParams,
+ state = this.getMinimizedParamRepresentation( $.extend(
+ true,
+ {},
+ this.getParametersFromFilters( this.getSelectedState() ),
+ this.getHighlightParameters(),
+ {
+ // HACK: Add highlight. This is only needed while it's
+ // stored as an outside state
+ highlight: String( Number( this.isHighlightEnabled() ) )
+ }
+ ) );
+
+ if ( removeExcludedParams ) {
+ excludedParams = this.getExcludedParams();
+ // Delete all excluded filters
+ $.each( state, function ( param ) {
+ if ( excludedParams.indexOf( param ) > -1 ) {
+ delete state[ param ];
+ }
+ } );
+ }
+
+ return state;
+ };
+
+ /**
+ * Delete excluded and sticky filters from given object. If object isn't given, output
+ * the current filter state without the excluded values
+ *
+ * @param {Object} [filterState] Filter state
+ * @return {Object} Filter state without excluded filters
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.removeExcludedFilters = function ( filterState ) {
+ filterState = filterState !== undefined ?
+ $.extend( true, {}, filterState ) :
+ this.getFiltersFromParameters();
+
+ // Remove excluded filters
+ Object.keys( this.getExcludedFiltersState() ).forEach( function ( filterName ) {
+ delete filterState[ filterName ];
+ } );
+
+ // Remove sticky filters
+ Object.keys( this.getStickyFiltersState() ).forEach( function ( filterName ) {
+ delete filterState[ filterName ];
+ } );
+
+ return filterState;
+ };
+ /**
+ * Delete excluded and sticky parameters from given object. If object isn't given, output
+ * the current param state without the excluded values
+ *
+ * @param {Object} [paramState] Parameter state
+ * @return {Object} Parameter state without excluded filters
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.removeExcludedParams = function ( paramState ) {
+ paramState = paramState !== undefined ?
+ $.extend( true, {}, paramState ) :
+ this.getCurrentParameterState();
+
+ // Remove excluded filters
+ this.getExcludedParams().forEach( function ( paramName ) {
+ delete paramState[ paramName ];
+ } );
+
+ // Remove sticky filters
+ this.getStickyParams().forEach( function ( paramName ) {
+ delete paramState[ paramName ];
+ } );
+
+ return paramState;
+ };
+
/**
* Get the names of all available filters
*
/**
* Get an object representing default parameters state
*
+ * @param {boolean} [excludeHiddenParams] Exclude hidden and sticky params
* @return {Object} Default parameter values
*/
- mw.rcfilters.dm.FiltersViewModel.prototype.getDefaultParams = function () {
+ mw.rcfilters.dm.FiltersViewModel.prototype.getDefaultParams = function ( excludeHiddenParams ) {
var result = {};
// Get default filter state
$.extend( true, result, model.getDefaultParams() );
} );
+ if ( excludeHiddenParams ) {
+ Object.keys( this.getDefaultHiddenParams() ).forEach( function ( paramName ) {
+ delete result[ paramName ];
+ } );
+ }
+
+ return result;
+ };
+
+ /**
+ * Get an object representing defaults for the hidden parameters state
+ *
+ * @return {Object} Default values for hidden parameters
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getDefaultHiddenParams = function () {
+ var result = {};
+
+ // Get default filter state
+ $.each( this.groups, function ( name, model ) {
+ if ( model.isHidden() ) {
+ $.extend( true, result, model.getDefaultParams() );
+ }
+ } );
+
return result;
};
* @return {Object} Sticky parameter values
*/
mw.rcfilters.dm.FiltersViewModel.prototype.getStickyParams = function () {
+ var result = [];
+
+ $.each( this.groups, function ( name, model ) {
+ if ( model.isSticky() ) {
+ if ( model.isPerGroupRequestParameter() ) {
+ result.push( name );
+ } else {
+ // Each filter is its own param
+ result = result.concat( model.getItems().map( function ( filterItem ) {
+ return filterItem.getParamName();
+ } ) );
+ }
+ }
+ } );
+
+ return result;
+ };
+
+ /**
+ * Get a parameter representation of all sticky parameters
+ *
+ * @return {Object} Sticky parameter values
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getStickyParamsValues = function () {
var result = {};
$.each( this.groups, function ( name, model ) {
var result = {};
this.getItems().forEach( function ( filterItem ) {
- result[ filterItem.getName() + '_color' ] = filterItem.getHighlightColor() || null;
+ if ( filterItem.isHighlightSupported() ) {
+ result[ filterItem.getName() + '_color' ] = filterItem.getHighlightColor() || null;
+ }
} );
result.highlight = String( Number( this.isHighlightEnabled() ) );
var result = {};
this.getItems().forEach( function ( filterItem ) {
- result[ filterItem.getName() + '_color' ] = null;
+ if ( filterItem.isHighlightSupported() ) {
+ result[ filterItem.getName() + '_color' ] = null;
+ }
} );
result.highlight = '0';
* @fires initialize
*/
mw.rcfilters.dm.SavedQueriesModel.prototype.initialize = function ( savedQueries ) {
- var model = this,
- excludedParams = this.filtersModel.getExcludedParams();
+ var model = this;
savedQueries = savedQueries || {};
if ( normalizedData && normalizedData.params ) {
// Backwards-compat fix: Remove excluded parameters from
// the given data, if they exist
- excludedParams.forEach( function ( name ) {
- delete normalizedData.params[ name ];
- } );
+ normalizedData.params = model.filtersModel.removeExcludedParams( normalizedData.params );
id = String( id );
- model.addNewQuery( obj.label, normalizedData, isDefault, id );
+
+ // Skip the addNewQuery method because we don't want to unnecessarily manipulate
+ // the given saved queries unless we literally intend to (like in backwards compat fixes)
+ // And the addNewQuery method also uses a minimization routine that checks for the
+ // validity of items and minimizes the query. This isn't necessary for queries loaded
+ // from the backend, and has the risk of removing values if they're temporarily
+ // invalid (example: if we temporarily removed a cssClass from a filter in the backend)
+ model.addItems( [
+ new mw.rcfilters.dm.SavedQueryItemModel(
+ id,
+ obj.label,
+ normalizedData,
+ { 'default': isDefault }
+ )
+ ] );
if ( isDefault ) {
model.default = id;
delete data.highlights.highlight;
// Filters
- newData.params = this.filtersModel.getParametersFromFilters( fullFilterRepresentation );
+ newData.params = this.filtersModel.getMinimizedParamRepresentation(
+ this.filtersModel.getParametersFromFilters( fullFilterRepresentation )
+ );
// Highlights (taking out 'highlight' itself, appending _color to keys)
newData.highlights = {};
- Object.keys( data.highlights ).forEach( function ( highlightedFilterName ) {
- newData.highlights[ highlightedFilterName + '_color' ] = data.highlights[ highlightedFilterName ];
+ $.each( data.highlights, function ( highlightedFilterName, value ) {
+ if ( value ) {
+ newData.highlights[ highlightedFilterName + '_color' ] = data.highlights[ highlightedFilterName ];
+ }
} );
// Add highlight
return newData;
};
- /**
- * 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 params, 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 params
- * are stored); When we initialize the system, we merge each minimal
- * query with the base state (using 'getMinimalParamList') 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 #getMinimalParamList does
- *
- * @return {Object} Base parameter state
- */
- mw.rcfilters.dm.SavedQueriesModel.prototype.getBaseParamState = function () {
- var allParams,
- highlightedItems = {};
-
- if ( !this.baseParamState ) {
- allParams = this.filtersModel.getParametersFromFilters( {} );
-
- // Prepare highlights
- this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) {
- highlightedItems[ item.getName() + '_color' ] = null;
- } );
-
- this.baseParamState = {
- params: $.extend( true, { highlight: '0' }, allParams ),
- highlights: highlightedItems
- };
- }
-
- return this.baseParamState;
- };
-
- /**
- * Get an object that holds only the parameters and highlights that have
- * values different than the base 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.dm.SavedQueriesModel.prototype.getMinimalParamList = function ( valuesObject ) {
- var result = { params: {}, highlights: {} },
- baseState = this.getBaseParamState();
-
- // XOR results
- $.each( valuesObject.params, function ( name, value ) {
- if ( baseState.params !== undefined && baseState.params[ name ] !== value ) {
- result.params[ name ] = value;
- }
- } );
-
- $.each( valuesObject.highlights, function ( name, value ) {
- if ( baseState.highlights !== undefined && baseState.highlights[ name ] !== value ) {
- result.highlights[ name ] = value;
- }
- } );
-
- return result;
- };
-
/**
* Add a query item
*
* @param {string} label Label for the new query
- * @param {Object} data Data for the new query
+ * @param {Object} fulldata Full data representation for the new query, combining highlights and filters
* @param {boolean} isDefault Item is default
* @param {string} [id] Query ID, if exists. If this isn't given, a random
* new ID will be created.
* @return {string} ID of the newly added query
*/
- mw.rcfilters.dm.SavedQueriesModel.prototype.addNewQuery = function ( label, data, isDefault, id ) {
- var randomID = String( id || ( new Date() ).getTime() ),
- normalizedData = this.getMinimalParamList( data );
+ mw.rcfilters.dm.SavedQueriesModel.prototype.addNewQuery = function ( label, fulldata, isDefault, id ) {
+ var normalizedData = { params: {}, highlights: {} },
+ highlightParamNames = Object.keys( this.filtersModel.getEmptyHighlightParameters() ),
+ randomID = String( id || ( new Date() ).getTime() ),
+ data = this.filtersModel.getMinimizedParamRepresentation( fulldata );
+
+ // Split highlight/params
+ $.each( data, function ( param, value ) {
+ if ( param !== 'highlight' && highlightParamNames.indexOf( param ) > -1 ) {
+ normalizedData.highlights[ param ] = value;
+ } else {
+ normalizedData.params[ param ] = value;
+ }
+ } );
// Add item
this.addItems( [
*/
mw.rcfilters.dm.SavedQueriesModel.prototype.findMatchingQuery = function ( fullQueryComparison ) {
// Minimize before comparison
- fullQueryComparison = this.getMinimalParamList( fullQueryComparison );
+ fullQueryComparison = this.filtersModel.getMinimizedParamRepresentation( fullQueryComparison );
return this.getItems().filter( function ( item ) {
return OO.compare(
- item.getData(),
+ item.getCombinedData(),
fullQueryComparison
);
} )[ 0 ];
};
/**
- * Get an item's full data
+ * Get the full data representation of the default query, if it exists
*
- * @param {string} queryID Query identifier
- * @return {Object} Item's full data
+ * @param {boolean} [excludeHiddenParams] Exclude hidden parameters in the result
+ * @return {Object|null} Representation of the default params if exists.
+ * Null if default doesn't exist or if the user is not logged in.
*/
- mw.rcfilters.dm.SavedQueriesModel.prototype.getItemFullData = function ( queryID ) {
- var item = this.getItemByID( queryID );
+ mw.rcfilters.dm.SavedQueriesModel.prototype.getDefaultParams = function ( excludeHiddenParams ) {
+ var data = ( !mw.user.isAnon() && this.getItemParams( this.getDefault() ) ) || {};
+
+ if ( excludeHiddenParams ) {
+ Object.keys( this.filtersModel.getDefaultHiddenParams() ).forEach( function ( paramName ) {
+ delete data[ paramName ];
+ } );
+ }
- // Fill in the base params
- return item ? $.extend( true, {}, this.getBaseParamState(), item.getData() ) : {};
+ return data;
+ };
+
+ /**
+ * Get a full parameter representation of an item data
+ *
+ * @param {Object} queryID Query ID
+ * @return {Object} Parameter representation
+ */
+ mw.rcfilters.dm.SavedQueriesModel.prototype.getItemParams = function ( queryID ) {
+ var item = this.getItemByID( queryID ),
+ data = item ? item.getData() : {};
+
+ return !$.isEmptyObject( data ) ? this.buildParamsFromData( data ) : {};
+ };
+
+ /**
+ * Build a full parameter representation given item data and model sticky values state
+ *
+ * @param {Object} data Item data
+ * @return {Object} Full param representation
+ */
+ mw.rcfilters.dm.SavedQueriesModel.prototype.buildParamsFromData = function ( data ) {
+ // Merge saved filter state with sticky filter values
+ var savedFilters;
+
+ data = data || {};
+
+ // In order to merge sticky filters with the data, we have to
+ // transform this to filters first, merge, and then back to
+ // parameters
+ savedFilters = $.extend(
+ true, {},
+ this.filtersModel.getFiltersFromParameters( data.params ),
+ this.filtersModel.getStickyFiltersState()
+ );
+
+ // Return parameter representation
+ return this.filtersModel.getMinimizedParamRepresentation( $.extend( true, {},
+ this.filtersModel.getParametersFromFilters( savedFilters ),
+ data.highlights,
+ { highlight: data.params.highlight }
+ ) );
};
/**
* @return {Object} Object representing the state of the model and items
*/
mw.rcfilters.dm.SavedQueriesModel.prototype.getState = function () {
- var model = this,
- obj = { queries: {}, version: '2' };
+ var obj = { queries: {}, version: '2' };
// Translate the items to the saved object
this.getItems().forEach( function ( item ) {
- var itemState = item.getState();
-
- itemState.data = model.getMinimalParamList( itemState.data );
-
- obj.queries[ item.getID() ] = itemState;
+ obj.queries[ item.getID() ] = item.getState();
} );
if ( this.getDefault() ) {
return this.data;
};
+ /**
+ * Get the combined data of this item as a flat object of parameters
+ *
+ * @return {Object} Combined parameter data
+ */
+ mw.rcfilters.dm.SavedQueryItemModel.prototype.getCombinedData = function () {
+ return $.extend( true, {}, this.data.params, this.data.highlights );
+ };
+
/**
* Check whether this item is the default
*
* Reset to default filters
*/
mw.rcfilters.Controller.prototype.resetToDefaults = function () {
- this.uriProcessor.updateModelBasedOnQuery( this._getDefaultParams() );
+ this.filtersModel.updateStateFromParams( this._getDefaultParams() );
this.updateChangesList();
};
* @return {boolean} Defaults are all false
*/
mw.rcfilters.Controller.prototype.areDefaultsEmpty = function () {
- var defaultParams = this._getDefaultParams(),
- defaultFilters = this.filtersModel.getFiltersFromParameters( defaultParams );
-
- this._deleteExcludedValuesFromFilterState( defaultFilters );
-
- if ( Object.keys( defaultParams ).some( function ( paramName ) {
- return paramName.match( /_color$/ ) && defaultParams[ paramName ] !== null;
- } ) ) {
- // There are highlights in the defaults, they're definitely
- // not empty
- return false;
- }
-
- // 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 ) );
};
/**
.getHighlightedItems()
.map( function ( filterItem ) { return { name: filterItem.getName() }; } );
- this.filtersModel.emptyAllFilters();
- this.filtersModel.clearAllHighlightColors();
- // Check all filter interactions
- this.filtersModel.reassessFilterInteractions();
+ this.filtersModel.updateStateFromParams( {} );
this.updateChangesList();
*/
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 highlightedItems = {},
- highlightEnabled = this.filtersModel.isHighlightEnabled(),
- selectedState = this.filtersModel.getSelectedState();
-
- // Prepare highlights
- this.filtersModel.getHighlightedItems().forEach( function ( item ) {
- highlightedItems[ item.getName() + '_color' ] = highlightEnabled ?
- item.getHighlightColor() : null;
- } );
-
- // Delete all excluded filters
- this._deleteExcludedValuesFromFilterState( selectedState );
-
// Add item
this.savedQueriesModel.addNewQuery(
label || mw.msg( 'rcfilters-savedqueries-defaultlabel' ),
- {
- params: $.extend(
- true,
- {
- highlight: String( Number( this.filtersModel.isHighlightEnabled() ) )
- },
- this.filtersModel.getParametersFromFilters( selectedState )
- ),
- highlights: highlightedItems
- },
+ this.filtersModel.getCurrentParameterState( true ),
setAsDefault
);
* @param {string} queryID Query id
*/
mw.rcfilters.Controller.prototype.applySavedQuery = function ( queryID ) {
- var highlights,
- queryItem = this.savedQueriesModel.getItemByID( queryID ),
- data = this.savedQueriesModel.getItemFullData( 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
) {
- highlights = data.highlights;
-
- // Update model state from filters
- this.filtersModel.toggleFiltersSelected(
- // Merge filters with excluded values
- $.extend(
- true,
- {},
- this.filtersModel.getFiltersFromParameters( data.params ),
- this.filtersModel.getExcludedFiltersState()
- )
- );
-
- // Update highlight state
- this.filtersModel.toggleHighlight( !!Number( data.params.highlight ) );
- this.filtersModel.getItems().forEach( function ( filterItem ) {
- var color = highlights[ filterItem.getName() + '_color' ];
- if ( color ) {
- filterItem.setHighlightColor( color );
- } else {
- filterItem.clearHighlightColor();
- }
- } );
+ // If the query we want to load is the one that is already
+ // loaded, don't reload it
+ return;
+ }
- // Check all filter interactions
- this.filtersModel.reassessFilterInteractions();
+ // Apply parameters to model
+ this.filtersModel.updateStateFromParams( params );
- this.updateChangesList();
+ this.updateChangesList();
- // Log filter grouping
- this.trackFilterGroupings( 'savedfilters' );
- }
+ // Log filter grouping
+ this.trackFilterGroupings( 'savedfilters' );
};
/**
* @return {boolean} Query exists
*/
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() + '_color' ] = item.getHighlightColor();
- } );
-
- // 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(
- {
- params: $.extend(
- true,
- {
- highlight: String( Number( this.filtersModel.isHighlightEnabled() ) )
- },
- this.filtersModel.getParametersFromFilters( selectedState )
- ),
- highlights: highlightedItems
- }
+ 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 ];
- } );
- };
-
/**
* Save the current state of the saved queries model with all
* query item representation in the user settings.
* without adding an history entry.
*/
mw.rcfilters.Controller.prototype.replaceUrl = function () {
- mw.rcfilters.UriProcessor.static.replaceState( this._getUpdatedUri() );
+ this.uriProcessor.replaceUpdatedUri();
};
/**
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();
* 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 savedFilters,
- data = ( !mw.user.isAnon() && this.savedQueriesModel.getItemFullData( this.savedQueriesModel.getDefault() ) ) || {};
-
- if ( !$.isEmptyObject( data ) ) {
- // Merge saved filter state with sticky filter values
- savedFilters = $.extend(
- true, {},
- this.filtersModel.getFiltersFromParameters( data.params ),
- this.filtersModel.getStickyFiltersState()
- );
-
- // Return parameter representation
- return $.extend( true, {},
- this.filtersModel.getParametersFromFilters( savedFilters ),
- data.highlights,
- { highlight: data.params.highlight }
- );
- }
- 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;
* @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters view model
*/
mw.rcfilters.UriProcessor = function MwRcfiltersController( filtersModel ) {
- this.emptyParameterState = {};
this.filtersModel = filtersModel;
-
- // Initialize
- this._buildEmptyParameterState();
};
/* Initialization */
};
/**
- * Update the filters model based on the URI query
- * This happens on initialization, and from this moment on,
- * we consider the system synchronized, and the model serves
- * as the source of truth for the URL.
- *
- * This methods should only be called once on initialiation.
- * After initialization, the model updates the URL, not the
- * other way around.
- *
- * @param {Object} [uriQuery] URI query
+ * Replace the current URI with an updated one from the model state
*/
- mw.rcfilters.UriProcessor.prototype.updateModelBasedOnQuery = function ( uriQuery ) {
- var parameters;
-
- uriQuery = uriQuery || new mw.Uri().query;
-
- // For arbitrary numeric single_option values, check the uri and see if it's beyond the limit
- $.each( this.filtersModel.getFilterGroups(), function ( groupName, groupModel ) {
- if (
- groupModel.getType() === 'single_option' &&
- groupModel.isAllowArbitrary()
- ) {
- if (
- groupModel.getMaxValue() !== null &&
- uriQuery[ groupName ] > groupModel.getMaxValue()
- ) {
- // Change the value to the actual max value
- uriQuery[ groupName ] = String( groupModel.getMaxValue() );
- } else if (
- groupModel.getMinValue() !== null &&
- uriQuery[ groupName ] < groupModel.getMinValue()
- ) {
- // Change the value to the actual min value
- uriQuery[ groupName ] = String( groupModel.getMinValue() );
- }
- }
- } );
-
- // Normalize
- parameters = this._getNormalizedQueryParams( uriQuery );
+ mw.rcfilters.UriProcessor.prototype.replaceUpdatedUri = function () {
+ this.constructor.static.replaceState( this.getUpdatedUri() );
+ };
- // Update filter states
- this.filtersModel.toggleFiltersSelected(
- this.filtersModel.getFiltersFromParameters(
- parameters
+ /**
+ * Get an updated mw.Uri object based on the model state
+ *
+ * @param {Object} [uriQuery] An external URI query to build the new uri
+ * with. This is mainly for tests, to be able to supply external parameters
+ * and make sure they are retained.
+ * @return {mw.Uri} Updated Uri
+ */
+ mw.rcfilters.UriProcessor.prototype.getUpdatedUri = function ( uriQuery ) {
+ var uri = new mw.Uri(),
+ unrecognizedParams = this.getUnrecognizedParams( uriQuery || uri.query );
+
+ if ( uriQuery ) {
+ // This is mainly for tests, to be able to give the method
+ // an initial URI Query and test that it retains parameters
+ uri.query = uriQuery;
+ }
+
+ uri.query = this.filtersModel.getMinimizedParamRepresentation(
+ $.extend(
+ true,
+ {},
+ uri.query,
+ // The representation must be expanded so it can
+ // override the uri query params but we then output
+ // a minimized version for the entire URI representation
+ // for the method
+ this.filtersModel.getExpandedParamRepresentation()
)
);
- // Update highlight state
- this.filtersModel.getItems().forEach( function ( filterItem ) {
- var color = parameters[ filterItem.getName() + '_color' ];
- if ( color ) {
- filterItem.setHighlightColor( color );
- } else {
- filterItem.clearHighlightColor();
- }
- } );
- this.filtersModel.toggleHighlight( !!Number( parameters.highlight ) );
+ // Reapply unrecognized params and url version
+ uri.query = $.extend( true, {}, uri.query, unrecognizedParams, { urlversion: '2' } );
- // Check all filter interactions
- this.filtersModel.reassessFilterInteractions();
+ return uri;
};
/**
- * Get parameters representing the current state of the model
+ * Get an object representing given parameters that are unrecognized by the model
*
- * @return {Object} Uri query parameters
+ * @param {Object} params Full params object
+ * @return {Object} Unrecognized params
*/
- mw.rcfilters.UriProcessor.prototype.getUriParametersFromModel = function () {
- return $.extend(
- true,
- {},
- this.filtersModel.getParametersFromFilters(),
- this.filtersModel.getHighlightParameters(),
- {
- highlight: String( Number( this.filtersModel.isHighlightEnabled() ) )
+ mw.rcfilters.UriProcessor.prototype.getUnrecognizedParams = function ( params ) {
+ // Start with full representation
+ var givenParamNames = Object.keys( params ),
+ unrecognizedParams = $.extend( true, {}, params );
+
+ // Extract unrecognized parameters
+ Object.keys( this.filtersModel.getEmptyParameterState() ).forEach( function ( paramName ) {
+ // Remove recognized params
+ if ( givenParamNames.indexOf( paramName ) > -1 ) {
+ delete unrecognizedParams[ paramName ];
}
- );
+ } );
+
+ return unrecognizedParams;
};
/**
- * Build the full parameter representation based on given query parameters
+ * Update the URL of the page to reflect current filters
*
- * @private
- * @param {Object} uriQuery Given URI query
- * @return {Object} Full parameter state representing the URI query
+ * 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.UriProcessor.prototype._expandModelParameters = function ( uriQuery ) {
- var filterRepresentation = this.filtersModel.getFiltersFromParameters( uriQuery );
+ mw.rcfilters.UriProcessor.prototype.updateURL = function ( params ) {
+ var currentUri = new mw.Uri(),
+ updatedUri = this.getUpdatedUri();
+
+ updatedUri.extend( params || {} );
+
+ if (
+ this.getVersion( currentUri.query ) !== 2 ||
+ this.isNewState( currentUri.query, updatedUri.query )
+ ) {
+ this.constructor.static.replaceState( updatedUri );
+ }
+ };
- return $.extend( true,
- {},
- uriQuery,
- this.filtersModel.getParametersFromFilters( filterRepresentation ),
- this.filtersModel.extractHighlightValues( uriQuery ),
- {
- highlight: String( Number( uriQuery.highlight ) )
- }
+ /**
+ * Update the filters model based on the URI query
+ * This happens on initialization, and from this moment on,
+ * we consider the system synchronized, and the model serves
+ * as the source of truth for the URL.
+ *
+ * This methods should only be called once on initialiation.
+ * After initialization, the model updates the URL, not the
+ * other way around.
+ *
+ * @param {Object} [uriQuery] URI query
+ */
+ mw.rcfilters.UriProcessor.prototype.updateModelBasedOnQuery = function ( uriQuery ) {
+ this.filtersModel.updateStateFromParams(
+ this._getNormalizedQueryParams( uriQuery || new mw.Uri().query )
);
};
// 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
- currentParamState = this._expandModelParameters( currentUriQuery );
- updatedParamState = this._expandModelParameters( updatedUriQuery );
+ currentParamState = $.extend(
+ true,
+ {},
+ this.filtersModel.getMinimizedParamRepresentation( currentUriQuery ),
+ this.getUnrecognizedParams( currentUriQuery )
+ );
+ updatedParamState = $.extend(
+ true,
+ {},
+ this.filtersModel.getMinimizedParamRepresentation( updatedUriQuery ),
+ this.getUnrecognizedParams( updatedUriQuery )
+ );
return notEquivalent( currentParamState, updatedParamState );
};
*/
mw.rcfilters.UriProcessor.prototype.doesQueryContainRecognizedParams = function ( uriQuery ) {
var anyValidInUrl,
- validParameterNames = Object.keys( this._getEmptyParameterState() )
+ validParameterNames = Object.keys( this.filtersModel.getEmptyParameterState() )
.filter( function ( param ) {
// Remove 'highlight' parameter from this check;
// if it's the only parameter in the URL we still
return anyValidInUrl || this.getVersion( uriQuery ) === 2;
};
- /**
- * Remove all parameters that have the same value as the base state
- * This method expects uri queries of the urlversion=2 format
- *
- * @private
- * @param {Object} uriQuery Current uri query
- * @return {Object} Minimized query
- */
- mw.rcfilters.UriProcessor.prototype.minimizeQuery = function ( uriQuery ) {
- var baseParams = this._getEmptyParameterState(),
- uriResult = $.extend( true, {}, uriQuery );
-
- $.each( uriResult, function ( paramName, paramValue ) {
- if (
- baseParams[ paramName ] !== undefined &&
- baseParams[ paramName ] === paramValue
- ) {
- // Remove parameter from query
- delete uriResult[ paramName ];
- }
- } );
-
- return uriResult;
- };
-
/**
* Get the adjusted URI params based on the url version
* If the urlversion is not 2, the parameters are merged with
* the model's defaults.
+ * Always merge in the hidden parameter defaults.
*
* @private
* @param {Object} uriQuery Current URI query
// wiki default.
// Any subsequent change of the URL through the RCFilters
// system will receive 'urlversion=2'
- var hiddenParamDefaults = {},
+ var hiddenParamDefaults = this.filtersModel.getDefaultHiddenParams(),
base = this.getVersion( uriQuery ) === 2 ?
{} :
this.filtersModel.getDefaultParams();
- // Go over the model and get all hidden parameters' defaults
- // These defaults should be applied regardless of the urlversion
- // but be overridden by the URL params if they exist
- $.each( this.filtersModel.getFilterGroups(), function ( groupName, groupModel ) {
- if ( groupModel.isHidden() ) {
- $.extend( true, hiddenParamDefaults, groupModel.getDefaultParams() );
- }
- } );
-
- return this.minimizeQuery(
- $.extend( true, {}, hiddenParamDefaults, base, uriQuery, { urlversion: '2' } )
- );
- };
-
- /**
- * Get the representation of an empty parameter state
- *
- * @private
- * @return {Object} Empty parameter state
- */
- mw.rcfilters.UriProcessor.prototype._getEmptyParameterState = function () {
- // Override empty parameter state with the sticky parameter values
- return $.extend( true, {}, this.emptyParameterState, this.filtersModel.getStickyParams() );
- };
-
- /**
- * 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.
- *
- * @private
- */
- mw.rcfilters.UriProcessor.prototype._buildEmptyParameterState = function () {
- var emptyParams = this.filtersModel.getParametersFromFilters( {} ),
- emptyHighlights = this.filtersModel.getEmptyHighlightParameters();
-
- this.emptyParameterState = $.extend(
+ return $.extend(
true,
{},
- emptyParams,
- emptyHighlights,
- { highlight: '0' }
+ this.filtersModel.getMinimizedParamRepresentation(
+ $.extend( true, {}, hiddenParamDefaults, base, uriQuery )
+ ),
+ { urlversion: '2' }
);
};
}( mediaWiki, jQuery ) );
title: 'Group 1',
type: 'send_unselected_if_any',
filters: [
- { name: 'filter1', default: true },
- { name: 'filter2' }
+ { name: 'filter1', cssClass: 'filter1class', default: true },
+ { name: 'filter2', cssClass: 'filter2class' }
]
}, {
name: 'group2',
title: 'Group 2',
type: 'send_unselected_if_any',
filters: [
- { name: 'filter3' },
- { name: 'filter4', default: true }
+ { name: 'filter3', cssClass: 'filter3class' },
+ { name: 'filter4', cssClass: 'filter4class', default: true }
]
}, {
name: 'group3',
title: 'Group 3',
type: 'string_options',
filters: [
- { name: 'filter5' },
- { name: 'filter6' }
+ { name: 'filter5', cssClass: 'filter5class' },
+ { name: 'filter6' } // Not supporting highlights
]
} ],
minimalDefaultParams = {
);
} );
- QUnit.test( 'updateModelBasedOnQuery & getUriParametersFromModel', function ( assert ) {
+ QUnit.test( 'getUpdatedUri', function ( assert ) {
var uriProcessor,
- filtersModel = new mw.rcfilters.dm.FiltersViewModel(),
- baseParams = {
- filter1: '0',
- filter2: '0',
- filter3: '0',
- filter4: '0',
- group3: '',
- highlight: '0',
- group1__filter1_color: null,
- group1__filter2_color: null,
- group2__filter3_color: null,
- group2__filter4_color: null,
- group3__filter5_color: null,
- group3__filter6_color: null
- };
+ filtersModel = new mw.rcfilters.dm.FiltersViewModel();
+
+ filtersModel.initializeFilters( mockFilterStructure );
+ uriProcessor = new mw.rcfilters.UriProcessor( filtersModel );
+
+ assert.deepEqual(
+ ( uriProcessor.getUpdatedUri( {} ) ).query,
+ { urlversion: '2' },
+ 'Empty model state with empty uri state, assumes the given uri is already normalized, and adds urlversion=2'
+ );
+
+ assert.deepEqual(
+ ( uriProcessor.getUpdatedUri( { foo: 'bar' } ) ).query,
+ { urlversion: '2', foo: 'bar' },
+ 'Empty model state with unrecognized params retains unrecognized params'
+ );
+
+ // Update the model
+ filtersModel.toggleFiltersSelected( {
+ group1__filter1: true, // Param: filter2: '1'
+ group3__filter5: true // Param: group3: 'filter5'
+ } );
+
+ assert.deepEqual(
+ ( uriProcessor.getUpdatedUri( {} ) ).query,
+ { urlversion: '2', filter2: '1', group3: 'filter5' },
+ 'Model state is reflected in the updated URI'
+ );
+
+ assert.deepEqual(
+ ( uriProcessor.getUpdatedUri( { foo: 'bar' } ) ).query,
+ { urlversion: '2', filter2: '1', group3: 'filter5', foo: 'bar' },
+ 'Model state is reflected in the updated URI with existing uri params'
+ );
+ } );
+
+ QUnit.test( 'updateModelBasedOnQuery', function ( assert ) {
+ var uriProcessor,
+ filtersModel = new mw.rcfilters.dm.FiltersViewModel();
filtersModel.initializeFilters( mockFilterStructure );
uriProcessor = new mw.rcfilters.UriProcessor( filtersModel );
uriProcessor.updateModelBasedOnQuery( {} );
assert.deepEqual(
- uriProcessor.getUriParametersFromModel(),
- $.extend( true, {}, baseParams, minimalDefaultParams ),
+ filtersModel.getCurrentParameterState(),
+ minimalDefaultParams,
'Version 1: Empty url query sets model to defaults'
);
uriProcessor.updateModelBasedOnQuery( { urlversion: '2' } );
assert.deepEqual(
- uriProcessor.getUriParametersFromModel(),
- baseParams,
+ filtersModel.getCurrentParameterState(),
+ {},
'Version 2: Empty url query sets model to all-false'
);
uriProcessor.updateModelBasedOnQuery( { filter1: '1', urlversion: '2' } );
assert.deepEqual(
- uriProcessor.getUriParametersFromModel(),
- $.extend( true, {}, baseParams, { filter1: '1' } ),
+ filtersModel.getCurrentParameterState(),
+ $.extend( true, {}, { filter1: '1' } ),
'Parameters in Uri query set parameter value in the model'
);
uriProcessor.updateModelBasedOnQuery( { highlight: '1', group1__filter1_color: 'c1', urlversion: '2' } );
assert.deepEqual(
- uriProcessor.getUriParametersFromModel(),
- $.extend( true, {}, baseParams, {
+ filtersModel.getCurrentParameterState(),
+ {
highlight: '1',
group1__filter1_color: 'c1'
- } ),
+ },
'Highlight parameters in Uri query set highlight state in the model'
);
} );
{
name: 'filter1', label: 'group1filter1-label', description: 'group1filter1-desc',
default: true,
+ cssClass: 'filter1class',
conflicts: [ { group: 'group2' } ],
subset: [
{
{
name: 'filter2', label: 'group1filter2-label', description: 'group1filter2-desc',
conflicts: [ { group: 'group2', filter: 'filter6' } ],
+ cssClass: 'filter2class',
subset: [
{
group: 'group1',
}
]
},
+ // NOTE: This filter has no highlight!
{ name: 'filter3', label: 'group1filter3-label', description: 'group1filter3-desc', default: true }
]
}, {
name: 'group2',
type: 'send_unselected_if_any',
fullCoverage: true,
+ excludedFromSavedQueries: true,
conflicts: [ { group: 'group1', filter: 'filter1' } ],
filters: [
- { name: 'filter4', label: 'group2filter4-label', description: 'group2filter4-desc' },
- { name: 'filter5', label: 'group2filter5-label', description: 'group2filter5-desc', default: true },
+ { name: 'filter4', label: 'group2filter4-label', description: 'group2filter4-desc', cssClass: 'filter4class' },
+ { name: 'filter5', label: 'group2filter5-label', description: 'group2filter5-desc', default: true, cssClass: 'filter5class' },
{
- name: 'filter6', label: 'group2filter6-label', description: 'group2filter6-desc',
+ name: 'filter6', label: 'group2filter6-label', description: 'group2filter6-desc', cssClass: 'filter6class',
conflicts: [ { group: 'group1', filter: 'filter2' } ]
}
]
separator: ',',
default: 'filter8',
filters: [
- { name: 'filter7', label: 'group3filter7-label', description: 'group3filter7-desc' },
- { name: 'filter8', label: 'group3filter8-label', description: 'group3filter8-desc' },
- { name: 'filter9', label: 'group3filter9-label', description: 'group3filter9-desc' }
+ { name: 'filter7', label: 'group3filter7-label', description: 'group3filter7-desc', cssClass: 'filter7class' },
+ { name: 'filter8', label: 'group3filter8-label', description: 'group3filter8-desc', cssClass: 'filter8class' },
+ { name: 'filter9', label: 'group3filter9-label', description: 'group3filter9-desc', cssClass: 'filter9class' }
]
}, {
name: 'group4',
type: 'single_option',
default: 'option2',
filters: [
+ // NOTE: The entire group has no highlight supported
{ name: 'option1', label: 'group4option1-label', description: 'group4option1-desc' },
{ name: 'option2', label: 'group4option2-label', description: 'group4option2-desc' },
{ name: 'option3', label: 'group4option3-label', description: 'group4option3-desc' }
name: 'group5',
type: 'single_option',
filters: [
- { name: 'option1', label: 'group5option1-label', description: 'group5option1-desc' },
- { name: 'option2', label: 'group5option2-label', description: 'group5option2-desc' },
- { name: 'option3', label: 'group5option3-label', description: 'group5option3-desc' }
+ { name: 'option1', label: 'group5option1-label', description: 'group5option1-desc', cssClass: 'group5opt1class' },
+ { name: 'option2', label: 'group5option2-label', description: 'group5option2-desc', cssClass: 'group5opt2class' },
+ { name: 'option3', label: 'group5option3-label', description: 'group5option3-desc', cssClass: 'group5opt3class' }
]
}, {
name: 'group6',
type: 'boolean',
isSticky: true,
filters: [
- { name: 'group6option1', label: 'group6option1-label', description: 'group6option1-desc' },
- { name: 'group6option2', label: 'group6option2-label', description: 'group6option2-desc', default: true },
- { name: 'group6option3', label: 'group6option3-label', description: 'group6option3-desc', default: true }
+ { name: 'group6option1', label: 'group6option1-label', description: 'group6option1-desc', cssClass: 'group6opt1class' },
+ { name: 'group6option2', label: 'group6option2-label', description: 'group6option2-desc', default: true, cssClass: 'group6opt2class' },
+ { name: 'group6option3', label: 'group6option3-label', description: 'group6option3-desc', default: true, cssClass: 'group6opt3class' }
]
}, {
name: 'group7',
isSticky: true,
default: 'group7option2',
filters: [
- { name: 'group7option1', label: 'group7option1-label', description: 'group7option1-desc' },
- { name: 'group7option2', label: 'group7option2-label', description: 'group7option2-desc' },
- { name: 'group7option3', label: 'group7option3-label', description: 'group7option3-desc' }
+ { name: 'group7option1', label: 'group7option1-label', description: 'group7option1-desc', cssClass: 'group7opt1class' },
+ { name: 'group7option2', label: 'group7option2-label', description: 'group7option2-desc', cssClass: 'group7opt2class' },
+ { name: 'group7option3', label: 'group7option3-label', description: 'group7option3-desc', cssClass: 'group7opt3class' }
]
} ],
viewsDefinition = {
type: 'string_options',
separator: ';',
filters: [
- { name: 0, label: 'Main' },
- { name: 1, label: 'Talk' },
- { name: 2, label: 'User' },
- { name: 3, label: 'User talk' }
+ { name: 0, label: 'Main', cssClass: 'namespace-0' },
+ { name: 1, label: 'Talk', cssClass: 'namespace-1' },
+ { name: 2, label: 'User', cssClass: 'namespace-2' },
+ { name: 3, label: 'User talk', cssClass: 'namespace-3' }
]
} ]
}
group7: 'group7option2',
namespace: ''
},
+ emptyParamRepresentation = {
+ filter1: '0',
+ filter2: '0',
+ filter3: '0',
+ filter4: '0',
+ filter5: '0',
+ filter6: '0',
+ group3: '',
+ group4: '',
+ group5: '',
+ group6option1: '0',
+ group6option2: '0',
+ group6option3: '0',
+ group7: '',
+ namespace: '',
+ highlight: '0',
+ // Null highlights
+ group1__filter1_color: null,
+ group1__filter2_color: null,
+ // group1__filter3_color: null, // Highlight isn't supported
+ group2__filter4_color: null,
+ group2__filter5_color: null,
+ group2__filter6_color: null,
+ group3__filter7_color: null,
+ group3__filter8_color: null,
+ group3__filter9_color: null,
+ // group4__option1_color: null, // Highlight isn't supported
+ // group4__option2_color: null, // Highlight isn't supported
+ // group4__option3_color: null, // Highlight isn't supported
+ group5__option1_color: null,
+ group5__option2_color: null,
+ group5__option3_color: null,
+ group6__group6option1_color: null,
+ group6__group6option2_color: null,
+ group6__group6option3_color: null,
+ group7__group7option1_color: null,
+ group7__group7option2_color: null,
+ group7__group7option3_color: null,
+ namespace__0_color: null,
+ namespace__1_color: null,
+ namespace__2_color: null,
+ namespace__3_color: null
+ },
baseFilterRepresentation = {
group1__filter1: false,
group1__filter2: false,
);
} );
+ QUnit.test( 'Parameter minimal state', function ( assert ) {
+ var model = new mw.rcfilters.dm.FiltersViewModel(),
+ cases = [
+ {
+ input: {},
+ result: {},
+ msg: 'Empty parameter representation produces an empty result'
+ },
+ {
+ input: {
+ filter1: '1',
+ filter2: '0',
+ filter3: '0',
+ group3: '',
+ group4: 'option2'
+ },
+ result: {
+ filter1: '1',
+ group4: 'option2'
+ },
+ msg: 'Mixed input results in only non-falsey values as result'
+ },
+ {
+ input: {
+ filter1: '0',
+ filter2: '0',
+ filter3: '0',
+ group3: '',
+ group4: '',
+ group1__filter1_color: null
+ },
+ result: {},
+ msg: 'An all-falsey input results in an empty result.'
+ },
+ {
+ input: {
+ filter1: '0',
+ filter2: '0',
+ filter3: '0',
+ group3: '',
+ group4: '',
+ group1__filter1_color: 'c1'
+ },
+ result: {
+ group1__filter1_color: 'c1'
+ },
+ msg: 'An all-falsey input with highlight params result in only the highlight param.'
+ },
+ {
+ input: {
+ group1__filter1_color: 'c1',
+ group1__filter3_color: 'c3' // Not supporting highlights
+ },
+ result: {
+ group1__filter1_color: 'c1'
+ },
+ msg: 'Unsupported highlights are removed.'
+ }
+ ];
+
+ model.initializeFilters( filterDefinition, viewsDefinition );
+
+ cases.forEach( function ( test ) {
+ assert.deepEqual(
+ model.getMinimizedParamRepresentation( test.input ),
+ test.result,
+ test.msg
+ );
+ } );
+ } );
+
+ QUnit.test( 'Parameter states', function ( assert ) {
+ // Some groups / params have their defaults immediately applied
+ // to their state. These include single_option which can never
+ // be empty, etc. These are these states:
+ var parametersWithoutExcluded,
+ appliedDefaultParameters = {
+ group4: 'option2',
+ group5: 'option1',
+ // Sticky, their defaults apply immediately
+ group6option2: '1',
+ group6option3: '1',
+ group7: 'group7option2'
+ },
+ model = new mw.rcfilters.dm.FiltersViewModel();
+
+ model.initializeFilters( filterDefinition, viewsDefinition );
+ assert.deepEqual(
+ model.getEmptyParameterState(),
+ emptyParamRepresentation,
+ 'Producing an empty parameter state'
+ );
+
+ model.toggleFiltersSelected( {
+ group1__filter1: true,
+ group3__filter7: true
+ } );
+
+ assert.deepEqual(
+ model.getCurrentParameterState(),
+ // appliedDefaultParams applies the default value to parameters
+ // who must have an initial value to begin with, so we have to
+ // take it into account in the current state
+ $.extend( true, {}, appliedDefaultParameters, {
+ filter2: '1',
+ filter3: '1',
+ group3: 'filter7'
+ } ),
+ 'Producing a current parameter state'
+ );
+
+ // Reset
+ model = new mw.rcfilters.dm.FiltersViewModel();
+ model.initializeFilters( filterDefinition, viewsDefinition );
+
+ parametersWithoutExcluded = $.extend( true, {}, appliedDefaultParameters );
+ delete parametersWithoutExcluded.group7;
+ delete parametersWithoutExcluded.group6option2;
+ delete parametersWithoutExcluded.group6option3;
+
+ assert.deepEqual(
+ model.getCurrentParameterState( true ),
+ parametersWithoutExcluded,
+ 'Producing a current clean parameter state without excluded filters'
+ );
+ } );
+
+ QUnit.test( 'Cleaning up parameter states', function ( assert ) {
+ var model = new mw.rcfilters.dm.FiltersViewModel(),
+ cases = [
+ {
+ input: {},
+ result: {},
+ msg: 'Empty parameter representation produces an empty result'
+ },
+ {
+ input: {
+ filter1: '1', // Regular (do not strip)
+ group6option1: '1', // Sticky
+ filter4: '1', // Excluded
+ filter5: '0' // Excluded
+ },
+ result: { filter1: '1' },
+ msg: 'Valid input strips all sticky and excluded params regardless of value'
+ }
+ ];
+
+ model.initializeFilters( filterDefinition, viewsDefinition );
+
+ cases.forEach( function ( test ) {
+ assert.deepEqual(
+ model.removeExcludedParams( test.input ),
+ test.result,
+ test.msg
+ );
+ } );
+
+ } );
+
QUnit.test( 'Finding matching filters', function ( assert ) {
var matches,
testCases = [
filters: [
// Note: The fact filter2 is default means that in the
// filter representation, filter1 and filter3 are 'true'
- { name: 'filter1' },
- { name: 'filter2', default: true },
- { name: 'filter3' }
+ { name: 'filter1', cssClass: 'filter1class' },
+ { name: 'filter2', cssClass: 'filter2class', default: true },
+ { name: 'filter3', cssClass: 'filter3class' }
]
}, {
name: 'group2',
type: 'string_options',
separator: ',',
filters: [
- { name: 'filter4' },
- { name: 'filter5' },
- { name: 'filter6' }
+ { name: 'filter4', cssClass: 'filter4class' },
+ { name: 'filter5' }, // NOTE: Not supporting highlights!
+ { name: 'filter6', cssClass: 'filter6class' }
]
}, {
name: 'group3',
type: 'boolean',
isSticky: true,
filters: [
- { name: 'group3option1' },
- { name: 'group3option2' },
- { name: 'group3option3' }
+ { name: 'group3option1', cssClass: 'filter1class' },
+ { name: 'group3option2', cssClass: 'filter1class' },
+ { name: 'group3option3', cssClass: 'filter1class' }
]
+ }, {
+ // Copy of the way the controller defines invert
+ // to check whether the conversion works
+ name: 'invertGroup',
+ type: 'boolean',
+ hidden: true,
+ filters: [ {
+ name: 'invert',
+ default: '0'
+ } ]
} ],
queriesFilterRepresentation = {
queries: {
},
highlights: {
highlight: true,
- filter1: 'c5',
- group3option1: 'c1'
- }
+ group1__filter1: 'c5',
+ group3__group3option1: 'c1'
+ },
+ invert: true
}
}
}
highlight: '1'
},
highlights: {
- filter1_color: 'c5',
- group3option1_color: 'c1'
+ group1__filter1_color: 'c5',
+ group3__group3option1_color: 'c1'
}
}
}
filter2: '1'
},
highlights: {
- filter5_color: 'c2'
+ group1__filter3_color: 'c2'
}
}
}
finalState: $.extend( true, {}, queriesParamRepresentation ),
msg: 'Conversion from filter representation to parameters retains data.'
},
+ {
+ // Converting from old structure
+ input: $.extend( true, {}, queriesFilterRepresentation, { queries: { 1234: { data: {
+ filters: {
+ // Entire group true: normalize params
+ filter1: true,
+ filter2: true,
+ filter3: true
+ },
+ highlights: {
+ filter3: null // Get rid of empty highlight
+ }
+ } } } } ),
+ finalState: $.extend( true, {}, queriesParamRepresentation ),
+ msg: 'Conversion from filter representation to parameters normalizes params and highlights.'
+ },
{
// Converting from old structure with default
input: $.extend( true, { default: '1234' }, queriesFilterRepresentation ),
input: $.extend( true, {}, queriesParamRepresentation ),
finalState: $.extend( true, {}, queriesParamRepresentation ),
msg: 'Parameter representation retains its queries structure'
+ },
+ {
+ // Do not touch invalid color parameters from the initialization routine
+ // (Normalization, or "fixing" the query should only happen when we add new query or actively convert queries)
+ input: $.extend( true, { queries: { 1234: { data: { highlights: { group2__filter5_color: 'c2' } } } } }, exampleQueryStructure ),
+ finalState: $.extend( true, { queries: { 1234: { data: { highlights: { group2__filter5_color: 'c2' } } } } }, exampleQueryStructure ),
+ msg: 'Structure that contains invalid highlights remains the same in initialization'
}
];
} );
} );
+ QUnit.test( 'Adding new queries', function ( assert ) {
+ var filtersModel = new mw.rcfilters.dm.FiltersViewModel(),
+ queriesModel = new mw.rcfilters.dm.SavedQueriesModel( filtersModel ),
+ cases = [
+ {
+ methodParams: [
+ 'label1', // Label
+ { // Data
+ filter1: '1',
+ filter2: '2',
+ group1__filter1_color: 'c2',
+ group1__filter3_color: 'c5'
+ },
+ true, // isDefault
+ '1234' // ID
+ ],
+ result: {
+ itemState: {
+ label: 'label1',
+ data: {
+ params: {
+ filter1: '1',
+ filter2: '2'
+ },
+ highlights: {
+ group1__filter1_color: 'c2',
+ group1__filter3_color: 'c5'
+ }
+ }
+ },
+ isDefault: true,
+ id: '1234'
+ },
+ msg: 'Given valid data is preserved.'
+ },
+ {
+ methodParams: [
+ 'label2',
+ {
+ filter1: '1',
+ invert: '1',
+ filter15: '1', // Invalid filter - removed
+ filter2: '0', // Falsey value - removed
+ group1__filter1_color: 'c3',
+ foobar: 'w00t' // Unrecognized parameter - removed
+ }
+ ],
+ result: {
+ itemState: {
+ label: 'label2',
+ data: {
+ params: {
+ filter1: '1',
+ invert: '1'
+ },
+ highlights: {
+ group1__filter1_color: 'c3'
+ }
+ }
+ },
+ isDefault: false
+ },
+ msg: 'Given data with invalid filters and highlights is normalized'
+ }
+ ];
+
+ filtersModel.initializeFilters( filterDefinition );
+
+ // Start with an empty saved queries model
+ queriesModel.initialize( {} );
+
+ cases.forEach( function ( testCase ) {
+ var itemID = queriesModel.addNewQuery.apply( queriesModel, testCase.methodParams ),
+ item = queriesModel.getItemByID( itemID );
+
+ assert.deepEqual(
+ item.getState(),
+ testCase.result.itemState,
+ testCase.msg + ' (itemState)'
+ );
+
+ assert.equal(
+ item.isDefault(),
+ testCase.result.isDefault,
+ testCase.msg + ' (isDefault)'
+ );
+
+ if ( testCase.result.id !== undefined ) {
+ assert.equal(
+ item.getID(),
+ testCase.result.id,
+ testCase.msg + ' (item ID)'
+ );
+ }
+ } );
+ } );
+
QUnit.test( 'Manipulating queries', function ( assert ) {
var id1, id2, item1, matchingItem,
queriesStructure = {},
id1 = queriesModel.addNewQuery(
'New query 1',
{
- params: {
- group2: 'filter5',
- highlight: '1'
- },
- highlights: {
- filter1_color: 'c5',
- group3option1_color: 'c1'
- }
+ group2: 'filter5',
+ highlight: '1',
+ group1__filter1_color: 'c5',
+ group3__group3option1_color: 'c1'
}
);
id2 = queriesModel.addNewQuery(
'New query 2',
{
- params: {
- filter1: '1',
- filter2: '1',
- invert: '1'
- },
- highlights: {}
+ filter1: '1',
+ filter2: '1',
+ invert: '1'
}
);
item1 = queriesModel.getItemByID( id1 );
highlight: '1'
},
highlights: {
- filter1_color: 'c5',
- group3option1_color: 'c1'
+ group1__filter1_color: 'c5',
+ group3__group3option1_color: 'c1'
}
}
};
// Find matching query
matchingItem = queriesModel.findMatchingQuery(
{
- params: {
- group2: 'filter5',
- highlight: '1'
- },
- highlights: {
- filter1_color: 'c5',
- group3option1_color: 'c1'
- }
+ highlight: '1',
+ group2: 'filter5',
+ group1__filter1_color: 'c5',
+ group3__group3option1_color: 'c1'
}
);
assert.deepEqual(
// Find matching query with 0-values (base state)
matchingItem = queriesModel.findMatchingQuery(
{
- params: {
- group2: 'filter5',
- filter1: '0',
- filter2: '0',
- highlight: '1'
- },
- highlights: {
- filter1_color: 'c5',
- group3option1_color: 'c1'
- }
+ group2: 'filter5',
+ filter1: '0',
+ filter2: '0',
+ highlight: '1',
+ invert: '0',
+ group1__filter1_color: 'c5',
+ group3__group3option1_color: 'c1'
}
);
assert.deepEqual(