RCFilters: Have the model accept multiple views
[lhc/web/wiklou.git] / resources / src / mediawiki.rcfilters / dm / mw.rcfilters.dm.FiltersViewModel.js
index 402dbf9..ad0794e 100644 (file)
                this.defaultParams = {};
                this.defaultFiltersEmpty = null;
                this.highlightEnabled = false;
+               this.invertedNamespaces = false;
                this.parameterMap = {};
 
+               this.views = {};
+               this.currentView = null;
+
                // Events
                this.aggregate( { update: 'filterItemUpdate' } );
                this.connect( this, { filterItemUpdate: [ 'emit', 'itemUpdate' ] } );
         * Filter list is initialized
         */
 
+       /**
+        * @event update
+        *
+        * Model has been updated
+        */
+
        /**
         * @event itemUpdate
         * @param {mw.rcfilters.dm.FilterItem} item Filter item updated
         * Highlight feature has been toggled enabled or disabled
         */
 
+       /**
+        * @event invertChange
+        * @param {boolean} isInverted Namespace selected is inverted
+        *
+        * Namespace selection is inverted or straight forward
+        */
+
        /* Methods */
 
        /**
         * Set filters and preserve a group relationship based on
         * the definition given by an object
         *
-        * @param {Array} filters Filter group definition
+        * @param {Array} filters Filters definition
+        * @param {Object} [views] Extra views definition
+        *  Expected in the following format:
+        *  {
+        *     namespaces: {
+        *       label: 'namespaces', // Message key
+        *       trigger: ':',
+        *       groups: [
+        *         {
+        *            // Group info
+        *            name: 'namespaces' // Parameter name
+        *            definition: {
+        *               title: 'namespaces' // Message key
+        *               type: 'string_options',
+        *               separator: ';',
+        *               labelPrefixKey: { 'default': 'rcfilters-tag-prefix-namespace', inverted: 'rcfilters-tag-prefix-namespace-inverted' },
+        *               fullCoverage: true
+        *            },
+        *            items: []
+        *         }
+        *       ]
+        *     }
+        *  }
         */
-       mw.rcfilters.dm.FiltersViewModel.prototype.initializeFilters = function ( filters ) {
+       mw.rcfilters.dm.FiltersViewModel.prototype.initializeFilters = function ( filters, views ) {
                var filterItem, filterConflictResult, groupConflictResult,
                        model = this,
                        items = [],
                // Reset
                this.clearItems();
                this.groups = {};
+               this.views = {};
+
+               views = views || {};
 
+               // Filters
+               this.views.default = { name: 'default', label: mw.msg( 'rcfilters-filterlist-title' ) };
                filters.forEach( function ( data ) {
                        var i,
                                group = data.name;
                        if ( !model.groups[ group ] ) {
                                model.groups[ group ] = new mw.rcfilters.dm.FilterGroup( group, {
                                        type: data.type,
-                                       title: mw.msg( data.title ),
+                                       title: data.title ? mw.msg( data.title ) : group,
                                        separator: data.separator,
                                        fullCoverage: !!data.fullCoverage,
                                        whatsThis: {
                                        }
                                } );
                        }
+
+                       // Filters are given to us with msg-keys, we need
+                       // to translate those before we hand them off
+                       for ( i = 0; i < data.filters.length; i++ ) {
+                               data.filters[ i ].label = data.filters[ i ].label ? mw.msg( data.filters[ i ].label ) : data.filters[ i ].name;
+                               data.filters[ i ].description = data.filters[ i ].description ? mw.msg( data.filters[ i ].description ) : '';
+                       }
+
                        model.groups[ group ].initializeFilters( data.filters, data.default );
                        items = items.concat( model.groups[ group ].getItems() );
 
                        }
                } );
 
+               if ( mw.config.get( 'wgStructuredChangeFiltersEnableExperimentalViews' ) ) {
+                       $.each( views, function ( viewName, viewData ) {
+                               model.views[ viewName ] = {
+                                       name: viewData.name,
+                                       title: viewData.title,
+                                       trigger: viewData.trigger
+                               };
+
+                               // Group
+                               viewData.groups.forEach( function ( groupData ) {
+                                       model.groups[ groupData.name ] = new mw.rcfilters.dm.FilterGroup(
+                                               groupData.name,
+                                               $.extend( true, {}, groupData.definition, { view: viewName } )
+                                       );
+
+                                       // Add items
+                                       model.groups[ groupData.name ].initializeFilters( groupData.items );
+
+                                       // Add to global search list
+                                       items = items.concat( model.groups[ groupData.name ].getItems() );
+                               } );
+                       } );
+               }
+
                // Add item references to the model, for lookup
                this.addItems( items );
 
                        }
                } );
 
