From 25ce7592b7ffe2dae2fc89efca52a7ff31e01129 Mon Sep 17 00:00:00 2001 From: Moriel Schottlender Date: Wed, 14 Dec 2016 18:01:20 -0800 Subject: [PATCH] Create active/inactive behavior for complementary filters Filters that are complementary or that contain one another should indicate that they are inactive (or ineffective/disabled/excluded) from the search. Bug: T149452 Bug: T149391 Change-Id: Ie58493ef940698dddb04362473664c404f392b2b --- .../dm/mw.rcfilters.dm.FilterItem.js | 47 +++ .../dm/mw.rcfilters.dm.FiltersViewModel.js | 195 ++++++++++- ...ers.ui.FilterCapsuleMultiselectWidget.less | 4 + .../mw.rcfilters.ui.FilterGroupWidget.less | 6 + .../mw.rcfilters.ui.FilterItemWidget.less | 4 + .../ui/mw.rcfilters.ui.FilterGroupWidget.js | 9 + .../ui/mw.rcfilters.ui.FilterItemWidget.js | 5 + .../ui/mw.rcfilters.ui.FilterWrapperWidget.js | 13 + .../dm.FiltersViewModel.test.js | 305 +++++++++++++++++- 9 files changed, 571 insertions(+), 17 deletions(-) diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js index 63db0ea68d..f6fef5b714 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js @@ -11,6 +11,9 @@ * @cfg {string} [label] The label for the filter * @cfg {string} [description] The description of the filter * @cfg {boolean} [selected] Filter is selected + * @cfg {boolean} [active=true] The filter is active and affecting the result + * @cfg {string[]} [excludes=[]] A list of filter names this filter, if + * selected, makes inactive. */ mw.rcfilters.dm.FilterItem = function MwRcfiltersDmFilterItem( name, config ) { config = config || {}; @@ -24,6 +27,8 @@ this.description = config.description; this.selected = !!config.selected; + this.active = config.active === undefined ? true : !!config.active; + this.excludes = config.excludes || []; }; /* Initialization */ @@ -86,6 +91,48 @@ return this.selected; }; + /** + * Check if this filter is active + * + * @return {boolean} Filter is active + */ + mw.rcfilters.dm.FilterItem.prototype.isActive = function () { + return this.active; + }; + + /** + * Check if this filter has a list of excluded filters + * + * @return {boolean} Filter has a list of excluded filters + */ + mw.rcfilters.dm.FilterItem.prototype.hasExcludedFilters = function () { + return !!this.excludes.length; + }; + + /** + * Get this filter's list of excluded filters + * + * @return {string[]} Array of excluded filter names + */ + mw.rcfilters.dm.FilterItem.prototype.getExcludedFilters = function () { + return this.excludes; + }; + + /** + * Toggle the active state of the item + * + * @param {boolean} [isActive] Filter is active + * @fires update + */ + mw.rcfilters.dm.FilterItem.prototype.toggleActive = function ( isActive ) { + isActive = isActive === undefined ? !this.active : isActive; + + if ( this.active !== isActive ) { + this.active = isActive; + this.emit( 'update' ); + } + }; + /** * Toggle the selected state of the item * diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js index 3217d0d7d4..59a34f429e 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js @@ -13,9 +13,11 @@ OO.EmitterList.call( this ); this.groups = {}; + this.excludedByMap = {}; // Events - this.aggregate( { update: 'itemUpdate' } ); + this.aggregate( { update: 'filterItemUpdate' } ); + this.connect( this, { filterItemUpdate: 'onFilterItemUpdate' } ); }; /* Initialization */ @@ -40,6 +42,96 @@ /* Methods */ + /** + * Respond to filter item change. + * + * @param {mw.rcfilters.dm.FilterItem} item Updated filter + * @fires itemUpdate + */ + mw.rcfilters.dm.FiltersViewModel.prototype.onFilterItemUpdate = function ( item ) { + // Reapply the active state of filters + this.reapplyActiveFilters( item ); + + this.emit( 'itemUpdate', item ); + }; + + /** + * Calculate the active state of the filters, based on selected filters in the group. + * + * @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 + ); + } ); + } 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() ); + } + } ); + } + }; + /** * Set filters and preserve a group relationship based on * the definition given by an object @@ -47,13 +139,20 @@ * @param {Object} filters Filter group definition */ mw.rcfilters.dm.FiltersViewModel.prototype.initializeFilters = function ( filters ) { - var i, filterItem, + var i, filterItem, excludedFilters, model = this, - items = []; + items = [], + addToMap = function ( excludedFilters ) { + excludedFilters.forEach( function ( filterName ) { + model.excludedByMap[ filterName ] = model.excludedByMap[ filterName ] || []; + model.excludedByMap[ filterName ].push( filterItem.getName() ); + } ); + }; // Reset this.clearItems(); this.groups = {}; + this.excludedByMap = {}; $.each( filters, function ( group, data ) { model.groups[ group ] = model.groups[ group ] || {}; @@ -62,15 +161,22 @@ 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'; for ( i = 0; i < data.filters.length; i++ ) { + excludedFilters = data.filters[ i ].excludes || []; + filterItem = new mw.rcfilters.dm.FilterItem( data.filters[ i ].name, { group: group, label: data.filters[ i ].label, description: data.filters[ i ].description, - selected: data.filters[ i ].selected + selected: data.filters[ i ].selected, + excludes: excludedFilters } ); + // Map filters and what excludes them + addToMap( excludedFilters ); + model.groups[ group ].filters.push( filterItem ); items.push( filterItem ); } @@ -107,11 +213,64 @@ }; /** - * Get the current state of the filters + * 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. * - * @return {Object} Filters current state + * @param {string} groupName Group name + * @return {boolean} Filter group is active */ - mw.rcfilters.dm.FiltersViewModel.prototype.getState = function () { + 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 + * + * @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 + * + * @return {Object} Filters selected state + */ + mw.rcfilters.dm.FiltersViewModel.prototype.getSelectedState = function () { var i, items = this.getItems(), result = {}; @@ -123,6 +282,26 @@ 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(), + active: items[ i ].isActive() + }; + } + + return result; + }; + /** * Analyze the groups and their filters and output an object representing * the state of the parameters they represent. @@ -221,7 +400,7 @@ model = this, base = this.getParametersFromFilters(), // Start with current state - result = this.getState(); + result = this.getSelectedState(); params = $.extend( {}, base, params ); diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.less index 4e55add938..ea1671b003 100644 --- a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.less +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.less @@ -4,6 +4,10 @@ color: #54595d; } + &-item-inactive { + opacity: 0.5; + } + .oo-ui-capsuleItemWidget { color: #222; background-color: #fff; diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterGroupWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterGroupWidget.less index 70982d446e..3723916163 100644 --- a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterGroupWidget.less +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterGroupWidget.less @@ -8,6 +8,12 @@ color: #555a5d; } + &-active { + .mw-rcfilters-ui-filterGroupWidget-title { + font-weight: bold; + } + } + &-invalid-notice { padding: 0.5em; font-style: italic; diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemWidget.less index ad0b816008..912e75cb29 100644 --- a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemWidget.less +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemWidget.less @@ -15,4 +15,8 @@ .oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline { margin-bottom: 0 !important; } + + &-inactive { + opacity: 0.5; + } } diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterGroupWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterGroupWidget.js index 92ae4d194f..27232585d4 100644 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterGroupWidget.js +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterGroupWidget.js @@ -48,4 +48,13 @@ return this.name; }; + /** + * Toggle the active state of this group + * + * @param {boolean} isActive The group is active + */ + mw.rcfilters.ui.FilterGroupWidget.prototype.toggleActiveState = function ( isActive ) { + this.$element.toggleClass( 'mw-rcfilters-ui-filterGroupWidget-active', isActive ); + }; + }( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemWidget.js index b77df3ba74..f353051209 100644 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemWidget.js +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemWidget.js @@ -78,6 +78,11 @@ */ mw.rcfilters.ui.FilterItemWidget.prototype.onModelUpdate = function () { this.checkboxWidget.setSelected( this.model.isSelected() ); + + this.$element.toggleClass( + 'mw-rcfilters-ui-filterItemWidget-inactive', + !this.model.isActive() + ); }; /** diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js index 3fcfc47c59..3353e54820 100644 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js @@ -121,10 +121,23 @@ * @param {mw.rcfilters.dm.FilterItem} item Filter item that was updated */ mw.rcfilters.ui.FilterWrapperWidget.prototype.onModelItemUpdate = function ( item ) { + var widget = this; + if ( item.isSelected() ) { this.capsule.addItemsFromData( [ item.getName() ] ); + + // Deal with active/inactive capsule filter items + this.capsule.getItemFromData( item.getName() ).$element + .toggleClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-item-inactive', !item.isActive() ); } else { this.capsule.removeItemsFromData( [ item.getName() ] ); } + + // Toggle the active state of the group + this.filterPopup.getItems().forEach( function ( groupWidget ) { + if ( groupWidget.getName() === item.getGroup() ) { + groupWidget.toggleActiveState( widget.model.isFilterGroupActive( groupWidget.getName() ) ); + } + } ); }; }( mediaWiki ) ); diff --git a/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js b/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js index b2857d9bfe..09e0f07407 100644 --- a/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js +++ b/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js @@ -67,7 +67,7 @@ ); assert.deepEqual( - model.getState(), + model.getSelectedState(), { group1filter1: false, group1filter2: false, @@ -85,7 +85,7 @@ group3filter1: true } ); assert.deepEqual( - model.getState(), + model.getSelectedState(), { group1filter1: true, group1filter2: false, @@ -576,7 +576,7 @@ // This can simulate separate filters in the same group being hidden different // ways (e.g. preferences and URL). assert.deepEqual( - model.getState(), + model.getSelectedState(), { hidefilter1: false, // The text is "show filter 1" hidefilter2: true, // The text is "show filter 2" @@ -609,7 +609,7 @@ // Simulates minor edits being hidden in preferences, then unhidden via URL // override. assert.deepEqual( - model.getState(), + model.getSelectedState(), { hidefilter1: false, // The text is "show filter 1" hidefilter2: false, // The text is "show filter 2" @@ -630,7 +630,7 @@ } ) ); assert.deepEqual( - model.getState(), + model.getSelectedState(), { hidefilter1: false, // The text is "show filter 1" hidefilter2: false, // The text is "show filter 2" @@ -651,7 +651,7 @@ } ) ); assert.deepEqual( - model.getState(), + model.getSelectedState(), { hidefilter1: false, // The text is "show filter 1" hidefilter2: false, // The text is "show filter 2" @@ -672,7 +672,7 @@ } ) ); assert.deepEqual( - model.getState(), + model.getSelectedState(), { hidefilter1: false, // The text is "show filter 1" hidefilter2: false, // The text is "show filter 2" @@ -693,7 +693,7 @@ } ) ); assert.deepEqual( - model.getState(), + model.getSelectedState(), { hidefilter1: false, // The text is "show filter 1" hidefilter2: false, // The text is "show filter 2" @@ -714,7 +714,7 @@ } ) ); assert.deepEqual( - model.getState(), + model.getSelectedState(), { hidefilter1: false, // The text is "show filter 1" hidefilter2: false, // The text is "show filter 2" @@ -776,4 +776,291 @@ 'If any value is "all", the only value is "all".' ); } ); + + QUnit.test( 'reapplyActiveFilters - "default" exclusion rules', function ( assert ) { + var definition = { + group1: { + title: 'Group 1', + type: 'send_unselected_if_any', + exclusionType: 'default', + filters: [ + { + name: 'hidefilter1', + label: 'Show filter 1', + description: 'Description of Filter 1 in Group 1' + }, + { + name: 'hidefilter2', + label: 'Show filter 2', + description: 'Description of Filter 2 in Group 1' + }, + { + name: 'hidefilter3', + label: 'Show filter 3', + description: 'Description of Filter 3 in Group 1' + } + ] + }, + group2: { + title: 'Group 2', + type: 'send_unselected_if_any', + filters: [ + { + name: 'hidefilter4', + label: 'Show filter 4', + description: 'Description of Filter 1 in Group 2' + }, + { + name: 'hidefilter5', + label: 'Show filter 5', + description: 'Description of Filter 2 in Group 2' + }, + { + name: 'hidefilter6', + label: 'Show filter 6', + description: 'Description of Filter 3 in Group 2' + } + ] + } + }, + model = new mw.rcfilters.dm.FiltersViewModel(); + + model.initializeFilters( definition ); + + assert.deepEqual( + model.getFullState(), + { + // Group 1 + hidefilter1: { selected: false, active: true }, + hidefilter2: { selected: false, active: true }, + hidefilter3: { selected: false, active: true }, + // Group 2 + hidefilter4: { selected: false, active: true }, + hidefilter5: { selected: false, active: true }, + hidefilter6: { selected: false, active: true }, + }, + 'Initial state: all filters are active.' + ); + + // Default behavior for 'exclusion' type with only 1 item selected, means that: + // - The items in the same group that are *not* selected are *not* active + // - Items in other groups are unaffected (all active) + model.updateFilters( { + hidefilter1: false, + hidefilter2: false, + hidefilter3: false, + hidefilter4: false, + hidefilter5: false, + hidefilter6: true + } ); + assert.deepEqual( + model.getFullState(), + { + // Group 1: not affected + hidefilter1: { selected: false, active: true }, + hidefilter2: { selected: false, active: true }, + hidefilter3: { selected: false, active: true }, + // Group 2: affected + hidefilter4: { selected: false, active: false }, + hidefilter5: { selected: false, active: false }, + hidefilter6: { selected: true, active: true }, + }, + 'Default exclusion behavior with 1 item selected in the group.' + ); + + // Default behavior for 'exclusion' type with multiple items selected, but not all, means that: + // - The items in the same group that are *not* selected are *not* active + // - Items in other groups are unaffected (all active) + model.updateFilters( { + // Literally updating filters to create a clean state + hidefilter1: false, + hidefilter2: false, + hidefilter3: false, + hidefilter4: false, + hidefilter5: true, + hidefilter6: true + } ); + assert.deepEqual( + model.getFullState(), + { + // Group 1: not affected + hidefilter1: { selected: false, active: true }, + hidefilter2: { selected: false, active: true }, + hidefilter3: { selected: false, active: true }, + // Group 2: affected + hidefilter4: { selected: false, active: false }, + hidefilter5: { selected: true, active: true }, + hidefilter6: { selected: true, active: true }, + }, + 'Default exclusion behavior with multiple items (but not all) selected in the group.' + ); + + // Default behavior for 'exclusion' type with all items in the group selected, means that: + // - All items in the group are NOT active + // - Items in other groups are unaffected (all active) + model.updateFilters( { + // Literally updating filters to create a clean state + hidefilter1: false, + hidefilter2: false, + hidefilter3: false, + hidefilter4: true, + hidefilter5: true, + hidefilter6: true + } ); + assert.deepEqual( + model.getFullState(), + { + // Group 1: not affected + hidefilter1: { selected: false, active: true }, + hidefilter2: { selected: false, active: true }, + hidefilter3: { selected: false, active: true }, + // Group 2: affected + hidefilter4: { selected: true, active: false }, + hidefilter5: { selected: true, active: false }, + hidefilter6: { selected: true, active: false }, + }, + 'Default exclusion behavior with all items in the group.' + ); + } ); + + QUnit.test( 'reapplyActiveFilters - "explicit" exclusion rules', function ( assert ) { + var definition = { + group1: { + title: 'Group 1', + type: 'send_unselected_if_any', + exclusionType: 'explicit', + filters: [ + { + name: 'filter1', + excludes: [ 'filter2', 'filter3' ], + label: 'Show filter 1', + description: 'Description of Filter 1 in Group 1' + }, + { + name: 'filter2', + excludes: [ 'filter3' ], + label: 'Show filter 2', + description: 'Description of Filter 2 in Group 1' + }, + { + name: 'filter3', + label: 'Show filter 3', + excludes: [ 'filter1' ], + description: 'Description of Filter 3 in Group 1' + }, + { + name: 'filter4', + label: 'Show filter 4', + description: 'Description of Filter 4 in Group 1' + } + ] + } + }, + model = new mw.rcfilters.dm.FiltersViewModel(); + + model.initializeFilters( definition ); + + assert.deepEqual( + model.getFullState(), + { + filter1: { selected: false, active: true }, + filter2: { selected: false, active: true }, + filter3: { selected: false, active: true }, + filter4: { selected: false, active: true } + }, + 'Initial state: all filters are active.' + ); + + // "Explicit" behavior for 'exclusion' with one item checked: + // - Items in the 'excluded' list of the selected filter are inactive + model.updateFilters( { + // Literally updating filters to create a clean state + filter1: true, // Excludes 'hidefilter2', 'hidefilter3' + filter2: false, // Excludes 'hidefilter3' + filter3: false, // Excludes 'hidefilter1' + filter4: false // No exclusion list + } ); + assert.deepEqual( + model.getFullState(), + { + filter1: { selected: true, active: true }, + filter2: { selected: false, active: false }, + filter3: { selected: false, active: false }, + filter4: { selected: false, active: true } + }, + '"Explicit" exclusion behavior with one item selected that has an exclusion list.' + ); + + // "Explicit" behavior for 'exclusion' with two item checked: + // - Items in the 'excluded' list of each of the selected filter are inactive + model.updateFilters( { + // Literally updating filters to create a clean state + filter1: true, // Excludes 'hidefilter2', 'hidefilter3' + filter2: false, // Excludes 'hidefilter3' + filter3: true, // Excludes 'hidefilter1' + filter4: false // No exclusion list + } ); + assert.deepEqual( + model.getFullState(), + { + filter1: { selected: true, active: false }, + filter2: { selected: false, active: false }, + filter3: { selected: true, active: false }, + filter4: { selected: false, active: true } + }, + '"Explicit" exclusion behavior with two selected items that both have an exclusion list.' + ); + + // "Explicit behavior" with two filters that exclude the same item + + // Two filters selected, both exclude 'hidefilter3' + model.updateFilters( { + // Literally updating filters to create a clean state + filter1: true, // Excludes 'hidefilter2', 'hidefilter3' + filter2: true, // Excludes 'hidefilter3' + filter3: false, // Excludes 'hidefilter1' + filter4: false // No exclusion list + } ); + assert.deepEqual( + model.getFullState(), + { + filter1: { selected: true, active: true }, + filter2: { selected: true, active: false }, // Excluded by filter1 + filter3: { selected: false, active: false }, // Excluded by both filter1 and filter2 + filter4: { selected: false, active: true } + }, + '"Explicit" exclusion behavior with two selected items that both exclude another item.' + ); + + // Unselect filter2: filter3 should still be excluded, because filter1 excludes it and is selected + model.updateFilters( { + filter2: false, // Excludes 'hidefilter3' + } ); + assert.deepEqual( + model.getFullState(), + { + filter1: { selected: true, active: true }, + filter2: { selected: false, active: false }, // Excluded by filter1 + filter3: { selected: false, active: false }, // Still excluded by filter1 + filter4: { selected: false, active: true } + }, + '"Explicit" exclusion behavior unselecting one item that excludes another item, that is being excluded by a third active item.' + ); + + // Unselect filter1: filter3 should now be active, since both filters that exclude it are unselected + model.updateFilters( { + filter1: false, // Excludes 'hidefilter3' and 'hidefilter2' + } ); + assert.deepEqual( + model.getFullState(), + { + filter1: { selected: false, active: true }, + filter2: { selected: false, active: true }, // No longer excluded by filter1 + filter3: { selected: false, active: true }, // No longer excluded by either filter1 nor filter2 + filter4: { selected: false, active: true } + }, + '"Explicit" exclusion behavior unselecting both items that excluded the same third item.' + ); + + } ); }( mediaWiki, jQuery ) ); -- 2.20.1