RCFilters: Adjust to use MenuTagMultiselectWidget
authorMoriel Schottlender <moriel@gmail.com>
Mon, 27 Mar 2017 16:58:29 +0000 (09:58 -0700)
committerMoriel Schottlender <moriel@gmail.com>
Thu, 13 Apr 2017 19:53:01 +0000 (12:53 -0700)
The new widget in OOUI is more stable and easier to manage,
and gives us a few features that we were missing, like
arrow behavior in the menu.

Depends on OOUI release 0.21.0

Bug: T162829
Bug: T159768
Bug: T162709
Bug: T162917
Change-Id: I42be0691304b1e93b4e9c02eba2e3a724a5ffd67
Depends-On: Ic216769f48e4677da5b7274f491aa08a95aa8076

28 files changed:
resources/Resources.php
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.CapsuleItemWidget.less [deleted file]
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.less [deleted file]
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.less [new file with mode: 0644]
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterGroupWidget.less [deleted file]
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemWidget.less [deleted file]
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuHeaderWidget.less [new file with mode: 0644]
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuOptionWidget.less [new file with mode: 0644]
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuSectionOptionWidget.less [new file with mode: 0644]
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagItemWidget.less [new file with mode: 0644]
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagMultiselectWidget.less [new file with mode: 0644]
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FiltersListWidget.less [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CheckboxInputWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterGroupWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemHighlightButton.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuHeaderWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuOptionWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuSectionOptionWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagItemWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FiltersListWidget.js [deleted file]

index 43582f0..64ef3bc 100644 (file)
@@ -1751,11 +1751,12 @@ return [
        'mediawiki.rcfilters.filters.ui' => [
                'scripts' => [
                        'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CheckboxInputWidget.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.FilterItemWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.js',
+                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js',
+                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuOptionWidget.js',
+                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuSectionOptionWidget.js',
+                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagItemWidget.js',
+                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuHeaderWidget.js',
+                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.js',
                        'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js',
                        'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesListWrapperWidget.js',
                        'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js',
@@ -1769,12 +1770,13 @@ return [
                        'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.variables.less',
                        'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.less',
                        'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.CapsuleItemWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterGroupWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FiltersListWidget.less',
+                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagMultiselectWidget.less',
+                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuOptionWidget.less',
+                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuSectionOptionWidget.less',
+                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagItemWidget.less',
+                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuHeaderWidget.less',
+                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.less',
                        'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.less',
                        'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ChangesListWrapperWidget.less',
                        'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.HighlightColorPickerWidget.less',
                        'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemHighlightButton.less',
index 8fd1553..69210be 100644 (file)
         * Find items whose labels match the given string
         *
         * @param {string} query Search string
+        * @param {boolean} [returnFlat] Return a flat array. If false, the result
+        *  is an object whose keys are the group names and values are an array of
+        *  filters per group. If set to true, returns an array of filters regardless
+        *  of their groups.
         * @return {Object} An object of items to show
         *  arranged by their group names
         */
-       mw.rcfilters.dm.FiltersViewModel.prototype.findMatches = function ( query ) {
+       mw.rcfilters.dm.FiltersViewModel.prototype.findMatches = function ( query, returnFlat ) {
                var i,
                        groupTitle,
                        result = {},
+                       flatResult = [],
                        items = this.getItems();
 
                // Normalize so we can search strings regardless of case
                        if ( items[ i ].getLabel().toLowerCase().indexOf( query ) === 0 ) {
                                result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
                                result[ items[ i ].getGroupName() ].push( items[ i ] );
+                               flatResult.push( items[ i ] );
                        }
                }
 
                                ) {
                                        result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
                                        result[ items[ i ].getGroupName() ].push( items[ i ] );
+                                       flatResult.push( items[ i ] );
                                }
                        }
                }
 
-               return result;
+               return returnFlat ? flatResult : result;
        };
 
        /**
diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.CapsuleItemWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.CapsuleItemWidget.less
deleted file mode 100644 (file)
index b16e84c..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-@import 'mw.rcfilters.mixins';
-
-.mw-rcfilters-ui-capsuleItemWidget {
-       background-color: #fff;
-       border-color: #979797;
-       color: #222;
-
-       // Background and color of the capsule widget need a bit
-       // more specificity to override ooui internals
-       &-muted.oo-ui-capsuleItemWidget.oo-ui-widget-enabled {
-               // Muted state
-               background-color: #eaecf0;
-               border-color: #c8ccd1;
-
-               .oo-ui-labelElement-label {
-                       color: #72777d;
-               }
-               .oo-ui-buttonWidget {
-                       opacity: @muted-opacity;
-               }
-       }
-
-       &-conflicted.oo-ui-capsuleItemWidget.oo-ui-widget-enabled {
-               background-color: #fee7e6; // Red90 AAA
-               border-color: #b32424; // Red30 AAA
-
-               .oo-ui-labelElement-label {
-                       color: #b32424;
-               }
-               .oo-ui-buttonWidget {
-                       opacity: @muted-opacity;
-               }
-       }
-
-       &-selected.oo-ui-capsuleItemWidget.oo-ui-widget-enabled {
-               background-color: #eaf3ff;
-               border-color: #36c;
-       }
-
-       &-popup-content {
-               padding: 0.5em;
-               color: #54595d;
-       }
-
-       &.oo-ui-labelElement .oo-ui-labelElement-label {
-               vertical-align: middle;
-               cursor: pointer;
-       }
-
-       &-highlight {
-               display: none;
-               padding-right: 0.5em;
-
-               &-highlighted {
-                       display: inline-block;
-
-               }
-
-               &[data-color='c1'] {
-                       .mw-rcfilters-mixin-circle( @highlight-c1, 10px, ~'0 0.5em 0 0' );
-               }
-               &[data-color='c2'] {
-                       .mw-rcfilters-mixin-circle( @highlight-c2, 10px, ~'0 0.5em 0 0' );
-               }
-               &[data-color='c3'] {
-                       .mw-rcfilters-mixin-circle( @highlight-c3, 10px, ~'0 0.5em 0 0' );
-               }
-               &[data-color='c4'] {
-                       .mw-rcfilters-mixin-circle( @highlight-c4, 10px, ~'0 0.5em 0 0' );
-               }
-               &[data-color='c5'] {
-                       .mw-rcfilters-mixin-circle( @highlight-c5, 10px, ~'0 0.5em 0 0' );
-               }
-       }
-}
diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.less
deleted file mode 100644 (file)
index b9dd3c1..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-.mw-rcfilters-ui-filterCapsuleMultiselectWidget {
-       max-width: none;
-
-       &.oo-ui-widget-enabled .oo-ui-capsuleMultiselectWidget-handle {
-               background-color: #f8f9fa;
-               border-radius: 2px 2px 0 0;
-               padding: 0.3em 0.6em 0.6em 0.6em;
-               margin-top: 1.6em;
-       }
-
-       .mw-rcfilters-ui-table {
-               margin-top: 0.3em;
-       }
-
-       &-wrapper-content-title {
-               font-weight: bold;
-               color: #54595d;
-       }
-
-       &-emptyFilters {
-               color: #72777d;
-       }
-
-       &-cell-filters {
-               width: 100%;
-       }
-       &-cell-reset {
-               text-align: right;
-               padding-left: 0.5em;
-       }
-}
diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.less
new file mode 100644 (file)
index 0000000..194b1b2
--- /dev/null
@@ -0,0 +1,16 @@
+@import 'mediawiki.mixins';
+
+.mw-rcfilters-ui-filterFloatingMenuSelectWidget {
+       z-index: auto;
+       max-width: 650px;
+
+       &-body {
+               max-height: 70vh;
+       }
+
+       &-footer {
+               background-color: #f8f9fa;
+               text-align: right;
+               padding: 0.5em;
+       }
+}
diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterGroupWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterGroupWidget.less
deleted file mode 100644 (file)
index 18fb4e6..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-@import 'mediawiki.mixins';
-
-.mw-rcfilters-ui-filterGroupWidget {
-       padding-bottom: 0.5em;
-
-       &-header {
-               background: #eaecf0;
-               padding: 0.5em 0.75em;
-
-               &-title {
-                       // TODO: Unify colors with official design palette
-                       color: #555a5d;
-                       .box-sizing( border-box );
-                       display: inline-block;
-               }
-       }
-
-       &-whatsThisButton {
-               display: inline-block;
-               margin-left: 1.5em;
-
-               &.oo-ui-buttonElement {
-                       vertical-align: text-bottom;
-
-                       & > .oo-ui-buttonElement-button {
-                               font-weight: normal;
-                               // Override OOUI's definitions for button
-                               border-color: transparent;
-                               padding: 0;
-
-                               &:focus {
-                                       box-shadow: none;
-                                       outline: 0;
-                               }
-                       }
-               }
-
-               &-popup-content {
-                       padding: 1em;
-
-                       &-header {
-                               font-weight: bold;
-                               margin-bottom: 1em;
-                       }
-
-                       &-link {
-                               margin: 1em 0;
-
-                       }
-
-                       .oo-ui-buttonElement-frameless.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
-                               margin-left: 0;
-                       }
-               }
-       }
-
-       &-active {
-               .mw-rcfilters-ui-filterGroupWidget-header-title {
-                       font-weight: bold;
-               }
-       }
-}
diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemWidget.less
deleted file mode 100644 (file)
index 3d63831..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-@import 'mediawiki.mixins';
-
-.mw-rcfilters-ui-filterItemWidget {
-       padding: 0 0.5em;
-       .box-sizing( border-box );
-
-       &:not( :last-child ) {
-               border-bottom: solid 1px #eaecf0; // Base 80 AAA
-       }
-
-       &:hover {
-               background-color: #fbfbfb;
-       }
-
-       .mw-rcfilters-ui-table {
-               padding-top: 0.5em;
-       }
-
-       &-muted {
-               background-color: #f8f9fa; // Base90 AAA
-               .mw-rcfilters-ui-filterItemWidget-label-title,
-               .mw-rcfilters-ui-filterItemWidget-label-desc {
-                       color: #54595d; // Base20 AAA
-               }
-       }
-
-       &-selected {
-               background-color: #eaf3ff; // Accent90 AAA
-       }
-
-       &-label {
-               &-title {
-                       font-weight: bold;
-                       font-size: 1.15em;
-                       color: #222;
-               }
-               &-desc {
-                       color: #464a4f;
-               }
-       }
-
-       &-filterCheckbox {
-               .oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline {
-                       // Override margin-top and -bottom rules from FieldLayout
-                       margin: 0 !important; /* stylelint-disable-line declaration-no-important */
-               }
-
-               .oo-ui-checkboxInputWidget {
-                       // Workaround for IE11 rendering issues. T162098
-                       display: block;
-               }
-       }
-
-       &-highlightButton {
-               width: 4em;
-               padding-left: 1em;
-       }
-}
diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuHeaderWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuHeaderWidget.less
new file mode 100644 (file)
index 0000000..1f3d343
--- /dev/null
@@ -0,0 +1,30 @@
+@import 'mediawiki.mixins';
+
+.mw-rcfilters-ui-filterMenuHeaderWidget {
+       &-title {
+               font-size: 1.2em;
+               padding: 0.75em 0.5em;
+               // TODO: Unify colors with official design palette
+               color: #54595d;
+       }
+
+       &-header {
+               border-bottom: 1px solid #c8ccd1;
+               background: #f8f9fa;
+               overflow: hidden;
+
+               &-highlight {
+                       width: 1em;
+                       vertical-align: middle;
+                       // Using the same padding that the filter item
+                       // uses, so the button is aligned with the highlight
+                       // buttons for the filters
+                       padding-right: 0.5em;
+               }
+
+               &-title {
+                       width: 100%;
+                       vertical-align: middle;
+               }
+       }
+}
diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuOptionWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuOptionWidget.less
new file mode 100644 (file)
index 0000000..9d78f85
--- /dev/null
@@ -0,0 +1,59 @@
+@import 'mediawiki.mixins';
+
+.mw-rcfilters-ui-filterMenuOptionWidget {
+       padding: 0 0.5em;
+       .box-sizing( border-box );
+
+       &:not( :last-child ) {
+               border-bottom: solid 1px #eaecf0; // Base 80 AAA
+       }
+
+       &:hover {
+               background-color: #fbfbfb;
+       }
+
+       .mw-rcfilters-ui-table {
+               padding-top: 0.5em;
+       }
+
+       &-muted {
+               background-color: #f8f9fa; // Base90 AAA
+               .mw-rcfilters-ui-filterMenuOptionWidget-label-title,
+               .mw-rcfilters-ui-filterMenuOptionWidget-label-desc {
+                       color: #54595d; // Base20 AAA
+               }
+       }
+
+       &.oo-ui-optionWidget-selected {
+               background-color: #eaf3ff; // Accent90 AAA
+       }
+
+       &-label {
+               &-title {
+                       font-weight: bold;
+                       font-size: 1.15em;
+                       color: #222;
+               }
+               &-desc {
+                       color: #464a4f;
+                       white-space: normal;
+               }
+       }
+
+       &-filterCheckbox {
+               .oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline {
+                       // Override margin-top and -bottom rules from FieldLayout
+                       margin: 0 !important; /* stylelint-disable-line declaration-no-important */
+               }
+
+               .oo-ui-checkboxInputWidget {
+                       // Workaround for IE11 rendering issues. T162098
+                       display: block;
+               }
+       }
+
+       &-highlightButton {
+               width: 4em;
+               padding-left: 1em;
+       }
+}
diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuSectionOptionWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuSectionOptionWidget.less
new file mode 100644 (file)
index 0000000..964f27e
--- /dev/null
@@ -0,0 +1,60 @@
+@import 'mediawiki.mixins';
+
+.mw-rcfilters-ui-filterMenuSectionOptionWidget {
+       background: #eaecf0;
+       padding-bottom: 0.7em;
+
+       &-header {
+               padding: 0 0.75em;
+               // Use a high specificity to override OOUI
+               .oo-ui-optionWidget.oo-ui-labelElement &-title.oo-ui-labelElement-label {
+                       // TODO: Unify colors with official design palette
+                       color: #555a5d;
+                       .box-sizing( border-box );
+                       display: inline-block;
+                       line-height: normal;
+               }
+       }
+
+       &-whatsThisButton {
+               margin-left: 1.5em;
+               &.oo-ui-buttonElement {
+                       vertical-align: text-bottom;
+
+                       & > .oo-ui-buttonElement-button {
+                               font-weight: normal;
+                               // Override OOUI's definitions for button
+                               border-color: transparent;
+                               padding: 0;
+
+                               &:focus {
+                                       box-shadow: none;
+                                       outline: 0;
+                               }
+                       }
+               }
+
+               &-popup-content {
+                       padding: 1em;
+
+                       &-header {
+                               font-weight: bold;
+                               margin-bottom: 1em;
+                       }
+
+                       &-link {
+                               margin: 1em 0;
+                       }
+
+                       .oo-ui-buttonElement-frameless.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+                               margin-left: 0;
+                       }
+               }
+       }
+
+       &-active {
+               .mw-rcfilters-ui-filterMenuSectionOptionWidget-header-title {
+                       font-weight: bold;
+               }
+       }
+}
diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagItemWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagItemWidget.less
new file mode 100644 (file)
index 0000000..c82903e
--- /dev/null
@@ -0,0 +1,87 @@
+@import 'mw.rcfilters.mixins';
+
+.mw-rcfilters-ui-filterTagItemWidget {
+       background-color: #fff;
+       border-color: #979797;
+       color: #222;
+
+       // Background and color of the capsule widget need a bit
+       // more specificity to override ooui internals
+       &-muted.oo-ui-tagItemWidget.oo-ui-widget-enabled {
+               // Muted state
+               background-color: #eaecf0;
+               border-color: #c8ccd1;
+
+               .oo-ui-labelElement-label {
+                       color: #72777d;
+               }
+               .oo-ui-buttonWidget {
+                       opacity: @muted-opacity;
+               }
+       }
+
+       &-conflicted.oo-ui-tagItemWidget.oo-ui-widget-enabled {
+               background-color: #fee7e6; // Red90 AAA
+               border-color: #b32424; // Red30 AAA
+
+               .oo-ui-labelElement-label {
+                       color: #b32424;
+               }
+               .oo-ui-buttonWidget {
+                       opacity: @muted-opacity;
+               }
+       }
+
+       &-selected.oo-ui-tagItemWidget.oo-ui-widget-enabled {
+               background-color: #eaf3ff;
+               border-color: #36c;
+       }
+
+       &-popup-content {
+               padding: 0.5em;
+               color: #54595d;
+       }
+
+       &.oo-ui-labelElement .oo-ui-labelElement-label {
+               vertical-align: middle;
+               cursor: pointer;
+       }
+
+       &-highlight {
+               display: none;
+               margin-right: 0.5em;
+               height: 100%;
+               width: 10px;
+
+               &-highlighted {
+                       display: inline-block;
+               }
+
+               &:before {
+                       content: '';
+                       position: absolute;
+                       display: block;
+                       top: 50%;
+               }
+
+               &[data-color='c1']:before {
+                       .mw-rcfilters-mixin-circle( @highlight-c1, 10px, ~'-5px 0.5em 0 0' );
+               }
+
+               &[data-color='c2']:before {
+                       .mw-rcfilters-mixin-circle( @highlight-c2, 10px, ~'-5px 0.5em 0 0' );
+               }
+
+               &[data-color='c3']:before {
+                       .mw-rcfilters-mixin-circle( @highlight-c3, 10px, ~'-5px 0.5em 0 0' );
+               }
+
+               &[data-color='c4']:before {
+                       .mw-rcfilters-mixin-circle( @highlight-c4, 10px, ~'-5px 0.5em 0 0' );
+               }
+
+               &[data-color='c5']:before {
+                       .mw-rcfilters-mixin-circle( @highlight-c5, 10px, ~'-5px 0.5em 0 0' );
+               }
+       }
+}
diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagMultiselectWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagMultiselectWidget.less
new file mode 100644 (file)
index 0000000..7371fdd
--- /dev/null
@@ -0,0 +1,34 @@
+.mw-rcfilters-ui-filterTagMultiselectWidget {
+       max-width: none;
+
+       &.oo-ui-widget-enabled .oo-ui-tagMultiselectWidget-handle {
+               border: 1px solid #a2a9b1;
+               border-bottom: 0;
+               background-color: #f8f9fa;
+               border-radius: 2px 2px 0 0;
+               padding: 0.3em 0.6em 0.6em 0.6em;
+               margin-top: 1.6em;
+       }
+
+       .mw-rcfilters-ui-table {
+               margin-top: 0.3em;
+       }
+
+       &-wrapper-content-title {
+               font-weight: bold;
+               color: #54595d;
+       }
+
+       &-emptyFilters {
+               color: #72777d;
+       }
+
+       &-cell-filters {
+               width: 100%;
+       }
+
+       &-cell-reset {
+               text-align: right;
+               padding-left: 0.5em;
+       }
+}
index b22abc6..f52b7ff 100644 (file)
@@ -3,21 +3,6 @@
        // Make sure this uses the interface direction, not the content direction
        direction: ltr;
 
