'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',
'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',
'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',
/**
* 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 || [];
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 */
};
};
- /**
- * 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 () {
/**
* 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
};
/**
- * 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,
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
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
*
}
};
- /**
- * 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
*
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 ) );
--- /dev/null
+( 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 ) );
+++ /dev/null
-@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;
- }
-}
@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;
- }
}
+++ /dev/null
-@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' );
- }
- }
-}
--- /dev/null
+@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;
+ }
+}
--- /dev/null
+@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;
+ }
+}
--- /dev/null
+@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' );
+ }
+ }
+}
+++ /dev/null
-( function ( mw ) {
- /**
- * A floating menu widget for the filter list
- *
- * @extends OO.ui.FloatingMenuSelectWidget
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller Controller
- * @param {mw.rcfilters.dm.FiltersViewModel} model View model
- * @param {Object} [config] Configuration object
- * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
- * @cfg {jQuery} [$footer] An optional footer for the menu
- */
- mw.rcfilters.ui.FilterFloatingMenuSelectWidget = function MwRcfiltersUiFilterFloatingMenuSelectWidget( controller, model, config ) {
- var header;
-
- config = config || {};
-
- this.controller = controller;
- this.model = model;
-
- this.inputValue = '';
- this.$overlay = config.$overlay || this.$element;
- this.$footer = config.$footer;
- this.$body = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterFloatingMenuSelectWidget-body' );
-
- // Parent
- mw.rcfilters.ui.FilterFloatingMenuSelectWidget.parent.call( this, $.extend( {
- $autoCloseIgnore: this.$overlay,
- width: 650
- }, config ) );
- this.setGroupElement(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterFloatingMenuSelectWidget-group' )
- );
- this.setClippableElement( this.$body );
- this.setClippableContainer( this.$element );
-
- header = new mw.rcfilters.ui.FilterMenuHeaderWidget(
- this.controller,
- this.model,
- {
- $overlay: this.$overlay
- }
- );
-
- this.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 ) );
/**
* A widget representing a single toggle filter
*
- * @extends OO.ui.MenuOptionWidget
+ * @extends mw.rcfilters.ui.ItemMenuOptionWidget
*
* @constructor
* @param {mw.rcfilters.Controller} controller RCFilters controller
* @param {Object} config Configuration object
*/
mw.rcfilters.ui.FilterMenuOptionWidget = function MwRcfiltersUiFilterMenuOptionWidget( controller, model, config ) {
- var layout,
- $label = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterMenuOptionWidget-label' );
-
config = config || {};
this.controller = controller;
this.model = model;
// Parent
- mw.rcfilters.ui.FilterMenuOptionWidget.parent.call( this, $.extend( {
- // Override the 'check' icon that OOUI defines
- icon: '',
- data: this.model.getName(),
- label: this.model.getLabel()
- }, config ) );
-
- this.checkboxWidget = new mw.rcfilters.ui.CheckboxInputWidget( {
- value: this.model.getName(),
- selected: this.model.isSelected()
- } );
-
- $label.append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterMenuOptionWidget-label-title' )
- .append( this.$label )
- );
- if ( this.model.getDescription() ) {
- $label.append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterMenuOptionWidget-label-desc' )
- .text( this.model.getDescription() )
- );
- }
-
- this.highlightButton = new mw.rcfilters.ui.FilterItemHighlightButton(
- this.controller,
- this.model,
- {
- $overlay: config.$overlay || this.$element,
- title: mw.msg( 'rcfilters-highlightmenu-help' )
- }
- );
- this.highlightButton.toggle( this.model.isHighlightEnabled() );
+ 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(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-table' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-row' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-filterMenuOptionWidget-filterCheckbox' )
- .append( layout.$element ),
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-filterMenuOptionWidget-highlightButton' )
- .append( this.highlightButton.$element )
- )
- )
- );
+ .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 */
/* 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();
};
};
/**
- * 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 ) );
-( 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: $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterTagItemWidget-popup-content' )
- .append( this.popupLabel.$element ),
- $floatableContainer: this.$element,
- classes: [ 'mw-rcfilters-ui-filterTagItemWidget-popup' ]
- }
- }, config ) );
-
- this.positioned = false;
- this.popupTimeoutShow = null;
- this.popupTimeoutHide = null;
-
- this.$highlight = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterTagItemWidget-highlight' );
-
- // Events
- this.model.connect( this, { update: 'onModelUpdate' } );
-
- // Initialization
- this.$overlay.append( this.popup.$element );
this.$element
- .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( {
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 ) );
* @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( {
--- /dev/null
+( 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 = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-floatingMenuSelectWidget-body' );
+
+ // Parent
+ mw.rcfilters.ui.FloatingMenuSelectWidget.parent.call( this, $.extend( {
+ $autoCloseIgnore: this.$overlay,
+ width: 650
+ }, config ) );
+ this.setGroupElement(
+ $( '<div>' )
+ .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 ) );
--- /dev/null
+( 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 = $( '<div>' )
+ .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(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-label-title' )
+ .append( this.$label )
+ );
+ if ( this.model.getDescription() ) {
+ $label.append(
+ $( '<div>' )
+ .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(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-table' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-row' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-itemCheckbox' )
+ .append( layout.$element ),
+ $( '<div>' )
+ .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 ) );
--- /dev/null
+( 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: $( '<div>' )
+ .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 = $( '<div>' )
+ .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 ) );