From a703e5236b69949fb50658e90c5cce53d8f4625a Mon Sep 17 00:00:00 2001 From: Moriel Schottlender Date: Mon, 27 Mar 2017 09:58:29 -0700 Subject: [PATCH] RCFilters: Adjust to use MenuTagMultiselectWidget 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 --- resources/Resources.php | 22 +- .../dm/mw.rcfilters.dm.FiltersViewModel.js | 11 +- ...ers.ui.FilterFloatingMenuSelectWidget.less | 16 + ....rcfilters.ui.FilterMenuHeaderWidget.less} | 10 +- ....rcfilters.ui.FilterMenuOptionWidget.less} | 9 +- ...ers.ui.FilterMenuSectionOptionWidget.less} | 18 +- ... mw.rcfilters.ui.FilterTagItemWidget.less} | 42 +- ...ilters.ui.FilterTagMultiselectWidget.less} | 7 +- .../mw.rcfilters.ui.FilterWrapperWidget.less | 15 - .../ui/mw.rcfilters.ui.CheckboxInputWidget.js | 24 +- ...lters.ui.FilterCapsuleMultiselectWidget.js | 342 --------------- ...lters.ui.FilterFloatingMenuSelectWidget.js | 137 ++++++ .../ui/mw.rcfilters.ui.FilterGroupWidget.js | 171 -------- ....rcfilters.ui.FilterItemHighlightButton.js | 6 +- .../mw.rcfilters.ui.FilterMenuHeaderWidget.js | 86 ++++ ...mw.rcfilters.ui.FilterMenuOptionWidget.js} | 83 ++-- ...ilters.ui.FilterMenuSectionOptionWidget.js | 123 ++++++ ...=> mw.rcfilters.ui.FilterTagItemWidget.js} | 101 ++--- ...rcfilters.ui.FilterTagMultiselectWidget.js | 393 ++++++++++++++++++ .../ui/mw.rcfilters.ui.FilterWrapperWidget.js | 175 +------- .../ui/mw.rcfilters.ui.FiltersListWidget.js | 256 ------------ 21 files changed, 930 insertions(+), 1117 deletions(-) create mode 100644 resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.less rename resources/src/mediawiki.rcfilters/styles/{mw.rcfilters.ui.FiltersListWidget.less => mw.rcfilters.ui.FilterMenuHeaderWidget.less} (78%) rename resources/src/mediawiki.rcfilters/styles/{mw.rcfilters.ui.FilterItemWidget.less => mw.rcfilters.ui.FilterMenuOptionWidget.less} (81%) rename resources/src/mediawiki.rcfilters/styles/{mw.rcfilters.ui.FilterGroupWidget.less => mw.rcfilters.ui.FilterMenuSectionOptionWidget.less} (72%) rename resources/src/mediawiki.rcfilters/styles/{mw.rcfilters.ui.CapsuleItemWidget.less => mw.rcfilters.ui.FilterTagItemWidget.less} (51%) rename resources/src/mediawiki.rcfilters/styles/{mw.rcfilters.ui.FilterCapsuleMultiselectWidget.less => mw.rcfilters.ui.FilterTagMultiselectWidget.less} (72%) delete mode 100644 resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.js create mode 100644 resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.js delete mode 100644 resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterGroupWidget.js create mode 100644 resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuHeaderWidget.js rename resources/src/mediawiki.rcfilters/ui/{mw.rcfilters.ui.FilterItemWidget.js => mw.rcfilters.ui.FilterMenuOptionWidget.js} (57%) create mode 100644 resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuSectionOptionWidget.js rename resources/src/mediawiki.rcfilters/ui/{mw.rcfilters.ui.CapsuleItemWidget.js => mw.rcfilters.ui.FilterTagItemWidget.js} (58%) create mode 100644 resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js delete mode 100644 resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FiltersListWidget.js diff --git a/resources/Resources.php b/resources/Resources.php index 43582f041a..64ef3bcdec 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -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', diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js index 8fd15531ae..69210be19f 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js @@ -734,13 +734,18 @@ * 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 @@ -751,6 +756,7 @@ 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 ] ); } } @@ -765,11 +771,12 @@ ) { 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.FilterFloatingMenuSelectWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.less new file mode 100644 index 0000000000..194b1b21d0 --- /dev/null +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.less @@ -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.FiltersListWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuHeaderWidget.less similarity index 78% rename from resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FiltersListWidget.less rename to resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuHeaderWidget.less index cb87989ad7..1f3d34392b 100644 --- a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FiltersListWidget.less +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuHeaderWidget.less @@ -1,4 +1,6 @@ -.mw-rcfilters-ui-filtersListWidget { +@import 'mediawiki.mixins'; + +.mw-rcfilters-ui-filterMenuHeaderWidget { &-title { font-size: 1.2em; padding: 0.75em 0.5em; @@ -25,10 +27,4 @@ vertical-align: middle; } } - - &-noresults { - padding: 0.5em; - // TODO: Unify colors with official design palette - color: #666; - } } diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuOptionWidget.less similarity index 81% rename from resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemWidget.less rename to resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuOptionWidget.less index 3d63831ccc..9d78f854c9 100644 --- a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemWidget.less +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuOptionWidget.less @@ -1,6 +1,6 @@ @import 'mediawiki.mixins'; -.mw-rcfilters-ui-filterItemWidget { +.mw-rcfilters-ui-filterMenuOptionWidget { padding: 0 0.5em; .box-sizing( border-box ); @@ -18,13 +18,13 @@ &-muted { background-color: #f8f9fa; // Base90 AAA - .mw-rcfilters-ui-filterItemWidget-label-title, - .mw-rcfilters-ui-filterItemWidget-label-desc { + .mw-rcfilters-ui-filterMenuOptionWidget-label-title, + .mw-rcfilters-ui-filterMenuOptionWidget-label-desc { color: #54595d; // Base20 AAA } } - &-selected { + &.oo-ui-optionWidget-selected { background-color: #eaf3ff; // Accent90 AAA } @@ -36,6 +36,7 @@ } &-desc { color: #464a4f; + white-space: normal; } } diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterGroupWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuSectionOptionWidget.less similarity index 72% rename from resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterGroupWidget.less rename to resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuSectionOptionWidget.less index 18fb4e6bde..964f27e0fc 100644 --- a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterGroupWidget.less +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuSectionOptionWidget.less @@ -1,24 +1,23 @@ @import 'mediawiki.mixins'; -.mw-rcfilters-ui-filterGroupWidget { - padding-bottom: 0.5em; +.mw-rcfilters-ui-filterMenuSectionOptionWidget { + background: #eaecf0; + padding-bottom: 0.7em; &-header { - background: #eaecf0; - padding: 0.5em 0.75em; - - &-title { + 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 { - display: inline-block; margin-left: 1.5em; - &.oo-ui-buttonElement { vertical-align: text-bottom; @@ -45,7 +44,6 @@ &-link { margin: 1em 0; - } .oo-ui-buttonElement-frameless.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label { @@ -55,7 +53,7 @@ } &-active { - .mw-rcfilters-ui-filterGroupWidget-header-title { + .mw-rcfilters-ui-filterMenuSectionOptionWidget-header-title { font-weight: bold; } } diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.CapsuleItemWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagItemWidget.less similarity index 51% rename from resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.CapsuleItemWidget.less rename to resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagItemWidget.less index b16e84c61c..c82903e757 100644 --- a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.CapsuleItemWidget.less +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagItemWidget.less @@ -1,13 +1,13 @@ @import 'mw.rcfilters.mixins'; -.mw-rcfilters-ui-capsuleItemWidget { +.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-capsuleItemWidget.oo-ui-widget-enabled { + &-muted.oo-ui-tagItemWidget.oo-ui-widget-enabled { // Muted state background-color: #eaecf0; border-color: #c8ccd1; @@ -20,7 +20,7 @@ } } - &-conflicted.oo-ui-capsuleItemWidget.oo-ui-widget-enabled { + &-conflicted.oo-ui-tagItemWidget.oo-ui-widget-enabled { background-color: #fee7e6; // Red90 AAA border-color: #b32424; // Red30 AAA @@ -32,7 +32,7 @@ } } - &-selected.oo-ui-capsuleItemWidget.oo-ui-widget-enabled { + &-selected.oo-ui-tagItemWidget.oo-ui-widget-enabled { background-color: #eaf3ff; border-color: #36c; } @@ -49,27 +49,39 @@ &-highlight { display: none; - padding-right: 0.5em; + margin-right: 0.5em; + height: 100%; + width: 10px; &-highlighted { display: inline-block; + } + &:before { + content: ''; + position: absolute; + display: block; + top: 50%; } - &[data-color='c1'] { - .mw-rcfilters-mixin-circle( @highlight-c1, 10px, ~'0 0.5em 0 0' ); + &[data-color='c1']:before { + .mw-rcfilters-mixin-circle( @highlight-c1, 10px, ~'-5px 0.5em 0 0' ); } - &[data-color='c2'] { - .mw-rcfilters-mixin-circle( @highlight-c2, 10px, ~'0 0.5em 0 0' ); + + &[data-color='c2']:before { + .mw-rcfilters-mixin-circle( @highlight-c2, 10px, ~'-5px 0.5em 0 0' ); } - &[data-color='c3'] { - .mw-rcfilters-mixin-circle( @highlight-c3, 10px, ~'0 0.5em 0 0' ); + + &[data-color='c3']:before { + .mw-rcfilters-mixin-circle( @highlight-c3, 10px, ~'-5px 0.5em 0 0' ); } - &[data-color='c4'] { - .mw-rcfilters-mixin-circle( @highlight-c4, 10px, ~'0 0.5em 0 0' ); + + &[data-color='c4']:before { + .mw-rcfilters-mixin-circle( @highlight-c4, 10px, ~'-5px 0.5em 0 0' ); } - &[data-color='c5'] { - .mw-rcfilters-mixin-circle( @highlight-c5, 10px, ~'0 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.FilterCapsuleMultiselectWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagMultiselectWidget.less similarity index 72% rename from resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.less rename to resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagMultiselectWidget.less index b9dd3c16f6..7371fdd048 100644 --- a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.less +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagMultiselectWidget.less @@ -1,7 +1,9 @@ -.mw-rcfilters-ui-filterCapsuleMultiselectWidget { +.mw-rcfilters-ui-filterTagMultiselectWidget { max-width: none; - &.oo-ui-widget-enabled .oo-ui-capsuleMultiselectWidget-handle { + &.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; @@ -24,6 +26,7 @@ &-cell-filters { width: 100%; } + &-cell-reset { text-align: right; padding-left: 0.5em; diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less index b22abc62c7..f52b7ff9eb 100644 --- a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less @@ -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/ui/mw.rcfilters.ui.CheckboxInputWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CheckboxInputWidget.js index 86b3b11f24..9fd4593944 100644 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CheckboxInputWidget.js +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CheckboxInputWidget.js @@ -14,7 +14,16 @@ 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 */ @@ -32,6 +41,19 @@ /* 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 index f4f460dca9..0000000000 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.js +++ /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 = $( '
' ) - .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, - $( '
' ) - .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 - $( '
' ) - .addClass( 'mw-rcfilters-ui-row' ) - .append( - this.$content - .addClass( 'mw-rcfilters-ui-cell' ) - .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-cell-filters' ), - $( '
' ) - .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 index 0000000000..ec85df98da --- /dev/null +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.js @@ -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 = $( '
' ) + .addClass( 'mw-rcfilters-ui-filterFloatingMenuSelectWidget-body' ); + + // Parent + mw.rcfilters.ui.FilterFloatingMenuSelectWidget.parent.call( this, $.extend( { + $autoCloseIgnore: this.$overlay, + width: 650 + }, config ) ); + this.setGroupElement( + $( '
' ) + .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 index e19208aa30..0000000000 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterGroupWidget.js +++ /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 = $( '
' ) - .addClass( 'mw-rcfilters-ui-filterGroupWidget-header' ), - $popupContent = $( '
' ) - .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: $( '
' ) - .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 ) ); diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemHighlightButton.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemHighlightButton.js index 889ba08cf4..1e0c8d5d74 100644 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemHighlightButton.js +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemHighlightButton.js @@ -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: { @@ -39,6 +39,10 @@ // 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.FilterMenuHeaderWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuHeaderWidget.js new file mode 100644 index 0000000000..15e7eee765 --- /dev/null +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuHeaderWidget.js @@ -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: $( '
' ) + .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( + $( '
' ) + .addClass( 'mw-rcfilters-ui-table' ) + .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header' ) + .append( + $( '
' ) + .addClass( 'mw-rcfilters-ui-row' ) + .append( + $( '
' ) + .addClass( 'mw-rcfilters-ui-cell' ) + .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header-title' ) + .append( this.$label ), + $( '
' ) + .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.FilterItemWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuOptionWidget.js similarity index 57% rename from resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemWidget.js rename to resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuOptionWidget.js index 7e6d776925..b46c069823 100644 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemWidget.js +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuOptionWidget.js @@ -1,27 +1,31 @@ -( function ( mw, $ ) { +( function ( mw ) { /** * A widget representing a single toggle filter * - * @extends OO.ui.Widget + * @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.FilterItemWidget = function MwRcfiltersUiFilterItemWidget( controller, model, config ) { + mw.rcfilters.ui.FilterMenuOptionWidget = function MwRcfiltersUiFilterMenuOptionWidget( controller, model, config ) { var layout, $label = $( '
' ) - .addClass( 'mw-rcfilters-ui-filterItemWidget-label' ); + .addClass( 'mw-rcfilters-ui-filterMenuOptionWidget-label' ); config = config || {}; - // Parent - mw.rcfilters.ui.FilterItemWidget.parent.call( this, config ); - this.controller = controller; this.model = model; - this.selected = false; + + // 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(), @@ -30,13 +34,13 @@ $label.append( $( '
' ) - .addClass( 'mw-rcfilters-ui-filterItemWidget-label-title' ) - .text( this.model.getLabel() ) + .addClass( 'mw-rcfilters-ui-filterMenuOptionWidget-label-title' ) + .append( this.$label ) ); if ( this.model.getDescription() ) { $label.append( $( '
' ) - .addClass( 'mw-rcfilters-ui-filterItemWidget-label-desc' ) + .addClass( 'mw-rcfilters-ui-filterMenuOptionWidget-label-desc' ) .text( this.model.getDescription() ) ); } @@ -57,12 +61,11 @@ } ); // 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' ) + .addClass( 'mw-rcfilters-ui-filterMenuOptionWidget' ) .append( $( '
' ) .addClass( 'mw-rcfilters-ui-table' ) @@ -71,10 +74,10 @@ .addClass( 'mw-rcfilters-ui-row' ) .append( $( '
' ) - .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-filterItemWidget-filterCheckbox' ) + .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-filterMenuOptionWidget-filterCheckbox' ) .append( layout.$element ), $( '
' ) - .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-filterItemWidget-highlightButton' ) + .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-filterMenuOptionWidget-highlightButton' ) .append( this.highlightButton.$element ) ) ) @@ -83,25 +86,19 @@ /* Initialization */ - OO.inheritClass( mw.rcfilters.ui.FilterItemWidget, OO.ui.Widget ); + OO.inheritClass( mw.rcfilters.ui.FilterMenuOptionWidget, OO.ui.MenuOptionWidget ); - /* Methods */ + /* Static properties */ - /** - * 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 ); - }; + // We do our own scrolling to top + mw.rcfilters.ui.FilterMenuOptionWidget.static.scrollIntoViewOnSelect = false; + + /* Methods */ /** * Respond to item model update event */ - mw.rcfilters.ui.FilterItemWidget.prototype.onModelUpdate = function () { + mw.rcfilters.ui.FilterMenuOptionWidget.prototype.onModelUpdate = function () { this.checkboxWidget.setSelected( this.model.isSelected() ); this.setCurrentMuteState(); @@ -110,31 +107,16 @@ /** * Respond to item group model update event */ - mw.rcfilters.ui.FilterItemWidget.prototype.onGroupModelUpdate = function () { + mw.rcfilters.ui.FilterMenuOptionWidget.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 () { + mw.rcfilters.ui.FilterMenuOptionWidget.prototype.setCurrentMuteState = function () { this.$element.toggleClass( - 'mw-rcfilters-ui-filterItemWidget-muted', + 'mw-rcfilters-ui-filterMenuOptionWidget-muted', this.model.isConflicted() || ( // Item is also muted when any of the items in its group is active @@ -154,7 +136,12 @@ * * @return {string} Filter name */ - mw.rcfilters.ui.FilterItemWidget.prototype.getName = function () { + mw.rcfilters.ui.FilterMenuOptionWidget.prototype.getName = function () { return this.model.getName(); }; -}( mediaWiki, jQuery ) ); + + 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 index 0000000000..9f417120a3 --- /dev/null +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuSectionOptionWidget.js @@ -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 = $( '
' ) + .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-header' ), + $popupContent = $( '
' ) + .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: $( '
' ) + .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.CapsuleItemWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagItemWidget.js similarity index 58% rename from resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js rename to resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagItemWidget.js index 20eff8bec8..cfffc65de2 100644 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagItemWidget.js @@ -1,9 +1,9 @@ ( function ( mw, $ ) { /** - * Extend OOUI's CapsuleItemWidget to also display a popup on hover. + * Extend OOUI's FilterTagItemWidget to also display a popup on hover. * * @class - * @extends OO.ui.CapsuleItemWidget + * @extends OO.ui.FilterTagItemWidget * @mixins OO.ui.mixin.PopupElement * * @constructor @@ -12,24 +12,21 @@ * @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 ) { + mw.rcfilters.ui.FilterTagItemWidget = function MwRcfiltersUiFilterTagItemWidget( 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( { + 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: { @@ -37,15 +34,19 @@ align: 'center', position: 'above', $content: $( '
' ) - .addClass( 'mw-rcfilters-ui-capsuleItemWidget-popup-content' ) + .addClass( 'mw-rcfilters-ui-filterTagItemWidget-popup-content' ) .append( this.popupLabel.$element ), $floatableContainer: this.$element, - classes: [ 'mw-rcfilters-ui-capsuleItemWidget-popup' ] + classes: [ 'mw-rcfilters-ui-filterTagItemWidget-popup' ] } }, config ) ); + this.positioned = false; + this.popupTimeoutShow = null; + this.popupTimeoutHide = null; + this.$highlight = $( '
' ) - .addClass( 'mw-rcfilters-ui-capsuleItemWidget-highlight' ); + .addClass( 'mw-rcfilters-ui-filterTagItemWidget-highlight' ); // Events this.model.connect( this, { update: 'onModelUpdate' } ); @@ -55,8 +56,6 @@ 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 ) ); @@ -64,58 +63,29 @@ this.setHighlightColor(); }; - OO.inheritClass( mw.rcfilters.ui.CapsuleItemWidget, OO.ui.CapsuleItemWidget ); - OO.mixinClass( mw.rcfilters.ui.CapsuleItemWidget, OO.ui.mixin.PopupElement ); + /* 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.CapsuleItemWidget.prototype.onModelUpdate = function () { + mw.rcfilters.ui.FilterTagItemWidget.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 () { + 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-capsuleItemWidget-highlight-highlighted', + 'mw-rcfilters-ui-filterTagItemWidget-highlight-highlighted', !!selectedColor ); }; @@ -123,16 +93,16 @@ /** * Set the current mute state for this item */ - mw.rcfilters.ui.CapsuleItemWidget.prototype.setCurrentMuteState = function () { + mw.rcfilters.ui.FilterTagItemWidget.prototype.setCurrentMuteState = function () { this.$element .toggleClass( - 'mw-rcfilters-ui-capsuleItemWidget-muted', + 'mw-rcfilters-ui-filterTagItemWidget-muted', !this.model.isSelected() || this.model.isIncluded() || this.model.isFullyCovered() ) .toggleClass( - 'mw-rcfilters-ui-capsuleItemWidget-conflicted', + 'mw-rcfilters-ui-filterTagItemWidget-conflicted', this.model.isSelected() && this.model.isConflicted() ); }; @@ -140,7 +110,7 @@ /** * Respond to mouse enter event */ - mw.rcfilters.ui.CapsuleItemWidget.prototype.onMouseEnter = function () { + mw.rcfilters.ui.FilterTagItemWidget.prototype.onMouseEnter = function () { var labelText = this.model.getStateMessage(); if ( labelText ) { @@ -166,7 +136,7 @@ /** * Respond to mouse leave event */ - mw.rcfilters.ui.CapsuleItemWidget.prototype.onMouseLeave = function () { + mw.rcfilters.ui.FilterTagItemWidget.prototype.onMouseLeave = function () { this.popupTimeoutHide = setTimeout( function () { this.popup.toggle( false ); }.bind( this ), 250 ); @@ -181,20 +151,29 @@ * * @param {boolean} [isSelected] Widget is selected */ - mw.rcfilters.ui.CapsuleItemWidget.prototype.toggleSelected = function ( isSelected ) { + 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-capsuleItemWidget-selected', this.selected ); + 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.CapsuleItemWidget.prototype.destroy = function () { + mw.rcfilters.ui.FilterTagItemWidget.prototype.destroy = function () { // Destroy the popup this.popup.$element.detach(); 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 index 0000000000..5fdcb68930 --- /dev/null +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js @@ -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 = $( '
' ) + .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: $( '
' ) + .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, + $( '
' ) + .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 + $( '
' ) + .addClass( 'mw-rcfilters-ui-row' ) + .append( + this.$content + .addClass( 'mw-rcfilters-ui-cell' ) + .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-filters' ), + $( '
' ) + .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 ) ); diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js index e17d028ab7..b7ebf3423a 100644 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js @@ -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 = $( '
' ); config = config || {}; // Parent @@ -25,188 +24,20 @@ 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 index 4011e6d5d2..0000000000 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FiltersListWidget.js +++ /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: $( '
' ) - .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( - $( '
' ) - .addClass( 'mw-rcfilters-ui-table' ) - .addClass( 'mw-rcfilters-ui-filtersListWidget-header' ) - .append( - $( '
' ) - .addClass( 'mw-rcfilters-ui-row' ) - .append( - $( '
' ) - .addClass( 'mw-rcfilters-ui-cell' ) - .addClass( 'mw-rcfilters-ui-filtersListWidget-header-title' ) - .append( this.$label ), - $( '
' ) - .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 ) ); -- 2.20.1