-       &-popup {
-               margin-top: 1px;
-               max-width: 650px;
-
-               .oo-ui-popupWidget-body {
-                       max-height: 70vh;
-               }
-
-               .oo-ui-popupWidget-footer {
-                       background-color: #f8f9fa;
-                       text-align: right;
-                       padding: 0.5em;
-               }
-       }
-
        &-search {
                max-width: none;
                margin-top: -1px;
diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FiltersListWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FiltersListWidget.less
deleted file mode 100644 (file)
index cb87989..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-.mw-rcfilters-ui-filtersListWidget {
-       &-title {
-               font-size: 1.2em;
-               padding: 0.75em 0.5em;
-               // TODO: Unify colors with official design palette
-               color: #54595d;
-       }
-
-       &-header {
-               border-bottom: 1px solid #c8ccd1;
-               background: #f8f9fa;
-               overflow: hidden;
-
-               &-highlight {
-                       width: 1em;
-                       vertical-align: middle;
-                       // Using the same padding that the filter item
-                       // uses, so the button is aligned with the highlight
-                       // buttons for the filters
-                       padding-right: 0.5em;
-               }
-
-               &-title {
-                       width: 100%;
-                       vertical-align: middle;
-               }
-       }
-
-       &-noresults {
-               padding: 0.5em;
-               // TODO: Unify colors with official design palette
-               color: #666;
-       }
-}
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js
deleted file mode 100644 (file)
index 20eff8b..0000000
+++ /dev/null
@@ -1,205 +0,0 @@
-( function ( mw, $ ) {
-       /**
-        * Extend OOUI's CapsuleItemWidget to also display a popup on hover.
-        *
-        * @class
-        * @extends OO.ui.CapsuleItemWidget
-        * @mixins OO.ui.mixin.PopupElement
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller
-        * @param {mw.rcfilters.dm.FilterItem} model Item model
-        * @param {Object} config Configuration object
-        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
-        */
-       mw.rcfilters.ui.CapsuleItemWidget = function MwRcfiltersUiCapsuleItemWidget( controller, model, config ) {
-               // Configuration initialization
-               config = config || {};
-
-               this.controller = controller;
-               this.model = model;
-               this.popupLabel = new OO.ui.LabelWidget();
-               this.$overlay = config.$overlay || this.$element;
-               this.positioned = false;
-               this.popupTimeoutShow = null;
-               this.popupTimeoutHide = null;
-
-               // Parent constructor
-               mw.rcfilters.ui.CapsuleItemWidget.parent.call( this, $.extend( {
-                       data: this.model.getName(),
-                       label: this.model.getLabel()
-               }, config ) );
-
-               // Mixin constructors
-               OO.ui.mixin.PopupElement.call( this, $.extend( {
-                       popup: {
-                               padded: false,
-                               align: 'center',
-                               position: 'above',
-                               $content: $( '<div>' )
-                                       .addClass( 'mw-rcfilters-ui-capsuleItemWidget-popup-content' )
-                                       .append( this.popupLabel.$element ),
-                               $floatableContainer: this.$element,
-                               classes: [ 'mw-rcfilters-ui-capsuleItemWidget-popup' ]
-                       }
-               }, config ) );
-
-               this.$highlight = $( '<div>' )
-                       .addClass( 'mw-rcfilters-ui-capsuleItemWidget-highlight' );
-
-               // Events
-               this.model.connect( this, { update: 'onModelUpdate' } );
-
-               // Initialization
-               this.$overlay.append( this.popup.$element );
-               this.$element
-                       .prepend( this.$highlight )
-                       .attr( 'aria-haspopup', 'true' )
-                       .addClass( 'mw-rcfilters-ui-capsuleItemWidget' )
-                       .on( 'mousedown', this.onMouseDown.bind( this ) )
-                       .on( 'mouseenter', this.onMouseEnter.bind( this ) )
-                       .on( 'mouseleave', this.onMouseLeave.bind( this ) );
-
-               this.setCurrentMuteState();
-               this.setHighlightColor();
-       };
-
-       OO.inheritClass( mw.rcfilters.ui.CapsuleItemWidget, OO.ui.CapsuleItemWidget );
-       OO.mixinClass( mw.rcfilters.ui.CapsuleItemWidget, OO.ui.mixin.PopupElement );
-
-       /**
-        * Respond to model update event
-        */
-       mw.rcfilters.ui.CapsuleItemWidget.prototype.onModelUpdate = function () {
-               this.setCurrentMuteState();
-
-               this.setHighlightColor();
-       };
-
-       /**
-        * Override mousedown event to prevent its propagation to the parent,
-        * since the parent (the multiselect widget) focuses the popup when its
-        * mousedown event is fired.
-        *
-        * @param {jQuery.Event} e Event
-        */
-       mw.rcfilters.ui.CapsuleItemWidget.prototype.onMouseDown = function ( e ) {
-               e.stopPropagation();
-       };
-
-       /**
-        * Emit a click event when the capsule is clicked so we can aggregate this
-        * in the parent (the capsule)
-        */
-       mw.rcfilters.ui.CapsuleItemWidget.prototype.onClick = function () {
-               this.emit( 'click' );
-       };
-
-       /**
-        * Override the event listening to the item close button click
-        */
-       mw.rcfilters.ui.CapsuleItemWidget.prototype.onCloseClick = function () {
-               var element = this.getElementGroup();
-
-               if ( element && $.isFunction( element.removeItems ) ) {
-                       element.removeItems( [ this ] );
-               }
-
-               // Respond to user removing the filter
-               this.controller.clearFilter( this.model.getName() );
-       };
-
-       mw.rcfilters.ui.CapsuleItemWidget.prototype.setHighlightColor = function () {
-               var selectedColor = this.model.isHighlightEnabled() ? this.model.getHighlightColor() : null;
-
-               this.$highlight
-                       .attr( 'data-color', selectedColor )
-                       .toggleClass(
-                               'mw-rcfilters-ui-capsuleItemWidget-highlight-highlighted',
-                               !!selectedColor
-                       );
-       };
-
-       /**
-        * Set the current mute state for this item
-        */
-       mw.rcfilters.ui.CapsuleItemWidget.prototype.setCurrentMuteState = function () {
-               this.$element
-                       .toggleClass(
-                               'mw-rcfilters-ui-capsuleItemWidget-muted',
-                               !this.model.isSelected() ||
-                               this.model.isIncluded() ||
-                               this.model.isFullyCovered()
-                       )
-                       .toggleClass(
-                               'mw-rcfilters-ui-capsuleItemWidget-conflicted',
-                               this.model.isSelected() && this.model.isConflicted()
-                       );
-       };
-
-       /**
-        * Respond to mouse enter event
-        */
-       mw.rcfilters.ui.CapsuleItemWidget.prototype.onMouseEnter = function () {
-               var labelText = this.model.getStateMessage();
-
-               if ( labelText ) {
-                       this.popupLabel.setLabel( labelText );
-
-                       if ( !this.positioned ) {
-                               // Recalculate anchor position to be center of the capsule item
-                               this.popup.$anchor.css( 'margin-left', ( this.$element.width() / 2 ) );
-                               this.positioned = true;
-                       }
-
-                       // Set timeout for the popup to show
-                       this.popupTimeoutShow = setTimeout( function () {
-                               this.popup.toggle( true );
-                       }.bind( this ), 500 );
-
-                       // Cancel the hide timeout
-                       clearTimeout( this.popupTimeoutHide );
-                       this.popupTimeoutHide = null;
-               }
-       };
-
-       /**
-        * Respond to mouse leave event
-        */
-       mw.rcfilters.ui.CapsuleItemWidget.prototype.onMouseLeave = function () {
-               this.popupTimeoutHide = setTimeout( function () {
-                       this.popup.toggle( false );
-               }.bind( this ), 250 );
-
-               // Clear the show timeout
-               clearTimeout( this.popupTimeoutShow );
-               this.popupTimeoutShow = null;
-       };
-
-       /**
-        * Set selected state on this widget
-        *
-        * @param {boolean} [isSelected] Widget is selected
-        */
-       mw.rcfilters.ui.CapsuleItemWidget.prototype.toggleSelected = function ( isSelected ) {
-               isSelected = isSelected !== undefined ? isSelected : !this.selected;
-
-               if ( this.selected !== isSelected ) {
-                       this.selected = isSelected;
-
-                       this.$element.toggleClass( 'mw-rcfilters-ui-capsuleItemWidget-selected', this.selected );
-               }
-       };
-
-       /**
-        * Remove and destroy external elements of this widget
-        */
-       mw.rcfilters.ui.CapsuleItemWidget.prototype.destroy = function () {
-               // Destroy the popup
-               this.popup.$element.detach();
-
-               // Disconnect events
-               this.model.disconnect( this );
-               this.closeButton.disconnect( this );
-       };
-}( mediaWiki, jQuery ) );
index 86b3b11..9fd4593 100644 (file)
                mw.rcfilters.ui.CheckboxInputWidget.parent.call( this, config );
 
                // Event
-               this.$input.on( 'change', this.onUserChange.bind( this ) );
+               this.$input
+                       // HACK: This widget just pretends to be a checkbox for visual purposes.
+                       // In reality, all actions - setting to true or false, etc - are
+                       // decided by the model, and executed by the controller. This means
+                       // that we want to let the controller and model make the decision
+                       // of whether to check/uncheck this checkboxInputWidget, and for that,
+                       // we have to bypass the browser action that checks/unchecks it during
+                       // click.
+                       .on( 'click', false )
+                       .on( 'change', this.onUserChange.bind( this ) );
        };
 
        /* Initialization */
 
        /* Methods */
 
