this.groups = {};
this.defaultParams = {};
this.defaultFiltersEmpty = null;
+ this.highlightEnabled = false;
+ this.parameterMap = {};
// Events
this.aggregate( { update: 'filterItemUpdate' } );
* Filter item has changed
*/
+ /**
+ * @event highlightChange
+ * @param {boolean} Highlight feature is enabled
+ *
+ * Highlight feature has been toggled enabled or disabled
+ */
+
/* Methods */
/**
// For example, see two groups with conflicts:
// userExpLevel: [
// {
- // name: 'experienced',
- // conflicts: [ 'unregistered' ]
+ // name: 'experienced',
+ // conflicts: [ 'unregistered' ]
// }
// ],
// registration: [
// {
- // name: 'registered',
+ // name: 'registered',
// },
// {
- // name: 'unregistered',
+ // name: 'unregistered',
// }
// ]
// If we select 'experienced', then 'unregistered' is in conflict (and vice versa),
} );
};
+ /**
+ * 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 {Object} filters Filter group definition
+ * @param {Array} filters Filter group definition
*/
mw.rcfilters.dm.FiltersViewModel.prototype.initializeFilters = function ( filters ) {
- var i, filterItem, selectedFilterNames,
+ var i, filterItem, selectedFilterNames, filterConflictResult, groupConflictResult, subsetNames,
model = this,
items = [],
+ supersetMap = {},
+ groupConflictMap = {},
+ filterConflictMap = {},
addArrayElementsUnique = function ( arr, elements ) {
elements = Array.isArray( elements ) ? elements : [ elements ];
return arr;
},
- conflictMap = {},
- supersetMap = {};
+ 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 ].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(),
+ item: groupItem
+ }
+ );
+ } );
+ }
+ } );
+
+ result[ key ] = adjustedConflicts;
+ } );
+
+ return result;
+ };
// Reset
this.clearItems();
this.groups = {};
- $.each( filters, function ( group, data ) {
+ filters.forEach( function ( data ) {
+ var group = data.name;
+
if ( !model.groups[ group ] ) {
model.groups[ group ] = new mw.rcfilters.dm.FilterGroup( group, {
type: data.type,
- title: data.title,
+ 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
+ }
} );
}
+ if ( data.conflicts ) {
+ 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 ) {
+ return el.filter;
+ } );
+
filterItem = new mw.rcfilters.dm.FilterItem( data.filters[ i ].name, model.groups[ group ], {
group: group,
- label: data.filters[ i ].label,
- description: data.filters[ i ].description,
- subset: data.filters[ i ].subset
+ label: mw.msg( data.filters[ i ].label ),
+ description: mw.msg( data.filters[ i ].description ),
+ 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 );
}
- // Conflicts are bi-directional, which means FilterA can define having
- // a conflict with FilterB, and this conflict should appear in **both**
- // filter definitions.
- // We need to remap all the 'conflicts' so they reflect the entire state
- // in either direction regardless of which filter defined the other as conflicting.
+ // Store conflicts
if ( data.filters[ i ].conflicts ) {
- conflictMap[ filterItem.getName() ] = conflictMap[ filterItem.getName() ] || [];
- addArrayElementsUnique(
- conflictMap[ filterItem.getName() ],
- data.filters[ i ].conflicts
- );
-
- data.filters[ i ].conflicts.forEach( function ( conflictingFilterName ) { // eslint-disable-line no-loop-func
- // Add this filter to the conflicts of each of the filters in its list
- conflictMap[ conflictingFilterName ] = conflictMap[ conflictingFilterName ] || [];
- addArrayElementsUnique(
- conflictMap[ conflictingFilterName ],
- filterItem.getName()
- );
- } );
+ filterConflictMap[ filterItem.getName() ] = data.filters[ i ].conflicts;
}
if ( data.type === 'send_unselected_if_any' ) {
}
} );
- items.forEach( function ( filterItem ) {
- // Apply conflict map to the items
- // Now that we mapped all items and conflicts bi-directionally
- // we need to apply the definition to each filter again
- filterItem.setConflicts( conflictMap[ filterItem.getName() ] );
+ // Add items to the model
+ 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 );
+ } );
+
+ items.forEach( function ( filterItem ) {
// Apply the superset map
filterItem.setSuperset( supersetMap[ filterItem.getName() ] );
+
+ // set conflicts for item
+ if ( filterConflictResult[ filterItem.getName() ] ) {
+ filterItem.setConflicts( filterConflictResult[ filterItem.getName() ] );
+ }
} );
- // 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' );
};
mw.rcfilters.dm.FiltersViewModel.prototype.setFiltersToDefaults = function () {
var defaultFilterStates = this.getFiltersFromParameters( this.getDefaultParams() );
- this.updateFilters( defaultFilterStates );
+ this.toggleFiltersSelected( defaultFilterStates );
};
/**
* @return {Object} Parameter state object
*/
mw.rcfilters.dm.FiltersViewModel.prototype.getParametersFromFilters = function ( filterGroups ) {
- var i, filterItems, anySelected, values,
- result = {},
+ var result = {},
groupItems = filterGroups || this.getFilterGroups();
$.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();
- } );
+ $.extend( result, model.getParamRepresentation() );
+ } );
- // 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() );
- }
- }
+ return result;
+ };
- if ( values.length === 0 || values.length === filterItems.length ) {
- result[ group ] = 'all';
- } else {
- result[ group ] = values.join( model.getSeparator() );
- }
- }
- } );
+ /**
+ * 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 result = { highlight: Number( this.isHighlightEnabled() ) };
+ this.getItems().forEach( function ( filterItem ) {
+ result[ filterItem.getName() + '_color' ] = filterItem.getHighlightColor();
+ } );
return result;
};
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 {boolean} Current filters are all empty
*/
mw.rcfilters.dm.FiltersViewModel.prototype.areCurrentFiltersEmpty = function () {
- var currFilters = this.getSelectedState();
-
- return Object.keys( currFilters ).every( function ( filterName ) {
- return !currFilters[ filterName ];
+ // Check if there are either any selected items or any items
+ // that have highlight enabled
+ return !this.getItems().some( function ( filterItem ) {
+ return filterItem.isSelected() || filterItem.isHighlighted();
} );
};
* @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 ];
// is the same as all filters set to false
false :
// 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;
};
* This is equivalent to display all.
*/
mw.rcfilters.dm.FiltersViewModel.prototype.emptyAllFilters = function () {
- var filters = {};
-
this.getItems().forEach( function ( filterItem ) {
- filters[ filterItem.getName() ] = false;
- } );
+ 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 );
- // Update filters
- this.updateFilters( filters );
+ if ( item ) {
+ item.toggleSelected( isSelected );
+ }
};
/**
*
* @param {Object} filterDef Filter definitions
*/
- mw.rcfilters.dm.FiltersViewModel.prototype.updateFilters = function ( filterDef ) {
- var name, filterItem;
-
- for ( name in filterDef ) {
- filterItem = this.getItemByName( name );
- filterItem.toggleSelected( filterDef[ name ] );
- }
+ mw.rcfilters.dm.FiltersViewModel.prototype.toggleFiltersSelected = function ( filterDef ) {
+ Object.keys( filterDef ).forEach( function ( name ) {
+ this.toggleFilterSelected( name, filterDef[ name ] );
+ }.bind( this ) );
};
/**
return 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();
+ } );
+ };
+
+ /**
+ * 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.getItems().forEach( function ( filterItem ) {
+ filterItem.toggleHighlight( this.highlightEnabled );
+ }.bind( this ) );
+
+ this.emit( 'highlightChange', this.highlightEnabled );
+ }
+ };
+
+ /**
+ * Check if the highlight feature is enabled
+ * @return {boolean}
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.isHighlightEnabled = function () {
+ return !!this.highlightEnabled;
+ };
+
+ /**
+ * 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();
+ };
+
+ /**
+ * Clear highlight for all filter items
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.clearAllHighlightColors = function () {
+ this.getItems().forEach( function ( filterItem ) {
+ filterItem.clearHighlightColor();
+ } );
+ };
}( mediaWiki, jQuery ) );