* @cfg {string} [type='send_unselected_if_any'] Group type
* @cfg {string} [view='default'] Name of the display group this group
* is a part of.
+ * @cfg {boolean} [isSticky] This group is using a 'sticky' default; meaning
+ * that every time a value is changed, it becomes the new default
* @cfg {string} [title] Group title
* @cfg {boolean} [hidden] This group is hidden from the regular menu views
+ * @cfg {boolean} [allowArbitrary] Allows for an arbitrary value to be added to the
+ * group from the URL, even if it wasn't initially set up.
+ * @cfg {number} [range] An object defining minimum and maximum values for numeric
+ * groups. { min: x, max: y }
+ * @cfg {number} [minValue] Minimum value for numeric groups
* @cfg {string} [separator='|'] Value separator for 'string_options' groups
* @cfg {boolean} [active] Group is active
* @cfg {boolean} [fullCoverage] This filters in this group collectively cover all results
this.name = name;
this.type = config.type || 'send_unselected_if_any';
this.view = config.view || 'default';
+ this.sticky = !!config.isSticky;
this.title = config.title || name;
this.hidden = !!config.hidden;
+ this.allowArbitrary = !!config.allowArbitrary;
+ this.numericRange = config.range;
this.separator = config.separator || '|';
this.labelPrefixKey = config.labelPrefixKey;
+ this.currSelected = null;
this.active = !!config.active;
this.fullCoverage = !!config.fullCoverage;
this.conflicts = config.conflicts || {};
this.defaultParams = {};
+ this.defaultFilters = {};
this.aggregate( { update: 'filterItemUpdate' } );
this.connect( this, { filterItemUpdate: 'onFilterItemUpdate' } );
* @param {string|Object} [groupDefault] Definition of the group default
*/
mw.rcfilters.dm.FilterGroup.prototype.initializeFilters = function ( filterDefinition, groupDefault ) {
- var supersetMap = {},
+ var defaultParam,
+ supersetMap = {},
model = this,
items = [];
items.push( filterItem );
// Store default parameter state; in this case, default is defined per filter
- if ( model.getType() === 'send_unselected_if_any' ) {
+ if (
+ model.getType() === 'send_unselected_if_any' ||
+ model.getType() === 'boolean'
+ ) {
// Store the default parameter state
// For this group type, parameter values are direct
// We need to convert from a boolean to a string ('1' and '0')
- model.defaultParams[ filter.name ] = String( Number( !!filter.default ) );
+ model.defaultParams[ filter.name ] = String( Number( filter.default || 0 ) );
}
} );
} )
).join( this.getSeparator() );
} else if ( this.getType() === 'single_option' ) {
+ defaultParam = groupDefault !== undefined ?
+ groupDefault : this.getItems()[ 0 ].getParamName();
+
// For this group, the parameter is the group name,
- // and a single item can be selected, or none at all
- // The item also must be recognized or none is set as
- // default
- model.defaultParams[ this.getName() ] = this.getItemByParamName( groupDefault ) ? groupDefault : '';
+ // and a single item can be selected: default or first item
+ this.defaultParams[ this.getName() ] = defaultParam;
+ }
+
+ // Store default filter state based on default params
+ this.defaultFilters = this.getFilterRepresentation( this.getDefaultParams() );
+
+ // Check for filters that should be initially selected by their default value
+ if ( this.isSticky() ) {
+ $.each( this.defaultFilters, function ( filterName, filterValue ) {
+ model.getItemByName( filterName ).toggleSelected( filterValue );
+ } );
+ }
+
+ // Verify that single_option group has at least one item selected
+ if (
+ this.getType() === 'single_option' &&
+ this.getSelectedItems().length === 0
+ ) {
+ defaultParam = groupDefault !== undefined ?
+ groupDefault : this.getItems()[ 0 ].getParamName();
+
+ // Single option means there must be a single option
+ // selected, so we have to either select the default
+ // or select the first option
+ this.selectItemByParamName( defaultParam );
}
};
*/
mw.rcfilters.dm.FilterGroup.prototype.onFilterItemUpdate = function ( item ) {
// Update state
- var active = this.areAnySelected(),
- itemName = item && item.getName();
+ var changed = false,
+ active = this.areAnySelected();
+
+ if (
+ item.isSelected() &&
+ this.getType() === 'single_option' &&
+ this.currSelected &&
+ this.currSelected !== item
+ ) {
+ this.currSelected.toggleSelected( false );
+ }
- if ( item.isSelected() && this.getType() === 'single_option' ) {
- // Change the selection to only be the newly selected item
- this.getItems().forEach( function ( filterItem ) {
- if ( filterItem.getName() !== itemName ) {
- filterItem.toggleSelected( false );
- }
- } );
+ // For 'single_option' groups, check if we just unselected all
+ // items. This should never be the result. If we did unselect
+ // all (like resetting all filters to false) then this group
+ // must choose its default item or the first item in the group
+ if (
+ this.getType() === 'single_option' &&
+ !this.getItems().some( function ( filterItem ) {
+ return filterItem.isSelected();
+ } )
+ ) {
+ // Single option means there must be a single option
+ // selected, so we have to either select the default
+ // or select the first option
+ this.currSelected = this.getItemByParamName( this.defaultParams[ this.getName() ] ) ||
+ this.getItems()[ 0 ];
+ this.currSelected.toggleSelected( true );
+ changed = true;
}
- if ( this.active !== active ) {
+ if (
+ changed ||
+ this.active !== active ||
+ this.currSelected !== item
+ ) {
+ if ( this.isSticky() ) {
+ // If this group is sticky, then change the default according to the
+ // current selection.
+ this.defaultParams = this.getParamRepresentation( this.getSelectedState() );
+ }
+
this.active = active;
+ this.currSelected = item;
+
this.emit( 'update' );
}
};
return this.hidden;
};
+ /**
+ * Get group allow arbitrary state
+ *
+ * @return {boolean} Group allows an arbitrary value from the URL
+ */
+ mw.rcfilters.dm.FilterGroup.prototype.isAllowArbitrary = function () {
+ return this.allowArbitrary;
+ };
+
+ /**
+ * Get group maximum value for numeric groups
+ *
+ * @return {number|null} Group max value
+ */
+ mw.rcfilters.dm.FilterGroup.prototype.getMaxValue = function () {
+ return this.numericRange && this.numericRange.max !== undefined ?
+ this.numericRange.max : null;
+ };
+
+ /**
+ * Get group minimum value for numeric groups
+ *
+ * @return {number|null} Group max value
+ */
+ mw.rcfilters.dm.FilterGroup.prototype.getMinValue = function () {
+ return this.numericRange && this.numericRange.min !== undefined ?
+ this.numericRange.min : null;
+ };
+
/**
* Get group name
*
return this.defaultParams;
};
+ /**
+ * Get the default filter state of this group
+ *
+ * @return {Object} Default filter state
+ */
+ mw.rcfilters.dm.FilterGroup.prototype.getDefaultFilters = function () {
+ return this.defaultFilters;
+ };
+
+ /**
+ * This is for a single_option and string_options group types
+ * it returns the value of the default
+ *
+ * @return {string} Value of the default
+ */
+ mw.rcfilters.dm.FilterGroup.prototype.getDefaulParamValue = function () {
+ return this.defaultParams[ this.getName() ];
+ };
/**
* Get the messags defining the 'whats this' popup for this group
*
var values,
areAnySelected = false,
buildFromCurrentState = !filterRepresentation,
+ defaultFilters = this.getDefaultFilters(),
result = {},
model = this,
filterParamNames = {},
// This means we have not been given a filter representation
// so we are building one based on current state
filterRepresentation[ item.getName() ] = item.isSelected();
- } else if ( !filterRepresentation[ item.getName() ] ) {
+ } else if ( filterRepresentation[ item.getName() ] === undefined ) {
// We are given a filter representation, but we have to make
// sure that we fill in the missing filters if there are any
// we will assume they are all falsey
- filterRepresentation[ item.getName() ] = false;
+ if ( model.isSticky() ) {
+ filterRepresentation[ item.getName() ] = !!defaultFilters[ item.getName() ];
+ } else {
+ filterRepresentation[ item.getName() ] = false;
+ }
}
if ( filterRepresentation[ item.getName() ] ) {
} );
// Build result
- if ( this.getType() === 'send_unselected_if_any' ) {
+ if (
+ this.getType() === 'send_unselected_if_any' ||
+ this.getType() === 'boolean'
+ ) {
// First, check if any of the items are selected at all.
// If none is selected, we're treating it as if they are
// all false
// Go over the items and define the correct values
$.each( filterRepresentation, function ( name, value ) {
- result[ filterParamNames[ name ] ] = areAnySelected ?
- // We must store all parameter values as strings '0' or '1'
- String( Number( !value ) ) :
- '0';
+ // We must store all parameter values as strings '0' or '1'
+ if ( model.getType() === 'send_unselected_if_any' ) {
+ result[ filterParamNames[ name ] ] = areAnySelected ?
+ String( Number( !value ) ) :
+ '0';
+ } else if ( model.getType() === 'boolean' ) {
+ // Representation is straight-forward and direct from
+ // the parameter value to the filter state
+ result[ filterParamNames[ name ] ] = String( Number( !!value ) );
+ }
} );
} else if ( this.getType() === 'string_options' ) {
values = [];
* Get the filter representation this group would provide
* based on given parameter states.
*
- * @param {Object|string} [paramRepresentation] An object defining a parameter
+ * @param {Object} [paramRepresentation] An object defining a parameter
* state to translate the filter state from. If not given, an object
* representing all filters as falsey is returned; same as if the parameter
* given were an empty object, or had some of the filters missing.
* @return {Object} Filter representation
*/
mw.rcfilters.dm.FilterGroup.prototype.getFilterRepresentation = function ( paramRepresentation ) {
- var areAnySelected, paramValues,
+ var areAnySelected, paramValues, item, currentValue,
+ oneWasSelected = false,
+ defaultParams = this.getDefaultParams(),
+ expandedParams = $.extend( true, {}, paramRepresentation ),
model = this,
paramToFilterMap = {},
result = {};
- if ( this.getType() === 'send_unselected_if_any' ) {
- paramRepresentation = paramRepresentation || {};
- // Expand param representation to include all filters in the group
+ if ( this.isSticky() ) {
+ // If the group is sticky, check if all parameters are represented
+ // and for those that aren't represented, add them with their default
+ // values
+ paramRepresentation = $.extend( true, {}, this.getDefaultParams(), paramRepresentation );
+ }
+
+ paramRepresentation = paramRepresentation || {};
+ if (
+ this.getType() === 'send_unselected_if_any' ||
+ this.getType() === 'boolean'
+ ) {
+ // Go over param representation; map and check for selections
this.getItems().forEach( function ( filterItem ) {
var paramName = filterItem.getParamName();
- paramRepresentation[ paramName ] = paramRepresentation[ paramName ] || '0';
+ expandedParams[ paramName ] = paramRepresentation[ paramName ] || '0';
paramToFilterMap[ paramName ] = filterItem;
if ( Number( paramRepresentation[ filterItem.getParamName() ] ) ) {
}
} );
- $.each( paramRepresentation, function ( paramName, paramValue ) {
+ $.each( expandedParams, function ( paramName, paramValue ) {
var filterItem = paramToFilterMap[ paramName ];
- result[ filterItem.getName() ] = areAnySelected ?
+ if ( model.getType() === 'send_unselected_if_any' ) {
// Flip the definition between the parameter
// state and the filter state
// This is what the 'toggleSelected' value of the filter is
- !Number( paramValue ) :
- // Otherwise, there are no selected items in the
- // group, which means the state is false
- false;
+ result[ filterItem.getName() ] = areAnySelected ?
+ !Number( paramValue ) :
+ // Otherwise, there are no selected items in the
+ // group, which means the state is false
+ false;
+ } else if ( model.getType() === 'boolean' ) {
+ // Straight-forward definition of state
+ result[ filterItem.getName() ] = !!Number( paramRepresentation[ filterItem.getParamName() ] );
+ }
} );
} else if ( this.getType() === 'string_options' ) {
- paramRepresentation = paramRepresentation || '';
+ currentValue = paramRepresentation[ this.getName() ] || '';
// Normalize the given parameter values
paramValues = mw.rcfilters.utils.normalizeParamOptions(
// Given
- paramRepresentation.split(
+ currentValue.split(
this.getSeparator()
),
// Allowed values
);
// Translate the parameter values into a filter selection state
this.getItems().forEach( function ( filterItem ) {
+ // All true (either because all values are written or the term 'all' is written)
+ // is the same as all filters set to true
result[ filterItem.getName() ] = (
- // If it is the word 'all'
- paramValues.length === 1 && paramValues[ 0 ] === 'all' ||
- // All values are written
- paramValues.length === model.getItemCount()
- ) ?
- // All true (either because all values are written or the term 'all' is written)
- // is the same as all filters set to true
+ // If it is the word 'all'
+ paramValues.length === 1 && paramValues[ 0 ] === 'all' ||
+ // All values are written
+ paramValues.length === model.getItemCount()
+ ) ?
true :
// Otherwise, the filter is selected only if it appears in the parameter values
paramValues.indexOf( filterItem.getParamName() ) > -1;
} );
} else if ( this.getType() === 'single_option' ) {
- // There is parameter that fits a single filter, or none at all
+ // There is parameter that fits a single filter and if not, get the default
this.getItems().forEach( function ( filterItem ) {
- result[ filterItem.getName() ] = filterItem.getParamName() === paramRepresentation;
+ var selected = filterItem.getParamName() === paramRepresentation[ model.getName() ];
+
+ result[ filterItem.getName() ] = selected;
+ oneWasSelected = oneWasSelected || selected;
} );
}
// Go over result and make sure all filters are represented.
// If any filters are missing, they will get a falsey value
this.getItems().forEach( function ( filterItem ) {
- result[ filterItem.getName() ] = !!result[ filterItem.getName() ];
+ if ( result[ filterItem.getName() ] === undefined ) {
+ result[ filterItem.getName() ] = false;
+ }
} );
+ // Make sure that at least one option is selected in
+ // single_option groups, no matter what path was taken
+ // If none was selected by the given definition, then
+ // we need to select the one in the base state -- either
+ // the default given, or the first item
+ if (
+ this.getType() === 'single_option' &&
+ !oneWasSelected
+ ) {
+ item = this.getItems()[ 0 ];
+ if ( defaultParams[ this.getName() ] ) {
+ item = this.getItemByParamName( defaultParams[ this.getName() ] );
+ }
+
+ result[ item.getName() ] = true;
+ }
+
return result;
};
+ /**
+ * Get current selected state of all filter items in this group
+ *
+ * @return {Object} Selected state
+ */
+ mw.rcfilters.dm.FilterGroup.prototype.getSelectedState = function () {
+ var state = {};
+
+ this.getItems().forEach( function ( filterItem ) {
+ state[ filterItem.getName() ] = filterItem.isSelected();
+ } );
+
+ return state;
+ };
+
/**
* Get item by its filter name
*
} )[ 0 ];
};
+ /**
+ * Select an item by its parameter name
+ *
+ * @param {string} paramName Filter parameter name
+ */
+ mw.rcfilters.dm.FilterGroup.prototype.selectItemByParamName = function ( paramName ) {
+ this.getItems().forEach( function ( item ) {
+ item.toggleSelected( item.getParamName() === String( paramName ) );
+ } );
+ };
+
/**
* Get item by its parameter name
*
*/
mw.rcfilters.dm.FilterGroup.prototype.getItemByParamName = function ( paramName ) {
return this.getItems().filter( function ( item ) {
- return item.getParamName() === paramName;
+ return item.getParamName() === String( paramName );
} )[ 0 ];
};
mw.rcfilters.dm.FilterGroup.prototype.isFullCoverage = function () {
return this.fullCoverage;
};
+
+ /**
+ * Check whether the group is defined as sticky default
+ *
+ * @return {boolean} Group is sticky default
+ */
+ mw.rcfilters.dm.FilterGroup.prototype.isSticky = function () {
+ return this.sticky;
+ };
}( mediaWiki ) );