Remove redundant closure for all packageFiles with own directory
[lhc/web/wiklou.git] / resources / src / mediawiki.rcfilters / dm / FilterGroup.js
index 831e6eb..8bd5eb2 100644 (file)
-( function () {
-       var FilterItem = require( './FilterItem.js' ),
-               FilterGroup;
-
-       /**
-        * View model for a filter group
-        *
-        * @class mw.rcfilters.dm.FilterGroup
-        * @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
-        */
-       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( FilterGroup );
-       OO.mixinClass( FilterGroup, OO.EventEmitter );
-       OO.mixinClass( 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
-        */
-       FilterGroup.prototype.initializeFilters = function ( filterDefinition, groupDefault ) {
-               var defaultParam,
-                       supersetMap = {},
-                       model = this,
-                       items = [];
-
-               filterDefinition.forEach( function ( filter ) {
-                       // Instantiate an item
-                       var subsetNames = [],
-                               filterItem = new 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 ) {
-                                       // 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() );
+var FilterItem = require( './FilterItem.js' ),
+       FilterGroup;
+
+/**
+ * View model for a filter group
+ *
+ * @class mw.rcfilters.dm.FilterGroup
+ * @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
+ */
+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( FilterGroup );
+OO.mixinClass( FilterGroup, OO.EventEmitter );
+OO.mixinClass( 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
+ */
+FilterGroup.prototype.initializeFilters = function ( filterDefinition, groupDefault ) {
+       var defaultParam,
+               supersetMap = {},
+               model = this,
+               items = [];
+
+       filterDefinition.forEach( function ( filter ) {
+               // Instantiate an item
+               var subsetNames = [],
+                       filterItem = new 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
+                       } );
 
-               // Check for filters that should be initially selected by their default value
-               if ( this.isSticky() ) {
-                       // eslint-disable-next-line jquery/no-each-util
-                       $.each( this.defaultFilters, function ( filterName, filterValue ) {
-                               model.getItemByName( filterName ).toggleSelected( filterValue );
+               if ( filter.subset ) {
+                       filter.subset = filter.subset.map( function ( el ) {
+                               return el.filter;
                        } );
-               }
 
-               // 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();
+                       subsetNames = [];
+
+                       filter.subset.forEach( function ( subsetFilterName ) {
+                               // 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 );
+                       } );
 
-                       // 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
-        */
-       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;
-                       }
+                       // Set translated subset
+                       filterItem.setSubset( subsetNames );
                }
 
-               if ( this.isSticky() ) {
-                       // If this group is sticky, then change the default according to the
-                       // current selection.
-                       this.defaultParams = this.getParamRepresentation( this.getSelectedState() );
-               }
+               items.push( filterItem );
 
+               // Store default parameter state; in this case, default is defined per filter
                if (
-                       changed ||
-                       this.active !== active ||
-                       this.currSelected !== item
+                       model.getType() === 'send_unselected_if_any' ||
+                       model.getType() === 'boolean'
                ) {
-                       this.active = active;
-                       this.currSelected = item;
-
-                       this.emit( 'update' );
+                       // 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;
                }
-       };
-
-       /**
-        * Get group active state
-        *
-        * @return {boolean} Active state
-        */
-       FilterGroup.prototype.isActive = function () {
-               return this.active;
-       };
-
-       /**
-        * Get group hidden state
-        *
-        * @return {boolean} Hidden state
-        */
-       FilterGroup.prototype.isHidden = function () {
-               return this.hidden;
-       };
-
-       /**
-        * Get group allow arbitrary state
-        *
-        * @return {boolean} Group allows an arbitrary value from the URL
-        */
-       FilterGroup.prototype.isAllowArbitrary = function () {
-               return this.allowArbitrary;
-       };
-
-       /**
-        * Get group maximum value for numeric groups
-        *
-        * @return {number|null} Group max value
-        */
-       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
-        */
-       FilterGroup.prototype.getMinValue = function () {
-               return this.numericRange && this.numericRange.min !== undefined ?
-                       this.numericRange.min : null;
-       };
-
-       /**
-        * Get group name
-        *
-        * @return {string} Group name
-        */
-       FilterGroup.prototype.getName = function () {
-               return this.name;
-       };
-
-       /**
-        * Get the default param state of this group
-        *
-        * @return {Object} Default param state
-        */
-       FilterGroup.prototype.getDefaultParams = function () {
-               return this.defaultParams;
-       };
-
-       /**
-        * Get the default filter state of this group
-        *
-        * @return {Object} Default filter state
-        */
-       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
-        */
-       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
-        */
-       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
-        */
-       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
-        */
-       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
-        */
-       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
-        */
-       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
-        */
-       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
-        */
-       FilterGroup.prototype.areAnySelected = function () {
-               return this.getItems().some( function ( filterItem ) {
-                       return filterItem.isSelected();
-               } );
-       };
+       } );
+
+       // 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 ) );
 
-       /**
-        * Check whether all items selected
-        *
-        * @return {boolean} All items are selected
-        */
-       FilterGroup.prototype.areAllSelected = function () {
-               var selected = [],
-                       unselected = [];
+       // Store default filter state based on default params
+       this.defaultFilters = this.getFilterRepresentation( this.getDefaultParams() );
 
-               this.getItems().forEach( function ( filterItem ) {
-                       if ( filterItem.isSelected() ) {
-                               selected.push( filterItem );
-                       } else {
-                               unselected.push( filterItem );
-                       }
+       // Check for filters that should be initially selected by their default value
+       if ( this.isSticky() ) {
+               // eslint-disable-next-line no-jquery/no-each-util
+               $.each( this.defaultFilters, function ( filterName, filterValue ) {
+                       model.getItemByName( filterName ).toggleSelected( filterValue );
                } );
-
-               if ( unselected.length === 0 ) {
-                       return true;
+       }
+
+       // 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
+ */
+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
+ */
+FilterGroup.prototype.isActive = function () {
+       return this.active;
+};
+
+/**
+ * Get group hidden state
+ *
+ * @return {boolean} Hidden state
+ */
+FilterGroup.prototype.isHidden = function () {
+       return this.hidden;
+};
+
+/**
+ * Get group allow arbitrary state
+ *
+ * @return {boolean} Group allows an arbitrary value from the URL
+ */
+FilterGroup.prototype.isAllowArbitrary = function () {
+       return this.allowArbitrary;
+};
+
+/**
+ * Get group maximum value for numeric groups
+ *
+ * @return {number|null} Group max value
+ */
+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
+ */
+FilterGroup.prototype.getMinValue = function () {
+       return this.numericRange && this.numericRange.min !== undefined ?
+               this.numericRange.min : null;
+};
+
+/**
+ * Get group name
+ *
+ * @return {string} Group name
+ */
+FilterGroup.prototype.getName = function () {
+       return this.name;
+};
+
+/**
+ * Get the default param state of this group
+ *
+ * @return {Object} Default param state
+ */
+FilterGroup.prototype.getDefaultParams = function () {
+       return this.defaultParams;
+};
+
+/**
+ * Get the default filter state of this group
+ *
+ * @return {Object} Default filter state
+ */
+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
+ */
+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
+ */
+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
+ */
+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
+ */
+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
+ */
+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
+ */
+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
+ */
+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
+ */
+FilterGroup.prototype.areAnySelected = function () {
+       return this.getItems().some( function ( filterItem ) {
+               return filterItem.isSelected();
+       } );
+};
+
+/**
+ * Check whether all items selected
+ *
+ * @return {boolean} All items are selected
+ */
+FilterGroup.prototype.areAllSelected = function () {
+       var selected = [],
+               unselected = [];
+
+       this.getItems().forEach( function ( filterItem ) {
+               if ( filterItem.isSelected() ) {
+                       selected.push( filterItem );
+               } else {
+                       unselected.push( filterItem );
+               }
+       } );
 
-               // 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
-        */
-       FilterGroup.prototype.findSelectedItems = function ( excludeItem ) {
-               var excludeName = ( excludeItem && excludeItem.getName() ) || '';
-
-               return this.getItems().filter( function ( item ) {
-                       return item.getName() !== excludeName && item.isSelected();
+       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() );
                } );
-       };
-
-       /**
-        * 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
-        */
-       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
-        */
-       FilterGroup.prototype.areAnySelectedInConflictWith = function ( filterItem ) {
-               var selectedItems = this.findSelectedItems( filterItem );
-
-               return selectedItems.length > 0 && (
+       } );
+};
+
+/**
+ * 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
+ */
+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
+ */
+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 ) ||
-                       // Any selected items are in conflict individually
-                       selectedItems.some( function ( selectedFilter ) {
+                       // All selected items are in conflict individually
+                       selectedItems.every( 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
-        */
-       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
-                               // eslint-disable-next-line jquery/no-each-util
-                               $.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
-                       // eslint-disable-next-line jquery/no-each-util
-                       $.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 = [];
-
-                       // eslint-disable-next-line jquery/no-each-util
-                       $.each( filterRepresentation, function ( name, value ) {
-                               // Collect values
+};
+
+/**
+ * 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
+ */
+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
+ */
+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
+                       // eslint-disable-next-line no-jquery/no-each-util
+                       $.each( filters, function ( name, value ) {
                                if ( value ) {
-                                       values.push( filterParamNames[ name ] );
+                                       selected.push( 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
-        */
-       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 );
+                       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;
+                       }
                }
 
-               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;
-                               }
-                       } );
-
-                       // eslint-disable-next-line jquery/no-each-util
-                       $.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;
-                       } );
+               if ( filterRepresentation[ item.getName() ] ) {
+                       areAnySelected = true;
                }
-
-               // 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();
+       } );
+
+       // 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
+               // eslint-disable-next-line no-jquery/no-each-util
+               $.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;
                        }
-               }.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() ] );
+               } );
+       } else if ( this.getType() === 'string_options' ) {
+               values = [];
+
+               // eslint-disable-next-line no-jquery/no-each-util
+               $.each( filterRepresentation, function ( name, value ) {
+                       // Collect values
+                       if ( value ) {
+                               values.push( filterParamNames[ name ] );
                        }
+               } );
 
-                       result[ item.getName() ] = true;
-               }
-
-               return result;
-       };
-
-       /**
-        * @return {*} The appropriate falsy value for this group type
-        */
-       FilterGroup.prototype.getFalsyValue = function () {
-               return this.getType() === 'any_value' ? '' : false;
-       };
+               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
+ */
+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();
 
