this.defaultParams = {};
this.defaultFiltersEmpty = null;
this.highlightEnabled = false;
+ this.parameterMap = {};
// Events
this.aggregate( { update: 'filterItemUpdate' } );
} );
};
+ /**
+ * 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;
+
+ $.each( this.getItems(), function ( index, 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} filters Filter group definition
*/
mw.rcfilters.dm.FiltersViewModel.prototype.initializeFilters = function ( filters ) {
- var i, filterItem, selectedFilterNames, filterConflictResult, groupConflictResult,
+ var i, filterItem, filterConflictResult, groupConflictResult, subsetNames,
model = this,
items = [],
supersetMap = {},
expandConflictDefinitions = function ( obj ) {
var result = {};
- $.each( obj, function ( group, conflicts ) {
- var adjustedConflicts = {};
+ $.each( obj, function ( key, conflicts ) {
+ var filterName,
+ adjustedConflicts = {};
+
conflicts.forEach( function ( conflict ) {
+ var filter;
+
if ( conflict.filter ) {
- adjustedConflicts[ conflict.filter ] = conflict;
+ filterName = model.groups[ conflict.group ].getNamePrefix() + 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() } );
+ adjustedConflicts[ groupItem.getName() ] = $.extend(
+ {},
+ conflict,
+ {
+ filter: groupItem.getName(),
+ item: groupItem
+ }
+ );
} );
}
} );
- result[ group ] = adjustedConflicts;
+ result[ key ] = adjustedConflicts;
} );
return result;
type: data.type,
title: mw.msg( data.title ),
separator: data.separator,
- fullCoverage: !!data.fullCoverage
+ fullCoverage: !!data.fullCoverage,
+ whatsThis: {
+ body: data.whatsThisBody,
+ header: data.whatsThisHeader,
+ linkText: data.whatsThisLinkText,
+ url: data.whatsThisUrl
+ }
} );
}
groupConflictMap[ group ] = data.conflicts;
}
- selectedFilterNames = [];
for ( i = 0; i < data.filters.length; i++ ) {
data.filters[ i ].subset = data.filters[ i ].subset || [];
data.filters[ i ].subset = data.filters[ i ].subset.map( function ( el ) {
group: group,
label: mw.msg( data.filters[ i ].label ),
description: mw.msg( data.filters[ i ].description ),
- subset: data.filters[ i ].subset,
cssClass: data.filters[ i ].cssClass
} );
- // 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
if ( data.filters[ i ].subset ) {
+ subsetNames = [];
data.filters[ i ].subset.forEach( function ( subsetFilterName ) { // eslint-disable-line no-loop-func
- supersetMap[ subsetFilterName ] = supersetMap[ subsetFilterName ] || [];
+ var subsetName = model.groups[ group ].getNamePrefix() + 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 ] || [];
addArrayElementsUnique(
- supersetMap[ subsetFilterName ],
+ 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 );
}
// Store conflicts
if ( data.filters[ i ].conflicts ) {
- filterConflictMap[ data.filters[ i ].name ] = data.filters[ i ].conflicts;
+ filterConflictMap[ filterItem.getName() ] = data.filters[ i ].conflicts;
}
if ( data.type === 'send_unselected_if_any' ) {
// Store the default parameter state
// For this group type, parameter values are direct
model.defaultParams[ data.filters[ i ].name ] = Number( !!data.filters[ i ].default );
- } else if (
- data.type === 'string_options' &&
- data.filters[ i ].default
- ) {
- selectedFilterNames.push( data.filters[ i ].name );
}
model.groups[ group ].addItems( filterItem );
// Store the default parameter group state
// For this group, the parameter is group name and value is the names
// of selected items
- model.defaultParams[ group ] = model.sanitizeStringOptionGroup( group, selectedFilterNames ).join( model.groups[ group ].getSeparator() );
+ model.defaultParams[ group ] = model.sanitizeStringOptionGroup(
+ group,
+ data.default ?
+ data.default.split( model.groups[ group ].getSeparator() ) :
+ []
+ ).join( model.groups[ group ].getSeparator() );
}
} );
+ // Add items to the model
+ this.addItems( items );
+
// Expand conflicts
groupConflictResult = expandConflictDefinitions( groupConflictMap );
filterConflictResult = expandConflictDefinitions( filterConflictMap );
}
} );
- // Add items to the model
- this.addItems( items );
+ // Create a map between known parameters and their models
+ $.each( this.groups, function ( group, groupModel ) {
+ if ( groupModel.getType() === 'send_unselected_if_any' ) {
+ // Individual filters
+ groupModel.getItems().forEach( function ( filterItem ) {
+ model.parameterMap[ filterItem.getParamName() ] = filterItem;
+ } );
+ } else if ( groupModel.getType() === 'string_options' ) {
+ // Group
+ model.parameterMap[ groupModel.getName() ] = groupModel;
+ }
+ } );
this.emit( 'initialize' );
};
return this.defaultParams;
};
- /**
- * Set all filter states to default values
- */
- mw.rcfilters.dm.FiltersViewModel.prototype.setFiltersToDefaults = function () {
- var defaultFilterStates = this.getFiltersFromParameters( this.getDefaultParams() );
-
- this.toggleFiltersSelected( defaultFilterStates );
- };
-
/**
* Analyze the groups and their filters and output an object representing
* the state of the parameters they represent.
*
- * @param {Object} [filterGroups] An object defining the filter groups to
- * translate to parameters. Its structure must follow that of this.groups
- * see #getFilterGroups
+ * @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 ( filterGroups ) {
- var i, filterItems, anySelected, values,
+ mw.rcfilters.dm.FiltersViewModel.prototype.getParametersFromFilters = function ( filterDefinition ) {
+ var groupItemDefinition,
result = {},
- groupItems = filterGroups || this.getFilterGroups();
+ 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() ] = !!filterDefinition[ filterItem.getName() ];
+ } );
+ }
$.each( groupItems, function ( group, model ) {
- filterItems = model.getItems();
-
- if ( model.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
- anySelected = filterItems.some( function ( filterItem ) {
- return filterItem.isSelected();
- } );
-
- // Go over the items and define the correct values
- for ( i = 0; i < filterItems.length; i++ ) {
- result[ filterItems[ i ].getName() ] = anySelected ?
- Number( !filterItems[ i ].isSelected() ) : 0;
- }
- } else if ( model.getType() === 'string_options' ) {
- values = [];
- for ( i = 0; i < filterItems.length; i++ ) {
- if ( filterItems[ i ].isSelected() ) {
- values.push( filterItems[ i ].getName() );
- }
- }
-
- if ( values.length === filterItems.length ) {
- result[ group ] = 'all';
- } else {
- result[ group ] = values.join( model.getSeparator() );
- }
- }
+ $.extend(
+ result,
+ model.getParamRepresentation(
+ groupItemDefinition ?
+ groupItemDefinition[ group ] : null
+ )
+ );
} );
return result;
* @param {string[]} valueArray Array of values
* @return {string[]} Array of valid values
*/
- mw.rcfilters.dm.FiltersViewModel.prototype.sanitizeStringOptionGroup = function( groupName, valueArray ) {
+ mw.rcfilters.dm.FiltersViewModel.prototype.sanitizeStringOptionGroup = function ( groupName, valueArray ) {
var result = [],
validNames = this.getGroupFilters( groupName ).map( function ( filterItem ) {
- return filterItem.getName();
+ return filterItem.getParamName();
} );
if ( valueArray.indexOf( 'all' ) > -1 ) {
* @return {Object} Filter state object
*/
mw.rcfilters.dm.FiltersViewModel.prototype.getFiltersFromParameters = function ( params ) {
- var i, filterItem,
+ var i,
groupMap = {},
model = this,
base = this.getDefaultParams(),
params = $.extend( {}, base, params );
+ // Go over the given parameters
$.each( params, function ( paramName, paramValue ) {
- // Find the filter item
- filterItem = model.getItemByName( paramName );
- // Ignore if no filter item exists
- if ( filterItem ) {
- groupMap[ filterItem.getGroupName() ] = groupMap[ filterItem.getGroupName() ] || {};
+ var itemOrGroup = model.parameterMap[ paramName ];
+ if ( itemOrGroup instanceof mw.rcfilters.dm.FilterItem ) {
// Mark the group if it has any items that are selected
- groupMap[ filterItem.getGroupName() ].hasSelected = (
- groupMap[ filterItem.getGroupName() ].hasSelected ||
+ groupMap[ itemOrGroup.getGroupName() ] = groupMap[ itemOrGroup.getGroupName() ] || {};
+ groupMap[ itemOrGroup.getGroupName() ].hasSelected = (
+ groupMap[ itemOrGroup.getGroupName() ].hasSelected ||
!!Number( paramValue )
);
- // Add the relevant filter into the group map
- groupMap[ filterItem.getGroupName() ].filters = groupMap[ filterItem.getGroupName() ].filters || [];
- groupMap[ filterItem.getGroupName() ].filters.push( filterItem );
- } else if ( model.groups.hasOwnProperty( paramName ) ) {
+ // Add filters
+ groupMap[ itemOrGroup.getGroupName() ].filters = groupMap[ itemOrGroup.getGroupName() ].filters || [];
+ groupMap[ itemOrGroup.getGroupName() ].filters.push( itemOrGroup );
+ } else if ( itemOrGroup instanceof mw.rcfilters.dm.FilterGroup ) {
+ groupMap[ itemOrGroup.getName() ] = groupMap[ itemOrGroup.getName() ] || {};
// This parameter represents a group (values are the filters)
// this is equivalent to checking if the group is 'string_options'
- groupMap[ paramName ] = { filters: model.groups[ paramName ].getItems() };
+ groupMap[ itemOrGroup.getName() ].filters = itemOrGroup.getItems();
}
} );
for ( i = 0; i < allItemsInGroup.length; i++ ) {
filterItem = allItemsInGroup[ i ];
- result[ filterItem.getName() ] = data.hasSelected ?
+ result[ filterItem.getName() ] = groupMap[ filterItem.getGroupName() ].hasSelected ?
// Flip the definition between the parameter
// state and the filter state
// This is what the 'toggleSelected' value of the filter is
- !Number( params[ filterItem.getName() ] ) :
+ !Number( params[ filterItem.getParamName() ] ) :
// Otherwise, there are no selected items in the
// group, which means the state is false
false;
}
} else if ( model.groups[ group ].getType() === 'string_options' ) {
- paramValues = model.sanitizeStringOptionGroup( group, params[ group ].split( model.groups[ group ].getSeparator() ) );
+ paramValues = model.sanitizeStringOptionGroup(
+ group,
+ params[ group ].split(
+ model.groups[ group ].getSeparator()
+ )
+ );
for ( i = 0; i < allItemsInGroup.length; i++ ) {
filterItem = allItemsInGroup[ i ];
paramValues.length === model.groups[ group ].getItemCount()
) ?
// All true (either because all values are written or the term 'all' is written)
- // is the same as all filters set to false
- false :
+ // is the same as all filters set to true
+ true :
// Otherwise, the filter is selected only if it appears in the parameter values
- paramValues.indexOf( filterItem.getName() ) > -1;
+ paramValues.indexOf( filterItem.getParamName() ) > -1;
}
}
} );
+
return result;
};
* @param {boolean} [isSelected] Filter selected state
*/
mw.rcfilters.dm.FiltersViewModel.prototype.toggleFilterSelected = function ( name, isSelected ) {
- this.getItemByName( name ).toggleSelected( isSelected );
+ var item = this.getItemByName( name );
+
+ if ( item ) {
+ item.toggleSelected( isSelected );
+ }
};
/**
* 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 ) {
+ mw.rcfilters.dm.FiltersViewModel.prototype.findMatches = function ( query, returnFlat ) {
var i,
groupTitle,
result = {},
+ flatResult = [],
items = this.getItems();
// Normalize so we can search strings regardless of case
if ( items[ i ].getLabel().toLowerCase().indexOf( query ) === 0 ) {
result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
result[ items[ i ].getGroupName() ].push( items[ i ] );
+ flatResult.push( items[ i ] );
}
}
) {
result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
result[ items[ i ].getGroupName() ].push( items[ i ] );
+ flatResult.push( items[ i ] );
}
}
}
- return result;
+ return returnFlat ? flatResult : result;
};
/**
} );
};
+ /**
+ * 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();
+ } );
+ };
+
/**
* Toggle the highlight feature on and off.
* Propagate the change to filter items.