diff --git a/resources/Resources.php b/resources/Resources.php
index 715cdb8c9e3..1721de807c8 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 221d2a54c63..4e2079dc408 100644
--- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js
+++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js
@@ -2,37 +2,29 @@
/**
* 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 || {};
+ this.groupModel = groupModel;
+
+ // Parent
+ mw.rcfilters.dm.FilterItem.parent.call( this, param, $.extend( {
+ namePrefix: this.groupModel.getNamePrefix()
+ }, 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;
-
// Interaction definitions
this.subset = config.subset || [];
this.conflicts = config.conflicts || {};
@@ -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 00000000000..675fcc72ad2
--- /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 9d78f854c9a..78ea014e6c7 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 7602465e310..67823c9a59a 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 00000000000..44c5529636b
--- /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 0c89660031d..4805f641c4e 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 bda537f1f1b..d235c3991a5 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 ) );
+ mw.rcfilters.ui.FilterMenuOptionWidget.parent.call( this, controller, model, 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() );
-
- 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 d7e5f80d791..8a36eb41928 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 6fd3585c121..4192aadaa07 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 748eea8a8e6..168f7d79b8f 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 00000000000..a88d119fa81
--- /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 00000000000..637dbdce16a
--- /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 ) );