-       /**
-        * Get current selected state of all filter items in this group
-        *
-        * @return {Object} Selected state
-        */
-       FilterGroup.prototype.getSelectedState = function () {
-               var state = {};
+                       expandedParams[ paramName ] = paramRepresentation[ paramName ] || '0';
+                       paramToFilterMap[ paramName ] = filterItem;
 
-               this.getItems().forEach( function ( filterItem ) {
-                       state[ filterItem.getName() ] = filterItem.getValue();
+                       if ( Number( paramRepresentation[ filterItem.getParamName() ] ) ) {
+                               areAnySelected = true;
+                       }
                } );
 
-               return state;
-       };
-
-       /**
-        * Get item by its filter name
-        *
-        * @param {string} filterName Filter name
-        * @return {mw.rcfilters.dm.FilterItem} Filter item
-        */
-       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
-        */
-       FilterGroup.prototype.selectItemByParamName = function ( paramName ) {
-               this.getItems().forEach( function ( item ) {
-                       item.toggleSelected( item.getParamName() === String( paramName ) );
+               // eslint-disable-next-line no-jquery/no-each-util
+               $.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() ];
+                       }
                } );
-       };
-
-       /**
-        * Get item by its parameter name
-        *
-        * @param {string} paramName Parameter name
-        * @return {mw.rcfilters.dm.FilterItem} Filter item
-        */
-       FilterGroup.prototype.getItemByParamName = function ( paramName ) {
-               return this.getItems().filter( function ( item ) {
-                       return item.getParamName() === String( paramName );
-               } )[ 0 ];
-       };
-
-       /**
-        * Get group type
-        *
-        * @return {string} Group type
-        */
-       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
-        */
-       FilterGroup.prototype.isPerGroupRequestParameter = function () {
-               return (
-                       this.getType() === 'string_options' ||
-                       this.getType() === 'single_option'
+       } 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();
+                       } )
                );
