OO.EmitterList.call( this );
this.groups = {};
- this.excludedByMap = {};
this.defaultParams = {};
this.defaultFiltersEmpty = null;
+ this.highlightEnabled = false;
// Events
this.aggregate( { update: 'filterItemUpdate' } );
- this.connect( this, { filterItemUpdate: 'onFilterItemUpdate' } );
+ this.connect( this, { filterItemUpdate: [ 'emit', 'itemUpdate' ] } );
};
/* Initialization */
* Filter item has changed
*/
- /* Methods */
-
/**
- * Respond to filter item change.
+ * @event highlightChange
+ * @param {boolean} Highlight feature is enabled
*
- * @param {mw.rcfilters.dm.FilterItem} item Updated filter
- * @fires itemUpdate
+ * Highlight feature has been toggled enabled or disabled
*/
- mw.rcfilters.dm.FiltersViewModel.prototype.onFilterItemUpdate = function ( item ) {
- // Reapply the active state of filters
- this.reapplyActiveFilters( item );
- this.emit( 'itemUpdate', item );
- };
+ /* Methods */
/**
- * Calculate the active state of the filters, based on selected filters in the group.
+ * Re-assess the states of filter items based on the interactions between them
*
- * @param {mw.rcfilters.dm.FilterItem} item Changed item
- */
- mw.rcfilters.dm.FiltersViewModel.prototype.reapplyActiveFilters = function ( item ) {
- var selectedItemsCount,
- group = item.getGroup(),
- model = this;
- if (
- !this.groups[ group ].exclusionType ||
- this.groups[ group ].exclusionType === 'default'
- ) {
- // Default behavior
- // If any parameter is selected, but:
- // - If there are unselected items in the group, they are inactive
- // - If the entire group is selected, all are inactive
-
- // Check what's selected in the group
- selectedItemsCount = this.groups[ group ].filters.filter( function ( filterItem ) {
- return filterItem.isSelected();
- } ).length;
-
- this.groups[ group ].filters.forEach( function ( filterItem ) {
- filterItem.toggleActive(
- selectedItemsCount > 0 ?
- // If some items are selected
- (
- selectedItemsCount === model.groups[ group ].filters.length ?
- // If **all** items are selected, they're all inactive
- false :
- // If not all are selected, then the selected are active
- // and the unselected are inactive
- filterItem.isSelected()
- ) :
- // No item is selected, everything is active
- true
+ * @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() );
+ } )
);
} );
- } else if ( this.groups[ group ].exclusionType === 'explicit' ) {
- // Explicit behavior
- // - Go over the list of excluded filters to change their
- // active states accordingly
-
- // For each item in the list, see if there are other selected
- // filters that also exclude it. If it does, it will still be
- // inactive.
-
- item.getExcludedFilters().forEach( function ( filterName ) {
- var filterItem = model.getItemByName( filterName );
-
- // Note to reduce confusion:
- // - item is the filter whose state changed and should exclude the other filters
- // in its list of exclusions
- // - filterItem is the filter that is potentially being excluded by the current item
- // - anotherExcludingFilter is any other filter that excludes filterItem; we must check
- // if that filter is selected, because if it is, we should not touch the excluded item
- if (
- // Check if there are any filters (other than the current one)
- // that also exclude the filterName
- !model.excludedByMap[ filterName ].some( function ( anotherExcludingFilterName ) {
- var anotherExcludingFilter = model.getItemByName( anotherExcludingFilterName );
-
- return (
- anotherExcludingFilterName !== item.getName() &&
- anotherExcludingFilter.isSelected()
- );
- } )
- ) {
- // Only change the state for filters that aren't
- // also affected by other excluding selected filters
- filterItem.toggleActive( !item.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.getSelectedItems().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 );
+ } );
};
/**
* 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, excludedFilters,
+ var i, filterItem, selectedFilterNames,
model = this,
items = [],
- addToMap = function ( excludedFilters ) {
- excludedFilters.forEach( function ( filterName ) {
- model.excludedByMap[ filterName ] = model.excludedByMap[ filterName ] || [];
- model.excludedByMap[ filterName ].push( filterItem.getName() );
+ addArrayElementsUnique = function ( arr, elements ) {
+ elements = Array.isArray( elements ) ? elements : [ elements ];
+
+ elements.forEach( function ( element ) {
+ if ( arr.indexOf( element ) === -1 ) {
+ arr.push( element );
+ }
} );
- };
+
+ return arr;
+ },
+ conflictMap = {},
+ supersetMap = {};
// Reset
this.clearItems();
this.groups = {};
- this.excludedByMap = {};
- $.each( filters, function ( group, data ) {
- model.groups[ group ] = model.groups[ group ] || {};
- model.groups[ group ].filters = model.groups[ group ].filters || [];
+ filters.forEach( function ( data ) {
+ var group = data.name;
- model.groups[ group ].title = data.title;
- model.groups[ group ].type = data.type;
- model.groups[ group ].separator = data.separator || '|';
- model.groups[ group ].exclusionType = data.exclusionType || 'default';
+ if ( !model.groups[ group ] ) {
+ model.groups[ group ] = new mw.rcfilters.dm.FilterGroup( group, {
+ type: data.type,
+ title: mw.msg( data.title ),
+ separator: data.separator,
+ fullCoverage: !!data.fullCoverage
+ } );
+ }
selectedFilterNames = [];
for ( i = 0; i < data.filters.length; i++ ) {
- excludedFilters = data.filters[ i ].excludes || [];
+ 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, {
+ 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,
- selected: data.filters[ i ].selected,
- excludes: excludedFilters,
- 'default': data.filters[ i ].default
+ label: mw.msg( data.filters[ i ].label ),
+ description: mw.msg( data.filters[ i ].description ),
+ subset: data.filters[ i ].subset,
+ cssClass: data.filters[ i ].cssClass
} );
- // Map filters and what excludes them
- addToMap( excludedFilters );
+ // 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 ) {
+ data.filters[ i ].subset.forEach( function ( subsetFilterName ) { // eslint-disable-line no-loop-func
+ supersetMap[ subsetFilterName ] = supersetMap[ subsetFilterName ] || [];
+ addArrayElementsUnique(
+ supersetMap[ subsetFilterName ],
+ filterItem.getName()
+ );
+ } );
+ }
+
+ // 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.
+ 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()
+ );
+ } );
+ }
if ( data.type === 'send_unselected_if_any' ) {
// Store the default parameter state
selectedFilterNames.push( data.filters[ i ].name );
}
- model.groups[ group ].filters.push( filterItem );
+ model.groups[ group ].addItems( filterItem );
items.push( 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 ].separator );
+ model.defaultParams[ group ] = model.sanitizeStringOptionGroup( group, selectedFilterNames ).join( model.groups[ group ].getSeparator() );
}
} );
+ 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() ] );
+
+ // Apply the superset map
+ filterItem.setSuperset( supersetMap[ filterItem.getName() ] );
+ } );
+
+ // Add items to the model
this.addItems( items );
this.emit( 'initialize' );
};
/**
- * Get the object that defines groups and their filter items.
- * The structure of this response:
- * {
- * groupName: {
- * title: {string} Group title
- * type: {string} Group type
- * filters: {string[]} Filters in the group
- * }
- * }
+ * Get the object that defines groups by their name.
*
* @return {Object} Filter groups
*/
return this.groups;
};
- /**
- * Get the current state of the filters.
- *
- * Checks whether the filter group is active. This means at least one
- * filter is selected, but not all filters are selected.
- *
- * @param {string} groupName Group name
- * @return {boolean} Filter group is active
- */
- mw.rcfilters.dm.FiltersViewModel.prototype.isFilterGroupActive = function ( groupName ) {
- var count = 0,
- filters = this.groups[ groupName ].filters;
-
- filters.forEach( function ( filterItem ) {
- count += Number( filterItem.isSelected() );
- } );
-
- return (
- count > 0 &&
- count < filters.length
- );
- };
-
- /**
- * Update the representation of the parameters. These are the back-end
- * parameters representing the filters, but they represent the given
- * current state regardless of validity.
- *
- * This should only run after filters are already set.
- *
- * @param {Object} params Parameter state
- */
- mw.rcfilters.dm.FiltersViewModel.prototype.updateParameters = function ( params ) {
- var model = this;
-
- $.each( params, function ( name, value ) {
- // Only store the parameters that exist in the system
- if ( model.getItemByName( name ) ) {
- model.parameters[ name ] = value;
- }
- } );
- };
-
/**
* Get the value of a specific parameter
*
for ( i = 0; i < items.length; i++ ) {
result[ items[ i ].getName() ] = {
selected: items[ i ].isSelected(),
- active: items[ i ].isActive()
+ conflicted: items[ i ].isConflicted(),
+ included: items[ i ].isIncluded()
};
}
mw.rcfilters.dm.FiltersViewModel.prototype.setFiltersToDefaults = function () {
var defaultFilterStates = this.getFiltersFromParameters( this.getDefaultParams() );
- this.updateFilters( defaultFilterStates );
+ this.toggleFiltersSelected( defaultFilterStates );
};
/**
result = {},
groupItems = filterGroups || this.getFilterGroups();
- $.each( groupItems, function ( group, data ) {
- filterItems = data.filters;
+ $.each( groupItems, function ( group, model ) {
+ filterItems = model.getItems();
- if ( data.type === 'send_unselected_if_any' ) {
+ 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
result[ filterItems[ i ].getName() ] = anySelected ?
Number( !filterItems[ i ].isSelected() ) : 0;
}
- } else if ( data.type === 'string_options' ) {
+ } else if ( model.getType() === 'string_options' ) {
values = [];
for ( i = 0; i < filterItems.length; i++ ) {
if ( filterItems[ i ].isSelected() ) {
}
}
- if ( values.length === 0 || values.length === filterItems.length ) {
+ if ( values.length === filterItems.length ) {
result[ group ] = 'all';
} else {
- result[ group ] = values.join( data.separator );
+ result[ group ] = values.join( model.getSeparator() );
}
}
} );
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 result = { highlight: Number( this.isHighlightEnabled() ) };
+
+ this.getItems().forEach( function ( filterItem ) {
+ result[ filterItem.getName() + '_color' ] = filterItem.getHighlightColor();
+ } );
+ return result;
+ };
+
/**
* Sanitize value group of a string_option groups type
* Remove duplicates and make sure to only use valid
*/
mw.rcfilters.dm.FiltersViewModel.prototype.sanitizeStringOptionGroup = function( groupName, valueArray ) {
var result = [],
- validNames = this.groups[ groupName ].filters.map( function ( filterItem ) {
+ validNames = this.getGroupFilters( groupName ).map( function ( filterItem ) {
return filterItem.getName();
} );
* @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();
} );
};
filterItem = model.getItemByName( paramName );
// Ignore if no filter item exists
if ( filterItem ) {
- groupMap[ filterItem.getGroup() ] = groupMap[ filterItem.getGroup() ] || {};
+ groupMap[ filterItem.getGroupName() ] = groupMap[ filterItem.getGroupName() ] || {};
// Mark the group if it has any items that are selected
- groupMap[ filterItem.getGroup() ].hasSelected = (
- groupMap[ filterItem.getGroup() ].hasSelected ||
+ groupMap[ filterItem.getGroupName() ].hasSelected = (
+ groupMap[ filterItem.getGroupName() ].hasSelected ||
!!Number( paramValue )
);
// Add the relevant filter into the group map
- groupMap[ filterItem.getGroup() ].filters = groupMap[ filterItem.getGroup() ].filters || [];
- groupMap[ filterItem.getGroup() ].filters.push( filterItem );
+ groupMap[ filterItem.getGroupName() ].filters = groupMap[ filterItem.getGroupName() ].filters || [];
+ groupMap[ filterItem.getGroupName() ].filters.push( filterItem );
} else if ( model.groups.hasOwnProperty( paramName ) ) {
// 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 ].filters };
+ groupMap[ paramName ] = { filters: model.groups[ paramName ].getItems() };
}
} );
var paramValues, filterItem,
allItemsInGroup = data.filters;
- if ( model.groups[ group ].type === 'send_unselected_if_any' ) {
+ if ( model.groups[ group ].getType() === 'send_unselected_if_any' ) {
for ( i = 0; i < allItemsInGroup.length; i++ ) {
filterItem = allItemsInGroup[ i ];
// group, which means the state is false
false;
}
- } else if ( model.groups[ group ].type === 'string_options' ) {
- paramValues = model.sanitizeStringOptionGroup( group, params[ group ].split( model.groups[ group ].separator ) );
+ } else if ( model.groups[ group ].getType() === 'string_options' ) {
+ paramValues = model.sanitizeStringOptionGroup( group, params[ group ].split( model.groups[ group ].getSeparator() ) );
for ( i = 0; i < allItemsInGroup.length; i++ ) {
filterItem = allItemsInGroup[ i ];
// If it is the word 'all'
paramValues.length === 1 && paramValues[ 0 ] === 'all' ||
// All values are written
- paramValues.length === model.groups[ group ].filters.length
+ 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
* 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 ) );
+ };
- // Update filters
- this.updateFilters( filters );
+ /**
+ * 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 ) {
+ this.getItemByName( name ).toggleSelected( isSelected );
};
/**
*
* @param {Object} filterDef Filter definitions
*/
- mw.rcfilters.dm.FiltersViewModel.prototype.updateFilters = function ( filterDef ) {
- var name, filterItem;
+ mw.rcfilters.dm.FiltersViewModel.prototype.toggleFiltersSelected = function ( filterDef ) {
+ Object.keys( filterDef ).forEach( function ( name ) {
+ this.toggleFilterSelected( name, filterDef[ name ] );
+ }.bind( this ) );
+ };
- for ( name in filterDef ) {
- filterItem = this.getItemByName( name );
- filterItem.toggleSelected( filterDef[ name ] );
- }
+ /**
+ * 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} str Search string
+ * @param {string} query Search string
* @return {Object} An object of items to show
* arranged by their group names
*/
- mw.rcfilters.dm.FiltersViewModel.prototype.findMatches = function ( str ) {
+ mw.rcfilters.dm.FiltersViewModel.prototype.findMatches = function ( query ) {
var i,
+ groupTitle,
result = {},
items = this.getItems();
// Normalize so we can search strings regardless of case
- str = str.toLowerCase();
+ query = query.toLowerCase();
+
+ // item label starting with the query string
for ( i = 0; i < items.length; i++ ) {
- if ( items[ i ].getLabel().toLowerCase().indexOf( str ) > -1 ) {
- result[ items[ i ].getGroup() ] = result[ items[ i ].getGroup() ] || [];
- result[ items[ i ].getGroup() ].push( items[ i ] );
+ if ( items[ i ].getLabel().toLowerCase().indexOf( query ) === 0 ) {
+ result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
+ result[ items[ i ].getGroupName() ].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 (
+ items[ i ].getLabel().toLowerCase().indexOf( query ) > -1 ||
+ items[ i ].getDescription().toLowerCase().indexOf( query ) > -1 ||
+ groupTitle.toLowerCase().indexOf( query ) > -1
+ ) {
+ result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
+ result[ items[ i ].getGroupName() ].push( items[ i ] );
+ }
}
}
+
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 ) );