diff options
Diffstat (limited to 'www/wiki/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js')
-rw-r--r-- | www/wiki/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js | 983 |
1 files changed, 983 insertions, 0 deletions
diff --git a/www/wiki/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js b/www/wiki/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js new file mode 100644 index 00000000..a62acc5b --- /dev/null +++ b/www/wiki/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js @@ -0,0 +1,983 @@ +( function ( mw ) { + /** + * View model for a filter group + * + * @mixins OO.EventEmitter + * @mixins OO.EmitterList + * + * @constructor + * @param {string} name Group name + * @param {Object} [config] Configuration options + * @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} [sticky] This group is 'sticky'. It is synchronized + * with a preference, does not participate in Saved Queries, and is + * not shown in the active filters area. + * @cfg {string} [title] Group title + * @cfg {boolean} [hidden] This group is hidden from the regular menu views + * and the active filters area. + * @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 + * @cfg {Object} [conflicts] Defines the conflicts for this filter group + * @cfg {string|Object} [labelPrefixKey] An i18n key defining the prefix label for this + * group. If the prefix has 'invert' state, the parameter is expected to be an object + * with 'default' and 'inverted' as keys. + * @cfg {Object} [whatsThis] Defines the messages that should appear for the 'what's this' popup + * @cfg {string} [whatsThis.header] The header of the whatsThis popup message + * @cfg {string} [whatsThis.body] The body of the whatsThis popup message + * @cfg {string} [whatsThis.url] The url for the link in the whatsThis popup message + * @cfg {string} [whatsThis.linkMessage] The text for the link in the whatsThis popup message + * @cfg {boolean} [visible=true] The visibility of the group + */ + mw.rcfilters.dm.FilterGroup = function MwRcfiltersDmFilterGroup( name, config ) { + config = config || {}; + + // Mixin constructor + OO.EventEmitter.call( this ); + OO.EmitterList.call( this ); + + this.name = name; + this.type = config.type || 'send_unselected_if_any'; + this.view = config.view || 'default'; + this.sticky = !!config.sticky; + 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.visible = config.visible === undefined ? true : !!config.visible; + + this.currSelected = null; + this.active = !!config.active; + this.fullCoverage = !!config.fullCoverage; + + this.whatsThis = config.whatsThis || {}; + + this.conflicts = config.conflicts || {}; + this.defaultParams = {}; + this.defaultFilters = {}; + + this.aggregate( { update: 'filterItemUpdate' } ); + this.connect( this, { filterItemUpdate: 'onFilterItemUpdate' } ); + }; + + /* Initialization */ + OO.initClass( mw.rcfilters.dm.FilterGroup ); + OO.mixinClass( mw.rcfilters.dm.FilterGroup, OO.EventEmitter ); + OO.mixinClass( mw.rcfilters.dm.FilterGroup, OO.EmitterList ); + + /* Events */ + + /** + * @event update + * + * Group state has been updated + */ + + /* Methods */ + + /** + * Initialize the group and create its filter items + * + * @param {Object} filterDefinition Filter definition for this group + * @param {string|Object} [groupDefault] Definition of the group default + */ + mw.rcfilters.dm.FilterGroup.prototype.initializeFilters = function ( filterDefinition, groupDefault ) { + var defaultParam, + supersetMap = {}, + model = this, + items = []; + + filterDefinition.forEach( function ( filter ) { + // Instantiate an item + var subsetNames = [], + filterItem = new mw.rcfilters.dm.FilterItem( filter.name, model, { + group: model.getName(), + label: filter.label || filter.name, + description: filter.description || '', + labelPrefixKey: model.labelPrefixKey, + cssClass: filter.cssClass, + identifiers: filter.identifiers, + defaultHighlightColor: filter.defaultHighlightColor + } ); + + if ( filter.subset ) { + filter.subset = filter.subset.map( function ( el ) { + return el.filter; + } ); + + subsetNames = []; + + filter.subset.forEach( function ( subsetFilterName ) { // eslint-disable-line no-loop-func + // Subsets (unlike conflicts) are always inside the same group + // We can re-map the names of the filters we are getting from + // the subsets with the group prefix + var subsetName = model.getPrefixedName( subsetFilterName ); + // For convenience, we should store each filter's "supersets" -- these are + // the filters that have that item in their subset list. This will just + // make it easier to go through whether the item has any other items + // that affect it (and are selected) at any given time + supersetMap[ subsetName ] = supersetMap[ subsetName ] || []; + mw.rcfilters.utils.addArrayElementsUnique( + supersetMap[ subsetName ], + filterItem.getName() + ); + + // Translate subset param name to add the group name, so we + // get consistent naming. We know that subsets are only within + // the same group + subsetNames.push( subsetName ); + } ); + + // Set translated subset + filterItem.setSubset( subsetNames ); + } + + items.push( filterItem ); + + // Store default parameter state; in this case, default is defined per filter + 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 || 0 ) ); + } else if ( model.getType() === 'any_value' ) { + model.defaultParams[ filter.name ] = filter.default; + } + } ); + + // Add items + this.addItems( items ); + + // Now that we have all items, we can apply the superset map + this.getItems().forEach( function ( filterItem ) { + filterItem.setSuperset( supersetMap[ filterItem.getName() ] ); + } ); + + // Store default parameter state; in this case, default is defined per the + // entire group, given by groupDefault method parameter + if ( this.getType() === 'string_options' ) { + // Store the default parameter group state + // For this group, the parameter is group name and value is the names + // of selected items + this.defaultParams[ this.getName() ] = mw.rcfilters.utils.normalizeParamOptions( + // Current values + groupDefault ? + groupDefault.split( this.getSeparator() ) : + [], + // Legal values + this.getItems().map( function ( item ) { + return item.getParamName(); + } ) + ).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: default or first item + this.defaultParams[ this.getName() ] = defaultParam; + } + + // add highlights to defaultParams + this.getItems().forEach( function ( filterItem ) { + if ( filterItem.isHighlighted() ) { + this.defaultParams[ filterItem.getName() + '_color' ] = filterItem.getHighlightColor(); + } + }.bind( this ) ); + + // 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.findSelectedItems().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 ); + } + }; + + /** + * Respond to filterItem update event + * + * @param {mw.rcfilters.dm.FilterItem} item Updated filter item + * @fires update + */ + mw.rcfilters.dm.FilterGroup.prototype.onFilterItemUpdate = function ( item ) { + // Update state + var changed = false, + active = this.areAnySelected(), + model = this; + + if ( this.getType() === 'single_option' ) { + // This group must have one item selected always + // and must never have more than one item selected at a time + if ( this.findSelectedItems().length === 0 ) { + // Nothing is selected anymore + // Select the default or the first item + this.currSelected = this.getItemByParamName( this.defaultParams[ this.getName() ] ) || + this.getItems()[ 0 ]; + this.currSelected.toggleSelected( true ); + changed = true; + } else if ( this.findSelectedItems().length > 1 ) { + // There is more than one item selected + // This should only happen if the item given + // is the one that is selected, so unselect + // all items that is not it + this.findSelectedItems().forEach( function ( itemModel ) { + // Note that in case the given item is actually + // not selected, this loop will end up unselecting + // all items, which would trigger the case above + // when the last item is unselected anyways + var selected = itemModel.getName() === item.getName() && + item.isSelected(); + + itemModel.toggleSelected( selected ); + if ( selected ) { + model.currSelected = itemModel; + } + } ); + changed = true; + } + } + + if ( this.isSticky() ) { + // If this group is sticky, then change the default according to the + // current selection. + this.defaultParams = this.getParamRepresentation( this.getSelectedState() ); + } + + if ( + changed || + this.active !== active || + this.currSelected !== item + ) { + this.active = active; + this.currSelected = item; + + this.emit( 'update' ); + } + }; + + /** + * Get group active state + * + * @return {boolean} Active state + */ + mw.rcfilters.dm.FilterGroup.prototype.isActive = function () { + return this.active; + }; + + /** + * Get group hidden state + * + * @return {boolean} Hidden state + */ + mw.rcfilters.dm.FilterGroup.prototype.isHidden = function () { + 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 {string} Group name + */ + mw.rcfilters.dm.FilterGroup.prototype.getName = function () { + return this.name; + }; + + /** + * Get the default param state of this group + * + * @return {Object} Default param state + */ + mw.rcfilters.dm.FilterGroup.prototype.getDefaultParams = function () { + 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 + * + * @return {Object} What's this messages + */ + mw.rcfilters.dm.FilterGroup.prototype.getWhatsThis = function () { + return this.whatsThis; + }; + + /** + * Check whether this group has a 'what's this' message + * + * @return {boolean} This group has a what's this message + */ + mw.rcfilters.dm.FilterGroup.prototype.hasWhatsThis = function () { + return !!this.whatsThis.body; + }; + + /** + * Get the conflicts associated with the entire group. + * Conflict object is set up by filter name keys and conflict + * definition. For example: + * [ + * { + * filterName: { + * filter: filterName, + * group: group1 + * } + * }, + * { + * filterName2: { + * filter: filterName2, + * group: group2 + * } + * } + * ] + * @return {Object} Conflict definition + */ + mw.rcfilters.dm.FilterGroup.prototype.getConflicts = function () { + return this.conflicts; + }; + + /** + * Set conflicts for this group. See #getConflicts for the expected + * structure of the definition. + * + * @param {Object} conflicts Conflicts for this group + */ + mw.rcfilters.dm.FilterGroup.prototype.setConflicts = function ( conflicts ) { + this.conflicts = conflicts; + }; + + /** + * Set conflicts for each filter item in the group based on the + * given conflict map + * + * @param {Object} conflicts Object representing the conflict map, + * keyed by the item name, where its value is an object for all its conflicts + */ + mw.rcfilters.dm.FilterGroup.prototype.setFilterConflicts = function ( conflicts ) { + this.getItems().forEach( function ( filterItem ) { + if ( conflicts[ filterItem.getName() ] ) { + filterItem.setConflicts( conflicts[ filterItem.getName() ] ); + } + } ); + }; + + /** + * Check whether this item has a potential conflict with the given item + * + * This checks whether the given item is in the list of conflicts of + * the current item, but makes no judgment about whether the conflict + * is currently at play (either one of the items may not be selected) + * + * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item + * @return {boolean} This item has a conflict with the given item + */ + mw.rcfilters.dm.FilterGroup.prototype.existsInConflicts = function ( filterItem ) { + return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() ); + }; + + /** + * Check whether there are any items selected + * + * @return {boolean} Any items in the group are selected + */ + mw.rcfilters.dm.FilterGroup.prototype.areAnySelected = function () { + return this.getItems().some( function ( filterItem ) { + return filterItem.isSelected(); + } ); + }; + + /** + * Check whether all items selected + * + * @return {boolean} All items are selected + */ + mw.rcfilters.dm.FilterGroup.prototype.areAllSelected = function () { + var selected = [], + unselected = []; + + this.getItems().forEach( function ( filterItem ) { + if ( filterItem.isSelected() ) { + selected.push( filterItem ); + } else { + unselected.push( filterItem ); + } + } ); + + if ( unselected.length === 0 ) { + return true; + } + + // check if every unselected is a subset of a selected + return unselected.every( function ( unselectedFilterItem ) { + return selected.some( function ( selectedFilterItem ) { + return selectedFilterItem.existsInSubset( unselectedFilterItem.getName() ); + } ); + } ); + }; + + /** + * Get all selected items in this group + * + * @param {mw.rcfilters.dm.FilterItem} [excludeItem] Item to exclude from the list + * @return {mw.rcfilters.dm.FilterItem[]} Selected items + */ + mw.rcfilters.dm.FilterGroup.prototype.findSelectedItems = function ( excludeItem ) { + var excludeName = ( excludeItem && excludeItem.getName() ) || ''; + + return this.getItems().filter( function ( item ) { + return item.getName() !== excludeName && item.isSelected(); + } ); + }; + + /** + * Check whether all selected items are in conflict with the given item + * + * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test + * @return {boolean} All selected items are in conflict with this item + */ + mw.rcfilters.dm.FilterGroup.prototype.areAllSelectedInConflictWith = function ( filterItem ) { + var selectedItems = this.findSelectedItems( filterItem ); + + return selectedItems.length > 0 && + ( + // The group as a whole is in conflict with this item + this.existsInConflicts( filterItem ) || + // All selected items are in conflict individually + selectedItems.every( function ( selectedFilter ) { + return selectedFilter.existsInConflicts( filterItem ); + } ) + ); + }; + + /** + * Check whether any of the selected items are in conflict with the given item + * + * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test + * @return {boolean} Any of the selected items are in conflict with this item + */ + mw.rcfilters.dm.FilterGroup.prototype.areAnySelectedInConflictWith = function ( filterItem ) { + var selectedItems = this.findSelectedItems( filterItem ); + + return selectedItems.length > 0 && ( + // The group as a whole is in conflict with this item + this.existsInConflicts( filterItem ) || + // Any selected items are in conflict individually + selectedItems.some( function ( selectedFilter ) { + return selectedFilter.existsInConflicts( filterItem ); + } ) + ); + }; + + /** + * Get the parameter representation from this group + * + * @param {Object} [filterRepresentation] An object defining the state + * of the filters in this group, keyed by their name and current selected + * state value. + * @return {Object} Parameter representation + */ + mw.rcfilters.dm.FilterGroup.prototype.getParamRepresentation = function ( filterRepresentation ) { + var values, + areAnySelected = false, + buildFromCurrentState = !filterRepresentation, + defaultFilters = this.getDefaultFilters(), + result = {}, + model = this, + filterParamNames = {}, + getSelectedParameter = function ( filters ) { + var item, + selected = []; + + // Find if any are selected + $.each( filters, function ( name, value ) { + if ( value ) { + selected.push( name ); + } + } ); + + item = model.getItemByName( selected[ 0 ] ); + return ( item && item.getParamName() ) || ''; + }; + + filterRepresentation = filterRepresentation || {}; + + // Create or complete the filterRepresentation definition + this.getItems().forEach( function ( item ) { + // Map filter names to their parameter names + filterParamNames[ item.getName() ] = item.getParamName(); + + if ( buildFromCurrentState ) { + // This means we have not been given a filter representation + // so we are building one based on current state + filterRepresentation[ item.getName() ] = item.getValue(); + } 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 + if ( model.isSticky() ) { + filterRepresentation[ item.getName() ] = !!defaultFilters[ item.getName() ]; + } else { + filterRepresentation[ item.getName() ] = false; + } + } + + if ( filterRepresentation[ item.getName() ] ) { + areAnySelected = true; + } + } ); + + // Build result + if ( + this.getType() === 'send_unselected_if_any' || + this.getType() === 'boolean' || + this.getType() === 'any_value' + ) { + // 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 ) { + // 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 ( model.getType() === 'any_value' ) { + result[ filterParamNames[ name ] ] = value; + } + } ); + } else if ( this.getType() === 'string_options' ) { + values = []; + + $.each( filterRepresentation, function ( name, value ) { + // Collect values + if ( value ) { + values.push( filterParamNames[ name ] ); + } + } ); + + result[ this.getName() ] = ( values.length === Object.keys( filterRepresentation ).length ) ? + 'all' : values.join( this.getSeparator() ); + } else if ( this.getType() === 'single_option' ) { + result[ this.getName() ] = getSelectedParameter( filterRepresentation ); + } + + return result; + }; + + /** + * Get the filter representation this group would provide + * based on given parameter states. + * + * @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, item, currentValue, + oneWasSelected = false, + defaultParams = this.getDefaultParams(), + expandedParams = $.extend( true, {}, paramRepresentation ), + model = this, + paramToFilterMap = {}, + result = {}; + + 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' || + this.getType() === 'any_value' + ) { + // Go over param representation; map and check for selections + this.getItems().forEach( function ( filterItem ) { + var paramName = filterItem.getParamName(); + + expandedParams[ paramName ] = paramRepresentation[ paramName ] || '0'; + paramToFilterMap[ paramName ] = filterItem; + + if ( Number( paramRepresentation[ filterItem.getParamName() ] ) ) { + areAnySelected = true; + } + } ); + + $.each( expandedParams, function ( paramName, paramValue ) { + var filterItem = paramToFilterMap[ paramName ]; + + 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 + 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 ( model.getType() === 'any_value' ) { + result[ filterItem.getName() ] = paramRepresentation[ filterItem.getParamName() ]; + } + } ); + } else if ( this.getType() === 'string_options' ) { + currentValue = paramRepresentation[ this.getName() ] || ''; + + // Normalize the given parameter values + paramValues = mw.rcfilters.utils.normalizeParamOptions( + // Given + currentValue.split( + this.getSeparator() + ), + // Allowed values + this.getItems().map( function ( filterItem ) { + return filterItem.getParamName(); + } ) + ); + // 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() + ) ? + 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 and if not, get the default + this.getItems().forEach( function ( filterItem ) { + 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 ) { + if ( result[ filterItem.getName() ] === undefined ) { + result[ filterItem.getName() ] = this.getFalsyValue(); + } + }.bind( this ) ); + + // 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; + }; + + /** + * @return {*} The appropriate falsy value for this group type + */ + mw.rcfilters.dm.FilterGroup.prototype.getFalsyValue = function () { + return this.getType() === 'any_value' ? '' : false; + }; + + /** + * 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.getValue(); + } ); + + return state; + }; + + /** + * Get item by its filter name + * + * @param {string} filterName Filter name + * @return {mw.rcfilters.dm.FilterItem} Filter item + */ + mw.rcfilters.dm.FilterGroup.prototype.getItemByName = function ( filterName ) { + return this.getItems().filter( function ( item ) { + return item.getName() === filterName; + } )[ 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 + * + * @param {string} paramName Parameter name + * @return {mw.rcfilters.dm.FilterItem} Filter item + */ + mw.rcfilters.dm.FilterGroup.prototype.getItemByParamName = function ( paramName ) { + return this.getItems().filter( function ( item ) { + return item.getParamName() === String( paramName ); + } )[ 0 ]; + }; + + /** + * Get group type + * + * @return {string} Group type + */ + mw.rcfilters.dm.FilterGroup.prototype.getType = function () { + return this.type; + }; + + /** + * Check whether this group is represented by a single parameter + * or whether each item is its own parameter + * + * @return {boolean} This group is a single parameter + */ + mw.rcfilters.dm.FilterGroup.prototype.isPerGroupRequestParameter = function () { + return ( + this.getType() === 'string_options' || + this.getType() === 'single_option' + ); + }; + + /** + * Get display group + * + * @return {string} Display group + */ + mw.rcfilters.dm.FilterGroup.prototype.getView = function () { + return this.view; + }; + + /** + * Get the prefix used for the filter names inside this group. + * + * @param {string} [name] Filter name to prefix + * @return {string} Group prefix + */ + mw.rcfilters.dm.FilterGroup.prototype.getNamePrefix = function () { + return this.getName() + '__'; + }; + + /** + * Get a filter name with the prefix used for the filter names inside this group. + * + * @param {string} name Filter name to prefix + * @return {string} Group prefix + */ + mw.rcfilters.dm.FilterGroup.prototype.getPrefixedName = function ( name ) { + return this.getNamePrefix() + name; + }; + + /** + * Get group's title + * + * @return {string} Title + */ + mw.rcfilters.dm.FilterGroup.prototype.getTitle = function () { + return this.title; + }; + + /** + * Get group's values separator + * + * @return {string} Values separator + */ + mw.rcfilters.dm.FilterGroup.prototype.getSeparator = function () { + return this.separator; + }; + + /** + * Check whether the group is defined as full coverage + * + * @return {boolean} Group is full coverage + */ + 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; + }; + + /** + * 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; + }; + + /** + * Toggle the visibility of this group + * + * @param {boolean} [isVisible] Item is visible + */ + mw.rcfilters.dm.FilterGroup.prototype.toggleVisible = function ( isVisible ) { + isVisible = isVisible === undefined ? !this.visible : isVisible; + + if ( this.visible !== isVisible ) { + this.visible = isVisible; + this.emit( 'update' ); + } + }; + + /** + * Check whether the group is visible + * + * @return {boolean} Group is visible + */ + mw.rcfilters.dm.FilterGroup.prototype.isVisible = function () { + return this.visible; + }; + + /** + * Set the visibility of the items under this group by the given items array + * + * @param {mw.rcfilters.dm.ItemModel[]} visibleItems An array of visible items + */ + mw.rcfilters.dm.FilterGroup.prototype.setVisibleItems = function ( visibleItems ) { + this.getItems().forEach( function ( itemModel ) { + itemModel.toggleVisible( visibleItems.indexOf( itemModel ) !== -1 ); + } ); + }; +}( mediaWiki ) ); |