-       };
-
-       /**
-        * Get display group
-        *
-        * @return {string} Display group
-        */
-       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
-        */
-       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
-        */
-       FilterGroup.prototype.getPrefixedName = function ( name ) {
-               return this.getNamePrefix() + name;
-       };
-
-       /**
-        * Get group's title
-        *
-        * @return {string} Title
-        */
-       FilterGroup.prototype.getTitle = function () {
-               return this.title;
-       };
-
-       /**
-        * Get group's values separator
-        *
-        * @return {string} Values separator
-        */
-       FilterGroup.prototype.getSeparator = function () {
-               return this.separator;
-       };
-
-       /**
-        * Check whether the group is defined as full coverage
-        *
-        * @return {boolean} Group is full coverage
-        */
-       FilterGroup.prototype.isFullCoverage = function () {
-               return this.fullCoverage;
-       };
-
-       /**
-        * Check whether the group is defined as sticky default
-        *
-        * @return {boolean} Group is sticky default
-        */
-       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
-        */
-       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;
-       };
+               // 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() ];
 
-       /**
-        * Toggle the visibility of this group
-        *
-        * @param {boolean} [isVisible] Item is visible
-        */
-       FilterGroup.prototype.toggleVisible = function ( isVisible ) {
-               isVisible = isVisible === undefined ? !this.visible : isVisible;
+                       result[ filterItem.getName() ] = selected;
+                       oneWasSelected = oneWasSelected || selected;
+               } );
+       }
 
