Make 'groups' a data model in the FiltersViewModel
authorMoriel Schottlender <moriel@gmail.com>
Tue, 31 Jan 2017 01:08:42 +0000 (17:08 -0800)
committerMoriel Schottlender <moriel@gmail.com>
Mon, 6 Feb 2017 02:08:02 +0000 (18:08 -0800)
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

resources/Resources.php
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterGroupWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FiltersListWidget.js

index f14787f..94d7d99 100644 (file)
@@ -1737,6 +1737,7 @@ return [
                'scripts' => [
                        '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.FilterGroup.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.FilterGroupWidget.js',
diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js
new file mode 100644 (file)
index 0000000..bc911f4
--- /dev/null
@@ -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 ) );
index 3f7fa53..d1b7925 100644 (file)
@@ -54,6 +54,9 @@
                // Reapply the active state of filters
                this.reapplyActiveFilters( item );
 
+               // Recheck group activity state
+               this.getGroup( item.getGroup() ).checkActive();
+
                this.emit( 'itemUpdate', item );
        };
 
@@ -67,8 +70,8 @@
                        group = item.getGroup(),
                        model = this;
                if (
-                       !this.groups[ group ].exclusionType ||
-                       this.groups[ group ].exclusionType === 'default'
+                       !this.getGroup( group ).getExclusionType() ||
+                       this.getGroup( group ).getExclusionType() === 'default'
                ) {
                        // Default behavior
                        // If any parameter is selected, but:
                        // - If the entire group is selected, all are inactive
 
                        // 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();
                        } ).length;
 
-                       this.groups[ group ].filters.forEach( function ( filterItem ) {
+                       this.getGroupFilters( group ).forEach( function ( filterItem ) {
                                filterItem.toggleActive(
                                        selectedItemsCount > 0 ?
                                                // 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
                                                        false :
                                                        // If not all are selected, then the selected are active
@@ -96,7 +99,7 @@
                                                true
                                );
                        } );
-               } else if ( this.groups[ group ].exclusionType === 'explicit' ) {
+               } else if ( this.getGroup( group ).getExclusionType() === 'explicit' ) {
                        // Explicit behavior
                        // - Go over the list of excluded filters to change their
                        //   active states accordingly
                this.excludedByMap = {};
 
                $.each( filters, function ( group, data ) {
-                       model.groups[ group ] = model.groups[ group ] || {};
-                       model.groups[ group ].filters = model.groups[ group ].filters || [];
-
-                       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( {
+                                       type: data.type,
+                                       title: data.title,
+                                       separator: data.separator,
+                                       exclusionType: data.exclusionType
+                               } );
+                       }
 
                        selectedFilterNames = [];
                        for ( i = 0; i < data.filters.length; i++ ) {
                                        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() );
                        }
                } );
 
        };
 
        /**
-        * 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
                        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 ) {
                                        result[ group ] = 'all';
                                } else {
-                                       result[ group ] = values.join( data.separator );
+                                       result[ group ] = values.join( model.getSeparator() );
                                }
                        }
                } );
         */
        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();
                        } );
 
                        } 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
                }
        };
 
+       /**
+        * 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
         *
index 2723258..37182d6 100644 (file)
@@ -7,22 +7,31 @@
         * @mixins OO.ui.mixin.LabelElement
         *
         * @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
         */
-       mw.rcfilters.ui.FilterGroupWidget = function MwRcfiltersUiFilterGroupWidget( name, config ) {
+       mw.rcfilters.ui.FilterGroupWidget = function MwRcfiltersUiFilterGroupWidget( controller, model, config ) {
                config = config || {};
 
                // Parent
                mw.rcfilters.ui.FilterGroupWidget.parent.call( this, config );
+
+               this.controller = controller;
+               this.model = model;
+
                // Mixin constructors
                OO.ui.mixin.GroupWidget.call( this, config );
                OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, {
+                       label: this.model.getTitle(),
                        $label: $( '<div>' )
                                .addClass( 'mw-rcfilters-ui-filterGroupWidget-title' )
                } ) );
 
-               this.name = name;
+               // Populate
+               this.populateFromModel();
+
+               this.model.connect( this, { update: 'onModelUpdate' } );
 
                this.$element
                        .addClass( 'mw-rcfilters-ui-filterGroupWidget' )
        OO.mixinClass( mw.rcfilters.ui.FilterGroupWidget, OO.ui.mixin.LabelElement );
 
        /**
-        * Get the group name
-        *
-        * @return {string} Group name
+        * Respond to model update event
         */
-       mw.rcfilters.ui.FilterGroupWidget.prototype.getName = function () {
-               return this.name;
+       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()
+                                       }
+                               );
+                       } )
+               );
        };
 
        /**
-        * Toggle the active state of this group
+        * Get the group name
         *
-        * @param {boolean} isActive The group is active
+        * @return {string} Group name
         */
-       mw.rcfilters.ui.FilterGroupWidget.prototype.toggleActiveState = function ( isActive ) {
-               this.$element.toggleClass( 'mw-rcfilters-ui-filterGroupWidget-active', isActive );
+       mw.rcfilters.ui.FilterGroupWidget.prototype.getName = function () {
+               return this.model.getName();
        };
-
 }( mediaWiki, jQuery ) );
index 34cc240..788ab3c 100644 (file)
         * @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.addCapsuleItemFromName( item.getName() );
                } 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() ) );
-                       }
-               } );
        };
 
        /**
index f5ec1fc..4ef3461 100644 (file)
         * Respond to initialize event from the model
         */
        mw.rcfilters.ui.FiltersListWidget.prototype.onModelInitialize = function () {
-               var i, group, groupWidget,
-                       itemWidgets = [],
-                       groupWidgets = [],
-                       groups = this.model.getFilterGroups();
+               var widget = this;
 
                // Reset
                this.clearItems();
 
-               for ( group in groups ) {
-                       groupWidget = new mw.rcfilters.ui.FilterGroupWidget( group, {
-                               label: groups[ group ].title
-                       } );
-                       groupWidgets.push( groupWidget );
-
-                       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 );
+               this.addItems(
+                       Object.keys( this.model.getFilterGroups() ).map( function ( groupName ) {
+                               return new mw.rcfilters.ui.FilterGroupWidget(
+                                       widget.controller,
+                                       widget.model.getGroup( groupName )
+                               );
+                       } )
+               );
        };
 
        /**