X-Git-Url: http://git.heureux-cyclage.org/?a=blobdiff_plain;f=resources%2Fsrc%2Fmediawiki.rcfilters%2Fdm%2Fmw.rcfilters.dm.FilterGroup.js;h=59c0a19e6c7f2e533861f5487e73ca52a650070a;hb=12b7a7ea555ff4bda637b60f28e069a3b22873f4;hp=3ec544c1bd9476affd86df21b8c8a0349cf1de58;hpb=b3d1931c349314e05c5192a95f6f3250d8183ed0;p=lhc%2Fweb%2Fwiklou.git diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js index 3ec544c1bd..4915803c9f 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js @@ -9,11 +9,17 @@ * @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 {string} [title] Group title + * @cfg {boolean} [hidden] This group is hidden from the regular menu views * @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 @@ -29,8 +35,11 @@ this.name = name; this.type = config.type || 'send_unselected_if_any'; - this.title = config.title; + this.view = config.view || 'default'; + this.title = config.title || name; + this.hidden = !!config.hidden; this.separator = config.separator || '|'; + this.labelPrefixKey = config.labelPrefixKey; this.active = !!config.active; this.fullCoverage = !!config.fullCoverage; @@ -38,6 +47,7 @@ this.whatsThis = config.whatsThis || {}; this.conflicts = config.conflicts || {}; + this.defaultParams = {}; this.aggregate( { update: 'filterItemUpdate' } ); this.connect( this, { filterItemUpdate: 'onFilterItemUpdate' } ); @@ -58,14 +68,124 @@ /* 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 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 + } ); + + 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' ) { + // 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 ) ); + } + } ); + + // 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' ) { + // For this group, the parameter is the group name, + // and a single item can be selected, or none at all + // The item also must be recognized or none is set as + // default + model.defaultParams[ this.getName() ] = this.getItemByParamName( groupDefault ) ? groupDefault : ''; + } + }; + /** * Respond to filterItem update event * + * @param {mw.rcfilters.dm.FilterItem} item Updated filter item * @fires update */ - mw.rcfilters.dm.FilterGroup.prototype.onFilterItemUpdate = function () { + mw.rcfilters.dm.FilterGroup.prototype.onFilterItemUpdate = function ( item ) { // Update state - var active = this.areAnySelected(); + var active = this.areAnySelected(), + itemName = item && item.getName(); + + if ( item.isSelected() && this.getType() === 'single_option' ) { + // Change the selection to only be the newly selected item + this.getItems().forEach( function ( filterItem ) { + if ( filterItem.getName() !== itemName ) { + filterItem.toggleSelected( false ); + } + } ); + } if ( this.active !== active ) { this.active = active; @@ -82,6 +202,15 @@ return this.active; }; + /** + * Get group hidden state + * + * @return {boolean} Hidden state + */ + mw.rcfilters.dm.FilterGroup.prototype.isHidden = function () { + return this.hidden; + }; + /** * Get group name * @@ -91,6 +220,15 @@ 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 messags defining the 'whats this' popup for this group * @@ -143,6 +281,21 @@ 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 * @@ -174,8 +327,26 @@ * @return {boolean} All items are selected */ mw.rcfilters.dm.FilterGroup.prototype.areAllSelected = function () { - return this.getItems().every( function ( filterItem ) { - return filterItem.isSelected(); + 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() ); + } ); } ); }; @@ -235,39 +406,198 @@ /** * 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 () { - var i, values, + mw.rcfilters.dm.FilterGroup.prototype.getParamRepresentation = function ( filterRepresentation ) { + var values, + areAnySelected = false, + buildFromCurrentState = !filterRepresentation, result = {}, - filterItems = this.getItems(); + 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.isSelected(); + } else if ( !filterRepresentation[ item.getName() ] ) { + // We are given a filter representation, but we have to make + // sure that we fill in the missing filters if there are any + // we will assume they are all falsey + filterRepresentation[ item.getName() ] = false; + } + + if ( filterRepresentation[ item.getName() ] ) { + areAnySelected = true; + } + } ); + // Build result if ( this.getType() === 'send_unselected_if_any' ) { // 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 - for ( i = 0; i < filterItems.length; i++ ) { - result[ filterItems[ i ].getParamName() ] = this.areAnySelected() ? - Number( !filterItems[ i ].isSelected() ) : 0; - } - + $.each( filterRepresentation, function ( name, value ) { + // We must store all parameter values as strings '0' or '1' + result[ filterParamNames[ name ] ] = areAnySelected ? + String( Number( !value ) ) : + '0'; + } ); } else if ( this.getType() === 'string_options' ) { values = []; - for ( i = 0; i < filterItems.length; i++ ) { - if ( filterItems[ i ].isSelected() ) { - values.push( filterItems[ i ].getParamName() ); + + $.each( filterRepresentation, function ( name, value ) { + // Collect values + if ( value ) { + values.push( filterParamNames[ name ] ); } - } + } ); - result[ this.getName() ] = ( values.length === filterItems.length ) ? + 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|string} [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, + model = this, + paramToFilterMap = {}, + result = {}; + + if ( this.getType() === 'send_unselected_if_any' ) { + paramRepresentation = paramRepresentation || {}; + // Expand param representation to include all filters in the group + this.getItems().forEach( function ( filterItem ) { + var paramName = filterItem.getParamName(); + + paramRepresentation[ paramName ] = paramRepresentation[ paramName ] || '0'; + paramToFilterMap[ paramName ] = filterItem; + + if ( Number( paramRepresentation[ filterItem.getParamName() ] ) ) { + areAnySelected = true; + } + } ); + + $.each( paramRepresentation, function ( paramName, paramValue ) { + var filterItem = paramToFilterMap[ paramName ]; + + // 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 ( this.getType() === 'string_options' ) { + paramRepresentation = paramRepresentation || ''; + + // Normalize the given parameter values + paramValues = mw.rcfilters.utils.normalizeParamOptions( + // Given + paramRepresentation.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, or none at all + this.getItems().forEach( function ( filterItem ) { + result[ filterItem.getName() ] = filterItem.getParamName() === paramRepresentation; + } ); + } + + // Go over result and make sure all filters are represented. + // If any filters are missing, they will get a falsey value + this.getItems().forEach( function ( filterItem ) { + result[ filterItem.getName() ] = !!result[ filterItem.getName() ]; + } ); + + return result; + }; + + /** + * 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 ]; + }; + + /** + * 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() === paramName; + } )[ 0 ]; + }; + /** * Get group type * @@ -278,14 +608,34 @@ }; /** - * Get the prefix used for the filter names inside this group + * 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 *