RC filters: update the state of the app on popstate.
authorStephane Bisson <sbisson@wikimedia.org>
Wed, 1 Mar 2017 12:04:05 +0000 (07:04 -0500)
committerStephane Bisson <sbisson@wikimedia.org>
Tue, 7 Mar 2017 20:26:24 +0000 (15:26 -0500)
Also re-fetch changes list.

Bug: T153949
Change-Id: Id3d4ea2a4de6074ae1c15cadb74e7a324a39e7ff

resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js
resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
resources/src/mediawiki.rcfilters/mw.rcfilters.init.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js
tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js

index 18f1299..0df34f8 100644 (file)
        mw.rcfilters.dm.FilterItem.prototype.isHighlightSupported = function () {
                return !!this.getCssClass();
        };
+
+       /**
+        * Check if the filter is currently highlighted
+        *
+        * @return {boolean}
+        */
+       mw.rcfilters.dm.FilterItem.prototype.isHighlighted = function () {
+               return this.isHighlightEnabled() && !!this.getHighlightColor();
+       };
 }( mediaWiki ) );
index 2afe286..5be3656 100644 (file)
        mw.rcfilters.dm.FiltersViewModel.prototype.setFiltersToDefaults = function () {
                var defaultFilterStates = this.getFiltersFromParameters( this.getDefaultParams() );
 
-               this.updateFilters( defaultFilterStates );
+               this.toggleFiltersSelected( defaultFilterStates );
        };
 
        /**
         *                  are the selected highlight colors.
         */
        mw.rcfilters.dm.FiltersViewModel.prototype.getHighlightParameters = function () {
-               var result = { highlight: this.isHighlightEnabled() };
+               var result = { highlight: Number( this.isHighlightEnabled() ) };
 
                this.getItems().forEach( function ( filterItem ) {
                        result[ filterItem.getName() + '_color' ] = filterItem.getHighlightColor();
         * @return {boolean} Current filters are all empty
         */
        mw.rcfilters.dm.FiltersViewModel.prototype.areCurrentFiltersEmpty = function () {
-               var model = this;
-
                // Check if there are either any selected items or any items
                // that have highlight enabled
                return !this.getItems().some( function ( filterItem ) {
-                       return (
-                               filterItem.isSelected() ||
-                               ( model.isHighlightEnabled() && filterItem.getHighlightColor() )
-                       );
+                       return filterItem.isSelected() || filterItem.isHighlighted();
                } );
        };
 
         * This is equivalent to display all.
         */
        mw.rcfilters.dm.FiltersViewModel.prototype.emptyAllFilters = function () {
-               var filters = {};
-
                this.getItems().forEach( function ( filterItem ) {
-                       filters[ filterItem.getName() ] = false;
-               } );
+                       this.toggleFilterSelected( filterItem.getName(), false );
+               }.bind( this ) );
+       };
 
-               // Update filters
-               this.updateFilters( filters );
+       /**
+        * Toggle selected state of one item
+        *
+        * @param {string} name Name of the filter item
+        * @param {boolean} [isSelected] Filter selected state
+        */
+       mw.rcfilters.dm.FiltersViewModel.prototype.toggleFilterSelected = function ( name, isSelected ) {
+               this.getItemByName( name ).toggleSelected( isSelected );
        };
 
        /**
         *
         * @param {Object} filterDef Filter definitions
         */
-       mw.rcfilters.dm.FiltersViewModel.prototype.updateFilters = function ( filterDef ) {
-               var name, filterItem;
-
-               for ( name in filterDef ) {
-                       filterItem = this.getItemByName( name );
-                       filterItem.toggleSelected( filterDef[ name ] );
-               }
+       mw.rcfilters.dm.FiltersViewModel.prototype.toggleFiltersSelected = function ( filterDef ) {
+               Object.keys( filterDef ).forEach( function ( name ) {
+                       this.toggleFilterSelected( name, filterDef[ name ] );
+               }.bind( this ) );
        };
 
        /**
         * @return {boolean}
         */
        mw.rcfilters.dm.FiltersViewModel.prototype.isHighlightEnabled = function () {
-               return this.highlightEnabled;
+               return !!this.highlightEnabled;
        };
 
        /**
index 1c05909..4c3d35f 100644 (file)
         * @param {Object} filterStructure Filter definition and structure for the model
         */
        mw.rcfilters.Controller.prototype.initialize = function ( filterStructure ) {
-               var uri = new mw.Uri();
-
                // Initialize the model
                this.filtersModel.initializeFilters( filterStructure );
+               this.updateStateBasedOnUrl();
+       };
+
+       /**
+        * Update filter state (selection and highlighting) based
+        * on current URL and default values.
+        */
+       mw.rcfilters.Controller.prototype.updateStateBasedOnUrl = function () {
+               var uri = new mw.Uri();
 
                // Set filter states based on defaults and URL params
-               this.filtersModel.updateFilters(
+               this.filtersModel.toggleFiltersSelected(
                        this.filtersModel.getFiltersFromParameters(
                                // Merge defaults with URL params for initialization
                                $.extend(
                this.filtersModel.toggleHighlight( !!uri.query.highlight );
                this.filtersModel.getItems().forEach( function ( filterItem ) {
                        var color = uri.query[ filterItem.getName() + '_color' ];
-                       if ( !color ) {
-                               return;
+                       if ( color ) {
+                               filterItem.setHighlightColor( color );
+                       } else {
+                               filterItem.clearHighlightColor();
                        }
-
-                       filterItem.setHighlightColor( color );
                } );
 
                // Check all filter interactions
         * @param {boolean} [isSelected] Filter selected state
         */
        mw.rcfilters.Controller.prototype.toggleFilterSelect = function ( filterName, isSelected ) {
-               var obj = {},
-                       filterItem = this.filtersModel.getItemByName( filterName );
+               var filterItem = this.filtersModel.getItemByName( filterName );
 
                isSelected = isSelected === undefined ? !filterItem.isSelected() : isSelected;
 
                if ( filterItem.isSelected() !== isSelected ) {
-                       obj[ filterName ] = isSelected;
-                       this.filtersModel.updateFilters( obj );
+                       this.filtersModel.toggleFilterSelected( filterName, isSelected );
 
                        this.updateChangesList();
 
                        // Check filter interactions
-                       this.filtersModel.reassessFilterInteractions( this.filtersModel.getItemByName( filterName ) );
+                       this.filtersModel.reassessFilterInteractions( filterItem );
                }
        };
 
         * @param {Object} [params] Extra parameters to add to the API call
         */
        mw.rcfilters.Controller.prototype.updateURL = function ( params ) {
-               var uri;
+               var updatedUri,
+                       notEquivalent = function ( obj1, obj2 ) {
+                               var keys = Object.keys( obj1 ).concat( Object.keys( obj2 ) );
+                               return keys.some( function ( key ) {
+                                       return obj1[ key ] != obj2[ key ]; // eslint-disable-line eqeqeq
+                               } );
+                       };
 
                params = params || {};
 
-               uri = this.getUpdatedUri();
-               uri.extend( params );
+               updatedUri = this.getUpdatedUri();
+               updatedUri.extend( params );
 
-               window.history.pushState( { tag: 'rcfilters' }, document.title, uri.toString() );
+               if ( notEquivalent( updatedUri.query, new mw.Uri().query ) ) {
+                       window.history.pushState( { tag: 'rcfilters' }, document.title, updatedUri.toString() );
+               }
        };
 
        /**
                this.filtersModel.clearHighlightColor( filterName );
                this.updateURL();
        };
+
+       /**
+        * Clear both highlight and selection of a filter
+        *
+        * @param {string} filterName Name of the filter item
+        */
+       mw.rcfilters.Controller.prototype.clearFilter = function ( filterName ) {
+               var filterItem = this.filtersModel.getItemByName( filterName );
+
+               if ( filterItem.isSelected() || filterItem.isHighlighted() ) {
+                       this.filtersModel.clearHighlightColor( filterName );
+                       this.filtersModel.toggleFilterSelected( filterName, false );
+                       this.updateChangesList();
+                       this.filtersModel.reassessFilterInteractions( filterItem );
+               }
+       };
+
+       /**
+        * Synchronize the URL with the current state of the filters
+        * without adding an history entry.
+        */
+       mw.rcfilters.Controller.prototype.replaceUrl = function () {
+               window.history.replaceState(
+                       { tag: 'rcfilters' },
+                       document.title,
+                       this.getUpdatedUri().toString()
+               );
+       };
 }( mediaWiki, jQuery ) );
index 255d93b..a0b785d 100644 (file)
                        $( '.rcfilters-head' ).addClass( 'mw-rcfilters-ui-ready' );
 
                        window.addEventListener( 'popstate', function () {
+                               controller.updateStateBasedOnUrl();
                                controller.updateChangesList();
                        } );
 
                                'href',
                                'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:New_filters_for_edit_review'
                        );
+
+                       controller.replaceUrl();
                }
        };
 
index 05f2f66..e7e3751 100644 (file)
                }
 
                // Respond to user removing the filter
-               this.controller.toggleFilterSelect( this.model.getName(), false );
-               this.controller.clearHighlightColor( this.model.getName() );
+               this.controller.clearFilter( this.model.getName() );
        };
 
        mw.rcfilters.ui.CapsuleItemWidget.prototype.setHighlightColor = function () {
index 3a940d0..ad0ed54 100644 (file)
@@ -79,7 +79,7 @@
                        'Initial state of filters'
                );
 
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        group1filter1: true,
                        group2filter2: true,
                        group3filter1: true
                );
 
                // Select 1 filter
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        hidefilter1: true,
                        hidefilter2: false,
                        hidefilter3: false,
                );
 
                // Select 2 filters
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        hidefilter1: true,
                        hidefilter2: true,
                        hidefilter3: false,
                );
 
                // Select 3 filters
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        hidefilter1: true,
                        hidefilter2: true,
                        hidefilter3: true,
                );
 
                // Select 1 filter from string_options
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter7: true,
                        filter8: false,
                        filter9: false
                );
 
                // Select 2 filters from string_options
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter7: true,
                        filter8: true,
                        filter9: false
                );
 
                // Select 3 filters from string_options
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter7: true,
                        filter8: true,
                        filter9: true
                // This test is demonstrating wrong usage of the method;
                // We should be aware that getFiltersFromParameters is stateless,
                // so each call gives us a filter state that only reflects the query given.
