RCFilters UI: Add 'remove' and 'restore defaults' to filter list
authorMoriel Schottlender <moriel@gmail.com>
Sat, 10 Dec 2016 00:18:59 +0000 (16:18 -0800)
committerRoan Kattouw <roan.kattouw@gmail.com>
Wed, 25 Jan 2017 04:18:15 +0000 (15:18 +1100)
Bug: T144448
Bug: T149391
Change-Id: I418ad6b34ae8a7456a7e66bc703d4dccf36764a5

languages/i18n/en.json
languages/i18n/qqq.json
resources/Resources.php
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js
resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js

index f2ff14b..9f03ce9 100644 (file)
        "recentchanges-legend-plusminus": "(<em>±123</em>)",
        "recentchanges-submit": "Show",
        "rcfilters-activefilters": "Active filters",
+       "rcfilters-restore-default-filters": "Restore default filters",
+       "rcfilters-clear-all-filters": "Clear all filters",
        "rcfilters-search-placeholder": "Filter recent changes (browse or start typing)",
        "rcfilters-invalid-filter": "Invalid filter",
+       "rcfilters-empty-filter": "No active filters. All contributions are shown.",
        "rcfilters-filterlist-title": "Filters",
        "rcfilters-filterlist-noresults": "No filters found",
        "rcfilters-filtergroup-registration": "User registration",
index 1e71b88..2219ef6 100644 (file)
        "recentchanges-legend-plusminus": "{{optional}}\nA plus/minus sign with a number for the legend.",
        "recentchanges-submit": "Label for submit button in [[Special:RecentChanges]]\n{{Identical|Show}}",
        "rcfilters-activefilters": "Title for the filters selection showing the active filters.",
+       "rcfilters-restore-default-filters": "Label for the button that resets filters to defaults",
+       "rcfilters-clear-all-filters": "Title for the button that clears all filters",
        "rcfilters-search-placeholder": "Placeholder for the filter search input.",
        "rcfilters-invalid-filter": "A label for an invalid filter.",
+       "rcfilters-empty-filter": "Placeholder for the filter list when no filters were chosen.",
        "rcfilters-filterlist-title": "Title for the filters list.\n{{Identical|Filter}}",
        "rcfilters-filterlist-noresults": "Message showing no results found for searching a filter.",
        "rcfilters-filtergroup-registration": "Title for the filter group for editor registration type.",
