From: Moriel Schottlender Date: Tue, 25 Apr 2017 18:26:20 +0000 (-0700) Subject: RCFilters UI: Create base classes for shared objects X-Git-Tag: 1.31.0-rc.0~3427^2 X-Git-Url: http://git.cyclocoop.org/%7B%7B%20url_for%28%27admin_vote_add%27%29%20%7D%7D?a=commitdiff_plain;h=856cc85f4b8650038eced7211610fa11dd27c5bd;p=lhc%2Fweb%2Fwiklou.git 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 --- 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.FilterFloatingMenuSelectWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.less deleted file mode 100644 index 7602465e31..0000000000 --- a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.less +++ /dev/null @@ -1,30 +0,0 @@ -@import 'mediawiki.mixins'; - -.mw-rcfilters-ui-filterFloatingMenuSelectWidget { - z-index: auto; - max-width: 650px; - - &.oo-ui-menuSelectWidget-invisible { - display: block; - } - - &-noresults { - display: none; - padding: 0.5em; - color: #666; - - .oo-ui-menuSelectWidget-invisible & { - display: inline-block; - } - } - - &-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.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.FilterTagItemWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagItemWidget.less deleted file mode 100644 index 0c89660031..0000000000 --- a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagItemWidget.less +++ /dev/null @@ -1,80 +0,0 @@ -@import 'mw.rcfilters.mixins'; - -.mw-rcfilters-ui-filterTagItemWidget { - // 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 { - // Muted state - background-color: #eaecf0; - border-color: #c8ccd1; - - .oo-ui-labelElement-label { - color: #72777d; - } - .oo-ui-buttonWidget { - opacity: @muted-opacity; - } - } - - &.oo-ui-flaggedElement-invalid.oo-ui-tagItemWidget.oo-ui-widget-enabled { - .oo-ui-labelElement-label { - color: #b32424; - } - } - - // OOUI classes require super-specificity in order to override - // the white background - // The specificity is fixed in the patch: https://gerrit.wikimedia.org/r/#/c/349525/ - // and will be available in the next OOUI release. - .oo-ui-tagMultiselectWidget.oo-ui-widget-enabled.oo-ui-tagMultiselectWidget-outlined .oo-ui-tagMultiselectWidget-handle &-selected.oo-ui-tagItemWidget.oo-ui-widget-enabled { - background-color: #eaf3ff; - border-color: #36c; - } - - &-popup-content { - padding: 0.5em; - color: #54595d; - } - - &.oo-ui-labelElement .oo-ui-labelElement-label { - cursor: pointer; - } - - &-highlight { - display: none; - margin-right: 0.5em; - height: 100%; - width: 10px; - - &-highlighted { - display: inline-block; - } - - &:before { - content: ''; - position: absolute; - display: block; - top: 50%; - } - - &[data-color='c1']:before { - .mw-rcfilters-mixin-circle( @highlight-c1, 10px, ~'-5px 0.5em 0 0' ); - } - - &[data-color='c2']:before { - .mw-rcfilters-mixin-circle( @highlight-c2, 10px, ~'-5px 0.5em 0 0' ); - } - - &[data-color='c3']:before { - .mw-rcfilters-mixin-circle( @highlight-c3, 10px, ~'-5px 0.5em 0 0' ); - } - - &[data-color='c4']:before { - .mw-rcfilters-mixin-circle( @highlight-c4, 10px, ~'-5px 0.5em 0 0' ); - } - - &[data-color='c5']:before { - .mw-rcfilters-mixin-circle( @highlight-c5, 10px, ~'-5px 0.5em 0 0' ); - } - } -} diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FloatingMenuSelectWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FloatingMenuSelectWidget.less new file mode 100644 index 0000000000..67823c9a59 --- /dev/null +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FloatingMenuSelectWidget.less @@ -0,0 +1,30 @@ +@import 'mediawiki.mixins'; + +.mw-rcfilters-ui-floatingMenuSelectWidget { + z-index: auto; + max-width: 650px; + + &.oo-ui-menuSelectWidget-invisible { + display: block; + } + + &-noresults { + display: none; + padding: 0.5em; + color: #666; + + .oo-ui-menuSelectWidget-invisible & { + display: inline-block; + } + } + + &-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.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.TagItemWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.TagItemWidget.less new file mode 100644 index 0000000000..4805f641c4 --- /dev/null +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.TagItemWidget.less @@ -0,0 +1,80 @@ +@import 'mw.rcfilters.mixins'; + +.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 { + // Muted state + background-color: #eaecf0; + border-color: #c8ccd1; + + .oo-ui-labelElement-label { + color: #72777d; + } + .oo-ui-buttonWidget { + opacity: @muted-opacity; + } + } + + &.oo-ui-flaggedElement-invalid.oo-ui-tagItemWidget.oo-ui-widget-enabled { + .oo-ui-labelElement-label { + color: #b32424; + } + } + + // OOUI classes require super-specificity in order to override + // the white background + // The specificity is fixed in the patch: https://gerrit.wikimedia.org/r/#/c/349525/ + // and will be available in the next OOUI release. + .oo-ui-tagMultiselectWidget.oo-ui-widget-enabled.oo-ui-tagMultiselectWidget-outlined .oo-ui-tagMultiselectWidget-handle &-selected.oo-ui-tagItemWidget.oo-ui-widget-enabled { + background-color: #eaf3ff; + border-color: #36c; + } + + &-popup-content { + padding: 0.5em; + color: #54595d; + } + + &.oo-ui-labelElement .oo-ui-labelElement-label { + cursor: pointer; + } + + &-highlight { + display: none; + margin-right: 0.5em; + height: 100%; + width: 10px; + + &-highlighted { + display: inline-block; + } + + &:before { + content: ''; + position: absolute; + display: block; + top: 50%; + } + + &[data-color='c1']:before { + .mw-rcfilters-mixin-circle( @highlight-c1, 10px, ~'-5px 0.5em 0 0' ); + } + + &[data-color='c2']:before { + .mw-rcfilters-mixin-circle( @highlight-c2, 10px, ~'-5px 0.5em 0 0' ); + } + + &[data-color='c3']:before { + .mw-rcfilters-mixin-circle( @highlight-c3, 10px, ~'-5px 0.5em 0 0' ); + } + + &[data-color='c4']:before { + .mw-rcfilters-mixin-circle( @highlight-c4, 10px, ~'-5px 0.5em 0 0' ); + } + + &[data-color='c5']:before { + .mw-rcfilters-mixin-circle( @highlight-c5, 10px, ~'-5px 0.5em 0 0' ); + } + } +} diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.js deleted file mode 100644 index 748eea8a8e..0000000000 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.js +++ /dev/null @@ -1,142 +0,0 @@ -( 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.noResults = new OO.ui.LabelWidget( { - label: mw.msg( 'rcfilters-filterlist-noresults' ), - classes: [ 'mw-rcfilters-ui-filterFloatingMenuSelectWidget-noresults' ] - } ); - - this.$element - .addClass( 'mw-rcfilters-ui-filterFloatingMenuSelectWidget' ) - .append( - this.$body - .append( header.$element, this.$group, this.noResults.$element ) - ); - - 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.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.FloatingMenuSelectWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FloatingMenuSelectWidget.js new file mode 100644 index 0000000000..168f7d79b8 --- /dev/null +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FloatingMenuSelectWidget.js @@ -0,0 +1,142 @@ +( 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.FloatingMenuSelectWidget = function MwRcfiltersUiFloatingMenuSelectWidget( 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-floatingMenuSelectWidget-body' ); + + // Parent + mw.rcfilters.ui.FloatingMenuSelectWidget.parent.call( this, $.extend( { + $autoCloseIgnore: this.$overlay, + width: 650 + }, config ) ); + this.setGroupElement( + $( '
' ) + .addClass( 'mw-rcfilters-ui-floatingMenuSelectWidget-group' ) + ); + this.setClippableElement( this.$body ); + this.setClippableContainer( this.$element ); + + header = new mw.rcfilters.ui.FilterMenuHeaderWidget( + this.controller, + this.model, + { + $overlay: this.$overlay + } + ); + + this.noResults = new OO.ui.LabelWidget( { + label: mw.msg( 'rcfilters-filterlist-noresults' ), + classes: [ 'mw-rcfilters-ui-floatingMenuSelectWidget-noresults' ] + } ); + + this.$element + .addClass( 'mw-rcfilters-ui-floatingMenuSelectWidget' ) + .append( + this.$body + .append( header.$element, this.$group, this.noResults.$element ) + ); + + if ( this.$footer ) { + this.$element.append( + this.$footer + .addClass( 'mw-rcfilters-ui-floatingMenuSelectWidget-footer' ) + ); + } + }; + + /* Initialize */ + + OO.inheritClass( mw.rcfilters.ui.FloatingMenuSelectWidget, OO.ui.FloatingMenuSelectWidget ); + + /* Events */ + + /** + * @event itemVisibilityChange + * + * Item visibility has changed + */ + + /* Methods */ + + /** + * @fires itemVisibilityChange + * @inheritdoc + */ + mw.rcfilters.ui.FloatingMenuSelectWidget.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.FloatingMenuSelectWidget.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.FloatingMenuSelectWidget.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.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 ) );