+               this.currentView = 'default';
+
                // Finish initialization
                this.emit( 'initialize' );
        };
                return this.groups;
        };
 
+       /**
+        * Get the object that defines groups that match a certain view by their name.
+        *
+        * @param {string} [view] Requested view. If not given, uses current view
+        * @return {Object} Filter groups matching a display group
+        */
+       mw.rcfilters.dm.FiltersViewModel.prototype.getFilterGroupsByView = function ( view ) {
+               var result = {};
+
+               view = view || this.getCurrentView();
+
+               $.each( this.groups, function ( groupName, groupModel ) {
+                       if ( groupModel.getView() === view ) {
+                               result[ groupName ] = groupModel;
+                       }
+               } );
+
+               return result;
+       };
+
+       /**
+        * Get an array of filters matching the given display group.
+        *
+        * @param {string} [view] Requested view. If not given, uses current view
+        * @return {mw.rcfilters.dm.FilterItem} Filter items matching the group
+        */
+       mw.rcfilters.dm.FiltersViewModel.prototype.getFiltersByView = function ( view ) {
+               var groups,
+                       result = [];
+
+               view = view || this.getCurrentView();
+
+               groups = this.getFilterGroupsByView( view );
+
+               $.each( groups, function ( groupName, groupModel ) {
+                       result = result.concat( groupModel.getItems() );
+               } );
+
+               return result;
+       };
+
+       /**
+        * Get the trigger for the requested view.
+        *
+        * @param {string} view View name
+        * @return {string} View trigger, if exists
+        */
+       mw.rcfilters.dm.FiltersViewModel.prototype.getViewTrigger = function ( view ) {
+               return this.views[ view ] && this.views[ view ].trigger;
+       };
        /**
         * Get the value of a specific parameter
         *
 
                // Get default filter state
                $.each( this.groups, function ( name, model ) {
-                       result = $.extend( true, {}, result, model.getDefaultParams() );
+                       $.extend( true, result, model.getDefaultParams() );
                } );
 
-               // Get default highlight state
-               result = $.extend( true, {}, result, this.getHighlightParameters() );
-
                return result;
        };
 
        /**
         * Get the highlight parameters based on current filter configuration
         *
-        * @return {object} Object where keys are "<filter name>_color" and values
+        * @return {Object} Object where keys are "<filter name>_color" and values
         *                  are the selected highlight colors.
         */
        mw.rcfilters.dm.FiltersViewModel.prototype.getHighlightParameters = function () {
                return result;
        };
 
