X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=resources%2Fsrc%2Fmediawiki.rcfilters%2Fdm%2Fmw.rcfilters.dm.FiltersViewModel.js;h=13f7d31292af2f63c60332ae0ecfc467093b2200;hb=9c84cacd49f9d1fc19622959e2a64a30c4083ee8;hp=d1b7925c028f1410ab9f1679b2bfb7728f081077;hpb=6fb3e46c6714af329f6feb0f949e203d17b27ea0;p=lhc%2Fweb%2Fwiklou.git 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 d1b7925c02..13f7d31292 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js @@ -13,13 +13,12 @@ OO.EmitterList.call( this ); this.groups = {}; - this.excludedByMap = {}; this.defaultParams = {}; this.defaultFiltersEmpty = null; // Events this.aggregate( { update: 'filterItemUpdate' } ); - this.connect( this, { filterItemUpdate: 'onFilterItemUpdate' } ); + this.connect( this, { filterItemUpdate: [ 'emit', 'itemUpdate' ] } ); }; /* Initialization */ @@ -45,96 +44,107 @@ /* Methods */ /** - * Respond to filter item change. + * Re-assess the states of filter items based on the interactions between them * - * @param {mw.rcfilters.dm.FilterItem} item Updated filter - * @fires itemUpdate + * @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.onFilterItemUpdate = function ( item ) { - // Reapply the active state of filters - this.reapplyActiveFilters( item ); + mw.rcfilters.dm.FiltersViewModel.prototype.reassessFilterInteractions = function ( item ) { + var allSelected, + model = this, + iterationItems = item !== undefined ? [ item ] : this.getItems(); - // Recheck group activity state - this.getGroup( item.getGroup() ).checkActive(); + iterationItems.forEach( function ( checkedItem ) { + var allCheckedItems = checkedItem.getSubset().concat( [ checkedItem.getName() ] ), + groupModel = checkedItem.getGroupModel(); - this.emit( 'itemUpdate', item ); - }; + // Check for subsets (included filters) plus the item itself: + allCheckedItems.forEach( function ( filterItemName ) { + var itemInSubset = model.getItemByName( filterItemName ); - /** - * 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.getGroup( group ).getExclusionType() || - this.getGroup( group ).getExclusionType() === '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.getGroupFilters( group ).filter( function ( filterItem ) { - return filterItem.isSelected(); - } ).length; - - this.getGroupFilters( group ).forEach( function ( filterItem ) { - filterItem.toggleActive( - selectedItemsCount > 0 ? - // If some items are selected - ( - selectedItemsCount === model.groups[ group ].getItemCount() ? - // 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 + 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.getGroup( group ).getExclusionType() === '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 ); + } ); }; /** @@ -144,46 +154,81 @@ * @param {Object} 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 ) { if ( !model.groups[ group ] ) { - model.groups[ group ] = new mw.rcfilters.dm.FilterGroup( { + model.groups[ group ] = new mw.rcfilters.dm.FilterGroup( group, { type: data.type, title: data.title, separator: data.separator, - exclusionType: data.exclusionType + fullCoverage: !!data.fullCoverage } ); } selectedFilterNames = []; for ( i = 0; i < data.filters.length; i++ ) { - excludedFilters = data.filters[ i ].excludes || []; - - 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 + subset: data.filters[ i ].subset } ); - // 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 @@ -208,6 +253,17 @@ } } ); + 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' ); @@ -231,26 +287,6 @@ return this.groups; }; - /** - * 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 * @@ -291,7 +327,8 @@ 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() }; } @@ -459,17 +496,17 @@ 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' @@ -583,23 +620,42 @@ /** * 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; };