index bd7f68e..f7aa97f 100644 (file)
@@ -1756,8 +1756,11 @@ return [
                ],
                'messages' => [
                        'rcfilters-activefilters',
+                       'rcfilters-restore-default-filters',
+                       'rcfilters-clear-all-filters',
                        'rcfilters-search-placeholder',
                        'rcfilters-invalid-filter',
+                       'rcfilters-empty-filter',
                        'rcfilters-filterlist-title',
                        'rcfilters-filterlist-noresults',
                        'rcfilters-filtergroup-registration',
@@ -1800,6 +1803,7 @@ return [
                'dependencies' => [
                        'oojs-ui',
                        'mediawiki.Uri',
+                       'oojs-ui.styles.icons-moderation'
                ],
        ],
        'mediawiki.special' => [
index 2496961..3f7fa53 100644 (file)
@@ -15,6 +15,7 @@
                this.groups = {};
                this.excludedByMap = {};
                this.defaultParams = {};
+               this.defaultFiltersEmpty = null;
 
                // Events
                this.aggregate( { update: 'filterItemUpdate' } );
                return result;
        };
 
+       /**
+        * Check whether the current filter state is set to all false.
+        *
+        * @return {boolean} Current filters are all empty
+        */
+       mw.rcfilters.dm.FiltersViewModel.prototype.areCurrentFiltersEmpty = function () {
+               var currFilters = this.getSelectedState();
+
+               return Object.keys( currFilters ).every( function ( filterName ) {
+                       return !currFilters[ filterName ];
+               } );
+       };
+
+       /**
+        * Check whether the default values of the filters are all false.
+        *
+        * @return {boolean} Default filters are all false
+        */
+       mw.rcfilters.dm.FiltersViewModel.prototype.areDefaultFiltersEmpty = function () {
+               var defaultFilters;
+
+               if ( this.defaultFiltersEmpty !== null ) {
+                       // We only need to do this test once,
+                       // because defaults are set once per session
+                       defaultFilters = this.getFiltersFromParameters();
+                       this.defaultFiltersEmpty = Object.keys( defaultFilters ).every( function ( filterName ) {
+                               return !defaultFilters[ filterName ];
+                       } );
+               }
+
+               return this.defaultFiltersEmpty;
+       };
+
        /**
         * This is the opposite of the #getParametersFromFilters method; this goes over
         * the given parameters and translates into a selected/unselected value in the filters.
                } )[ 0 ];
        };
 
+       /**
+        * Set all filters to false or empty/all
+        * This is equivalent to display all.
+        */
+       mw.rcfilters.dm.FiltersViewModel.prototype.emptyAllFilters = function () {
+               var filters = {};
+
+               this.getItems().forEach( function ( filterItem ) {
+                       filters[ filterItem.getName() ] = false;
+               } );
+
+               // Update filters
+               this.updateFilters( filters );
+       };
+
        /**
         * Toggle selected state of items by their names
         *
index 025dc89..28d9f28 100644 (file)
                );
        };
 
+       /**
+        * Reset to default filters
+        */
+       mw.rcfilters.Controller.prototype.resetToDefaults = function () {
+               this.model.setFiltersToDefaults();
+       };
+
+       /**
+        * Empty all selected filters
+        */
+       mw.rcfilters.Controller.prototype.emptyFilters = function () {
+               this.model.emptyAllFilters();
+       };
+
        /**
         * Update the state of a filter
         *
index ea1671b..2ff731c 100644 (file)
@@ -8,6 +8,27 @@
                opacity: 0.5;
        }
 
+       &-emptyFilters {
+               color: #72777d;
+       }
+
+       &-table {
+               display: table;
+               width: 100%;
+       }
+
+       &-row {
+               display: table-row;
+       }
+
+       &-cell {
+               display: table-cell;
+
+               &:last-child {
+                       text-align: right;
+               }
+       }
+
        .oo-ui-capsuleItemWidget {
                color: #222;
                background-color: #fff;
index a610e8f..f138d7e 100644 (file)
@@ -17,7 +17,7 @@
        &-popup {
                // We have to override OOUI's definition, which is set
                // on the inline style of the popup
-               margin-top: 2em !important;
+               margin-top: 2.4em !important;
                max-width: 650px;
        }
 
index db21542..c498ce9 100644 (file)
@@ -5,15 +5,19 @@
         * @extends OO.ui.CapsuleMultiselectWidget
         *
         * @constructor
+        * @param {mw.rcfilters.Controller} controller RCFilters controller
+        * @param {mw.rcfilters.dm.FiltersViewModel} model RCFilters view model
         * @param {OO.ui.InputWidget} filterInput A filter input that focuses the capsule widget
         * @param {Object} config Configuration object
         */
-       mw.rcfilters.ui.FilterCapsuleMultiselectWidget = function MwRcfiltersUiFilterCapsuleMultiselectWidget( filterInput, config ) {
+       mw.rcfilters.ui.FilterCapsuleMultiselectWidget = function MwRcfiltersUiFilterCapsuleMultiselectWidget( controller, model, filterInput, config ) {
                // Parent
                mw.rcfilters.ui.FilterCapsuleMultiselectWidget.parent.call( this, $.extend( {
                        $autoCloseIgnore: filterInput.$element
                }, config ) );
 
+               this.controller = controller;
+               this.model = model;
                this.filterInput = filterInput;
 
                this.$content.prepend(
                                .text( mw.msg( 'rcfilters-activefilters' ) )
                );
 
+               this.resetButton = new OO.ui.ButtonWidget( {
+                       icon: 'trash',
+                       framed: false,
+                       title: mw.msg( 'rcfilters-clear-all-filters' ),
+                       classes: [ 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-resetButton' ]
+               } );
+
+               this.emptyFilterMessage = new OO.ui.LabelWidget( {
+                       label: mw.msg( 'rcfilters-empty-filter' ),
+                       classes: [ 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-emptyFilters' ]
+               } );
+
                // Events
+               this.resetButton.connect( this, { click: 'onResetButtonClick' } );
+               this.model.connect( this, { itemUpdate: 'onModelItemUpdate' } );
                // Add the filterInput as trigger
                this.filterInput.$input
                        .on( 'focus', this.focus.bind( this ) );
 
+               // Initialize
+               this.$content.append( this.emptyFilterMessage.$element );
+               this.$handle
+                       .append(
+                               // The content and button should appear side by side regardless of how
+                               // wide the button is; the button also changes its width depending
+                               // on language and its state, so the safest way to present both side
+                               // by side is with a table layout
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-table' )
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-row' )
+                                                       .append(
+                                                               $( '<div>' )
+                                                                       .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-content' )
+                                                                       .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-cell' )
+                                                                       .append( this.$content ),
+                                                               $( '<div>' )
+                                                                       .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-cell' )
+                                                                       .append( this.resetButton.$element )
+                                                       )
+                                       )
+                       );
+
                this.$element
                        .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget' );
+
+               this.reevaluateResetRestoreState();
        };
 
        /* Initialization */
 
        /* Methods */
 
+       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onModelItemUpdate = function () {
+               // Re-evaluate reset state
+               this.reevaluateResetRestoreState();
+       };
+
+       /**
+        * Respond to click event on the reset button
+        */
+       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onResetButtonClick = function () {
+               if ( this.model.areCurrentFiltersEmpty() ) {
+                       // Reset to default filters
+                       this.controller.resetToDefaults();
+               } else {
+                       // Reset to have no filters
+                       this.controller.emptyFilters();
+               }
+       };
+
+       /**
+        * Reevaluate the restore state for the widget between setting to defaults and clearing all filters
+        */
+       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.reevaluateResetRestoreState = function () {
+               var defaultsAreEmpty = this.model.areDefaultFiltersEmpty(),
+                       currFiltersAreEmpty = this.model.areCurrentFiltersEmpty(),
+                       hideResetButton = currFiltersAreEmpty && defaultsAreEmpty;
+
+               this.resetButton.setIcon(
+                       currFiltersAreEmpty ? 'history' : 'trash'
+               );
+
+               this.resetButton.setLabel(
+                       currFiltersAreEmpty ? mw.msg( 'rcfilters-restore-default-filters' ) : ''
+               );
+
+               this.resetButton.toggle( !hideResetButton );
+               this.emptyFilterMessage.toggle( currFiltersAreEmpty );
+       };
+
        /**
         * @inheritdoc
         */
index 44531a1..34cc240 100644 (file)
@@ -37,7 +37,7 @@
                        placeholder: mw.msg( 'rcfilters-search-placeholder' )
                } );
 
-               this.capsule = new mw.rcfilters.ui.FilterCapsuleMultiselectWidget( this.textInput, {
+               this.capsule = new mw.rcfilters.ui.FilterCapsuleMultiselectWidget( controller, this.model, this.textInput, {
                        popup: {
                                $content: this.filterPopup.$element,
                                classes: [ 'mw-rcfilters-ui-filterWrapperWidget-popup' ]