+       /**
+        * @inheritdoc
+        */
+       mw.rcfilters.ui.CheckboxInputWidget.prototype.onEdit = function () {
+               // Similarly to preventing defaults in 'click' event, we want
+               // to prevent this widget from deciding anything about its own
+               // state; it emits a change event and the model and controller
+               // make a decision about what its select state is.
+               // onEdit has a widget.$input.prop( 'checked' ) inside a setTimeout()
+               // so we really want to prevent that from messing with what
+               // the model decides the state of the widget is.
+       };
+
        /**
         * Respond to checkbox change by a user and emit 'userChange'.
         */
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.js
deleted file mode 100644 (file)
index f4f460d..0000000
+++ /dev/null
@@ -1,342 +0,0 @@
-( function ( mw, $ ) {
-       /**
-        * Filter-specific CapsuleMultiselectWidget
-        *
-        * @class
-        * @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
-        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
-        */
-       mw.rcfilters.ui.FilterCapsuleMultiselectWidget = function MwRcfiltersUiFilterCapsuleMultiselectWidget( controller, model, filterInput, config ) {
-               var title = new OO.ui.LabelWidget( {
-                               label: mw.msg( 'rcfilters-activefilters' ),
-                               classes: [ 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-wrapper-content-title' ]
-                       } ),
-                       $contentWrapper = $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-wrapper' );
-
-               this.$overlay = config.$overlay || this.$element;
-
-               // Parent
-               mw.rcfilters.ui.FilterCapsuleMultiselectWidget.parent.call( this, $.extend( true, {
-                       popup: {
-                               $autoCloseIgnore: filterInput.$element.add( this.$overlay ),
-                               $floatableContainer: filterInput.$element
-                       }
-               }, config ) );
-
-               this.controller = controller;
-               this.model = model;
-               this.filterInput = filterInput;
-               this.isSelecting = false;
-               this.selected = null;
-
-               this.resetButton = new OO.ui.ButtonWidget( {
-                       framed: false,
-                       classes: [ 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-resetButton' ]
-               } );
-
-               this.emptyFilterMessage = new OO.ui.LabelWidget( {
-                       label: mw.msg( 'rcfilters-empty-filter' ),
-                       classes: [ 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-emptyFilters' ]
-               } );
-               this.$content.append( this.emptyFilterMessage.$element );
-
-               // Events
-               this.resetButton.connect( this, { click: 'onResetButtonClick' } );
-               this.resetButton.$element.on( 'mousedown', this.onResetButtonMouseDown.bind( this ) );
-               this.model.connect( this, {
-                       itemUpdate: 'onModelItemUpdate',
-                       highlightChange: 'onModelHighlightChange'
-               } );
-               this.aggregate( { click: 'capsuleItemClick' } );
-
-               // Add the filterInput as trigger
-               this.filterInput.$input
-                       .on( 'focus', this.focus.bind( this ) );
-
-               // Build the content
-               $contentWrapper.append(
-                       title.$element,
-                       $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-table' )
-                               .append(
-                                       // The filter list 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-row' )
-                                               .append(
-                                                       this.$content
-                                                               .addClass( 'mw-rcfilters-ui-cell' )
-                                                               .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-cell-filters' ),
-                                                       $( '<div>' )
-                                                               .addClass( 'mw-rcfilters-ui-cell' )
-                                                               .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-cell-reset' )
-                                                               .append( this.resetButton.$element )
-                                               )
-                               )
-               );
-
-               // Initialize
-               this.$handle.append( $contentWrapper );
-
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget' );
-
-               this.reevaluateResetRestoreState();
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.FilterCapsuleMultiselectWidget, OO.ui.CapsuleMultiselectWidget );
-
-       /* Events */
-
-       /**
-        * @event remove
-        * @param {string[]} filters Array of names of removed filters
-        *
-        * Filters were removed
-        */
-
-       /* Methods */
-
-       /**
-        * Respond to model itemUpdate event
-        *
-        * @param {mw.rcfilters.dm.FilterItem} item Filter item model
-        */
-       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onModelItemUpdate = function ( item ) {
-               if (
-                       item.isSelected() ||
-                       (
-                               this.model.isHighlightEnabled() &&
-                               item.isHighlightSupported() &&
-                               item.getHighlightColor()
-                       )
-               ) {
-                       this.addItemByName( item.getName() );
-               } else {
-                       this.removeItemByName( item.getName() );
-               }
-
-               // Re-evaluate reset state
-               this.reevaluateResetRestoreState();
-       };
-
-       /**
-        * Respond to highlightChange event
-        *
-        * @param {boolean} isHighlightEnabled Highlight is enabled
-        */
-       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onModelHighlightChange = function ( isHighlightEnabled ) {
-               var highlightedItems = this.model.getHighlightedItems();
-
-               if ( isHighlightEnabled ) {
-                       // Add capsule widgets
-                       highlightedItems.forEach( function ( filterItem ) {
-                               this.addItemByName( filterItem.getName() );
-                       }.bind( this ) );
-               } else {
-                       // Remove capsule widgets if they're not selected
-                       highlightedItems.forEach( function ( filterItem ) {
-                               if ( !filterItem.isSelected() ) {
-                                       this.removeItemByName( filterItem.getName() );
-                               }
-                       }.bind( this ) );
-               }
-       };
-
-       /**
-        * 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();
-               }
-       };
-
-       /**
-        * Respond to mouse down event on the reset button to prevent the popup from opening
-        *
-        * @param {jQuery.Event} e Event
-        */
-       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onResetButtonMouseDown = function ( e ) {
-               e.stopPropagation();
-       };
-
-       /**
-        * 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.setTitle(
-                       currFiltersAreEmpty ? null : mw.msg( 'rcfilters-clear-all-filters' )
-               );
-
-               this.resetButton.toggle( !hideResetButton );
-               this.emptyFilterMessage.toggle( currFiltersAreEmpty );
-       };
-
-       /**
-        * Mark an item widget as selected
-        *
-        * @param {mw.rcfilters.ui.CapsuleItemWidget} item Capsule widget
-        */
-       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.select = function ( item ) {
-               if ( this.selected !== item ) {
-                       // Unselect previous
-                       if ( this.selected ) {
-                               this.selected.toggleSelected( false );
-                       }
-
-                       // Select new one
-                       this.selected = item;
-                       if ( this.selected ) {
-                               item.toggleSelected( true );
-                       }
-               }
-       };
-
-       /**
-        * Reset selection and remove selected states from all items
-        */
-       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.resetSelection = function () {
-               if ( this.selected !== null ) {
-                       this.selected = null;
-                       this.getItems().forEach( function ( capsuleWidget ) {
-                               capsuleWidget.toggleSelected( false );
-                       } );
-               }
-       };
-
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.createItemWidget = function ( data ) {
-               var item = this.model.getItemByName( data );
-
-               if ( !item ) {
-                       return;
-               }
-
-               return new mw.rcfilters.ui.CapsuleItemWidget(
-                       this.controller,
-                       item,
-                       { $overlay: this.$overlay }
-               );
-       };
-
-       /**
-        * Add items by their filter name
-        *
-        * @param {string} name Filter name
-        */
-       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.addItemByName = function ( name ) {
-               var item = this.model.getItemByName( name );
-
-               if ( !item ) {
-                       return;
-               }
-
-               // Check that the item isn't already added
-               if ( !this.getItemFromData( name ) ) {
-                       this.addItems( [ this.createItemWidget( name ) ] );
-               }
-       };
-
-       /**
-        * Remove items by their filter name
-        *
-        * @param {string} name Filter name
-        */
-       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.removeItemByName = function ( name ) {
-               this.removeItemsFromData( [ name ] );
-       };
-
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.focus = function () {
-               // Override this method; we don't want to focus on the popup, and we
-               // don't want to bind the size to the handle.
-               if ( !this.isDisabled() ) {
-                       this.popup.toggle( true );
-                       this.filterInput.$input.get( 0 ).focus();
-               }
-               return this;
-       };
-
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onFocusForPopup = function () {
-               // HACK can be removed once I21b8cff4048 is merged in oojs-ui
-               this.focus();
-       };
-
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onKeyDown = function () {};
-
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onPopupFocusOut = function () {};
-
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.clearInput = function () {
-               if ( this.filterInput ) {
-                       this.filterInput.setValue( '' );
-               }
-               this.menu.toggle( false );
-               this.menu.selectItem();
-               this.menu.highlightItem();
-       };
-
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.removeItems = function ( items ) {
-               // Parent call
-               mw.rcfilters.ui.FilterCapsuleMultiselectWidget.parent.prototype.removeItems.call( this, items );
-
-               // Destroy the item widget when it is removed
-               // This is done because we re-add items by recreating them, rather than hiding them
-               // and items include popups, that will just continue to be created and appended
-               // unnecessarily.
-               items.forEach( function ( widget ) {
-                       widget.destroy();
-               } );
-       };
-
-       /**
-        * Override 'editItem' since it tries to use $input which does
-        * not exist when a popup is available.
-        */
-       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.editItem = function () {};
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.js
new file mode 100644 (file)
index 0000000..ec85df9
--- /dev/null
@@ -0,0 +1,137 @@
+( function ( mw ) {
+       /**
+        * A floating menu widget for the filter list
+        *
+        * @extends OO.ui.FloatingMenuSelectWidget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller Controller
+        * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+        * @param {Object} [config] Configuration object
+        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
+        * @cfg {jQuery} [$footer] An optional footer for the menu
+        */
+       mw.rcfilters.ui.FilterFloatingMenuSelectWidget = function MwRcfiltersUiFilterFloatingMenuSelectWidget( controller, model, config ) {
+               var header;
+
+               config = config || {};
+
+               this.controller = controller;
+               this.model = model;
+
+               this.inputValue = '';
+               this.$overlay = config.$overlay || this.$element;
+               this.$footer = config.$footer;
+               this.$body = $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-filterFloatingMenuSelectWidget-body' );
+
+               // Parent
+               mw.rcfilters.ui.FilterFloatingMenuSelectWidget.parent.call( this, $.extend( {
+                       $autoCloseIgnore: this.$overlay,
+                       width: 650
+               }, config ) );
+               this.setGroupElement(
+                       $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-filterFloatingMenuSelectWidget-group' )
+               );
+               this.setClippableElement( this.$body );
+               this.setClippableContainer( this.$element );
+
+               header = new mw.rcfilters.ui.FilterMenuHeaderWidget(
+                       this.controller,
+                       this.model,
+                       {
+                               $overlay: this.$overlay
+                       }
+               );
+
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-filterFloatingMenuSelectWidget' )
+                       .append(
+                               this.$body
+                                       .append( header.$element, this.$group )
+                       );
+
+               if ( this.$footer ) {
+                       this.$element.append(
+                               this.$footer
+                                       .addClass( 'mw-rcfilters-ui-filterFloatingMenuSelectWidget-footer' )
+                       );
+               }
+       };
+
+       /* Initialize */
+
+       OO.inheritClass( mw.rcfilters.ui.FilterFloatingMenuSelectWidget, OO.ui.FloatingMenuSelectWidget );
+
+       /* Events */
+
+       /**
+        * @event itemVisibilityChange
+        *
+        * Item visibility has changed
+        */
+
+       /* Methods */
+
+       /**
+        * @fires itemVisibilityChange
+        * @inheritdoc
+        */
+       mw.rcfilters.ui.FilterFloatingMenuSelectWidget.prototype.updateItemVisibility = function () {
+               var i,
+                       itemWasHighlighted = false,
+                       inputVal = this.$input.val(),
+                       items = this.getItems();
+
+               // Since the method hides/shows items, we don't want to
+               // call it unless the input actually changed
+               if ( this.inputValue !== inputVal ) {
+                       // Parent method
+                       mw.rcfilters.ui.FilterFloatingMenuSelectWidget.parent.prototype.updateItemVisibility.call( this );
+
+                       if ( inputVal !== '' ) {
+                               // Highlight the first item in the list
+                               for ( i = 0; i < items.length; i++ ) {
+                                       if (
+                                               !( items[ i ] instanceof OO.ui.MenuSectionOptionWidget ) &&
+                                               items[ i ].isVisible()
+                                       ) {
+                                               itemWasHighlighted = true;
+                                               this.highlightItem( items[ i ] );
+                                               break;
+                                       }
+                               }
+                       }
+
+                       if ( !itemWasHighlighted ) {
+                               this.highlightItem( null );
+                       }
+
+                       // Cache value
+                       this.inputValue = inputVal;
+
+                       this.emit( 'itemVisibilityChange' );
+               }
+       };
+
+       /**
+        * Override the item matcher to use the model's match process
+        *
+        * @inheritdoc
+        */
+       mw.rcfilters.ui.FilterFloatingMenuSelectWidget.prototype.getItemMatcher = function ( s ) {
+               var results = this.model.findMatches( s, true );
+
+               return function ( item ) {
+                       return results.indexOf( item.getModel() ) > -1;
+               };
+       };
+
+       /**
+        * Scroll to the top of the menu
+        */
+       mw.rcfilters.ui.FilterFloatingMenuSelectWidget.prototype.scrollToTop = function () {
+               this.$body.scrollTop( 0 );
+       };
+}( mediaWiki ) );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterGroupWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterGroupWidget.js
deleted file mode 100644 (file)
index e19208a..0000000
+++ /dev/null
@@ -1,171 +0,0 @@
-( function ( mw, $ ) {
-       /**
-        * A group of filters
-        *
-        * @extends OO.ui.Widget
-        * @mixins OO.ui.mixin.GroupWidget
-        * @mixins OO.ui.mixin.LabelElement
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller Controller
-        * @param {mw.rcfilters.dm.FilterGroup} model Filter group model
-        * @param {Object} config Configuration object
-        * @cfg {jQuery} [$overlay] Overlay
-        */
-       mw.rcfilters.ui.FilterGroupWidget = function MwRcfiltersUiFilterGroupWidget( controller, model, config ) {
-               var whatsThisMessages,
-                       $header = $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-filterGroupWidget-header' ),
-                       $popupContent = $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-filterGroupWidget-whatsThisButton-popup-content' );
-
-               config = config || {};
-
-               // Parent
-               mw.rcfilters.ui.FilterGroupWidget.parent.call( this, config );
-
-               this.controller = controller;
-               this.model = model;
-               this.filters = {};
-               this.$overlay = config.$overlay || this.$element;
-
-               // 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-header-title' )
-               } ) );
-
-               $header.append( this.$label );
-
-               if ( this.model.hasWhatsThis() ) {
-                       whatsThisMessages = this.model.getWhatsThis();
-
-                       // Create popup
-                       if ( whatsThisMessages.header ) {
-                               $popupContent.append(
-                                       ( new OO.ui.LabelWidget( {
-                                               label: mw.msg( whatsThisMessages.header ),
-                                               classes: [ 'mw-rcfilters-ui-filterGroupWidget-whatsThisButton-popup-content-header' ]
-                                       } ) ).$element
-                               );
-                       }
-                       if ( whatsThisMessages.body ) {
-                               $popupContent.append(
-                                       ( new OO.ui.LabelWidget( {
-                                               label: mw.msg( whatsThisMessages.body ),
-                                               classes: [ 'mw-rcfilters-ui-filterGroupWidget-whatsThisButton-popup-content-body' ]
-                                       } ) ).$element
-                               );
-                       }
-                       if ( whatsThisMessages.linkText && whatsThisMessages.url ) {
-                               $popupContent.append(
-                                       ( new OO.ui.ButtonWidget( {
-                                               framed: false,
-                                               flags: [ 'progressive' ],
-                                               href: whatsThisMessages.url,
-                                               label: mw.msg( whatsThisMessages.linkText ),
-                                               classes: [ 'mw-rcfilters-ui-filterGroupWidget-whatsThisButton-popup-content-link' ]
-                                       } ) ).$element
-                               );
-                       }
-
-                       // Add button
-                       this.whatsThisButton = new OO.ui.PopupButtonWidget( {
-                               framed: false,
-                               label: mw.msg( 'rcfilters-filterlist-whatsthis' ),
-                               $overlay: this.$overlay,
-                               classes: [ 'mw-rcfilters-ui-filterGroupWidget-whatsThisButton' ],
-                               flags: [ 'progressive' ],
-                               popup: {
-                                       padded: false,
-                                       align: 'center',
-                                       position: 'above',
-                                       $content: $popupContent,
-                                       classes: [ 'mw-rcfilters-ui-filterGroupWidget-whatsThisButton-popup' ]
-                               }
-                       } );
-
-                       $header
-                               .append( this.whatsThisButton.$element );
-               }
-
-               // Populate
-               this.populateFromModel();
-
-               this.model.connect( this, { update: 'onModelUpdate' } );
-
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-filterGroupWidget' )
-                       .addClass( 'mw-rcfilters-ui-filterGroupWidget-name-' + this.model.getName() )
-                       .append(
-                               $header,
-                               this.$group
-                                       .addClass( 'mw-rcfilters-ui-filterGroupWidget-group' )
-                       );
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.FilterGroupWidget, OO.ui.Widget );
-       OO.mixinClass( mw.rcfilters.ui.FilterGroupWidget, OO.ui.mixin.GroupWidget );
-       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()
-               );
-       };
-
-       /**
-        * Get an item widget from its filter name
-        *
-        * @param {string} filterName Filter name
-        * @return {mw.rcfilters.ui.FilterItemWidget} Item widget
-        */
-       mw.rcfilters.ui.FilterGroupWidget.prototype.getItemWidget = function ( filterName ) {
-               return this.filters[ filterName ];
-       };
-
-       /**
-        * Populate data from the model
-        */
-       mw.rcfilters.ui.FilterGroupWidget.prototype.populateFromModel = function () {
-               var widget = this;
-
-               this.clearItems();
-               this.filters = {};
-
-               this.addItems(
-                       this.model.getItems().map( function ( filterItem ) {
-                               var groupWidget = new mw.rcfilters.ui.FilterItemWidget(
-                                       widget.controller,
-                                       filterItem,
-                                       {
-                                               label: filterItem.getLabel(),
-                                               description: filterItem.getDescription(),
-                                               $overlay: widget.$overlay
-                                       }
-                               );
-
-                               widget.filters[ filterItem.getName() ] = groupWidget;
-
-                               return groupWidget;
-                       } )
-               );
-       };
-
-       /**
-        * Get the group name
-        *
-        * @return {string} Group name
-        */
-       mw.rcfilters.ui.FilterGroupWidget.prototype.getName = function () {
-               return this.model.getName();
-       };
-}( mediaWiki, jQuery ) );
index 889ba08..1e0c8d5 100644 (file)
@@ -15,7 +15,7 @@
                this.colorPickerWidget = new mw.rcfilters.ui.HighlightColorPickerWidget( controller, model );
 
                // Parent
