From 856cc85f4b8650038eced7211610fa11dd27c5bd Mon Sep 17 00:00:00 2001 From: Moriel Schottlender Date: Tue, 25 Apr 2017 11:26:20 -0700 Subject: [PATCH] RCFilters UI: Create base classes for shared objects Preparing for adding other types of items than filters (namespaces, users, tags, etc) this commit creates base classes for the model and relevant widgets and extends Filter* from them. Bug: T159942 Bug: T163521 Change-Id: I61c88a1f14a3ca9d91aa831187eda156468a6591 --- resources/Resources.php | 10 +- .../dm/mw.rcfilters.dm.FilterItem.js | 199 +------------- .../dm/mw.rcfilters.dm.ItemModel.js | 257 ++++++++++++++++++ ...w.rcfilters.ui.FilterMenuOptionWidget.less | 54 +--- ...cfilters.ui.FloatingMenuSelectWidget.less} | 2 +- .../mw.rcfilters.ui.ItemMenuOptionWidget.less | 51 ++++ ...ess => mw.rcfilters.ui.TagItemWidget.less} | 2 +- .../mw.rcfilters.ui.FilterMenuOptionWidget.js | 119 ++------ .../ui/mw.rcfilters.ui.FilterTagItemWidget.js | 163 +---------- ...rcfilters.ui.FilterTagMultiselectWidget.js | 2 +- ....rcfilters.ui.FloatingMenuSelectWidget.js} | 24 +- .../mw.rcfilters.ui.ItemMenuOptionWidget.js | 125 +++++++++ .../ui/mw.rcfilters.ui.TagItemWidget.js | 183 +++++++++++++ 13 files changed, 678 insertions(+), 513 deletions(-) create mode 100644 resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ItemModel.js rename resources/src/mediawiki.rcfilters/styles/{mw.rcfilters.ui.FilterFloatingMenuSelectWidget.less => mw.rcfilters.ui.FloatingMenuSelectWidget.less} (88%) create mode 100644 resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ItemMenuOptionWidget.less rename resources/src/mediawiki.rcfilters/styles/{mw.rcfilters.ui.FilterTagItemWidget.less => mw.rcfilters.ui.TagItemWidget.less} (97%) rename resources/src/mediawiki.rcfilters/ui/{mw.rcfilters.ui.FilterFloatingMenuSelectWidget.js => mw.rcfilters.ui.FloatingMenuSelectWidget.js} (71%) create mode 100644 resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ItemMenuOptionWidget.js create mode 100644 resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.TagItemWidget.js diff --git a/resources/Resources.php b/resources/Resources.php index 715cdb8c9e..1721de807c 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -1742,6 +1742,7 @@ return [ 'mediawiki.rcfilters.filters.dm' => [ 'scripts' => [ 'resources/src/mediawiki.rcfilters/mw.rcfilters.js', + 'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ItemModel.js', 'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js', 'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js', 'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js', @@ -1757,11 +1758,13 @@ return [ 'scripts' => [ 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CheckboxInputWidget.js', 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js', + 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ItemMenuOptionWidget.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.TagItemWidget.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.FloatingMenuSelectWidget.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', @@ -1776,11 +1779,12 @@ return [ '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.FilterTagMultiselectWidget.less', + 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ItemMenuOptionWidget.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.TagItemWidget.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.FloatingMenuSelectWidget.less', 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less', 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ChangesListWrapperWidget.less', 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.HighlightColorPickerWidget.less', diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js index 221d2a54c6..4e2079dc40 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js @@ -2,36 +2,28 @@ /** * Filter item model * - * @mixins OO.EventEmitter + * @extends mw.rcfilters.dm.ItemModel * * @constructor * @param {string} param Filter param name * @param {mw.rcfilters.dm.FilterGroup} groupModel Filter group model * @param {Object} config Configuration object - * @cfg {string} [group] The group this item belongs to - * @cfg {string} [label] The label for the filter - * @cfg {string} [description] The description of the filter - * @cfg {boolean} [active=true] The filter is active and affecting the result * @cfg {string[]} [excludes=[]] A list of filter names this filter, if * selected, makes inactive. - * @cfg {boolean} [selected] The item is selected * @cfg {string[]} [subset] Defining the names of filters that are a subset of this filter * @cfg {Object} [conflicts] Defines the conflicts for this filter - * @cfg {string} [cssClass] The class identifying the results that match this filter */ mw.rcfilters.dm.FilterItem = function MwRcfiltersDmFilterItem( param, groupModel, config ) { config = config || {}; - // Mixin constructor - OO.EventEmitter.call( this ); - - this.param = param; this.groupModel = groupModel; - this.name = this.groupModel.getNamePrefix() + param; - this.label = config.label || this.name; - this.description = config.description; - this.selected = !!config.selected; + // Parent + mw.rcfilters.dm.FilterItem.parent.call( this, param, $.extend( { + namePrefix: this.groupModel.getNamePrefix() + }, config ) ); + // Mixin constructor + OO.EventEmitter.call( this ); // Interaction definitions this.subset = config.subset || []; @@ -42,25 +34,11 @@ this.included = false; this.conflicted = false; this.fullyCovered = false; - - // Highlight - this.cssClass = config.cssClass; - this.highlightColor = null; - this.highlightEnabled = false; }; /* Initialization */ - OO.initClass( mw.rcfilters.dm.FilterItem ); - OO.mixinClass( mw.rcfilters.dm.FilterItem, OO.EventEmitter ); - - /* Events */ - - /** - * @event update - * - * The state of this filter has changed - */ + OO.inheritClass( mw.rcfilters.dm.FilterItem, mw.rcfilters.dm.ItemModel ); /* Methods */ @@ -78,27 +56,10 @@ }; }; - /** - * Get the name of this filter - * - * @return {string} Filter name - */ - mw.rcfilters.dm.FilterItem.prototype.getName = function () { - return this.name; - }; - - /** - * Get the param name or value of this filter - * - * @return {string} Filter param name - */ - mw.rcfilters.dm.FilterItem.prototype.getParamName = function () { - return this.param; - }; - /** * Get the message for the display area for the currently active conflict * + * @private * @return {string} Conflict result message key */ mw.rcfilters.dm.FilterItem.prototype.getCurrentConflictResultMessage = function () { @@ -117,6 +78,7 @@ /** * Get the details of the active conflict on this filter * + * @private * @param {Object} conflicts Conflicts to examine * @param {string} [key='contextDescription'] Message key * @return {Object} Object with conflict message and conflict items @@ -153,9 +115,7 @@ }; /** - * Get the message representing the state of this model. - * - * @return {string} State message + * @inheritdoc */ mw.rcfilters.dm.FilterItem.prototype.getStateMessage = function () { var messageKey, details, superset, @@ -227,33 +187,6 @@ return this.groupModel.getName(); }; - /** - * Get the label of this filter - * - * @return {string} Filter label - */ - mw.rcfilters.dm.FilterItem.prototype.getLabel = function () { - return this.label; - }; - - /** - * Get the description of this filter - * - * @return {string} Filter description - */ - mw.rcfilters.dm.FilterItem.prototype.getDescription = function () { - return this.description; - }; - - /** - * Get the default value of this filter - * - * @return {boolean} Filter default - */ - mw.rcfilters.dm.FilterItem.prototype.getDefault = function () { - return this.default; - }; - /** * Get filter subset * This is a list of filter names that are defined to be included @@ -276,15 +209,6 @@ return this.superset; }; - /** - * Get the selected state of this filter - * - * @return {boolean} Filter is selected - */ - mw.rcfilters.dm.FilterItem.prototype.isSelected = function () { - return this.selected; - }; - /** * Check whether the filter is currently in a conflict state * @@ -431,21 +355,6 @@ } }; - /** - * Toggle the selected state of the item - * - * @param {boolean} [isSelected] Filter is selected - * @fires update - */ - mw.rcfilters.dm.FilterItem.prototype.toggleSelected = function ( isSelected ) { - isSelected = isSelected === undefined ? !this.selected : isSelected; - - if ( this.selected !== isSelected ) { - this.selected = isSelected; - this.emit( 'update' ); - } - }; - /** * Toggle the fully covered state of the item * @@ -460,90 +369,4 @@ this.emit( 'update' ); } }; - - /** - * Set the highlight color - * - * @param {string|null} highlightColor - */ - mw.rcfilters.dm.FilterItem.prototype.setHighlightColor = function ( highlightColor ) { - if ( this.highlightColor !== highlightColor ) { - this.highlightColor = highlightColor; - this.emit( 'update' ); - } - }; - - /** - * Clear the highlight color - */ - mw.rcfilters.dm.FilterItem.prototype.clearHighlightColor = function () { - this.setHighlightColor( null ); - }; - - /** - * Get the highlight color, or null if none is configured - * - * @return {string|null} - */ - mw.rcfilters.dm.FilterItem.prototype.getHighlightColor = function () { - return this.highlightColor; - }; - - /** - * Get the CSS class that matches changes that fit this filter - * or null if none is configured - * - * @return {string|null} - */ - mw.rcfilters.dm.FilterItem.prototype.getCssClass = function () { - return this.cssClass; - }; - - /** - * Toggle the highlight feature on and off for this filter. - * It only works if highlight is supported for this filter. - * - * @param {boolean} enable Highlight should be enabled - */ - mw.rcfilters.dm.FilterItem.prototype.toggleHighlight = function ( enable ) { - enable = enable === undefined ? !this.highlightEnabled : enable; - - if ( !this.isHighlightSupported() ) { - return; - } - - if ( enable === this.highlightEnabled ) { - return; - } - - this.highlightEnabled = enable; - this.emit( 'update' ); - }; - - /** - * Check if the highlight feature is currently enabled for this filter - * - * @return {boolean} - */ - mw.rcfilters.dm.FilterItem.prototype.isHighlightEnabled = function () { - return !!this.highlightEnabled; - }; - - /** - * Check if the highlight feature is supported for this filter - * - * @return {boolean} - */ - mw.rcfilters.dm.FilterItem.prototype.isHighlightSupported = function () { - return !!this.getCssClass(); - }; - - /** - * Check if the filter is currently highlighted - * - * @return {boolean} - */ - mw.rcfilters.dm.FilterItem.prototype.isHighlighted = function () { - return this.isHighlightEnabled() && !!this.getHighlightColor(); - }; }( mediaWiki ) ); diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ItemModel.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ItemModel.js new file mode 100644 index 0000000000..675fcc72ad --- /dev/null +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ItemModel.js @@ -0,0 +1,257 @@ +( function ( mw ) { + /** + * RCFilter base item model + * + * @mixins OO.EventEmitter + * + * @constructor + * @param {string} param Filter param name + * @param {Object} config Configuration object + * @cfg {string} [label] The label for the filter + * @cfg {string} [description] The description of the filter + * @cfg {boolean} [active=true] The filter is active and affecting the result + * @cfg {boolean} [selected] The item is selected + * @cfg {boolean} [inverted] The item is inverted, meaning the search is excluding + * this parameter. + * @cfg {string} [namePrefix='item_'] A prefix to add to the param name to act as a unique + * identifier + * @cfg {string} [cssClass] The class identifying the results that match this filter + */ + mw.rcfilters.dm.ItemModel = function MwRcfiltersDmItemModel( param, config ) { + config = config || {}; + + // Mixin constructor + OO.EventEmitter.call( this ); + + this.param = param; + this.namePrefix = config.namePrefix || 'item_'; + this.name = this.namePrefix + param; + + this.label = config.label || this.name; + this.description = config.description; + this.selected = !!config.selected; + + this.inverted = !!config.inverted; + + // Highlight + this.cssClass = config.cssClass; + this.highlightColor = null; + this.highlightEnabled = false; + }; + + /* Initialization */ + + OO.initClass( mw.rcfilters.dm.ItemModel ); + OO.mixinClass( mw.rcfilters.dm.ItemModel, OO.EventEmitter ); + + /* Events */ + + /** + * @event update + * + * The state of this filter has changed + */ + + /* Methods */ + + /** + * Return the representation of the state of this item. + * + * @return {Object} State of the object + */ + mw.rcfilters.dm.ItemModel.prototype.getState = function () { + return { + selected: this.isSelected(), + inverted: this.isInverted() + }; + }; + + /** + * Get the name of this filter + * + * @return {string} Filter name + */ + mw.rcfilters.dm.ItemModel.prototype.getName = function () { + return this.name; + }; + + /** + * Get the param name or value of this filter + * + * @return {string} Filter param name + */ + mw.rcfilters.dm.ItemModel.prototype.getParamName = function () { + return this.param; + }; + + /** + * Get the message representing the state of this model. + * + * @return {string} State message + */ + mw.rcfilters.dm.ItemModel.prototype.getStateMessage = function () { + // Display description + return this.getDescription(); + }; + + /** + * Get the label of this filter + * + * @return {string} Filter label + */ + mw.rcfilters.dm.ItemModel.prototype.getLabel = function () { + return this.label; + }; + + /** + * Get the description of this filter + * + * @return {string} Filter description + */ + mw.rcfilters.dm.ItemModel.prototype.getDescription = function () { + return this.description; + }; + + /** + * Get the default value of this filter + * + * @return {boolean} Filter default + */ + mw.rcfilters.dm.ItemModel.prototype.getDefault = function () { + return this.default; + }; + + /** + * Get the selected state of this filter + * + * @return {boolean} Filter is selected + */ + mw.rcfilters.dm.ItemModel.prototype.isSelected = function () { + return this.selected; + }; + + /** + * Toggle the selected state of the item + * + * @param {boolean} [isSelected] Filter is selected + * @fires update + */ + mw.rcfilters.dm.ItemModel.prototype.toggleSelected = function ( isSelected ) { + isSelected = isSelected === undefined ? !this.selected : isSelected; + + if ( this.selected !== isSelected ) { + this.selected = isSelected; + this.emit( 'update' ); + } + }; + + /** + * Get the inverted state of this item + * + * @return {boolean} Item is inverted + */ + mw.rcfilters.dm.ItemModel.prototype.isInverted = function () { + return this.inverted; + }; + + /** + * Toggle the inverted state of the item + * + * @param {boolean} [isInverted] Item is inverted + * @fires update + */ + mw.rcfilters.dm.ItemModel.prototype.toggleInverted = function ( isInverted ) { + isInverted = isInverted === undefined ? !this.inverted : isInverted; + + if ( this.inverted !== isInverted ) { + this.inverted = isInverted; + this.emit( 'update' ); + } + }; + + /** + * Set the highlight color + * + * @param {string|null} highlightColor + */ + mw.rcfilters.dm.ItemModel.prototype.setHighlightColor = function ( highlightColor ) { + if ( this.highlightColor !== highlightColor ) { + this.highlightColor = highlightColor; + this.emit( 'update' ); + } + }; + + /** + * Clear the highlight color + */ + mw.rcfilters.dm.ItemModel.prototype.clearHighlightColor = function () { + this.setHighlightColor( null ); + }; + + /** + * Get the highlight color, or null if none is configured + * + * @return {string|null} + */ + mw.rcfilters.dm.ItemModel.prototype.getHighlightColor = function () { + return this.highlightColor; + }; + + /** + * Get the CSS class that matches changes that fit this filter + * or null if none is configured + * + * @return {string|null} + */ + mw.rcfilters.dm.ItemModel.prototype.getCssClass = function () { + return this.cssClass; + }; + + /** + * Toggle the highlight feature on and off for this filter. + * It only works if highlight is supported for this filter. + * + * @param {boolean} enable Highlight should be enabled + */ + mw.rcfilters.dm.ItemModel.prototype.toggleHighlight = function ( enable ) { + enable = enable === undefined ? !this.highlightEnabled : enable; + + if ( !this.isHighlightSupported() ) { + return; + } + + if ( enable === this.highlightEnabled ) { + return; + } + + this.highlightEnabled = enable; + this.emit( 'update' ); + }; + + /** + * Check if the highlight feature is currently enabled for this filter + * + * @return {boolean} + */ + mw.rcfilters.dm.ItemModel.prototype.isHighlightEnabled = function () { + return !!this.highlightEnabled; + }; + + /** + * Check if the highlight feature is supported for this filter + * + * @return {boolean} + */ + mw.rcfilters.dm.ItemModel.prototype.isHighlightSupported = function () { + return !!this.getCssClass(); + }; + + /** + * Check if the filter is currently highlighted + * + * @return {boolean} + */ + mw.rcfilters.dm.ItemModel.prototype.isHighlighted = function () { + return this.isHighlightEnabled() && !!this.getHighlightColor(); + }; +}( mediaWiki ) ); diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuOptionWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuOptionWidget.less index 9d78f854c9..78ea014e6c 100644 --- a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuOptionWidget.less +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuOptionWidget.less @@ -1,59 +1,11 @@ @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 { + &.oo-ui-flaggedElement-muted { background-color: #f8f9fa; // Base90 AAA - .mw-rcfilters-ui-filterMenuOptionWidget-label-title, - .mw-rcfilters-ui-filterMenuOptionWidget-label-desc { + .mw-rcfilters-ui-itemMenuOptionWidget-label-title, + .mw-rcfilters-ui-itemMenuOptionWidget-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.FilterFloatingMenuSelectWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FloatingMenuSelectWidget.less similarity index 88% rename from resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.less rename to resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FloatingMenuSelectWidget.less index 7602465e31..67823c9a59 100644 --- a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.less +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FloatingMenuSelectWidget.less @@ -1,6 +1,6 @@ @import 'mediawiki.mixins'; -.mw-rcfilters-ui-filterFloatingMenuSelectWidget { +.mw-rcfilters-ui-floatingMenuSelectWidget { z-index: auto; max-width: 650px; diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ItemMenuOptionWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ItemMenuOptionWidget.less new file mode 100644 index 0000000000..44c5529636 --- /dev/null +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ItemMenuOptionWidget.less @@ -0,0 +1,51 @@ +@import 'mediawiki.mixins'; + +.mw-rcfilters-ui-itemMenuOptionWidget { + 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; + } + + &.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; + } + } + + &-itemCheckbox { + .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.FilterTagItemWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.TagItemWidget.less similarity index 97% rename from resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagItemWidget.less rename to resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.TagItemWidget.less index 0c89660031..4805f641c4 100644 --- a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagItemWidget.less +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.TagItemWidget.less @@ -1,6 +1,6 @@ @import 'mw.rcfilters.mixins'; -.mw-rcfilters-ui-filterTagItemWidget { +.mw-rcfilters-ui-tagItemWidget { // Background and color of the capsule widget need a bit // more specificity to override ooui internals &.oo-ui-flaggedElement-muted.oo-ui-tagItemWidget.oo-ui-widget-enabled { diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuOptionWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuOptionWidget.js index bda537f1f1..d235c3991a 100644 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuOptionWidget.js +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuOptionWidget.js @@ -2,7 +2,7 @@ /** * A widget representing a single toggle filter * - * @extends OO.ui.MenuOptionWidget + * @extends mw.rcfilters.ui.ItemMenuOptionWidget * * @constructor * @param {mw.rcfilters.Controller} controller RCFilters controller @@ -10,88 +10,23 @@ * @param {Object} config Configuration object */ mw.rcfilters.ui.FilterMenuOptionWidget = function MwRcfiltersUiFilterMenuOptionWidget( controller, model, config ) { - var layout, - $label = $( '
' ) - .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( - $( '
' ) - .addClass( 'mw-rcfilters-ui-filterMenuOptionWidget-label-title' ) - .append( this.$label ) - ); - if ( this.model.getDescription() ) { - $label.append( - $( '
' ) - .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() ); + mw.rcfilters.ui.FilterMenuOptionWidget.parent.call( this, controller, model, config ); - 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' } ); - // HACK: Prevent defaults on 'click' for the label so it - // doesn't steal the focus away from the input. This means - // we can continue arrow-movement after we click the label - // and is consistent with the checkbox *itself* also preventing - // defaults on 'click' as well. - layout.$label.on( 'click', false ); this.$element - .addClass( 'mw-rcfilters-ui-filterMenuOptionWidget' ) - .append( - $( '
' ) - .addClass( 'mw-rcfilters-ui-table' ) - .append( - $( '
' ) - .addClass( 'mw-rcfilters-ui-row' ) - .append( - $( '
' ) - .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-filterMenuOptionWidget-filterCheckbox' ) - .append( layout.$element ), - $( '
' ) - .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-filterMenuOptionWidget-highlightButton' ) - .append( this.highlightButton.$element ) - ) - ) - ); + .addClass( 'mw-rcfilters-ui-filterMenuOptionWidget' ); }; /* Initialization */ - - OO.inheritClass( mw.rcfilters.ui.FilterMenuOptionWidget, OO.ui.MenuOptionWidget ); + OO.inheritClass( mw.rcfilters.ui.FilterMenuOptionWidget, mw.rcfilters.ui.ItemMenuOptionWidget ); /* Static properties */ @@ -101,10 +36,11 @@ /* Methods */ /** - * Respond to item model update event + * @inheritdoc */ mw.rcfilters.ui.FilterMenuOptionWidget.prototype.onModelUpdate = function () { - this.checkboxWidget.setSelected( this.model.isSelected() ); + // Parent + mw.rcfilters.ui.FilterMenuOptionWidget.parent.prototype.onModelUpdate.call( this ); this.setCurrentMuteState(); }; @@ -117,36 +53,21 @@ }; /** - * Set the current mute state for this item + * Set the current muted view of the widget based on its state */ 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.setFlags( { + 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.FilterTagItemWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagItemWidget.js index d7e5f80d79..8a36eb4192 100644 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagItemWidget.js +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagItemWidget.js @@ -1,99 +1,32 @@ -( function ( mw, $ ) { +( function ( mw ) { /** * Extend OOUI's FilterTagItemWidget to also display a popup on hover. * * @class - * @extends OO.ui.FilterTagItemWidget - * @mixins OO.ui.mixin.PopupElement + * @extends mw.rcfilters.ui.TagItemWidget * * @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; - this.selected = false; + mw.rcfilters.ui.FilterTagItemWidget.parent.call( this, controller, model, config ); - 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: $( '
' ) - .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 = $( '
' ) - .addClass( 'mw-rcfilters-ui-filterTagItemWidget-highlight' ); - - // Events - this.model.connect( this, { update: 'onModelUpdate' } ); - - // Initialization - this.$overlay.append( this.popup.$element ); this.$element - .addClass( 'mw-rcfilters-ui-filterTagItemWidget' ) - .prepend( this.$highlight ) - .attr( 'aria-haspopup', 'true' ) - .on( 'mouseenter', this.onMouseEnter.bind( this ) ) - .on( 'mouseleave', this.onMouseLeave.bind( this ) ); - - this.setCurrentMuteState(); - this.setHighlightColor(); + .addClass( 'mw-rcfilters-ui-filterTagItemWidget' ); }; /* Initialization */ - OO.inheritClass( mw.rcfilters.ui.FilterTagItemWidget, OO.ui.TagItemWidget ); - OO.mixinClass( mw.rcfilters.ui.FilterTagItemWidget, OO.ui.mixin.PopupElement ); + OO.inheritClass( mw.rcfilters.ui.FilterTagItemWidget, mw.rcfilters.ui.TagItemWidget ); /* 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 + * @inheritdoc */ mw.rcfilters.ui.FilterTagItemWidget.prototype.setCurrentMuteState = function () { this.setFlags( { @@ -105,88 +38,4 @@ invalid: 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 the selected state of this widget - * - * @return {boolean} Tag is selected - */ - mw.rcfilters.ui.FilterTagItemWidget.prototype.isSelected = function () { - return 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 index 6fd3585c12..4192aadaa0 100644 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js @@ -338,7 +338,7 @@ * @inheritdoc */ mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.createMenuWidget = function ( menuConfig ) { - return new mw.rcfilters.ui.FilterFloatingMenuSelectWidget( + return new mw.rcfilters.ui.FloatingMenuSelectWidget( this.controller, this.model, $.extend( { diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FloatingMenuSelectWidget.js similarity index 71% rename from resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.js rename to resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FloatingMenuSelectWidget.js index 748eea8a8e..168f7d79b8 100644 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.js +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FloatingMenuSelectWidget.js @@ -11,7 +11,7 @@ * @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 ) { + mw.rcfilters.ui.FloatingMenuSelectWidget = function MwRcfiltersUiFloatingMenuSelectWidget( controller, model, config ) { var header; config = config || {}; @@ -23,16 +23,16 @@ this.$overlay = config.$overlay || this.$element; this.$footer = config.$footer; this.$body = $( '
' ) - .addClass( 'mw-rcfilters-ui-filterFloatingMenuSelectWidget-body' ); + .addClass( 'mw-rcfilters-ui-floatingMenuSelectWidget-body' ); // Parent - mw.rcfilters.ui.FilterFloatingMenuSelectWidget.parent.call( this, $.extend( { + mw.rcfilters.ui.FloatingMenuSelectWidget.parent.call( this, $.extend( { $autoCloseIgnore: this.$overlay, width: 650 }, config ) ); this.setGroupElement( $( '
' ) - .addClass( 'mw-rcfilters-ui-filterFloatingMenuSelectWidget-group' ) + .addClass( 'mw-rcfilters-ui-floatingMenuSelectWidget-group' ) ); this.setClippableElement( this.$body ); this.setClippableContainer( this.$element ); @@ -47,11 +47,11 @@ this.noResults = new OO.ui.LabelWidget( { label: mw.msg( 'rcfilters-filterlist-noresults' ), - classes: [ 'mw-rcfilters-ui-filterFloatingMenuSelectWidget-noresults' ] + classes: [ 'mw-rcfilters-ui-floatingMenuSelectWidget-noresults' ] } ); this.$element - .addClass( 'mw-rcfilters-ui-filterFloatingMenuSelectWidget' ) + .addClass( 'mw-rcfilters-ui-floatingMenuSelectWidget' ) .append( this.$body .append( header.$element, this.$group, this.noResults.$element ) @@ -60,14 +60,14 @@ if ( this.$footer ) { this.$element.append( this.$footer - .addClass( 'mw-rcfilters-ui-filterFloatingMenuSelectWidget-footer' ) + .addClass( 'mw-rcfilters-ui-floatingMenuSelectWidget-footer' ) ); } }; /* Initialize */ - OO.inheritClass( mw.rcfilters.ui.FilterFloatingMenuSelectWidget, OO.ui.FloatingMenuSelectWidget ); + OO.inheritClass( mw.rcfilters.ui.FloatingMenuSelectWidget, OO.ui.FloatingMenuSelectWidget ); /* Events */ @@ -83,7 +83,7 @@ * @fires itemVisibilityChange * @inheritdoc */ - mw.rcfilters.ui.FilterFloatingMenuSelectWidget.prototype.updateItemVisibility = function () { + mw.rcfilters.ui.FloatingMenuSelectWidget.prototype.updateItemVisibility = function () { var i, itemWasHighlighted = false, inputVal = this.$input.val(), @@ -93,7 +93,7 @@ // call it unless the input actually changed if ( this.inputValue !== inputVal ) { // Parent method - mw.rcfilters.ui.FilterFloatingMenuSelectWidget.parent.prototype.updateItemVisibility.call( this ); + mw.rcfilters.ui.FloatingMenuSelectWidget.parent.prototype.updateItemVisibility.call( this ); if ( inputVal !== '' ) { // Highlight the first item in the list @@ -125,7 +125,7 @@ * * @inheritdoc */ - mw.rcfilters.ui.FilterFloatingMenuSelectWidget.prototype.getItemMatcher = function ( s ) { + mw.rcfilters.ui.FloatingMenuSelectWidget.prototype.getItemMatcher = function ( s ) { var results = this.model.findMatches( s, true ); return function ( item ) { @@ -136,7 +136,7 @@ /** * Scroll to the top of the menu */ - mw.rcfilters.ui.FilterFloatingMenuSelectWidget.prototype.scrollToTop = function () { + mw.rcfilters.ui.FloatingMenuSelectWidget.prototype.scrollToTop = function () { this.$body.scrollTop( 0 ); }; }( mediaWiki ) ); diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ItemMenuOptionWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ItemMenuOptionWidget.js new file mode 100644 index 0000000000..a88d119fa8 --- /dev/null +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ItemMenuOptionWidget.js @@ -0,0 +1,125 @@ +( function ( mw ) { + /** + * A widget representing a base toggle item + * + * @extends OO.ui.MenuOptionWidget + * + * @constructor + * @param {mw.rcfilters.Controller} controller RCFilters controller + * @param {mw.rcfilters.dm.ItemModel} model Item model + * @param {Object} config Configuration object + */ + mw.rcfilters.ui.ItemMenuOptionWidget = function MwRcfiltersUiItemMenuOptionWidget( controller, model, config ) { + var layout, + $label = $( '
' ) + .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-label' ); + + config = config || {}; + + this.controller = controller; + this.model = model; + + // Parent + mw.rcfilters.ui.ItemMenuOptionWidget.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( + $( '
' ) + .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-label-title' ) + .append( this.$label ) + ); + if ( this.model.getDescription() ) { + $label.append( + $( '
' ) + .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-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' + } ); + + // Events + this.model.connect( this, { update: 'onModelUpdate' } ); + // HACK: Prevent defaults on 'click' for the label so it + // doesn't steal the focus away from the input. This means + // we can continue arrow-movement after we click the label + // and is consistent with the checkbox *itself* also preventing + // defaults on 'click' as well. + layout.$label.on( 'click', false ); + + this.$element + .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget' ) + .append( + $( '
' ) + .addClass( 'mw-rcfilters-ui-table' ) + .append( + $( '
' ) + .addClass( 'mw-rcfilters-ui-row' ) + .append( + $( '
' ) + .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-itemCheckbox' ) + .append( layout.$element ), + $( '
' ) + .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-highlightButton' ) + .append( this.highlightButton.$element ) + ) + ) + ); + }; + + /* Initialization */ + + OO.inheritClass( mw.rcfilters.ui.ItemMenuOptionWidget, OO.ui.MenuOptionWidget ); + + /* Static properties */ + + // We do our own scrolling to top + mw.rcfilters.ui.ItemMenuOptionWidget.static.scrollIntoViewOnSelect = false; + + /* Methods */ + + /** + * Respond to item model update event + */ + mw.rcfilters.ui.ItemMenuOptionWidget.prototype.onModelUpdate = function () { + this.checkboxWidget.setSelected( this.model.isSelected() ); + + this.highlightButton.toggle( this.model.isHighlightEnabled() ); + }; + + /** + * Get the name of this filter + * + * @return {string} Filter name + */ + mw.rcfilters.ui.ItemMenuOptionWidget.prototype.getName = function () { + return this.model.getName(); + }; + + mw.rcfilters.ui.ItemMenuOptionWidget.prototype.getModel = function () { + return this.model; + }; + +}( mediaWiki ) ); diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.TagItemWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.TagItemWidget.js new file mode 100644 index 0000000000..637dbdce16 --- /dev/null +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.TagItemWidget.js @@ -0,0 +1,183 @@ +( function ( mw, $ ) { + /** + * Extend OOUI's TagItemWidget to also display a popup on hover. + * + * @class + * @extends OO.ui.TagItemWidget + * @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.TagItemWidget = function MwRcfiltersUiTagItemWidget( controller, model, config ) { + // Configuration initialization + config = config || {}; + + this.controller = controller; + this.model = model; + this.selected = false; + + mw.rcfilters.ui.TagItemWidget.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: $( '
' ) + .addClass( 'mw-rcfilters-ui-tagItemWidget-popup-content' ) + .append( this.popupLabel.$element ), + $floatableContainer: this.$element, + classes: [ 'mw-rcfilters-ui-tagItemWidget-popup' ] + } + }, config ) ); + + this.positioned = false; + this.popupTimeoutShow = null; + this.popupTimeoutHide = null; + + this.$highlight = $( '
' ) + .addClass( 'mw-rcfilters-ui-tagItemWidget-highlight' ); + + // Events + this.model.connect( this, { update: 'onModelUpdate' } ); + + // Initialization + this.$overlay.append( this.popup.$element ); + this.$element + .addClass( 'mw-rcfilters-ui-tagItemWidget' ) + .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.TagItemWidget, OO.ui.TagItemWidget ); + OO.mixinClass( mw.rcfilters.ui.TagItemWidget, OO.ui.mixin.PopupElement ); + + /* Methods */ + + /** + * Respond to model update event + */ + mw.rcfilters.ui.TagItemWidget.prototype.onModelUpdate = function () { + this.setCurrentMuteState(); + + this.setHighlightColor(); + }; + + mw.rcfilters.ui.TagItemWidget.prototype.setHighlightColor = function () { + var selectedColor = this.model.isHighlightEnabled() ? this.model.getHighlightColor() : null; + + this.$highlight + .attr( 'data-color', selectedColor ) + .toggleClass( + 'mw-rcfilters-ui-tagItemWidget-highlight-highlighted', + !!selectedColor + ); + }; + + /** + * Set the current mute state for this item + */ + mw.rcfilters.ui.TagItemWidget.prototype.setCurrentMuteState = function () {}; + + /** + * Respond to mouse enter event + */ + mw.rcfilters.ui.TagItemWidget.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.TagItemWidget.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.TagItemWidget.prototype.toggleSelected = function ( isSelected ) { + isSelected = isSelected !== undefined ? isSelected : !this.selected; + + if ( this.selected !== isSelected ) { + this.selected = isSelected; + + this.$element.toggleClass( 'mw-rcfilters-ui-tagItemWidget-selected', this.selected ); + } + }; + + /** + * Get the selected state of this widget + * + * @return {boolean} Tag is selected + */ + mw.rcfilters.ui.TagItemWidget.prototype.isSelected = function () { + return this.selected; + }; + + /** + * Get item name + * + * @return {string} Filter name + */ + mw.rcfilters.ui.TagItemWidget.prototype.getName = function () { + return this.model.getName(); + }; + + /** + * Remove and destroy external elements of this widget + */ + mw.rcfilters.ui.TagItemWidget.prototype.destroy = function () { + // Destroy the popup + this.popup.$element.detach(); + + // Disconnect events + this.model.disconnect( this ); + this.closeButton.disconnect( this ); + }; +}( mediaWiki, jQuery ) ); -- 2.20.1