Make 'groups' a data model in the FiltersViewModel

Transform the groups Object to a full data model that
handles events, and connect the FilterGroupWidget to
its model for responding to these events.

Bug: T156533
Change-Id: Iebde3138e16bac7f62e8f557e5ce08f41a9535cb
This commit is contained in:
Moriel Schottlender 2017-01-30 17:08:42 -08:00
parent 728cd57b2d
commit 1ac69cd38d
6 changed files with 215 additions and 111 deletions

View file

@ -1737,6 +1737,7 @@ return [
'scripts' => [ 'scripts' => [
'resources/src/mediawiki.rcfilters/mw.rcfilters.js', 'resources/src/mediawiki.rcfilters/mw.rcfilters.js',
'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js', 'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js',
'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js',
'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js', 'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FiltersListWidget.js', 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FiltersListWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterGroupWidget.js', 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterGroupWidget.js',

View file

@ -0,0 +1,114 @@
( function ( mw ) {
/**
* View model for a filter group
*
* @mixins OO.EventEmitter
* @mixins OO.EmitterList
*
* @constructor
* @param {Object} [config] Configuration options
* @cfg {string} [type='send_unselected_if_any'] Group type
* @cfg {string} [title] Group title
* @cfg {string} [separator='|'] Value separator for 'string_options' groups
* @cfg {string} [exclusionType='default'] Group exclusion type
* @cfg {boolean} [active] Group is active
*/
mw.rcfilters.dm.FilterGroup = function MwRcfiltersDmFilterGroup( config ) {
config = config || {};
// Mixin constructor
OO.EventEmitter.call( this );
OO.EmitterList.call( this );
this.type = config.type || 'send_unselected_if_any';
this.title = config.title;
this.separator = config.separator || '|';
this.exclusionType = config.exclusionType || 'default';
this.active = !!config.active;
};
/* Initialization */
OO.initClass( mw.rcfilters.dm.FilterGroup );
OO.mixinClass( mw.rcfilters.dm.FilterGroup, OO.EventEmitter );
OO.mixinClass( mw.rcfilters.dm.FilterGroup, OO.EmitterList );
/* Events */
/**
* @event update
*
* Group state has been updated
*/
/* Methods */
/**
* Check the active status of the group and set it accordingly.
*
* @fires update
*/
mw.rcfilters.dm.FilterGroup.prototype.checkActive = function () {
var active,
count = 0;
// Recheck group activity
this.getItems().forEach( function ( filterItem ) {
count += Number( filterItem.isSelected() );
} );
active = (
count > 0 &&
count < this.getItemCount()
);
if ( this.active !== active ) {
this.active = active;
this.emit( 'update' );
}
};
/**
* Get group active state
*
* @return {boolean} Active state
*/
mw.rcfilters.dm.FilterGroup.prototype.isActive = function () {
return this.active;
};
/**
* Get group type
*
* @return {string} Group type
*/
mw.rcfilters.dm.FilterGroup.prototype.getType = function () {
return this.type;
};
/**
* Get group's title
*
* @return {string} Title
*/
mw.rcfilters.dm.FilterGroup.prototype.getTitle = function () {
return this.title;
};
/**
* Get group's values separator
*
* @return {string} Values separator
*/
mw.rcfilters.dm.FilterGroup.prototype.getSeparator = function () {
return this.separator;
};
/**
* Get group exclusion type
*
* @return {string} Exclusion type
*/
mw.rcfilters.dm.FilterGroup.prototype.getExclusionType = function () {
return this.exclusionType;
};
}( mediaWiki ) );

View file

@ -54,6 +54,9 @@
// Reapply the active state of filters // Reapply the active state of filters
this.reapplyActiveFilters( item ); this.reapplyActiveFilters( item );
// Recheck group activity state
this.getGroup( item.getGroup() ).checkActive();
this.emit( 'itemUpdate', item ); this.emit( 'itemUpdate', item );
}; };
@ -67,8 +70,8 @@
group = item.getGroup(), group = item.getGroup(),
model = this; model = this;
if ( if (
!this.groups[ group ].exclusionType || !this.getGroup( group ).getExclusionType() ||
this.groups[ group ].exclusionType === 'default' this.getGroup( group ).getExclusionType() === 'default'
) { ) {
// Default behavior // Default behavior
// If any parameter is selected, but: // If any parameter is selected, but:
@ -76,16 +79,16 @@
// - If the entire group is selected, all are inactive // - If the entire group is selected, all are inactive
// Check what's selected in the group // Check what's selected in the group
selectedItemsCount = this.groups[ group ].filters.filter( function ( filterItem ) { selectedItemsCount = this.getGroupFilters( group ).filter( function ( filterItem ) {
return filterItem.isSelected(); return filterItem.isSelected();
} ).length; } ).length;
this.groups[ group ].filters.forEach( function ( filterItem ) { this.getGroupFilters( group ).forEach( function ( filterItem ) {
filterItem.toggleActive( filterItem.toggleActive(
selectedItemsCount > 0 ? selectedItemsCount > 0 ?
// If some items are selected // If some items are selected
( (
selectedItemsCount === model.groups[ group ].filters.length ? selectedItemsCount === model.groups[ group ].getItemCount() ?
// If **all** items are selected, they're all inactive // If **all** items are selected, they're all inactive
false : false :
// If not all are selected, then the selected are active // If not all are selected, then the selected are active
@ -96,7 +99,7 @@
true true
); );
} ); } );
} else if ( this.groups[ group ].exclusionType === 'explicit' ) { } else if ( this.getGroup( group ).getExclusionType() === 'explicit' ) {
// Explicit behavior // Explicit behavior
// - Go over the list of excluded filters to change their // - Go over the list of excluded filters to change their
// active states accordingly // active states accordingly
@ -157,13 +160,14 @@
this.excludedByMap = {}; this.excludedByMap = {};
$.each( filters, function ( group, data ) { $.each( filters, function ( group, data ) {
model.groups[ group ] = model.groups[ group ] || {}; if ( !model.groups[ group ] ) {
model.groups[ group ].filters = model.groups[ group ].filters || []; model.groups[ group ] = new mw.rcfilters.dm.FilterGroup( {
type: data.type,
model.groups[ group ].title = data.title; title: data.title,
model.groups[ group ].type = data.type; separator: data.separator,
model.groups[ group ].separator = data.separator || '|'; exclusionType: data.exclusionType
model.groups[ group ].exclusionType = data.exclusionType || 'default'; } );
}
selectedFilterNames = []; selectedFilterNames = [];
for ( i = 0; i < data.filters.length; i++ ) { for ( i = 0; i < data.filters.length; i++ ) {
@ -192,7 +196,7 @@
selectedFilterNames.push( data.filters[ i ].name ); selectedFilterNames.push( data.filters[ i ].name );
} }
model.groups[ group ].filters.push( filterItem ); model.groups[ group ].addItems( filterItem );
items.push( filterItem ); items.push( filterItem );
} }
@ -200,7 +204,7 @@
// Store the default parameter group state // Store the default parameter group state
// For this group, the parameter is group name and value is the names // For this group, the parameter is group name and value is the names
// of selected items // 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() );
} }
} ); } );
@ -219,15 +223,7 @@
}; };
/** /**
* Get the object that defines groups and their filter items. * Get the object that defines groups by their name.
* The structure of this response:
* {
* groupName: {
* title: {string} Group title
* type: {string} Group type
* filters: {string[]} Filters in the group
* }
* }
* *
* @return {Object} Filter groups * @return {Object} Filter groups
*/ */
@ -235,29 +231,6 @@
return this.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 * Update the representation of the parameters. These are the back-end
* parameters representing the filters, but they represent the given * parameters representing the filters, but they represent the given
@ -357,10 +330,10 @@
result = {}, result = {},
groupItems = filterGroups || this.getFilterGroups(); groupItems = filterGroups || this.getFilterGroups();
$.each( groupItems, function ( group, data ) { $.each( groupItems, function ( group, model ) {
filterItems = data.filters; 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. // First, check if any of the items are selected at all.
// If none is selected, we're treating it as if they are // If none is selected, we're treating it as if they are
// all false // all false
@ -373,7 +346,7 @@
result[ filterItems[ i ].getName() ] = anySelected ? result[ filterItems[ i ].getName() ] = anySelected ?
Number( !filterItems[ i ].isSelected() ) : 0; Number( !filterItems[ i ].isSelected() ) : 0;
} }
} else if ( data.type === 'string_options' ) { } else if ( model.getType() === 'string_options' ) {
values = []; values = [];
for ( i = 0; i < filterItems.length; i++ ) { for ( i = 0; i < filterItems.length; i++ ) {
if ( filterItems[ i ].isSelected() ) { if ( filterItems[ i ].isSelected() ) {
@ -384,7 +357,7 @@
if ( values.length === 0 || values.length === filterItems.length ) { if ( values.length === 0 || values.length === filterItems.length ) {
result[ group ] = 'all'; result[ group ] = 'all';
} else { } else {
result[ group ] = values.join( data.separator ); result[ group ] = values.join( model.getSeparator() );
} }
} }
} ); } );
@ -404,7 +377,7 @@
*/ */
mw.rcfilters.dm.FiltersViewModel.prototype.sanitizeStringOptionGroup = function( groupName, valueArray ) { mw.rcfilters.dm.FiltersViewModel.prototype.sanitizeStringOptionGroup = function( groupName, valueArray ) {
var result = [], var result = [],
validNames = this.groups[ groupName ].filters.map( function ( filterItem ) { validNames = this.getGroupFilters( groupName ).map( function ( filterItem ) {
return filterItem.getName(); return filterItem.getName();
} ); } );
@ -500,7 +473,7 @@
} else if ( model.groups.hasOwnProperty( paramName ) ) { } else if ( model.groups.hasOwnProperty( paramName ) ) {
// This parameter represents a group (values are the filters) // This parameter represents a group (values are the filters)
// this is equivalent to checking if the group is 'string_options' // 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() };
} }
} ); } );
@ -510,7 +483,7 @@
var paramValues, filterItem, var paramValues, filterItem,
allItemsInGroup = data.filters; 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++ ) { for ( i = 0; i < allItemsInGroup.length; i++ ) {
filterItem = allItemsInGroup[ i ]; filterItem = allItemsInGroup[ i ];
@ -523,8 +496,8 @@
// group, which means the state is false // group, which means the state is false
false; false;
} }
} else if ( model.groups[ group ].type === 'string_options' ) { } else if ( model.groups[ group ].getType() === 'string_options' ) {
paramValues = model.sanitizeStringOptionGroup( group, params[ group ].split( model.groups[ group ].separator ) ); paramValues = model.sanitizeStringOptionGroup( group, params[ group ].split( model.groups[ group ].getSeparator() ) );
for ( i = 0; i < allItemsInGroup.length; i++ ) { for ( i = 0; i < allItemsInGroup.length; i++ ) {
filterItem = allItemsInGroup[ i ]; filterItem = allItemsInGroup[ i ];
@ -533,7 +506,7 @@
// If it is the word 'all' // If it is the word 'all'
paramValues.length === 1 && paramValues[ 0 ] === 'all' || paramValues.length === 1 && paramValues[ 0 ] === 'all' ||
// All values are written // 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) // All true (either because all values are written or the term 'all' is written)
// is the same as all filters set to false // is the same as all filters set to false
@ -587,6 +560,26 @@
} }
}; };
/**
* 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 * Find items whose labels match the given string
* *

View file

@ -7,22 +7,31 @@
* @mixins OO.ui.mixin.LabelElement * @mixins OO.ui.mixin.LabelElement
* *
* @constructor * @constructor
* @param {string} name Group name * @param {mw.rcfilters.Controller} controller Controller
* @param {mw.rcfilters.dm.FilterGroup} model Filter group model
* @param {Object} config Configuration object * @param {Object} config Configuration object
*/ */
mw.rcfilters.ui.FilterGroupWidget = function MwRcfiltersUiFilterGroupWidget( name, config ) { mw.rcfilters.ui.FilterGroupWidget = function MwRcfiltersUiFilterGroupWidget( controller, model, config ) {
config = config || {}; config = config || {};
// Parent // Parent
mw.rcfilters.ui.FilterGroupWidget.parent.call( this, config ); mw.rcfilters.ui.FilterGroupWidget.parent.call( this, config );
this.controller = controller;
this.model = model;
// Mixin constructors // Mixin constructors
OO.ui.mixin.GroupWidget.call( this, config ); OO.ui.mixin.GroupWidget.call( this, config );
OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, {
label: this.model.getTitle(),
$label: $( '<div>' ) $label: $( '<div>' )
.addClass( 'mw-rcfilters-ui-filterGroupWidget-title' ) .addClass( 'mw-rcfilters-ui-filterGroupWidget-title' )
} ) ); } ) );
this.name = name; // Populate
this.populateFromModel();
this.model.connect( this, { update: 'onModelUpdate' } );
this.$element this.$element
.addClass( 'mw-rcfilters-ui-filterGroupWidget' ) .addClass( 'mw-rcfilters-ui-filterGroupWidget' )
@ -39,22 +48,39 @@
OO.mixinClass( mw.rcfilters.ui.FilterGroupWidget, OO.ui.mixin.GroupWidget ); OO.mixinClass( mw.rcfilters.ui.FilterGroupWidget, OO.ui.mixin.GroupWidget );
OO.mixinClass( mw.rcfilters.ui.FilterGroupWidget, OO.ui.mixin.LabelElement ); OO.mixinClass( mw.rcfilters.ui.FilterGroupWidget, OO.ui.mixin.LabelElement );
/**
* Respond to model update event
*/
mw.rcfilters.ui.FilterGroupWidget.prototype.onModelUpdate = function () {
this.$element.toggleClass(
'mw-rcfilters-ui-filterGroupWidget-active',
this.model.isActive()
);
};
mw.rcfilters.ui.FilterGroupWidget.prototype.populateFromModel = function () {
var widget = this;
this.addItems(
this.model.getItems().map( function ( filterItem ) {
return new mw.rcfilters.ui.FilterItemWidget(
widget.controller,
filterItem,
{
label: filterItem.getLabel(),
description: filterItem.getDescription()
}
);
} )
);
};
/** /**
* Get the group name * Get the group name
* *
* @return {string} Group name * @return {string} Group name
*/ */
mw.rcfilters.ui.FilterGroupWidget.prototype.getName = function () { mw.rcfilters.ui.FilterGroupWidget.prototype.getName = function () {
return this.name; return this.model.getName();
}; };
/**
* 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 ) ); }( mediaWiki, jQuery ) );

View file

@ -132,20 +132,11 @@
* @param {mw.rcfilters.dm.FilterItem} item Filter item that was updated * @param {mw.rcfilters.dm.FilterItem} item Filter item that was updated
*/ */
mw.rcfilters.ui.FilterWrapperWidget.prototype.onModelItemUpdate = function ( item ) { mw.rcfilters.ui.FilterWrapperWidget.prototype.onModelItemUpdate = function ( item ) {
var widget = this;
if ( item.isSelected() ) { if ( item.isSelected() ) {
this.addCapsuleItemFromName( item.getName() ); this.addCapsuleItemFromName( item.getName() );
} else { } else {
this.capsule.removeItemsFromData( [ item.getName() ] ); 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() ) );
}
} );
}; };
/** /**

View file

@ -60,40 +60,19 @@
* Respond to initialize event from the model * Respond to initialize event from the model
*/ */
mw.rcfilters.ui.FiltersListWidget.prototype.onModelInitialize = function () { mw.rcfilters.ui.FiltersListWidget.prototype.onModelInitialize = function () {
var i, group, groupWidget, var widget = this;
itemWidgets = [],
groupWidgets = [],
groups = this.model.getFilterGroups();
// Reset // Reset
this.clearItems(); this.clearItems();
for ( group in groups ) { this.addItems(
groupWidget = new mw.rcfilters.ui.FilterGroupWidget( group, { Object.keys( this.model.getFilterGroups() ).map( function ( groupName ) {
label: groups[ group ].title return new mw.rcfilters.ui.FilterGroupWidget(
} ); widget.controller,
groupWidgets.push( groupWidget ); widget.model.getGroup( groupName )
);
itemWidgets = []; } )
if ( groups[ group ].filters ) {
for ( i = 0; i < groups[ group ].filters.length; i++ ) {
itemWidgets.push(
new mw.rcfilters.ui.FilterItemWidget(
this.controller,
groups[ group ].filters[ i ],
{
label: groups[ group ].filters[ i ].getLabel(),
description: groups[ group ].filters[ i ].getDescription()
}
)
); );
}
groupWidget.addItems( itemWidgets );
}
}
this.addItems( groupWidgets );
}; };
/** /**