-               if ( this.visible !== isVisible ) {
-                       this.visible = isVisible;
-                       this.emit( 'update' );
+       // 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() ] );
                }
-       };
-
-       /**
-        * Check whether the group is visible
-        *
-        * @return {boolean} Group is visible
-        */
-       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
-        */
-       FilterGroup.prototype.setVisibleItems = function ( visibleItems ) {
-               this.getItems().forEach( function ( itemModel ) {
-                       itemModel.toggleVisible( visibleItems.indexOf( itemModel ) !== -1 );
-               } );
-       };
 
-       module.exports = FilterGroup;
-}() );
+               result[ item.getName() ] = true;
+       }
+
+       return result;
+};
+
+/**
+ * @return {*} The appropriate falsy value for this group type
+ */
+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
+ */
+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
+ */
+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
+ */
+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
+ */
+FilterGroup.prototype.getItemByParamName = function ( paramName ) {
+       return this.getItems().filter( function ( item ) {
+               return item.getParamName() === String( paramName );
+       } )[ 0 ];
+};
+
+/**
+ * Get group type
+ *
+ * @return {string} Group type
+ */
+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
+ */
+FilterGroup.prototype.isPerGroupRequestParameter = function () {
+       return (
+               this.getType() === 'string_options' ||
+               this.getType() === 'single_option'
+       );
+};
+
+/**
+ * Get display group
+ *
+ * @return {string} Display group
+ */
+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
+ */
+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
+ */
+FilterGroup.prototype.getPrefixedName = function ( name ) {
+       return this.getNamePrefix() + name;
+};
+
+/**
+ * Get group's title
+ *
+ * @return {string} Title
+ */
+FilterGroup.prototype.getTitle = function () {
+       return this.title;
+};
+
+/**
+ * Get group's values separator
+ *
+ * @return {string} Values separator
+ */
+FilterGroup.prototype.getSeparator = function () {
+       return this.separator;
+};
+
+/**
+ * Check whether the group is defined as full coverage
+ *
+ * @return {boolean} Group is full coverage
+ */
+FilterGroup.prototype.isFullCoverage = function () {
+       return this.fullCoverage;
+};
+
+/**
+ * Check whether the group is defined as sticky default
+ *
+ * @return {boolean} Group is sticky default
+ */
+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
+ */
+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
+ */
+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
+ */
+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
+ */
+FilterGroup.prototype.setVisibleItems = function ( visibleItems ) {
+       this.getItems().forEach( function ( itemModel ) {
+               itemModel.toggleVisible( visibleItems.indexOf( itemModel ) !== -1 );
+       } );
+};
+
+module.exports = FilterGroup;