+       /**
+        * Extract the highlight values from given object. Since highlights are
+        * the same for filter and parameters, it doesn't matter which one is
+        * given; values will be returned with a full list of the highlights
+        * with colors or null values.
+        *
+        * @param {Object} representation Object containing representation of
+        *  some or all highlight values
+        * @return {Object} Object where keys are "<filter name>_color" and values
+        *                  are the selected highlight colors. The returned object
+        *                  contains all available filters either with a color value
+        *                  or with null.
+        */
+       mw.rcfilters.dm.FiltersViewModel.prototype.extractHighlightValues = function ( representation ) {
+               var result = {};
+
+               this.getItems().forEach( function ( filterItem ) {
+                       var highlightName = filterItem.getName() + '_color';
+                       result[ highlightName ] = representation[ highlightName ] || null;
+               } );
+
+               return result;
+       };
+
        /**
         * Sanitize value group of a string_option groups type
         * Remove duplicates and make sure to only use valid
         *  arranged by their group names
         */
        mw.rcfilters.dm.FiltersViewModel.prototype.findMatches = function ( query, returnFlat ) {
-               var i,
+               var i, searchIsEmpty,
                        groupTitle,
                        result = {},
                        flatResult = [],
-                       items = this.getItems();
+                       view = this.getViewByTrigger( query.substr( 0, 1 ) ),
+                       items = this.getFiltersByView( view );
 
-               // Normalize so we can search strings regardless of case
+               // Normalize so we can search strings regardless of case and view
                query = query.toLowerCase();
+               if ( view !== 'default' ) {
+                       query = query.substr( 1 );
+               }
+
+               // Check if the search if actually empty; this can be a problem when
+               // we use prefixes to denote different views
+               searchIsEmpty = query.length === 0;
 
                // item label starting with the query string
                for ( i = 0; i < items.length; i++ ) {
-                       if ( items[ i ].getLabel().toLowerCase().indexOf( query ) === 0 ) {
+                       if (
+                               searchIsEmpty ||
+                               items[ i ].getLabel().toLowerCase().indexOf( query ) === 0 ||
+                               (
+                                       // For tags, we want the parameter name to be included in the search
+                                       view === 'tags' &&
+                                       items[ i ].getParamName().toLowerCase().indexOf( query ) > -1
+                               )
+                       ) {
                                result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
                                result[ items[ i ].getGroupName() ].push( items[ i ] );
                                flatResult.push( items[ i ] );
                        for ( i = 0; i < items.length; i++ ) {
                                groupTitle = items[ i ].getGroupModel().getTitle();
                                if (
+                                       searchIsEmpty ||
                                        items[ i ].getLabel().toLowerCase().indexOf( query ) > -1 ||
                                        items[ i ].getDescription().toLowerCase().indexOf( query ) > -1 ||
-                                       groupTitle.toLowerCase().indexOf( query ) > -1
+                                       groupTitle.toLowerCase().indexOf( query ) > -1 ||
+                                       (
+                                               // For tags, we want the parameter name to be included in the search
+                                               view === 'tags' &&
+                                               items[ i ].getParamName().toLowerCase().indexOf( query ) > -1
+                                       )
                                ) {
                                        result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
                                        result[ items[ i ].getGroupName() ].push( items[ i ] );
                } );
        };
 
+       /**
+        * Switch the current view
+        *
+        * @param {string} view View name
+        * @fires update
+        */
+       mw.rcfilters.dm.FiltersViewModel.prototype.switchView = function ( view ) {
+               if ( this.views[ view ] && this.currentView !== view ) {
+                       this.currentView = view;
+                       this.emit( 'update' );
+               }
+       };
+
+       /**
+        * Get the current view
+        *
+        * @return {string} Current view
+        */
+       mw.rcfilters.dm.FiltersViewModel.prototype.getCurrentView = function () {
+               return this.currentView;
+       };
+
+       /**
+        * Get the label for the current view
+        *
+        * @return {string} Label for the current view
+        */
+       mw.rcfilters.dm.FiltersViewModel.prototype.getCurrentViewLabel = function () {
+               return this.views[ this.getCurrentView() ].title;
+       };
+
+       /**
+        * Get an array of all available view names
+        *
+        * @return {string} Available view names
+        */
+       mw.rcfilters.dm.FiltersViewModel.prototype.getAvailableViews = function () {
+               return Object.keys( this.views );
+       };
+
+       /**
+        * Get the view that fits the given trigger
+        *
+        * @param {string} trigger Trigger
+        * @return {string} Name of view
+        */
+       mw.rcfilters.dm.FiltersViewModel.prototype.getViewByTrigger = function ( trigger ) {
+               var result = 'default';
+
+               $.each( this.views, function ( name, data ) {
+                       if ( data.trigger === trigger ) {
+                               result = name;
+                       }
+               } );
+
+               return result;
+       };
+
        /**
         * Toggle the highlight feature on and off.
         * Propagate the change to filter items.
                return !!this.highlightEnabled;
        };
 
+       /**
+        * Toggle the inverted namespaces property on and off.
+        * Propagate the change to namespace filter items.
+        *
+        * @param {boolean} enable Inverted property is enabled
+        * @fires invertChange
+        */
+       mw.rcfilters.dm.FiltersViewModel.prototype.toggleInvertedNamespaces = function ( enable ) {
+               enable = enable === undefined ? !this.invertedNamespaces : enable;
+
+               if ( this.invertedNamespaces !== enable ) {
+                       this.invertedNamespaces = enable;
+
+                       this.getFiltersByView( 'namespaces' ).forEach( function ( filterItem ) {
+                               filterItem.toggleInverted( this.invertedNamespaces );
+                       }.bind( this ) );
+
+                       this.emit( 'invertChange', this.invertedNamespaces );
+               }
+       };
+
+       /**
+        * Check if the namespaces selection is set to be inverted
+        * @return {boolean}
+        */
+       mw.rcfilters.dm.FiltersViewModel.prototype.areNamespacesInverted = function () {
+               return !!this.invertedNamespaces;
+       };
+
        /**
         * Set highlight color for a specific filter item
         *