-               // This means that the two calls to updateFilters() below collide.
+               // This means that the two calls to toggleFiltersSelected() below collide.
                // The result of the first is overridden by the result of the second,
                // since both get a full state object from getFiltersFromParameters that **only** relates
                // to the input it receives.
-               model.updateFilters(
+               model.toggleFiltersSelected(
                        model.getFiltersFromParameters( {
                                hidefilter1: '1'
                        } )
                );
 
-               model.updateFilters(
+               model.toggleFiltersSelected(
                        model.getFiltersFromParameters( {
                                hidefilter6: '1'
                        } )
                );
 
-               // The result here is ignoring the first updateFilters call
+               // The result here is ignoring the first toggleFiltersSelected call
                // We should receive default values + hidefilter6 as false
                assert.deepEqual(
                        model.getSelectedState(),
                model = new mw.rcfilters.dm.FiltersViewModel();
                model.initializeFilters( definition );
 
-               model.updateFilters(
+               model.toggleFiltersSelected(
                        model.getFiltersFromParameters( {
                                hidefilter1: '0'
                        } )
                );
-               model.updateFilters(
+               model.toggleFiltersSelected(
                        model.getFiltersFromParameters( {
                                hidefilter1: '1'
                        } )
                        'After checking and then unchecking a \'send_unselected_if_any\' filter (without touching other filters in that group), results are default'
                );
 
-               model.updateFilters(
+               model.toggleFiltersSelected(
                        model.getFiltersFromParameters( {
                                group3: 'filter7'
                        } )
                        'A \'string_options\' parameter containing 1 value, results in the corresponding filter as checked'
                );
 
-               model.updateFilters(
+               model.toggleFiltersSelected(
                        model.getFiltersFromParameters( {
                                group3: 'filter7,filter8'
                        } )
                        'A \'string_options\' parameter containing 2 values, results in both corresponding filters as checked'
                );
 
-               model.updateFilters(
+               model.toggleFiltersSelected(
                        model.getFiltersFromParameters( {
                                group3: 'filter7,filter8,filter9'
                        } )
                        'A \'string_options\' parameter containing all values, results in all filters of the group as unchecked.'
                );
 
-               model.updateFilters(
+               model.toggleFiltersSelected(
                        model.getFiltersFromParameters( {
                                group3: 'filter7,all,filter9'
                        } )
                        'A \'string_options\' parameter containing the value \'all\', results in all filters of the group as unchecked.'
                );
 
-               model.updateFilters(
+               model.toggleFiltersSelected(
                        model.getFiltersFromParameters( {
                                group3: 'filter7,foo,filter9'
                        } )
                        'Initial state: default filters are not selected (controller selects defaults explicitly).'
                );
 
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        hidefilter1: false,
                        hidefilter3: false
                } );
 
                model.initializeFilters( definition );
                // Select a filter that has subset with another filter
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter1: true
                } );
 
                );
 
                // Select another filter that has a subset with the same previous filter
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter4: true
                } );
                model.reassessFilterInteractions( model.getItemByName( 'filter4' ) );
                );
 
                // Remove one filter (but leave the other) that affects filter2
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter1: false
                } );
                model.reassessFilterInteractions( model.getItemByName( 'filter1' ) );
                        'Removing a filter only un-includes its subset if there is no other filter affecting.'
                );
 
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter4: false
                } );
                model.reassessFilterInteractions( model.getItemByName( 'filter4' ) );
                );
 
                // Select most (but not all) items in each group
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter1: true,
                        filter2: true,
                        filter4: true,
                );
 
                // Select all items in 'fullCoverage' group (group2)
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter6: true
                } );
 
                );
 
                // Select all items in non 'fullCoverage' group (group1)
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter3: true
                } );
 
                );
 
                // Uncheck an item from each group
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter3: false,
                        filter5: false
                } );
                );
 
                // Select a filter that has a conflict with another
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter1: true // conflicts: filter2, filter4
                } );
 
                );
 
                // Select one of the conflicts (both filters are now conflicted and selected)
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter4: true // conflicts: filter 1
                } );
                model.reassessFilterInteractions( model.getItemByName( 'filter4' ) );
 
                // Select another filter from filter4 group, meaning:
                // now filter1 no longer conflicts with filter4
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter6: true // conflicts: filter2
                } );
                model.reassessFilterInteractions( model.getItemByName( 'filter6' ) );