summaryrefslogtreecommitdiff
path: root/www/wiki/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js')
-rw-r--r--www/wiki/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js1276
1 files changed, 1276 insertions, 0 deletions
diff --git a/www/wiki/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js b/www/wiki/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js
new file mode 100644
index 00000000..cdf1f635
--- /dev/null
+++ b/www/wiki/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js
@@ -0,0 +1,1276 @@
+( function ( mw, $ ) {
+ /**
+ * View model for the filters selection and display
+ *
+ * @mixins OO.EventEmitter
+ * @mixins OO.EmitterList
+ *
+ * @constructor
+ */
+ mw.rcfilters.dm.FiltersViewModel = function MwRcfiltersDmFiltersViewModel() {
+ // Mixin constructor
+ OO.EventEmitter.call( this );
+ OO.EmitterList.call( this );
+
+ this.groups = {};
+ this.defaultParams = {};
+ this.highlightEnabled = false;
+ this.parameterMap = {};
+ this.emptyParameterState = null;
+
+ this.views = {};
+ this.currentView = 'default';
+ this.searchQuery = null;
+
+ // Events
+ this.aggregate( { update: 'filterItemUpdate' } );
+ this.connect( this, { filterItemUpdate: [ 'emit', 'itemUpdate' ] } );
+ };
+
+ /* Initialization */
+ OO.initClass( mw.rcfilters.dm.FiltersViewModel );
+ OO.mixinClass( mw.rcfilters.dm.FiltersViewModel, OO.EventEmitter );
+ OO.mixinClass( mw.rcfilters.dm.FiltersViewModel, OO.EmitterList );
+
+ /* Events */
+
+ /**
+ * @event initialize
+ *
+ * Filter list is initialized
+ */
+
+ /**
+ * @event update
+ *
+ * Model has been updated
+ */
+
+ /**
+ * @event itemUpdate
+ * @param {mw.rcfilters.dm.FilterItem} item Filter item updated
+ *
+ * Filter item has changed
+ */
+
+ /**
+ * @event highlightChange
+ * @param {boolean} Highlight feature is enabled
+ *
+ * Highlight feature has been toggled enabled or disabled
+ */
+
+ /* Methods */
+
+ /**
+ * Re-assess the states of filter items based on the interactions between them
+ *
+ * @param {mw.rcfilters.dm.FilterItem} [item] Changed item. If not given, the
+ * method will go over the state of all items
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.reassessFilterInteractions = function ( item ) {
+ var allSelected,
+ model = this,
+ iterationItems = item !== undefined ? [ item ] : this.getItems();
+
+ iterationItems.forEach( function ( checkedItem ) {
+ var allCheckedItems = checkedItem.getSubset().concat( [ checkedItem.getName() ] ),
+ groupModel = checkedItem.getGroupModel();
+
+ // Check for subsets (included filters) plus the item itself:
+ allCheckedItems.forEach( function ( filterItemName ) {
+ var itemInSubset = model.getItemByName( filterItemName );
+
+ itemInSubset.toggleIncluded(
+ // If any of itemInSubset's supersets are selected, this item
+ // is included
+ itemInSubset.getSuperset().some( function ( supersetName ) {
+ return ( model.getItemByName( supersetName ).isSelected() );
+ } )
+ );
+ } );
+
+ // Update coverage for the changed group
+ if ( groupModel.isFullCoverage() ) {
+ allSelected = groupModel.areAllSelected();
+ groupModel.getItems().forEach( function ( filterItem ) {
+ filterItem.toggleFullyCovered( allSelected );
+ } );
+ }
+ } );
+
+ // Check for conflicts
+ // In this case, we must go over all items, since
+ // conflicts are bidirectional and depend not only on
+ // individual items, but also on the selected states of
+ // the groups they're in.
+ this.getItems().forEach( function ( filterItem ) {
+ var inConflict = false,
+ filterItemGroup = filterItem.getGroupModel();
+
+ // For each item, see if that item is still conflicting
+ $.each( model.groups, function ( groupName, groupModel ) {
+ if ( filterItem.getGroupName() === groupName ) {
+ // Check inside the group
+ inConflict = groupModel.areAnySelectedInConflictWith( filterItem );
+ } else {
+ // According to the spec, if two items conflict from two different
+ // groups, the conflict only lasts if the groups **only have selected
+ // items that are conflicting**. If a group has selected items that
+ // are conflicting and non-conflicting, the scope of the result has
+ // expanded enough to completely remove the conflict.
+
+ // For example, see two groups with conflicts:
+ // userExpLevel: [
+ // {
+ // name: 'experienced',
+ // conflicts: [ 'unregistered' ]
+ // }
+ // ],
+ // registration: [
+ // {
+ // name: 'registered',
+ // },
+ // {
+ // name: 'unregistered',
+ // }
+ // ]
+ // If we select 'experienced', then 'unregistered' is in conflict (and vice versa),
+ // because, inherently, 'experienced' filter only includes registered users, and so
+ // both filters are in conflict with one another.
+ // However, the minute we select 'registered', the scope of our results
+ // has expanded to no longer have a conflict with 'experienced' filter, and
+ // so the conflict is removed.
+
+ // In our case, we need to check if the entire group conflicts with
+ // the entire item's group, so we follow the above spec
+ inConflict = (
+ // The foreign group is in conflict with this item
+ groupModel.areAllSelectedInConflictWith( filterItem ) &&
+ // Every selected member of the item's own group is also
+ // in conflict with the other group
+ filterItemGroup.findSelectedItems().every( function ( otherGroupItem ) {
+ return groupModel.areAllSelectedInConflictWith( otherGroupItem );
+ } )
+ );
+ }
+
+ // If we're in conflict, this will return 'false' which
+ // will break the loop. Otherwise, we're not in conflict
+ // and the loop continues
+ return !inConflict;
+ } );
+
+ // Toggle the item state
+ filterItem.toggleConflicted( inConflict );
+ } );
+ };
+
+ /**
+ * Get whether the model has any conflict in its items
+ *
+ * @return {boolean} There is a conflict
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.hasConflict = function () {
+ return this.getItems().some( function ( filterItem ) {
+ return filterItem.isSelected() && filterItem.isConflicted();
+ } );
+ };
+
+ /**
+ * Get the first item with a current conflict
+ *
+ * @return {mw.rcfilters.dm.FilterItem} Conflicted item
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getFirstConflictedItem = function () {
+ var conflictedItem;
+
+ this.getItems().forEach( function ( filterItem ) {
+ if ( filterItem.isSelected() && filterItem.isConflicted() ) {
+ conflictedItem = filterItem;
+ return false;
+ }
+ } );
+
+ return conflictedItem;
+ };
+
+ /**
+ * Set filters and preserve a group relationship based on
+ * the definition given by an object
+ *
+ * @param {Array} filterGroups Filters definition
+ * @param {Object} [views] Extra views definition
+ * Expected in the following format:
+ * {
+ * namespaces: {
+ * label: 'namespaces', // Message key
+ * trigger: ':',
+ * groups: [
+ * {
+ * // Group info
+ * name: 'namespaces' // Parameter name
+ * title: 'namespaces' // Message key
+ * type: 'string_options',
+ * separator: ';',
+ * labelPrefixKey: { 'default': 'rcfilters-tag-prefix-namespace', inverted: 'rcfilters-tag-prefix-namespace-inverted' },
+ * fullCoverage: true
+ * items: []
+ * }
+ * ]
+ * }
+ * }
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.initializeFilters = function ( filterGroups, views ) {
+ var filterConflictResult, groupConflictResult,
+ allViews = {},
+ model = this,
+ items = [],
+ groupConflictMap = {},
+ filterConflictMap = {},
+ /*!
+ * Expand a conflict definition from group name to
+ * the list of all included filters in that group.
+ * We do this so that the direct relationship in the
+ * models are consistently item->items rather than
+ * mixing item->group with item->item.
+ *
+ * @param {Object} obj Conflict definition
+ * @return {Object} Expanded conflict definition
+ */
+ expandConflictDefinitions = function ( obj ) {
+ var result = {};
+
+ $.each( obj, function ( key, conflicts ) {
+ var filterName,
+ adjustedConflicts = {};
+
+ conflicts.forEach( function ( conflict ) {
+ var filter;
+
+ if ( conflict.filter ) {
+ filterName = model.groups[ conflict.group ].getPrefixedName( conflict.filter );
+ filter = model.getItemByName( filterName );
+
+ // Rename
+ adjustedConflicts[ filterName ] = $.extend(
+ {},
+ conflict,
+ {
+ filter: filterName,
+ item: filter
+ }
+ );
+ } else {
+ // This conflict is for an entire group. Split it up to
+ // represent each filter
+
+ // Get the relevant group items
+ model.groups[ conflict.group ].getItems().forEach( function ( groupItem ) {
+ // Rebuild the conflict
+ adjustedConflicts[ groupItem.getName() ] = $.extend(
+ {},
+ conflict,
+ {
+ filter: groupItem.getName(),
+ item: groupItem
+ }
+ );
+ } );
+ }
+ } );
+
+ result[ key ] = adjustedConflicts;
+ } );
+
+ return result;
+ };
+
+ // Reset
+ this.clearItems();
+ this.groups = {};
+ this.views = {};
+
+ // Clone
+ filterGroups = OO.copy( filterGroups );
+
+ // Normalize definition from the server
+ filterGroups.forEach( function ( data ) {
+ var i;
+ // What's this information needs to be normalized
+ data.whatsThis = {
+ body: data.whatsThisBody,
+ header: data.whatsThisHeader,
+ linkText: data.whatsThisLinkText,
+ url: data.whatsThisUrl
+ };
+
+ // Title is a msg-key
+ data.title = data.title ? mw.msg( data.title ) : data.name;
+
+ // Filters are given to us with msg-keys, we need
+ // to translate those before we hand them off
+ for ( i = 0; i < data.filters.length; i++ ) {
+ data.filters[ i ].label = data.filters[ i ].label ? mw.msg( data.filters[ i ].label ) : data.filters[ i ].name;
+ data.filters[ i ].description = data.filters[ i ].description ? mw.msg( data.filters[ i ].description ) : '';
+ }
+ } );
+
+ // Collect views
+ allViews = $.extend( true, {
+ 'default': {
+ title: mw.msg( 'rcfilters-filterlist-title' ),
+ groups: filterGroups
+ }
+ }, views );
+
+ // Go over all views
+ $.each( allViews, function ( viewName, viewData ) {
+ // Define the view
+ model.views[ viewName ] = {
+ name: viewData.name,
+ title: viewData.title,
+ trigger: viewData.trigger
+ };
+
+ // Go over groups
+ viewData.groups.forEach( function ( groupData ) {
+ var group = groupData.name;
+
+ if ( !model.groups[ group ] ) {
+ model.groups[ group ] = new mw.rcfilters.dm.FilterGroup(
+ group,
+ $.extend( true, {}, groupData, { view: viewName } )
+ );
+ }
+
+ model.groups[ group ].initializeFilters( groupData.filters, groupData.default );
+ items = items.concat( model.groups[ group ].getItems() );
+
+ // Prepare conflicts
+ if ( groupData.conflicts ) {
+ // Group conflicts
+ groupConflictMap[ group ] = groupData.conflicts;
+ }
+
+ groupData.filters.forEach( function ( itemData ) {
+ var filterItem = model.groups[ group ].getItemByParamName( itemData.name );
+ // Filter conflicts
+ if ( itemData.conflicts ) {
+ filterConflictMap[ filterItem.getName() ] = itemData.conflicts;
+ }
+ } );
+ } );
+ } );
+
+ // Add item references to the model, for lookup
+ this.addItems( items );
+
+ // Expand conflicts
+ groupConflictResult = expandConflictDefinitions( groupConflictMap );
+ filterConflictResult = expandConflictDefinitions( filterConflictMap );
+
+ // Set conflicts for groups
+ $.each( groupConflictResult, function ( group, conflicts ) {
+ model.groups[ group ].setConflicts( conflicts );
+ } );
+
+ // Set conflicts for items
+ $.each( filterConflictResult, function ( filterName, conflicts ) {
+ var filterItem = model.getItemByName( filterName );
+ // set conflicts for items in the group
+ filterItem.setConflicts( conflicts );
+ } );
+
+ // Create a map between known parameters and their models
+ $.each( this.groups, function ( group, groupModel ) {
+ if (
+ groupModel.getType() === 'send_unselected_if_any' ||
+ groupModel.getType() === 'boolean' ||
+ groupModel.getType() === 'any_value'
+ ) {
+ // Individual filters
+ groupModel.getItems().forEach( function ( filterItem ) {
+ model.parameterMap[ filterItem.getParamName() ] = filterItem;
+ } );
+ } else if (
+ groupModel.getType() === 'string_options' ||
+ groupModel.getType() === 'single_option'
+ ) {
+ // Group
+ model.parameterMap[ groupModel.getName() ] = groupModel;
+ }
+ } );
+
+ this.setSearch( '' );
+
+ this.updateHighlightedState();
+
+ // Finish initialization
+ this.emit( 'initialize' );
+ };
+
+ /**
+ * Update filter view model state based on a parameter object
+ *
+ * @param {Object} params Parameters object
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.updateStateFromParams = function ( params ) {
+ var filtersValue;
+ // For arbitrary numeric single_option values make sure the values
+ // are normalized to fit within the limits
+ $.each( this.getFilterGroups(), function ( groupName, groupModel ) {
+ params[ groupName ] = groupModel.normalizeArbitraryValue( params[ groupName ] );
+ } );
+
+ // Update filter values
+ filtersValue = this.getFiltersFromParameters( params );
+ Object.keys( filtersValue ).forEach( function ( filterName ) {
+ this.getItemByName( filterName ).setValue( filtersValue[ filterName ] );
+ }.bind( this ) );
+
+ // Update highlight state
+ this.getItemsSupportingHighlights().forEach( function ( filterItem ) {
+ var color = params[ filterItem.getName() + '_color' ];
+ if ( color ) {
+ filterItem.setHighlightColor( color );
+ } else {
+ filterItem.clearHighlightColor();
+ }
+ } );
+ this.updateHighlightedState();
+
+ // Check all filter interactions
+ this.reassessFilterInteractions();
+ };
+
+ /**
+ * Get a representation of an empty (falsey) parameter state
+ *
+ * @return {Object} Empty parameter state
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getEmptyParameterState = function () {
+ if ( !this.emptyParameterState ) {
+ this.emptyParameterState = $.extend(
+ true,
+ {},
+ this.getParametersFromFilters( {} ),
+ this.getEmptyHighlightParameters()
+ );
+ }
+ return this.emptyParameterState;
+ };
+
+ /**
+ * Get a representation of only the non-falsey parameters
+ *
+ * @param {Object} [parameters] A given parameter state to minimize. If not given the current
+ * state of the system will be used.
+ * @return {Object} Empty parameter state
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getMinimizedParamRepresentation = function ( parameters ) {
+ var result = {};
+
+ parameters = parameters ? $.extend( true, {}, parameters ) : this.getCurrentParameterState();
+
+ // Params
+ $.each( this.getEmptyParameterState(), function ( param, value ) {
+ if ( parameters[ param ] !== undefined && parameters[ param ] !== value ) {
+ result[ param ] = parameters[ param ];
+ }
+ } );
+
+ // Highlights
+ Object.keys( this.getEmptyHighlightParameters() ).forEach( function ( param ) {
+ if ( parameters[ param ] ) {
+ // If a highlight parameter is not undefined and not null
+ // add it to the result
+ result[ param ] = parameters[ param ];
+ }
+ } );
+
+ return result;
+ };
+
+ /**
+ * Get a representation of the full parameter list, including all base values
+ *
+ * @return {Object} Full parameter representation
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getExpandedParamRepresentation = function () {
+ return $.extend(
+ true,
+ {},
+ this.getEmptyParameterState(),
+ this.getCurrentParameterState()
+ );
+ };
+
+ /**
+ * Get a parameter representation of the current state of the model
+ *
+ * @param {boolean} [removeStickyParams] Remove sticky filters from final result
+ * @return {Object} Parameter representation of the current state of the model
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getCurrentParameterState = function ( removeStickyParams ) {
+ var state = this.getMinimizedParamRepresentation( $.extend(
+ true,
+ {},
+ this.getParametersFromFilters( this.getSelectedState() ),
+ this.getHighlightParameters()
+ ) );
+
+ if ( removeStickyParams ) {
+ state = this.removeStickyParams( state );
+ }
+
+ return state;
+ };
+
+ /**
+ * Delete sticky parameters from given object.
+ *
+ * @param {Object} paramState Parameter state
+ * @return {Object} Parameter state without sticky parameters
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.removeStickyParams = function ( paramState ) {
+ this.getStickyParams().forEach( function ( paramName ) {
+ delete paramState[ paramName ];
+ } );
+
+ return paramState;
+ };
+
+ /**
+ * Turn the highlight feature on or off
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.updateHighlightedState = function () {
+ this.toggleHighlight( this.getHighlightedItems().length > 0 );
+ };
+
+ /**
+ * Get the object that defines groups by their name.
+ *
+ * @return {Object} Filter groups
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getFilterGroups = function () {
+ return this.groups;
+ };
+
+ /**
+ * Get the object that defines groups that match a certain view by their name.
+ *
+ * @param {string} [view] Requested view. If not given, uses current view
+ * @return {Object} Filter groups matching a display group
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getFilterGroupsByView = function ( view ) {
+ var result = {};
+
+ view = view || this.getCurrentView();
+
+ $.each( this.groups, function ( groupName, groupModel ) {
+ if ( groupModel.getView() === view ) {
+ result[ groupName ] = groupModel;
+ }
+ } );
+
+ return result;
+ };
+
+ /**
+ * Get an array of filters matching the given display group.
+ *
+ * @param {string} [view] Requested view. If not given, uses current view
+ * @return {mw.rcfilters.dm.FilterItem} Filter items matching the group
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getFiltersByView = function ( view ) {
+ var groups,
+ result = [];
+
+ view = view || this.getCurrentView();
+
+ groups = this.getFilterGroupsByView( view );
+
+ $.each( groups, function ( groupName, groupModel ) {
+ result = result.concat( groupModel.getItems() );
+ } );
+
+ return result;
+ };
+
+ /**
+ * Get the trigger for the requested view.
+ *
+ * @param {string} view View name
+ * @return {string} View trigger, if exists
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getViewTrigger = function ( view ) {
+ return ( this.views[ view ] && this.views[ view ].trigger ) || '';
+ };
+
+ /**
+ * Get the value of a specific parameter
+ *
+ * @param {string} name Parameter name
+ * @return {number|string} Parameter value
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getParamValue = function ( name ) {
+ return this.parameters[ name ];
+ };
+
+ /**
+ * Get the current selected state of the filters
+ *
+ * @param {boolean} [onlySelected] return an object containing only the filters with a value
+ * @return {Object} Filters selected state
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getSelectedState = function ( onlySelected ) {
+ var i,
+ items = this.getItems(),
+ result = {};
+
+ for ( i = 0; i < items.length; i++ ) {
+ if ( !onlySelected || items[ i ].getValue() ) {
+ result[ items[ i ].getName() ] = items[ i ].getValue();
+ }
+ }
+
+ return result;
+ };
+
+ /**
+ * Get the current full state of the filters
+ *
+ * @return {Object} Filters full state
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getFullState = function () {
+ var i,
+ items = this.getItems(),
+ result = {};
+
+ for ( i = 0; i < items.length; i++ ) {
+ result[ items[ i ].getName() ] = {
+ selected: items[ i ].isSelected(),
+ conflicted: items[ i ].isConflicted(),
+ included: items[ i ].isIncluded()
+ };
+ }
+
+ return result;
+ };
+
+ /**
+ * Get an object representing default parameters state
+ *
+ * @return {Object} Default parameter values
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getDefaultParams = function () {
+ var result = {};
+
+ // Get default filter state
+ $.each( this.groups, function ( name, model ) {
+ if ( !model.isSticky() ) {
+ $.extend( true, result, model.getDefaultParams() );
+ }
+ } );
+
+ return result;
+ };
+
+ /**
+ * Get a parameter representation of all sticky parameters
+ *
+ * @return {Object} Sticky parameter values
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getStickyParams = function () {
+ var result = [];
+
+ $.each( this.groups, function ( name, model ) {
+ if ( model.isSticky() ) {
+ if ( model.isPerGroupRequestParameter() ) {
+ result.push( name );
+ } else {
+ // Each filter is its own param
+ result = result.concat( model.getItems().map( function ( filterItem ) {
+ return filterItem.getParamName();
+ } ) );
+ }
+ }
+ } );
+
+ return result;
+ };
+
+ /**
+ * Get a parameter representation of all sticky parameters
+ *
+ * @return {Object} Sticky parameter values
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getStickyParamsValues = function () {
+ var result = {};
+
+ $.each( this.groups, function ( name, model ) {
+ if ( model.isSticky() ) {
+ $.extend( true, result, model.getParamRepresentation() );
+ }
+ } );
+
+ return result;
+ };
+
+ /**
+ * Analyze the groups and their filters and output an object representing
+ * the state of the parameters they represent.
+ *
+ * @param {Object} [filterDefinition] An object defining the filter values,
+ * keyed by filter names.
+ * @return {Object} Parameter state object
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getParametersFromFilters = function ( filterDefinition ) {
+ var groupItemDefinition,
+ result = {},
+ groupItems = this.getFilterGroups();
+
+ if ( filterDefinition ) {
+ groupItemDefinition = {};
+ // Filter definition is "flat", but in effect
+ // each group needs to tell us its result based
+ // on the values in it. We need to split this list
+ // back into groupings so we can "feed" it to the
+ // loop below, and we need to expand it so it includes
+ // all filters (set to false)
+ this.getItems().forEach( function ( filterItem ) {
+ groupItemDefinition[ filterItem.getGroupName() ] = groupItemDefinition[ filterItem.getGroupName() ] || {};
+ groupItemDefinition[ filterItem.getGroupName() ][ filterItem.getName() ] = filterItem.coerceValue( filterDefinition[ filterItem.getName() ] );
+ } );
+ }
+
+ $.each( groupItems, function ( group, model ) {
+ $.extend(
+ result,
+ model.getParamRepresentation(
+ groupItemDefinition ?
+ groupItemDefinition[ group ] : null
+ )
+ );
+ } );
+
+ return result;
+ };
+
+ /**
+ * This is the opposite of the #getParametersFromFilters method; this goes over
+ * the given parameters and translates into a selected/unselected value in the filters.
+ *
+ * @param {Object} params Parameters query object
+ * @return {Object} Filter state object
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getFiltersFromParameters = function ( params ) {
+ var groupMap = {},
+ model = this,
+ result = {};
+
+ // Go over the given parameters, break apart to groupings
+ // The resulting object represents the group with its parameter
+ // values. For example:
+ // {
+ // group1: {
+ // param1: "1",
+ // param2: "0",
+ // param3: "1"
+ // },
+ // group2: "param4|param5"
+ // }
+ $.each( params, function ( paramName, paramValue ) {
+ var groupName,
+ itemOrGroup = model.parameterMap[ paramName ];
+
+ if ( itemOrGroup ) {
+ groupName = itemOrGroup instanceof mw.rcfilters.dm.FilterItem ?
+ itemOrGroup.getGroupName() : itemOrGroup.getName();
+
+ groupMap[ groupName ] = groupMap[ groupName ] || {};
+ groupMap[ groupName ][ paramName ] = paramValue;
+ }
+ } );
+
+ // Go over all groups, so we make sure we get the complete output
+ // even if the parameters don't include a certain group
+ $.each( this.groups, function ( groupName, groupModel ) {
+ result = $.extend( true, {}, result, groupModel.getFilterRepresentation( groupMap[ groupName ] ) );
+ } );
+
+ return result;
+ };
+
+ /**
+ * Get the highlight parameters based on current filter configuration
+ *
+ * @return {Object} Object where keys are `<filter name>_color` and values
+ * are the selected highlight colors.
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getHighlightParameters = function () {
+ var highlightEnabled = this.isHighlightEnabled(),
+ result = {};
+
+ this.getItems().forEach( function ( filterItem ) {
+ if ( filterItem.isHighlightSupported() ) {
+ result[ filterItem.getName() + '_color' ] = highlightEnabled && filterItem.isHighlighted() ?
+ filterItem.getHighlightColor() :
+ null;
+ }
+ } );
+
+ return result;
+ };
+
+ /**
+ * Get an object representing the complete empty state of highlights
+ *
+ * @return {Object} Object containing all the highlight parameters set to their negative value
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getEmptyHighlightParameters = function () {
+ var result = {};
+
+ this.getItems().forEach( function ( filterItem ) {
+ if ( filterItem.isHighlightSupported() ) {
+ result[ filterItem.getName() + '_color' ] = null;
+ }
+ } );
+
+ return result;
+ };
+
+ /**
+ * Get an array of currently applied highlight colors
+ *
+ * @return {string[]} Currently applied highlight colors
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getCurrentlyUsedHighlightColors = function () {
+ var result = [];
+
+ if ( this.isHighlightEnabled() ) {
+ this.getHighlightedItems().forEach( function ( filterItem ) {
+ var color = filterItem.getHighlightColor();
+
+ if ( result.indexOf( color ) === -1 ) {
+ result.push( color );
+ }
+ } );
+ }
+
+ return result;
+ };
+
+ /**
+ * Sanitize value group of a string_option groups type
+ * Remove duplicates and make sure to only use valid
+ * values.
+ *
+ * @private
+ * @param {string} groupName Group name
+ * @param {string[]} valueArray Array of values
+ * @return {string[]} Array of valid values
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.sanitizeStringOptionGroup = function ( groupName, valueArray ) {
+ var validNames = this.getGroupFilters( groupName ).map( function ( filterItem ) {
+ return filterItem.getParamName();
+ } );
+
+ return mw.rcfilters.utils.normalizeParamOptions( valueArray, validNames );
+ };
+
+ /**
+ * Check whether no visible filter is selected.
+ *
+ * Filter groups that are hidden or sticky are not shown in the
+ * active filters area and therefore not included in this check.
+ *
+ * @return {boolean} No visible filter is selected
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.areVisibleFiltersEmpty = function () {
+ // Check if there are either any selected items or any items
+ // that have highlight enabled
+ return !this.getItems().some( function ( filterItem ) {
+ var visible = !filterItem.getGroupModel().isSticky() && !filterItem.getGroupModel().isHidden(),
+ active = ( filterItem.isSelected() || filterItem.isHighlighted() );
+ return visible && active;
+ } );
+ };
+
+ /**
+ * Check whether the invert state is a valid one. A valid invert state is one where
+ * there are actual namespaces selected.
+ *
+ * This is done to compare states to previous ones that may have had the invert model
+ * selected but effectively had no namespaces, so are not effectively different than
+ * ones where invert is not selected.
+ *
+ * @return {boolean} Invert is effectively selected
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.areNamespacesEffectivelyInverted = function () {
+ return this.getInvertModel().isSelected() &&
+ this.findSelectedItems().some( function ( itemModel ) {
+ return itemModel.getGroupModel().getName() === 'namespace';
+ } );
+ };
+
+ /**
+ * Get the item that matches the given name
+ *
+ * @param {string} name Filter name
+ * @return {mw.rcfilters.dm.FilterItem} Filter item
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getItemByName = function ( name ) {
+ return this.getItems().filter( function ( item ) {
+ return name === item.getName();
+ } )[ 0 ];
+ };
+
+ /**
+ * Set all filters to false or empty/all
+ * This is equivalent to display all.
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.emptyAllFilters = function () {
+ this.getItems().forEach( function ( filterItem ) {
+ if ( !filterItem.getGroupModel().isSticky() ) {
+ this.toggleFilterSelected( filterItem.getName(), false );
+ }
+ }.bind( this ) );
+ };
+
+ /**
+ * Toggle selected state of one item
+ *
+ * @param {string} name Name of the filter item
+ * @param {boolean} [isSelected] Filter selected state
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.toggleFilterSelected = function ( name, isSelected ) {
+ var item = this.getItemByName( name );
+
+ if ( item ) {
+ item.toggleSelected( isSelected );
+ }
+ };
+
+ /**
+ * Toggle selected state of items by their names
+ *
+ * @param {Object} filterDef Filter definitions
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.toggleFiltersSelected = function ( filterDef ) {
+ Object.keys( filterDef ).forEach( function ( name ) {
+ this.toggleFilterSelected( name, filterDef[ name ] );
+ }.bind( this ) );
+ };
+
+ /**
+ * Get a group model from its name
+ *
+ * @param {string} groupName Group name
+ * @return {mw.rcfilters.dm.FilterGroup} Group model
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getGroup = function ( groupName ) {
+ return this.groups[ groupName ];
+ };
+
+ /**
+ * Get all filters within a specified group by its name
+ *
+ * @param {string} groupName Group name
+ * @return {mw.rcfilters.dm.FilterItem[]} Filters belonging to this group
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getGroupFilters = function ( groupName ) {
+ return ( this.getGroup( groupName ) && this.getGroup( groupName ).getItems() ) || [];
+ };
+
+ /**
+ * Find items whose labels match the given string
+ *
+ * @param {string} query Search string
+ * @param {boolean} [returnFlat] Return a flat array. If false, the result
+ * is an object whose keys are the group names and values are an array of
+ * filters per group. If set to true, returns an array of filters regardless
+ * of their groups.
+ * @return {Object} An object of items to show
+ * arranged by their group names
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.findMatches = function ( query, returnFlat ) {
+ var i, searchIsEmpty,
+ groupTitle,
+ result = {},
+ flatResult = [],
+ view = this.getViewByTrigger( query.substr( 0, 1 ) ),
+ items = this.getFiltersByView( view );
+
+ // Normalize so we can search strings regardless of case and view
+ query = query.trim().toLowerCase();
+ if ( view !== 'default' ) {
+ query = query.substr( 1 );
+ }
+ // Trim again to also intercept cases where the spaces were after the trigger
+ // eg: '# str'
+ query = query.trim();
+
+ // Check if the search if actually empty; this can be a problem when
+ // we use prefixes to denote different views
+ searchIsEmpty = query.length === 0;
+
+ // item label starting with the query string
+ for ( i = 0; i < items.length; i++ ) {
+ if (
+ searchIsEmpty ||
+ items[ i ].getLabel().toLowerCase().indexOf( query ) === 0 ||
+ (
+ // For tags, we want the parameter name to be included in the search
+ view === 'tags' &&
+ items[ i ].getParamName().toLowerCase().indexOf( query ) > -1
+ )
+ ) {
+ result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
+ result[ items[ i ].getGroupName() ].push( items[ i ] );
+ flatResult.push( items[ i ] );
+ }
+ }
+
+ if ( $.isEmptyObject( result ) ) {
+ // item containing the query string in their label, description, or group title
+ for ( i = 0; i < items.length; i++ ) {
+ groupTitle = items[ i ].getGroupModel().getTitle();
+ if (
+ searchIsEmpty ||
+ items[ i ].getLabel().toLowerCase().indexOf( query ) > -1 ||
+ items[ i ].getDescription().toLowerCase().indexOf( query ) > -1 ||
+ groupTitle.toLowerCase().indexOf( query ) > -1 ||
+ (
+ // For tags, we want the parameter name to be included in the search
+ view === 'tags' &&
+ items[ i ].getParamName().toLowerCase().indexOf( query ) > -1
+ )
+ ) {
+ result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
+ result[ items[ i ].getGroupName() ].push( items[ i ] );
+ flatResult.push( items[ i ] );
+ }
+ }
+ }
+
+ return returnFlat ? flatResult : result;
+ };
+
+ /**
+ * Get items that are highlighted
+ *
+ * @return {mw.rcfilters.dm.FilterItem[]} Highlighted items
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getHighlightedItems = function () {
+ return this.getItems().filter( function ( filterItem ) {
+ return filterItem.isHighlightSupported() &&
+ filterItem.getHighlightColor();
+ } );
+ };
+
+ /**
+ * Get items that allow highlights even if they're not currently highlighted
+ *
+ * @return {mw.rcfilters.dm.FilterItem[]} Items supporting highlights
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getItemsSupportingHighlights = function () {
+ return this.getItems().filter( function ( filterItem ) {
+ return filterItem.isHighlightSupported();
+ } );
+ };
+
+ /**
+ * Get all selected items
+ *
+ * @return {mw.rcfilters.dm.FilterItem[]} Selected items
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.findSelectedItems = function () {
+ var allSelected = [];
+
+ $.each( this.getFilterGroups(), function ( groupName, groupModel ) {
+ allSelected = allSelected.concat( groupModel.findSelectedItems() );
+ } );
+
+ return allSelected;
+ };
+
+ /**
+ * Get the current view
+ *
+ * @return {string} Current view
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getCurrentView = function () {
+ return this.currentView;
+ };
+
+ /**
+ * Get the label for the current view
+ *
+ * @param {string} viewName View name
+ * @return {string} Label for the current view
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getViewTitle = function ( viewName ) {
+ viewName = viewName || this.getCurrentView();
+
+ return this.views[ viewName ] && this.views[ viewName ].title;
+ };
+
+ /**
+ * Get the view that fits the given trigger
+ *
+ * @param {string} trigger Trigger
+ * @return {string} Name of view
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getViewByTrigger = function ( trigger ) {
+ var result = 'default';
+
+ $.each( this.views, function ( name, data ) {
+ if ( data.trigger === trigger ) {
+ result = name;
+ }
+ } );
+
+ return result;
+ };
+
+ /**
+ * Return a version of the given string that is without any
+ * view triggers.
+ *
+ * @param {string} str Given string
+ * @return {string} Result
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.removeViewTriggers = function ( str ) {
+ if ( this.getViewFromString( str ) !== 'default' ) {
+ str = str.substr( 1 );
+ }
+
+ return str;
+ };
+
+ /**
+ * Get the view from the given string by a trigger, if it exists
+ *
+ * @param {string} str Given string
+ * @return {string} View name
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getViewFromString = function ( str ) {
+ return this.getViewByTrigger( str.substr( 0, 1 ) );
+ };
+
+ /**
+ * Set the current search for the system.
+ * This also dictates what items and groups are visible according
+ * to the search in #findMatches
+ *
+ * @param {string} searchQuery Search query, including triggers
+ * @fires searchChange
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.setSearch = function ( searchQuery ) {
+ var visibleGroups, visibleGroupNames;
+
+ if ( this.searchQuery !== searchQuery ) {
+ // Check if the view changed
+ this.switchView( this.getViewFromString( searchQuery ) );
+
+ visibleGroups = this.findMatches( searchQuery );
+ visibleGroupNames = Object.keys( visibleGroups );
+
+ // Update visibility of items and groups
+ $.each( this.getFilterGroups(), function ( groupName, groupModel ) {
+ // Check if the group is visible at all
+ groupModel.toggleVisible( visibleGroupNames.indexOf( groupName ) !== -1 );
+ groupModel.setVisibleItems( visibleGroups[ groupName ] || [] );
+ } );
+
+ this.searchQuery = searchQuery;
+ this.emit( 'searchChange', this.searchQuery );
+ }
+ };
+
+ /**
+ * Get the current search
+ *
+ * @return {string} Current search query
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getSearch = function () {
+ return this.searchQuery;
+ };
+
+ /**
+ * Switch the current view
+ *
+ * @private
+ * @param {string} view View name
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.switchView = function ( view ) {
+ if ( this.views[ view ] && this.currentView !== view ) {
+ this.currentView = view;
+ }
+ };
+
+ /**
+ * Toggle the highlight feature on and off.
+ * Propagate the change to filter items.
+ *
+ * @param {boolean} enable Highlight should be enabled
+ * @fires highlightChange
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.toggleHighlight = function ( enable ) {
+ enable = enable === undefined ? !this.highlightEnabled : enable;
+
+ if ( this.highlightEnabled !== enable ) {
+ this.highlightEnabled = enable;
+ this.emit( 'highlightChange', this.highlightEnabled );
+ }
+ };
+
+ /**
+ * Check if the highlight feature is enabled
+ * @return {boolean}
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.isHighlightEnabled = function () {
+ return !!this.highlightEnabled;
+ };
+
+ /**
+ * Toggle the inverted namespaces property on and off.
+ * Propagate the change to namespace filter items.
+ *
+ * @param {boolean} enable Inverted property is enabled
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.toggleInvertedNamespaces = function ( enable ) {
+ this.toggleFilterSelected( this.getInvertModel().getName(), enable );
+ };
+
+ /**
+ * Get the model object that represents the 'invert' filter
+ *
+ * @return {mw.rcfilters.dm.FilterItem}
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getInvertModel = function () {
+ return this.getGroup( 'invertGroup' ).getItemByParamName( 'invert' );
+ };
+
+ /**
+ * Set highlight color for a specific filter item
+ *
+ * @param {string} filterName Name of the filter item
+ * @param {string} color Selected color
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.setHighlightColor = function ( filterName, color ) {
+ this.getItemByName( filterName ).setHighlightColor( color );
+ };
+
+ /**
+ * Clear highlight for a specific filter item
+ *
+ * @param {string} filterName Name of the filter item
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.clearHighlightColor = function ( filterName ) {
+ this.getItemByName( filterName ).clearHighlightColor();
+ };
+
+}( mediaWiki, jQuery ) );