-               mw.rcfilters.ui.FilterItemHighlightButton.parent.call( this, $.extend( {}, config, {
+               mw.rcfilters.ui.FilterItemHighlightButton.parent.call( this, $.extend( true, {}, config, {
                        icon: 'highlight',
                        indicator: 'down',
                        popup: {
                // Event
                this.model.connect( this, { update: 'onModelUpdate' } );
                this.colorPickerWidget.connect( this, { chooseColor: 'onChooseColor' } );
+               // This lives inside a MenuOptionWidget, which intercepts mousedown
+               // to select the item. We want to prevent that when we click the highlight
+               // button
+               this.$element.on( 'mousedown', function ( e ) { e.stopPropagation(); } );
 
                this.$element
                        .addClass( 'mw-rcfilters-ui-filterItemHighlightButton' );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemWidget.js
deleted file mode 100644 (file)
index 7e6d776..0000000
+++ /dev/null
@@ -1,160 +0,0 @@
-( function ( mw, $ ) {
-       /**
-        * A widget representing a single toggle filter
-        *
-        * @extends OO.ui.Widget
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller RCFilters controller
-        * @param {mw.rcfilters.dm.FilterItem} model Filter item model
-        * @param {Object} config Configuration object
-        */
-       mw.rcfilters.ui.FilterItemWidget = function MwRcfiltersUiFilterItemWidget( controller, model, config ) {
-               var layout,
-                       $label = $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-filterItemWidget-label' );
-
-               config = config || {};
-
-               // Parent
-               mw.rcfilters.ui.FilterItemWidget.parent.call( this, config );
-
-               this.controller = controller;
-               this.model = model;
-               this.selected = false;
-
-               this.checkboxWidget = new mw.rcfilters.ui.CheckboxInputWidget( {
-                       value: this.model.getName(),
-                       selected: this.model.isSelected()
-               } );
-
-               $label.append(
-                       $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-filterItemWidget-label-title' )
-                               .text( this.model.getLabel() )
-               );
-               if ( this.model.getDescription() ) {
-                       $label.append(
-                               $( '<div>' )
-                                       .addClass( 'mw-rcfilters-ui-filterItemWidget-label-desc' )
-                                       .text( this.model.getDescription() )
-                       );
-               }
-
-               this.highlightButton = new mw.rcfilters.ui.FilterItemHighlightButton(
-                       this.controller,
-                       this.model,
-                       {
-                               $overlay: config.$overlay || this.$element,
-                               title: mw.msg( 'rcfilters-highlightmenu-help' )
-                       }
-               );
-               this.highlightButton.toggle( this.model.isHighlightEnabled() );
-
-               layout = new OO.ui.FieldLayout( this.checkboxWidget, {
-                       label: $label,
-                       align: 'inline'
-               } );
-
-               // Event
-               this.checkboxWidget.connect( this, { userChange: 'onCheckboxChange' } );
-               this.model.connect( this, { update: 'onModelUpdate' } );
-               this.model.getGroupModel().connect( this, { update: 'onGroupModelUpdate' } );
-
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-filterItemWidget' )
-                       .append(
-                               $( '<div>' )
-                                       .addClass( 'mw-rcfilters-ui-table' )
-                                       .append(
-                                               $( '<div>' )
-                                                       .addClass( 'mw-rcfilters-ui-row' )
-                                                       .append(
-                                                               $( '<div>' )
-                                                                       .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-filterItemWidget-filterCheckbox' )
-                                                                       .append( layout.$element ),
-                                                               $( '<div>' )
-                                                                       .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-filterItemWidget-highlightButton' )
-                                                                       .append( this.highlightButton.$element )
-                                                       )
-                                       )
-                       );
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.FilterItemWidget, OO.ui.Widget );
-
-       /* Methods */
-
-       /**
-        * Respond to checkbox change.
-        * NOTE: This event is emitted both for deliberate user action and for
-        * a change that the code requests ('setSelected')
-        *
-        * @param {boolean} isSelected The checkbox is selected
-        */
-       mw.rcfilters.ui.FilterItemWidget.prototype.onCheckboxChange = function ( isSelected ) {
-               this.controller.toggleFilterSelect( this.model.getName(), isSelected );
-       };
-
-       /**
-        * Respond to item model update event
-        */
-       mw.rcfilters.ui.FilterItemWidget.prototype.onModelUpdate = function () {
-               this.checkboxWidget.setSelected( this.model.isSelected() );
-
-               this.setCurrentMuteState();
-       };
-
-       /**
-        * Respond to item group model update event
-        */
-       mw.rcfilters.ui.FilterItemWidget.prototype.onGroupModelUpdate = function () {
-               this.setCurrentMuteState();
-       };
-
-       /**
-        * Set selected state on this widget
-        *
-        * @param {boolean} [isSelected] Widget is selected
-        */
-       mw.rcfilters.ui.FilterItemWidget.prototype.toggleSelected = function ( isSelected ) {
-               isSelected = isSelected !== undefined ? isSelected : !this.selected;
-
-               if ( this.selected !== isSelected ) {
-                       this.selected = isSelected;
-
-                       this.$element.toggleClass( 'mw-rcfilters-ui-filterItemWidget-selected', this.selected );
-               }
-       };
-
-       /**
-        * Set the current mute state for this item
-        */
-       mw.rcfilters.ui.FilterItemWidget.prototype.setCurrentMuteState = function () {
-               this.$element.toggleClass(
-                       'mw-rcfilters-ui-filterItemWidget-muted',
-                       this.model.isConflicted() ||
-                       (
-                               // Item is also muted when any of the items in its group is active
-                               this.model.getGroupModel().isActive() &&
-                               // But it isn't selected
-                               !this.model.isSelected() &&
-                               // And also not included
-                               !this.model.isIncluded()
-                       )
-               );
-
-               this.highlightButton.toggle( this.model.isHighlightEnabled() );
-       };
-
-       /**
-        * Get the name of this filter
-        *
-        * @return {string} Filter name
-        */
-       mw.rcfilters.ui.FilterItemWidget.prototype.getName = function () {
-               return this.model.getName();
-       };
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuHeaderWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuHeaderWidget.js
new file mode 100644 (file)
index 0000000..15e7eee
--- /dev/null
@@ -0,0 +1,86 @@
+( function ( mw, $ ) {
+       /**
+        * Menu header for the RCFilters filters menu
+        *
+        * @extends OO.ui.Widget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller Controller
+        * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+        * @param {Object} config Configuration object
+        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
+        */
+       mw.rcfilters.ui.FilterMenuHeaderWidget = function MwRcfiltersUiFilterMenuHeaderWidget( controller, model, config ) {
+               config = config || {};
+
+               this.controller = controller;
+               this.model = model;
+               this.$overlay = config.$overlay || this.$element;
+
+               // Parent
+               mw.rcfilters.ui.FilterMenuHeaderWidget.parent.call( this, config );
+               OO.ui.mixin.LabelElement.call( this, $.extend( {
+                       label: mw.msg( 'rcfilters-filterlist-title' ),
+                       $label: $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-title' )
+               }, config ) );
+
+               // Highlight button
+               this.highlightButton = new OO.ui.ToggleButtonWidget( {
+                       icon: 'highlight',
+                       label: mw.message( 'rcfilters-highlightbutton-title' ).text(),
+                       classes: [ 'mw-rcfilters-ui-filterMenuHeaderWidget-hightlightButton' ]
+               } );
+
+               // Events
+               this.highlightButton
+                       .connect( this, { click: 'onHighlightButtonClick' } );
+               this.model.connect( this, { highlightChange: 'onModelHighlightChange' } );
+
+               // Initialize
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget' )
+                       .append(
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-table' )
+                                       .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header' )
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 'mw-rcfilters-ui-row' )
+                                                       .append(
+                                                               $( '<div>' )
+                                                                       .addClass( 'mw-rcfilters-ui-cell' )
+                                                                       .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header-title' )
+                                                                       .append( this.$label ),
+                                                               $( '<div>' )
+                                                                       .addClass( 'mw-rcfilters-ui-cell' )
+                                                                       .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header-highlight' )
+                                                                       .append( this.highlightButton.$element )
+                                                       )
+                                       )
+                       );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( mw.rcfilters.ui.FilterMenuHeaderWidget, OO.ui.Widget );
+       OO.mixinClass( mw.rcfilters.ui.FilterMenuHeaderWidget, OO.ui.mixin.LabelElement );
+
+       /* Methods */
+
+       /**
+        * Respond to model highlight change event
+        *
+        * @param {boolean} highlightEnabled Highlight is enabled
+        */
+       mw.rcfilters.ui.FilterMenuHeaderWidget.prototype.onModelHighlightChange = function ( highlightEnabled ) {
+               this.highlightButton.setActive( highlightEnabled );
+       };
+
+       /**
+        * Respond to highlight button click
+        */
+       mw.rcfilters.ui.FilterMenuHeaderWidget.prototype.onHighlightButtonClick = function () {
+               this.controller.toggleHighlight();
+       };
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuOptionWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuOptionWidget.js
new file mode 100644 (file)
index 0000000..b46c069
--- /dev/null
@@ -0,0 +1,147 @@
+( function ( mw ) {
+       /**
+        * A widget representing a single toggle filter
+        *
+        * @extends OO.ui.MenuOptionWidget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller RCFilters controller
+        * @param {mw.rcfilters.dm.FilterItem} model Filter item model
+        * @param {Object} config Configuration object
+        */
+       mw.rcfilters.ui.FilterMenuOptionWidget = function MwRcfiltersUiFilterMenuOptionWidget( controller, model, config ) {
+               var layout,
+                       $label = $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-filterMenuOptionWidget-label' );
+
+               config = config || {};
+
+               this.controller = controller;
+               this.model = model;
+
+               // Parent
+               mw.rcfilters.ui.FilterMenuOptionWidget.parent.call( this, $.extend( {
+                       // Override the 'check' icon that OOUI defines
+                       icon: '',
+                       data: this.model.getName(),
+                       label: this.model.getLabel()
+               }, config ) );
+
+               this.checkboxWidget = new mw.rcfilters.ui.CheckboxInputWidget( {
+                       value: this.model.getName(),
+                       selected: this.model.isSelected()
+               } );
+
+               $label.append(
+                       $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-filterMenuOptionWidget-label-title' )
+                               .append( this.$label )
+               );
+               if ( this.model.getDescription() ) {
+                       $label.append(
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-filterMenuOptionWidget-label-desc' )
+                                       .text( this.model.getDescription() )
+                       );
+               }
+
+               this.highlightButton = new mw.rcfilters.ui.FilterItemHighlightButton(
+                       this.controller,
+                       this.model,
+                       {
+                               $overlay: config.$overlay || this.$element,
+                               title: mw.msg( 'rcfilters-highlightmenu-help' )
+                       }
+               );
+               this.highlightButton.toggle( this.model.isHighlightEnabled() );
+
+               layout = new OO.ui.FieldLayout( this.checkboxWidget, {
+                       label: $label,
+                       align: 'inline'
+               } );
+
+               // Event
+               this.model.connect( this, { update: 'onModelUpdate' } );
+               this.model.getGroupModel().connect( this, { update: 'onGroupModelUpdate' } );
+
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-filterMenuOptionWidget' )
+                       .append(
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-table' )
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 'mw-rcfilters-ui-row' )
+                                                       .append(
+                                                               $( '<div>' )
+                                                                       .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-filterMenuOptionWidget-filterCheckbox' )
+                                                                       .append( layout.$element ),
+                                                               $( '<div>' )
+                                                                       .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-filterMenuOptionWidget-highlightButton' )
+                                                                       .append( this.highlightButton.$element )
+                                                       )
+                                       )
+                       );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( mw.rcfilters.ui.FilterMenuOptionWidget, OO.ui.MenuOptionWidget );
+
+       /* Static properties */
+
+       // We do our own scrolling to top
+       mw.rcfilters.ui.FilterMenuOptionWidget.static.scrollIntoViewOnSelect = false;
+
+       /* Methods */
+
+       /**
+        * Respond to item model update event
+        */
+       mw.rcfilters.ui.FilterMenuOptionWidget.prototype.onModelUpdate = function () {
+               this.checkboxWidget.setSelected( this.model.isSelected() );
+
+               this.setCurrentMuteState();
+       };
+
+       /**
+        * Respond to item group model update event
+        */
+       mw.rcfilters.ui.FilterMenuOptionWidget.prototype.onGroupModelUpdate = function () {
+               this.setCurrentMuteState();
+       };
+
+       /**
+        * Set the current mute state for this item
+        */
+       mw.rcfilters.ui.FilterMenuOptionWidget.prototype.setCurrentMuteState = function () {
+               this.$element.toggleClass(
+                       'mw-rcfilters-ui-filterMenuOptionWidget-muted',
+                       this.model.isConflicted() ||
+                       (
+                               // Item is also muted when any of the items in its group is active
+                               this.model.getGroupModel().isActive() &&
+                               // But it isn't selected
+                               !this.model.isSelected() &&
+                               // And also not included
+                               !this.model.isIncluded()
+                       )
+               );
+
+               this.highlightButton.toggle( this.model.isHighlightEnabled() );
+       };
+
+       /**
+        * Get the name of this filter
+        *
+        * @return {string} Filter name
+        */
+       mw.rcfilters.ui.FilterMenuOptionWidget.prototype.getName = function () {
+               return this.model.getName();
+       };
+
+       mw.rcfilters.ui.FilterMenuOptionWidget.prototype.getModel = function () {
+               return this.model;
+       };
+
+}( mediaWiki ) );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuSectionOptionWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuSectionOptionWidget.js
new file mode 100644 (file)
index 0000000..9f41712
--- /dev/null
@@ -0,0 +1,123 @@
+( function ( mw ) {
+       /**
+        * A widget representing a menu section for filter groups
+        *
+        * @extends OO.ui.MenuSectionOptionWidget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller RCFilters controller
+        * @param {mw.rcfilters.dm.FilterGroup} model Filter group model
+        * @param {Object} config Configuration object
+        * @cfg {jQuery} [$overlay] Overlay
+        */
+       mw.rcfilters.ui.FilterMenuSectionOptionWidget = function MwRcfiltersUiFilterMenuSectionOptionWidget( controller, model, config ) {
+               var whatsThisMessages,
+                       $header = $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-header' ),
+                       $popupContent = $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content' );
+
+               config = config || {};
+
+               this.controller = controller;
+               this.model = model;
+               this.$overlay = config.$overlay || this.$element;
+
+               // Parent
+               mw.rcfilters.ui.FilterMenuSectionOptionWidget.parent.call( this, $.extend( {
+                       label: this.model.getTitle(),
+                       $label: $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-header-title' )
+               }, config ) );
+
+               $header.append( this.$label );
+
+               if ( this.model.hasWhatsThis() ) {
+                       whatsThisMessages = this.model.getWhatsThis();
+
+                       // Create popup
+                       if ( whatsThisMessages.header ) {
+                               $popupContent.append(
+                                       ( new OO.ui.LabelWidget( {
+                                               label: mw.msg( whatsThisMessages.header ),
+                                               classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content-header' ]
+                                       } ) ).$element
+                               );
+                       }
+                       if ( whatsThisMessages.body ) {
+                               $popupContent.append(
+                                       ( new OO.ui.LabelWidget( {
+                                               label: mw.msg( whatsThisMessages.body ),
+                                               classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content-body' ]
+                                       } ) ).$element
+                               );
+                       }
+                       if ( whatsThisMessages.linkText && whatsThisMessages.url ) {
+                               $popupContent.append(
+                                       ( new OO.ui.ButtonWidget( {
+                                               framed: false,
+                                               flags: [ 'progressive' ],
+                                               href: whatsThisMessages.url,
+                                               label: mw.msg( whatsThisMessages.linkText ),
+                                               classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content-link' ]
+                                       } ) ).$element
+                               );
+                       }
+
+                       // Add button
+                       this.whatsThisButton = new OO.ui.PopupButtonWidget( {
+                               framed: false,
+                               label: mw.msg( 'rcfilters-filterlist-whatsthis' ),
+                               $overlay: this.$overlay,
+                               classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton' ],
+                               flags: [ 'progressive' ],
+                               popup: {
+                                       $autoCloseIgnore: this.$element.add( this.$overlay ),
+                                       padded: false,
+                                       align: 'center',
+                                       position: 'above',
+                                       $content: $popupContent,
+                                       classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup' ]
+                               }
+                       } );
+
+                       $header
+                               .append( this.whatsThisButton.$element );
+               }
+
+               // Events
+               this.model.connect( this, { update: 'onModelUpdate' } );
+
+               // Initialize
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget' )
+                       .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-name-' + this.model.getName() )
+                       .append( $header );
+       };
+
+       /* Initialize */
+
+       OO.inheritClass( mw.rcfilters.ui.FilterMenuSectionOptionWidget, OO.ui.MenuSectionOptionWidget );
+
+       /* Methods */
+
+       /**
+        * Respond to model update event
+        */
+       mw.rcfilters.ui.FilterMenuSectionOptionWidget.prototype.onModelUpdate = function () {
+               this.$element.toggleClass(
+                       'mw-rcfilters-ui-filterMenuSectionOptionWidget-active',
+                       this.model.isActive()
+               );
+       };
+
+       /**
+        * Get the group name
+        *
+        * @return {string} Group name
+        */
+       mw.rcfilters.ui.FilterMenuSectionOptionWidget.prototype.getName = function () {
+               return this.model.getName();
+       };
+
+}( mediaWiki ) );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagItemWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagItemWidget.js
new file mode 100644 (file)
index 0000000..cfffc65
--- /dev/null
@@ -0,0 +1,184 @@
+( function ( mw, $ ) {
+       /**
+        * Extend OOUI's FilterTagItemWidget to also display a popup on hover.
+        *
+        * @class
+        * @extends OO.ui.FilterTagItemWidget
+        * @mixins OO.ui.mixin.PopupElement
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller
+        * @param {mw.rcfilters.dm.FilterItem} model Item model
+        * @param {Object} config Configuration object
+        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
+        */
+       mw.rcfilters.ui.FilterTagItemWidget = function MwRcfiltersUiFilterTagItemWidget( controller, model, config ) {
+               // Configuration initialization
+               config = config || {};
+
+               this.controller = controller;
+               this.model = model;
+
+               mw.rcfilters.ui.FilterTagItemWidget.parent.call( this, $.extend( {
+                       data: this.model.getName(),
+                       label: this.model.getLabel()
+               }, config ) );
+
+               this.$overlay = config.$overlay || this.$element;
+               this.popupLabel = new OO.ui.LabelWidget();
+
+               // Mixin constructors
+               OO.ui.mixin.PopupElement.call( this, $.extend( {
+                       popup: {
+                               padded: false,
+                               align: 'center',
+                               position: 'above',
+                               $content: $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-filterTagItemWidget-popup-content' )
+                                       .append( this.popupLabel.$element ),
+                               $floatableContainer: this.$element,
+                               classes: [ 'mw-rcfilters-ui-filterTagItemWidget-popup' ]
+                       }
+               }, config ) );
+
+               this.positioned = false;
+               this.popupTimeoutShow = null;
+               this.popupTimeoutHide = null;
+
+               this.$highlight = $( '<div>' )
+                       .addClass( 'mw-rcfilters-ui-filterTagItemWidget-highlight' );
+
+               // Events
+               this.model.connect( this, { update: 'onModelUpdate' } );
+
+               // Initialization
+               this.$overlay.append( this.popup.$element );
+               this.$element
+                       .prepend( this.$highlight )
+                       .attr( 'aria-haspopup', 'true' )
+                       .on( 'mouseenter', this.onMouseEnter.bind( this ) )
+                       .on( 'mouseleave', this.onMouseLeave.bind( this ) );
+
+               this.setCurrentMuteState();
+               this.setHighlightColor();
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( mw.rcfilters.ui.FilterTagItemWidget, OO.ui.TagItemWidget );
+       OO.mixinClass( mw.rcfilters.ui.FilterTagItemWidget, OO.ui.mixin.PopupElement );
+
+       /* Methods */
+
+       /**
+        * Respond to model update event
+        */
+       mw.rcfilters.ui.FilterTagItemWidget.prototype.onModelUpdate = function () {
+               this.setCurrentMuteState();
+
+               this.setHighlightColor();
+       };
+
+       mw.rcfilters.ui.FilterTagItemWidget.prototype.setHighlightColor = function () {
+               var selectedColor = this.model.isHighlightEnabled() ? this.model.getHighlightColor() : null;
+
+               this.$highlight
+                       .attr( 'data-color', selectedColor )
+                       .toggleClass(
+                               'mw-rcfilters-ui-filterTagItemWidget-highlight-highlighted',
+                               !!selectedColor
+                       );
+       };
+
+       /**
+        * Set the current mute state for this item
+        */
+       mw.rcfilters.ui.FilterTagItemWidget.prototype.setCurrentMuteState = function () {
+               this.$element
+                       .toggleClass(
+                               'mw-rcfilters-ui-filterTagItemWidget-muted',
+                               !this.model.isSelected() ||
+                               this.model.isIncluded() ||
+                               this.model.isFullyCovered()
+                       )
+                       .toggleClass(
+                               'mw-rcfilters-ui-filterTagItemWidget-conflicted',
+                               this.model.isSelected() && this.model.isConflicted()
+                       );
+       };
+
+       /**
+        * Respond to mouse enter event
+        */
+       mw.rcfilters.ui.FilterTagItemWidget.prototype.onMouseEnter = function () {
+               var labelText = this.model.getStateMessage();
+
+               if ( labelText ) {
+                       this.popupLabel.setLabel( labelText );
+
+                       if ( !this.positioned ) {
+                               // Recalculate anchor position to be center of the capsule item
+                               this.popup.$anchor.css( 'margin-left', ( this.$element.width() / 2 ) );
+                               this.positioned = true;
+                       }
+
+                       // Set timeout for the popup to show
+                       this.popupTimeoutShow = setTimeout( function () {
+                               this.popup.toggle( true );
+                       }.bind( this ), 500 );
+
+                       // Cancel the hide timeout
+                       clearTimeout( this.popupTimeoutHide );
+                       this.popupTimeoutHide = null;
+               }
+       };
+
+       /**
+        * Respond to mouse leave event
+        */
+       mw.rcfilters.ui.FilterTagItemWidget.prototype.onMouseLeave = function () {
+               this.popupTimeoutHide = setTimeout( function () {
+                       this.popup.toggle( false );
+               }.bind( this ), 250 );
+
+               // Clear the show timeout
+               clearTimeout( this.popupTimeoutShow );
+               this.popupTimeoutShow = null;
+       };
+
+       /**
+        * Set selected state on this widget
+        *
+        * @param {boolean} [isSelected] Widget is selected
+        */
+       mw.rcfilters.ui.FilterTagItemWidget.prototype.toggleSelected = function ( isSelected ) {
+               isSelected = isSelected !== undefined ? isSelected : !this.selected;
+
+               if ( this.selected !== isSelected ) {
+                       this.selected = isSelected;
+
+                       this.$element.toggleClass( 'mw-rcfilters-ui-filterTagItemWidget-selected', this.selected );
+               }
+       };
+
+       /**
+        * Get item name
+        *
+        * @return {string} Filter name
+        */
+       mw.rcfilters.ui.FilterTagItemWidget.prototype.getName = function () {
+               return this.model.getName();
+       };
+
+       /**
+        * Remove and destroy external elements of this widget
+        */
+       mw.rcfilters.ui.FilterTagItemWidget.prototype.destroy = function () {
+               // Destroy the popup
+               this.popup.$element.detach();
+
+               // Disconnect events
+               this.model.disconnect( this );
+               this.closeButton.disconnect( this );
+       };
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js
new file mode 100644 (file)
index 0000000..5fdcb68
--- /dev/null
@@ -0,0 +1,393 @@
+( function ( mw ) {
+       /**
+        * List displaying all filter groups
+        *
+        * @extends OO.ui.Widget
+        * @mixins OO.ui.mixin.PendingElement
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller Controller
+        * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+        * @param {Object} config Configuration object
+        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
+        */
+       mw.rcfilters.ui.FilterTagMultiselectWidget = function MwRcfiltersUiFilterTagMultiselectWidget( controller, model, config ) {
+               var title = new OO.ui.LabelWidget( {
+                               label: mw.msg( 'rcfilters-activefilters' ),
+                               classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-title' ]
+                       } ),
+                       $contentWrapper = $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper' );
+
+               config = config || {};
+
+               this.controller = controller;
+               this.model = model;
+               this.$overlay = config.$overlay || this.$element;
+
+               // Parent
+               mw.rcfilters.ui.FilterTagMultiselectWidget.parent.call( this, $.extend( true, {
+                       label: mw.msg( 'rcfilters-filterlist-title' ),
+                       placeholder: mw.msg( 'rcfilters-empty-filter' ),
+                       inputPosition: 'outline',
+                       allowArbitrary: false,
+                       allowDisplayInvalidTags: false,
+                       allowReordering: false,
+                       $overlay: this.$overlay,
+                       menu: {
+                               hideWhenOutOfView: false,
+                               hideOnChoose: false,
+                               width: 650,
+                               $footer: $( '<div>' )
+                                       .append(
+                                               new OO.ui.ButtonWidget( {
+                                                       framed: false,
+                                                       icon: 'feedback',
+                                                       flags: [ 'progressive' ],
+                                                       label: mw.msg( 'rcfilters-filterlist-feedbacklink' ),
+                                                       href: 'https://www.mediawiki.org/wiki/Help_talk:New_filters_for_edit_review'
+                                               } ).$element
+                                       )
+                       },
+                       input: {
+                               icon: 'search',
+                               placeholder: mw.msg( 'rcfilters-search-placeholder' )
+                       }
+               }, config ) );
+
+               this.resetButton = new OO.ui.ButtonWidget( {
+                       framed: false,
+                       classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-resetButton' ]
+               } );
+
+               this.emptyFilterMessage = new OO.ui.LabelWidget( {
+                       label: mw.msg( 'rcfilters-empty-filter' ),
+                       classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-emptyFilters' ]
+               } );
+               this.$content.append( this.emptyFilterMessage.$element );
+
+               // Events
+               this.resetButton.connect( this, { click: 'onResetButtonClick' } );
+               // Stop propagation for mousedown, so that the widget doesn't
+               // trigger the focus on the input and scrolls up when we click the reset button
+               this.resetButton.$element.on( 'mousedown', function ( e ) { e.stopPropagation(); } );
+               this.model.connect( this, {
+                       initialize: 'onModelInitialize',
+                       itemUpdate: 'onModelItemUpdate',
+                       highlightChange: 'onModelHighlightChange'
+               } );
+               this.menu.connect( this, { toggle: 'onMenuToggle' } );
+
+               // Build the content
+               $contentWrapper.append(
+                       title.$element,
+                       $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-table' )
+                               .append(
+                                       // The filter list 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-row' )
+                                               .append(
+                                                       this.$content
+                                                               .addClass( 'mw-rcfilters-ui-cell' )
+                                                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-filters' ),
+                                                       $( '<div>' )
+                                                               .addClass( 'mw-rcfilters-ui-cell' )
+                                                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-reset' )
+                                                               .append( this.resetButton.$element )
+                                               )
+                               )
+               );
+
+               // Initialize
+               this.$handle.append( $contentWrapper );
+               this.emptyFilterMessage.toggle( this.isEmpty() );
+
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget' );
+
+               this.populateFromModel();
+               this.reevaluateResetRestoreState();
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( mw.rcfilters.ui.FilterTagMultiselectWidget, OO.ui.MenuTagMultiselectWidget );
+
+       /* Methods */
+
+       /**
+        * Respond to menu toggle
+        *
+        * @param {boolean} isVisible Menu is visible
+        */
+       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onMenuToggle = function ( isVisible ) {
+               if ( isVisible ) {
+                       mw.hook( 'RcFilters.popup.open' ).fire( this.getMenu().getSelectedItem() );
+
+                       if ( !this.getMenu().getSelectedItem() ) {
+                               // If there are no selected items, scroll menu to top
+                               // This has to be in a setTimeout so the menu has time
+                               // to be positioned and fixed
+                               setTimeout( function () { this.getMenu().scrollToTop(); }.bind( this ), 0 );
+                       }
+               } else {
+                       // Clear selection
+                       this.getMenu().selectItem( null );
+               }
+       };
+
+       /**
+        * @inheritdoc
+        */
+       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onInputFocus = function () {
+               // Parent
+               mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onInputFocus.call( this );
+
+               // Scroll to top
+               this.scrollToTop( this.$element );
+       };
+
+       /**
+        * @inheridoc
+        */
+       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onChangeTags = function () {
+               // Parent method
+               mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onChangeTags.call( this );
+
+               this.emptyFilterMessage.toggle( this.isEmpty() );
+       };
+
+       /**
+        * Respond to model initialize event
+        */
+       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelInitialize = function () {
+               this.populateFromModel();
+       };
+
+       /**
+        * Respond to model itemUpdate event
+        *
+        * @param {mw.rcfilters.dm.FilterItem} item Filter item model
+        */
+       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelItemUpdate = function ( item ) {
+               if (
+                       item.isSelected() ||
+                       (
+                               this.model.isHighlightEnabled() &&
+                               item.isHighlightSupported() &&
+                               item.getHighlightColor()
+                       )
+               ) {
+                       this.addTag( item.getName(), item.getLabel() );
+               } else {
+                       this.removeTagByData( item.getName() );
+               }
+
+               // Re-evaluate reset state
+               this.reevaluateResetRestoreState();
+       };
+
+       /**
+        * @inheritdoc
+        */
+       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.isAllowedData = function ( data ) {
+               return (
+                       this.menu.getItemFromData( data ) &&
+                       !this.isDuplicateData( data )
+               );
+       };
+
+       /**
+        * @inheritdoc
+        */
+       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onMenuChoose = function ( item ) {
+               this.controller.toggleFilterSelect( item.model.getName() );
+       };
+
+       /**
+        * Respond to highlightChange event
+        *
+        * @param {boolean} isHighlightEnabled Highlight is enabled
+        */
+       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelHighlightChange = function ( isHighlightEnabled ) {
+               var highlightedItems = this.model.getHighlightedItems();
+
+               if ( isHighlightEnabled ) {
+                       // Add capsule widgets
+                       highlightedItems.forEach( function ( filterItem ) {
+                               this.addTag( filterItem.getName(), filterItem.getLabel() );
+                       }.bind( this ) );
+               } else {
+                       // Remove capsule widgets if they're not selected
+                       highlightedItems.forEach( function ( filterItem ) {
+                               if ( !filterItem.isSelected() ) {
+                                       this.removeTagByData( filterItem.getName() );
+                               }
+                       }.bind( this ) );
+               }
+       };
+
+       /**
+        * @inheritdoc
+        */
+       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onTagSelect = function ( tagItem ) {
+               var widget = this,
+                       menuOption = this.menu.getItemFromData( tagItem.getData() );
+
+               // Reset input
+               this.input.setValue( '' );
+
+               // Parent method
+               mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onTagSelect.call( this, tagItem );
+
+               this.menu.selectItem( menuOption );
+
+               // Scroll to the item
+               // We're binding a 'once' to the itemVisibilityChange event
+               // so this happens when the menu is ready after the items
+               // are visible again, in case this is done right after the
+               // user filtered the results
+               this.getMenu().once(
+                       'itemVisibilityChange',
+                       function () { widget.scrollToTop( menuOption.$element ); }
+               );
+       };
+       /**
+        * @inheritdoc
+        */
+       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onTagRemove = function ( tagItem ) {
+               // Parent method
+               mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onTagRemove.call( this, tagItem );
+
+               this.controller.clearFilter( tagItem.getName() );
+       };
+
+       /**
+        * Respond to click event on the reset button
+        */
+       mw.rcfilters.ui.FilterTagMultiselectWidget.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.FilterTagMultiselectWidget.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.setTitle(
+                       currFiltersAreEmpty ? null : mw.msg( 'rcfilters-clear-all-filters' )
+               );
+
+               this.resetButton.toggle( !hideResetButton );
+               this.emptyFilterMessage.toggle( currFiltersAreEmpty );
+       };
+
+       /**
+        * @inheritdoc
+        */
+       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.createMenuWidget = function ( menuConfig ) {
+               return new mw.rcfilters.ui.FilterFloatingMenuSelectWidget(
+                       this.controller,
+                       this.model,
+                       $.extend( {
+                               filterFromInput: true
+                       }, menuConfig )
+               );
+       };
+
+       /**
+        * Populate the menu from the model
+        */
+       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.populateFromModel = function () {
+               var widget = this,
+                       items = [];
+
+               // Reset
+               this.getMenu().clearItems();
+
+               $.each( this.model.getFilterGroups(), function ( groupName, groupModel ) {
+                       items.push(
+                               // Group section
+                               new mw.rcfilters.ui.FilterMenuSectionOptionWidget(
+                                       widget.controller,
+                                       groupModel,
+                                       {
+                                               $overlay: widget.$overlay
+                                       }
+                               )
+                       );
+
+                       // Add items
+                       widget.model.getGroupFilters( groupName ).forEach( function ( filterItem ) {
+                               items.push(
+                                       new mw.rcfilters.ui.FilterMenuOptionWidget(
+                                               widget.controller,
+                                               filterItem,
+                                               {
+                                                       $overlay: widget.$overlay
+                                               }
+                                       )
+                               );
+                       } );
+               } );
+
+               // Add all items to the menu
+               this.getMenu().addItems( items );
+       };
+
+       /**
+        * @inheritdoc
+        */
+       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.createTagItemWidget = function ( data ) {
+               var filterItem = this.model.getItemByName( data );
+
+               if ( filterItem ) {
+                       return new mw.rcfilters.ui.FilterTagItemWidget(
+                               this.controller,
+                               filterItem,
+                               {
+                                       $overlay: this.$overlay
+                               }
+                       );
+               }
+       };
+
+       /**
+        * Scroll the element to top within its container
+        *
+        * @private
+        * @param {jQuery} $element Element to position
+        * @param {number} [marginFromTop] When scrolling the entire widget to the top, leave this
+        *  much space (in pixels) above the widget.
+        */
+       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.scrollToTop = function ( $element, marginFromTop ) {
+               var container = OO.ui.Element.static.getClosestScrollableContainer( $element[ 0 ], 'y' ),
+                       pos = OO.ui.Element.static.getRelativePosition( $element, $( container ) ),
+                       containerScrollTop = $( container ).is( 'body, html' ) ? 0 : $( container ).scrollTop();
+
+               // Scroll to item
+               $( container ).animate( {
+                       scrollTop: containerScrollTop + pos.top - ( marginFromTop || 0 )
+               } );
+       };
+}( mediaWiki ) );
index e17d028..b7ebf34 100644 (file)
@@ -13,7 +13,6 @@
         * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
         */
        mw.rcfilters.ui.FilterWrapperWidget = function MwRcfiltersUiFilterWrapperWidget( controller, model, config ) {
-               var $footer = $( '<div>' );
                config = config || {};
 
                // Parent
                this.model = model;
                this.$overlay = config.$overlay || this.$element;
 
-               this.filterPopup = new mw.rcfilters.ui.FiltersListWidget(
+               this.filterTagWidget = new mw.rcfilters.ui.FilterTagMultiselectWidget(
                        this.controller,
                        this.model,
-                       {
-                               label: mw.msg( 'rcfilters-filterlist-title' ),
-                               $overlay: this.$overlay
-                       }
+                       { $overlay: this.$overlay }
                );
 
-               $footer.append(
-                       new OO.ui.ButtonWidget( {
-                               framed: false,
-                               icon: 'feedback',
-                               flags: [ 'progressive' ],
-                               label: mw.msg( 'rcfilters-filterlist-feedbacklink' ),
-                               href: 'https://www.mediawiki.org/wiki/Help_talk:New_filters_for_edit_review'
-                       } ).$element
-               );
-
-               this.textInput = new OO.ui.TextInputWidget( {
-                       classes: [ 'mw-rcfilters-ui-filterWrapperWidget-search' ],
-                       icon: 'search',
-                       placeholder: mw.msg( 'rcfilters-search-placeholder' )
-               } );
-
-               this.capsule = new mw.rcfilters.ui.FilterCapsuleMultiselectWidget( controller, this.model, this.textInput, {
-                       $overlay: this.$overlay,
-                       popup: {
-                               $content: this.filterPopup.$element,
-                               $footer: $footer,
-                               classes: [ 'mw-rcfilters-ui-filterWrapperWidget-popup' ],
-                               width: 650,
-                               hideWhenOutOfView: false
-                       }
-               } );
-
-               // Events
-               this.model.connect( this, {
-                       initialize: 'onModelInitialize',
-                       itemUpdate: 'onModelItemUpdate'
-               } );
-               this.textInput.connect( this, {
-                       change: 'onTextInputChange',
-                       enter: 'onTextInputEnter'
-               } );
-               this.capsule.connect( this, { capsuleItemClick: 'onCapsuleItemClick' } );
-               this.capsule.popup.connect( this, {
-                       toggle: 'onCapsulePopupToggle',
-                       ready: 'onCapsulePopupReady'
-               } );
-
                // Initialize
                this.$element
                        .addClass( 'mw-rcfilters-ui-filterWrapperWidget' )
-                       .append( this.capsule.$element, this.textInput.$element );
+                       .append( this.filterTagWidget.$element );
        };
 
        /* Initialization */
 
        OO.inheritClass( mw.rcfilters.ui.FilterWrapperWidget, OO.ui.Widget );
        OO.mixinClass( mw.rcfilters.ui.FilterWrapperWidget, OO.ui.mixin.PendingElement );
-
-       /**
-        * Respond to capsule item click and make the popup scroll down to the requested item
-        *
-        * @param {mw.rcfilters.ui.CapsuleItemWidget} item Clicked item
-        */
-       mw.rcfilters.ui.FilterWrapperWidget.prototype.onCapsuleItemClick = function ( item ) {
-               var filterName = item.getData(),
-                       // Find the item in the popup
-                       filterWidget = this.filterPopup.getItemWidget( filterName );
-
-               // Highlight item
-               this.filterPopup.select( filterName );
-               this.capsule.select( item );
-
-               this.capsule.popup.toggle( true );
-               this.scrollToTop( filterWidget.$element );
-       };
-
-       /**
-        * Respond to capsule popup ready event, fired after the popup is visible, positioned and clipped
-        */
-       mw.rcfilters.ui.FilterWrapperWidget.prototype.onCapsulePopupReady = function () {
-               mw.hook( 'RcFilters.popup.open' ).fire( this.filterPopup.getSelectedFilter() );
-
-               this.scrollToTop( this.capsule.$element, 10 );
-               if ( !this.filterPopup.getSelectedFilter() ) {
-                       // No selection, scroll the popup list to top
-                       setTimeout( function () { this.capsule.popup.$body.scrollTop( 0 ); }.bind( this ), 0 );
-               }
-       };
-
-       /**
-        * Respond to popup toggle event. Reset selection in the list when the popup is closed.
-        *
-        * @param {boolean} isVisible Popup is visible
-        */
-       mw.rcfilters.ui.FilterWrapperWidget.prototype.onCapsulePopupToggle = function ( isVisible ) {
-               if ( !isVisible && !this.textInput.getValue() ) {
-                       // Only reset selection if we are not filtering
-                       this.filterPopup.resetSelection();
-                       this.capsule.resetSelection();
-               }
-       };
-
-       /**
-        * Respond to text input change
-        *
-        * @param {string} newValue Current value
-        */
-       mw.rcfilters.ui.FilterWrapperWidget.prototype.onTextInputChange = function ( newValue ) {
-               // Filter the results
-               this.filterPopup.filter( this.model.findMatches( newValue ) );
-
-               if ( !newValue ) {
-                       // If the value is empty, we didn't actually
-                       // filter anything. the filter method will run
-                       // and show all, but then will select the
-                       // top item - but in this case, no selection
-                       // should be made.
-                       this.filterPopup.resetSelection();
-               }
-               this.capsule.popup.clip();
-       };
-
-       /**
-        * Respond to text input enter event
-        */
-       mw.rcfilters.ui.FilterWrapperWidget.prototype.onTextInputEnter = function () {
-               var filter = this.filterPopup.getSelectedFilter();
-
-               // Toggle the filter
-               if ( filter ) {
-                       this.controller.toggleFilterSelect( filter );
-               }
-       };
-
-       /**
-        * Respond to model update event and set up the available filters to choose
-        * from.
-        */
-       mw.rcfilters.ui.FilterWrapperWidget.prototype.onModelInitialize = function () {
-               var wrapper = this;
-
-               // Add defaults to capsule. We have to do this
-               // after we added to the capsule menu, since that's
-               // how the capsule multiselect widget knows which
-               // object to add
-               this.model.getItems().forEach( function ( filterItem ) {
-                       if ( filterItem.isSelected() ) {
-                               wrapper.capsule.addItemByName( filterItem.getName() );
-                       }
-               } );
-       };
-
-       /**
-        * Respond to item update and reset the selection. This will make it so that
-        * any actual interaction with the system resets the selection state of any item.
-        */
-       mw.rcfilters.ui.FilterWrapperWidget.prototype.onModelItemUpdate = function () {
-               if ( !this.textInput.getValue() ) {
-                       this.filterPopup.resetSelection();
-               }
-       };
-
-       /**
-        * Scroll the element to top within its container
-        *
-        * @private
-        * @param {jQuery} $element Element to position
-        * @param {number} [marginFromTop] When scrolling the entire widget to the top, leave this
-        *  much space (in pixels) above the widget.
-        */
-       mw.rcfilters.ui.FilterWrapperWidget.prototype.scrollToTop = function ( $element, marginFromTop ) {
-               var container = OO.ui.Element.static.getClosestScrollableContainer( $element[ 0 ], 'y' ),
-                       pos = OO.ui.Element.static.getRelativePosition( $element, $( container ) ),
-                       containerScrollTop = $( container ).is( 'body, html' ) ? 0 : $( container ).scrollTop();
-
-               // Scroll to item
-               $( container ).animate( {
-                       scrollTop: containerScrollTop + pos.top - ( marginFromTop || 0 )
-               } );
-       };
 }( mediaWiki ) );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FiltersListWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FiltersListWidget.js
deleted file mode 100644 (file)
index 4011e6d..0000000
+++ /dev/null
@@ -1,256 +0,0 @@
-( function ( mw, $ ) {
-       /**
-        * List displaying all filter groups
-        *
-        * @extends OO.ui.Widget
-        * @mixins OO.ui.mixin.GroupWidget
-        * @mixins OO.ui.mixin.LabelElement
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller Controller
-        * @param {mw.rcfilters.dm.FiltersViewModel} model View model
-        * @param {Object} config Configuration object
-        */
-       mw.rcfilters.ui.FiltersListWidget = function MwRcfiltersUiFiltersListWidget( controller, model, config ) {
-               config = config || {};
-
-               // Parent
-               mw.rcfilters.ui.FiltersListWidget.parent.call( this, config );
-               // Mixin constructors
-               OO.ui.mixin.GroupWidget.call( this, config );
-               OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, {
-                       $label: $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-filtersListWidget-title' )
-               } ) );
-
-               this.controller = controller;
-               this.model = model;
-               this.$overlay = config.$overlay || this.$element;
-               this.groups = {};
-               this.selected = null;
-
-               this.highlightButton = new OO.ui.ToggleButtonWidget( {
-                       icon: 'highlight',
-                       label: mw.message( 'rcfilters-highlightbutton-title' ).text(),
-                       classes: [ 'mw-rcfilters-ui-filtersListWidget-hightlightButton' ]
-               } );
-
-               this.noResultsLabel = new OO.ui.LabelWidget( {
-                       label: mw.msg( 'rcfilters-filterlist-noresults' ),
-                       classes: [ 'mw-rcfilters-ui-filtersListWidget-noresults' ]
-               } );
-
-               // Events
-               this.highlightButton.connect( this, { click: 'onHighlightButtonClick' } );
-               this.model.connect( this, {
-                       initialize: 'onModelInitialize',
-                       highlightChange: 'onModelHighlightChange'
-               } );
-
-               // Initialize
-               this.showNoResultsMessage( false );
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-filtersListWidget' )
-                       .append(
-                               $( '<div>' )
-                                       .addClass( 'mw-rcfilters-ui-table' )
-                                       .addClass( 'mw-rcfilters-ui-filtersListWidget-header' )
-                                       .append(
-                                               $( '<div>' )
-                                                       .addClass( 'mw-rcfilters-ui-row' )
-                                                       .append(
-                                                               $( '<div>' )
-                                                                       .addClass( 'mw-rcfilters-ui-cell' )
-                                                                       .addClass( 'mw-rcfilters-ui-filtersListWidget-header-title' )
-                                                                       .append( this.$label ),
-                                                               $( '<div>' )
-                                                                       .addClass( 'mw-rcfilters-ui-cell' )
-                                                                       .addClass( 'mw-rcfilters-ui-filtersListWidget-header-highlight' )
-                                                                       .append( this.highlightButton.$element )
-                                                       )
-                                       ),
-                               // this.$label,
-                               this.$group
-                                       .addClass( 'mw-rcfilters-ui-filtersListWidget-group' ),
-                               this.noResultsLabel.$element
-                       );
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.FiltersListWidget, OO.ui.Widget );
-       OO.mixinClass( mw.rcfilters.ui.FiltersListWidget, OO.ui.mixin.GroupWidget );
-       OO.mixinClass( mw.rcfilters.ui.FiltersListWidget, OO.ui.mixin.LabelElement );
-
-       /* Methods */
-
-       /**
-        * Respond to initialize event from the model
-        */
-       mw.rcfilters.ui.FiltersListWidget.prototype.onModelInitialize = function () {
-               var widget = this;
-
-               // Reset
-               this.clearItems();
-               this.groups = {};
-
-               this.addItems(
-                       Object.keys( this.model.getFilterGroups() ).map( function ( groupName ) {
-                               var groupWidget = new mw.rcfilters.ui.FilterGroupWidget(
-                                       widget.controller,
-                                       widget.model.getGroup( groupName ),
-                                       {
-                                               $overlay: widget.$overlay
-                                       }
-                               );
-
-                               widget.groups[ groupName ] = groupWidget;
-                               return groupWidget;
-                       } )
-               );
-       };
-
-       /**
-        * Respond to model highlight change event
-        *
-        * @param {boolean} highlightEnabled Highlight is enabled
-        */
-       mw.rcfilters.ui.FiltersListWidget.prototype.onModelHighlightChange = function ( highlightEnabled ) {
-               this.highlightButton.setActive( highlightEnabled );
-       };
-
-       /**
-        * Respond to highlight button click
-        */
-       mw.rcfilters.ui.FiltersListWidget.prototype.onHighlightButtonClick = function () {
-               this.controller.toggleHighlight();
-       };
-
-       /**
-        * Find the filter item widget that corresponds to the item name
-        *
-        * @param {string} itemName Filter name
-        * @return {mw.rcfilters.ui.FilterItemWidget} Filter widget
-        */
-       mw.rcfilters.ui.FiltersListWidget.prototype.getItemWidget = function ( itemName ) {
-               var filterItem = this.model.getItemByName( itemName ),
-                       // Find the group
-                       groupWidget = this.groups[ filterItem.getGroupName() ];
-
-               // Find the item inside the group
-               return groupWidget.getItemWidget( itemName );
-       };
-
-       /**
-        * Get the current selection
-        *
-        * @return {string|null} Selected filter. Null if none is selected.
-        */
-       mw.rcfilters.ui.FiltersListWidget.prototype.getSelectedFilter = function () {
-               return this.selected;
-       };
-
-       /**
-        * Mark an item widget as selected
-        *
-        * @param {string} itemName Filter name
-        */
-       mw.rcfilters.ui.FiltersListWidget.prototype.select = function ( itemName ) {
-               var filterWidget;
-
-               if ( this.selected !== itemName ) {
-                       // Unselect previous
-                       if ( this.selected ) {
-                               filterWidget = this.getItemWidget( this.selected );
-                               filterWidget.toggleSelected( false );
-                       }
-
-                       // Select new one
-                       this.selected = itemName;
-                       if ( this.selected ) {
-                               filterWidget = this.getItemWidget( this.selected );
-                               filterWidget.toggleSelected( true );
-                       }
-               }
-       };
-
-       /**
-        * Reset selection and remove selected states from all items
-        */
-       mw.rcfilters.ui.FiltersListWidget.prototype.resetSelection = function () {
-               if ( this.selected !== null ) {
-                       this.selected = null;
-                       this.getItems().forEach( function ( groupWidget ) {
-                               groupWidget.getItems().forEach( function ( filterItemWidget ) {
-                                       filterItemWidget.toggleSelected( false );
-                               } );
-                       } );
-               }
-       };
-
-       /**
-        * Switch between showing the 'no results' message for filtering results or the result list.
-        *
-        * @param {boolean} showNoResults Show no results message
-        */
-       mw.rcfilters.ui.FiltersListWidget.prototype.showNoResultsMessage = function ( showNoResults ) {
-               this.noResultsLabel.toggle( !!showNoResults );
-               this.$group.toggleClass( 'oo-ui-element-hidden', !!showNoResults );
-       };
-
-       /**
-        * Show only the items matching with the models in the given list
-        *
-        * @param {Object} groupItems An object of items to show
-        *  arranged by their group names
-        */
-       mw.rcfilters.ui.FiltersListWidget.prototype.filter = function ( groupItems ) {
-               var i, j, groupName, itemWidgets, topItem, isVisible,
-                       groupWidgets = this.getItems(),
-                       hasItemWithName = function ( itemArr, name ) {
-                               return !!itemArr.filter( function ( item ) {
-                                       return item.getName() === name;
-                               } ).length;
-                       };
-
-               this.resetSelection();
-
-               if ( $.isEmptyObject( groupItems ) ) {
-                       // No results. Hide everything, show only 'no results'
-                       // message
-                       this.showNoResultsMessage( true );
-                       return;
-               }
-
-               this.showNoResultsMessage( false );
-               for ( i = 0; i < groupWidgets.length; i++ ) {
-                       groupName = groupWidgets[ i ].getName();
-
-                       // If this group widget is in the filtered results,
-                       // show it - otherwise, hide it
-                       groupWidgets[ i ].toggle( !!groupItems[ groupName ] );
-
-                       if ( !groupItems[ groupName ] ) {
-                               // Continue to next group
-                               continue;
-                       }
-
-                       // We have items to show
-                       itemWidgets = groupWidgets[ i ].getItems();
-                       for ( j = 0; j < itemWidgets.length; j++ ) {
-                               isVisible = hasItemWithName( groupItems[ groupName ], itemWidgets[ j ].getName() );
-                               // Only show items that are in the filtered list
-                               itemWidgets[ j ].toggle( isVisible );
-
-                               if ( !topItem && isVisible ) {
-                                       topItem = itemWidgets[ j ];
-                               }
-                       }
-               }
-
-               // Select the first item
-               if ( topItem ) {
-                       this.select( topItem.getName() );
-               }
-       };
-}( mediaWiki, jQuery ) );