RCFilters: Adjust to use MenuTagMultiselectWidget

The new widget in OOUI is more stable and easier to manage,
and gives us a few features that we were missing, like
arrow behavior in the menu.

Depends on OOUI release 0.21.0

Bug: T162829
Bug: T159768
Bug: T162709
Bug: T162917
Change-Id: I42be0691304b1e93b4e9c02eba2e3a724a5ffd67
Depends-On: Ic216769f48e4677da5b7274f491aa08a95aa8076
This commit is contained in:
Moriel Schottlender 2017-03-27 09:58:29 -07:00
parent a9ea3a3ee9
commit a703e5236b
21 changed files with 931 additions and 1118 deletions

View file

@ -1751,11 +1751,12 @@ return [
'mediawiki.rcfilters.filters.ui' => [
'scripts' => [
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CheckboxInputWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FiltersListWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterGroupWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuOptionWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuSectionOptionWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagItemWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuHeaderWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesListWrapperWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js',
@ -1769,12 +1770,13 @@ return [
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.variables.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.CapsuleItemWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterGroupWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FiltersListWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagMultiselectWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuOptionWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuSectionOptionWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagItemWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuHeaderWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterFloatingMenuSelectWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ChangesListWrapperWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.HighlightColorPickerWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemHighlightButton.less',

View file

@ -734,13 +734,18 @@
* Find items whose labels match the given string
*
* @param {string} query Search string
* @param {boolean} [returnFlat] Return a flat array. If false, the result
* is an object whose keys are the group names and values are an array of
* filters per group. If set to true, returns an array of filters regardless
* of their groups.
* @return {Object} An object of items to show
* arranged by their group names
*/
mw.rcfilters.dm.FiltersViewModel.prototype.findMatches = function ( query ) {
mw.rcfilters.dm.FiltersViewModel.prototype.findMatches = function ( query, returnFlat ) {
var i,
groupTitle,
result = {},
flatResult = [],
items = this.getItems();
// Normalize so we can search strings regardless of case
@ -751,6 +756,7 @@
if ( items[ i ].getLabel().toLowerCase().indexOf( query ) === 0 ) {
result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
result[ items[ i ].getGroupName() ].push( items[ i ] );
flatResult.push( items[ i ] );
}
}
@ -765,11 +771,12 @@
) {
result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
result[ items[ i ].getGroupName() ].push( items[ i ] );
flatResult.push( items[ i ] );
}
}
}
return result;
return returnFlat ? flatResult : result;
};
/**

View file

@ -0,0 +1,16 @@
@import 'mediawiki.mixins';
.mw-rcfilters-ui-filterFloatingMenuSelectWidget {
z-index: auto;
max-width: 650px;
&-body {
max-height: 70vh;
}
&-footer {
background-color: #f8f9fa;
text-align: right;
padding: 0.5em;
}
}

View file

@ -1,4 +1,6 @@
.mw-rcfilters-ui-filtersListWidget {
@import 'mediawiki.mixins';
.mw-rcfilters-ui-filterMenuHeaderWidget {
&-title {
font-size: 1.2em;
padding: 0.75em 0.5em;
@ -25,10 +27,4 @@
vertical-align: middle;
}
}
&-noresults {
padding: 0.5em;
// TODO: Unify colors with official design palette
color: #666;
}
}

View file

@ -1,6 +1,6 @@
@import 'mediawiki.mixins';
.mw-rcfilters-ui-filterItemWidget {
.mw-rcfilters-ui-filterMenuOptionWidget {
padding: 0 0.5em;
.box-sizing( border-box );
@ -18,13 +18,13 @@
&-muted {
background-color: #f8f9fa; // Base90 AAA
.mw-rcfilters-ui-filterItemWidget-label-title,
.mw-rcfilters-ui-filterItemWidget-label-desc {
.mw-rcfilters-ui-filterMenuOptionWidget-label-title,
.mw-rcfilters-ui-filterMenuOptionWidget-label-desc {
color: #54595d; // Base20 AAA
}
}
&-selected {
&.oo-ui-optionWidget-selected {
background-color: #eaf3ff; // Accent90 AAA
}
@ -36,6 +36,7 @@
}
&-desc {
color: #464a4f;
white-space: normal;
}
}

View file

@ -1,24 +1,23 @@
@import 'mediawiki.mixins';
.mw-rcfilters-ui-filterGroupWidget {
padding-bottom: 0.5em;
.mw-rcfilters-ui-filterMenuSectionOptionWidget {
background: #eaecf0;
padding-bottom: 0.7em;
&-header {
background: #eaecf0;
padding: 0.5em 0.75em;
&-title {
padding: 0 0.75em;
// Use a high specificity to override OOUI
.oo-ui-optionWidget.oo-ui-labelElement &-title.oo-ui-labelElement-label {
// TODO: Unify colors with official design palette
color: #555a5d;
.box-sizing( border-box );
display: inline-block;
line-height: normal;
}
}
&-whatsThisButton {
display: inline-block;
margin-left: 1.5em;
&.oo-ui-buttonElement {
vertical-align: text-bottom;
@ -45,7 +44,6 @@
&-link {
margin: 1em 0;
}
.oo-ui-buttonElement-frameless.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
@ -55,7 +53,7 @@
}
&-active {
.mw-rcfilters-ui-filterGroupWidget-header-title {
.mw-rcfilters-ui-filterMenuSectionOptionWidget-header-title {
font-weight: bold;
}
}

View file

@ -1,13 +1,13 @@
@import 'mw.rcfilters.mixins';
.mw-rcfilters-ui-capsuleItemWidget {
.mw-rcfilters-ui-filterTagItemWidget {
background-color: #fff;
border-color: #979797;
color: #222;
// Background and color of the capsule widget need a bit
// more specificity to override ooui internals
&-muted.oo-ui-capsuleItemWidget.oo-ui-widget-enabled {
&-muted.oo-ui-tagItemWidget.oo-ui-widget-enabled {
// Muted state
background-color: #eaecf0;
border-color: #c8ccd1;
@ -20,7 +20,7 @@
}
}
&-conflicted.oo-ui-capsuleItemWidget.oo-ui-widget-enabled {
&-conflicted.oo-ui-tagItemWidget.oo-ui-widget-enabled {
background-color: #fee7e6; // Red90 AAA
border-color: #b32424; // Red30 AAA
@ -32,7 +32,7 @@
}
}
&-selected.oo-ui-capsuleItemWidget.oo-ui-widget-enabled {
&-selected.oo-ui-tagItemWidget.oo-ui-widget-enabled {
background-color: #eaf3ff;
border-color: #36c;
}
@ -49,27 +49,39 @@
&-highlight {
display: none;
padding-right: 0.5em;
margin-right: 0.5em;
height: 100%;
width: 10px;
&-highlighted {
display: inline-block;
}
&[data-color='c1'] {
.mw-rcfilters-mixin-circle( @highlight-c1, 10px, ~'0 0.5em 0 0' );
&:before {
content: '';
position: absolute;
display: block;
top: 50%;
}
&[data-color='c2'] {
.mw-rcfilters-mixin-circle( @highlight-c2, 10px, ~'0 0.5em 0 0' );
&[data-color='c1']:before {
.mw-rcfilters-mixin-circle( @highlight-c1, 10px, ~'-5px 0.5em 0 0' );
}
&[data-color='c3'] {
.mw-rcfilters-mixin-circle( @highlight-c3, 10px, ~'0 0.5em 0 0' );
&[data-color='c2']:before {
.mw-rcfilters-mixin-circle( @highlight-c2, 10px, ~'-5px 0.5em 0 0' );
}
&[data-color='c4'] {
.mw-rcfilters-mixin-circle( @highlight-c4, 10px, ~'0 0.5em 0 0' );
&[data-color='c3']:before {
.mw-rcfilters-mixin-circle( @highlight-c3, 10px, ~'-5px 0.5em 0 0' );
}
&[data-color='c5'] {
.mw-rcfilters-mixin-circle( @highlight-c5, 10px, ~'0 0.5em 0 0' );
&[data-color='c4']:before {
.mw-rcfilters-mixin-circle( @highlight-c4, 10px, ~'-5px 0.5em 0 0' );
}
&[data-color='c5']:before {
.mw-rcfilters-mixin-circle( @highlight-c5, 10px, ~'-5px 0.5em 0 0' );
}
}
}

View file

@ -1,7 +1,9 @@
.mw-rcfilters-ui-filterCapsuleMultiselectWidget {
.mw-rcfilters-ui-filterTagMultiselectWidget {
max-width: none;
&.oo-ui-widget-enabled .oo-ui-capsuleMultiselectWidget-handle {
&.oo-ui-widget-enabled .oo-ui-tagMultiselectWidget-handle {
border: 1px solid #a2a9b1;
border-bottom: 0;
background-color: #f8f9fa;
border-radius: 2px 2px 0 0;
padding: 0.3em 0.6em 0.6em 0.6em;
@ -24,6 +26,7 @@
&-cell-filters {
width: 100%;
}
&-cell-reset {
text-align: right;
padding-left: 0.5em;

View file

@ -3,21 +3,6 @@
// Make sure this uses the interface direction, not the content direction
direction: ltr;
&-popup {
margin-top: 1px;
max-width: 650px;
.oo-ui-popupWidget-body {
max-height: 70vh;
}
.oo-ui-popupWidget-footer {
background-color: #f8f9fa;
text-align: right;
padding: 0.5em;
}
}
&-search {
max-width: none;
margin-top: -1px;

View file

@ -14,7 +14,16 @@
mw.rcfilters.ui.CheckboxInputWidget.parent.call( this, config );
// Event
this.$input.on( 'change', this.onUserChange.bind( this ) );
this.$input
// HACK: This widget just pretends to be a checkbox for visual purposes.
// In reality, all actions - setting to true or false, etc - are
// decided by the model, and executed by the controller. This means
// that we want to let the controller and model make the decision
// of whether to check/uncheck this checkboxInputWidget, and for that,
// we have to bypass the browser action that checks/unchecks it during
// click.
.on( 'click', false )
.on( 'change', this.onUserChange.bind( this ) );
};
/* Initialization */
@ -32,6 +41,19 @@
/* Methods */
/**
* @inheritdoc
*/
mw.rcfilters.ui.CheckboxInputWidget.prototype.onEdit = function () {
// Similarly to preventing defaults in 'click' event, we want
// to prevent this widget from deciding anything about its own
// state; it emits a change event and the model and controller
// make a decision about what its select state is.
// onEdit has a widget.$input.prop( 'checked' ) inside a setTimeout()
// so we really want to prevent that from messing with what
// the model decides the state of the widget is.
};
/**
* Respond to checkbox change by a user and emit 'userChange'.
*/

View file

@ -1,342 +0,0 @@
( function ( mw, $ ) {
/**
* Filter-specific CapsuleMultiselectWidget
*
* @class
* @extends OO.ui.CapsuleMultiselectWidget
*
* @constructor
* @param {mw.rcfilters.Controller} controller RCFilters controller
* @param {mw.rcfilters.dm.FiltersViewModel} model RCFilters view model
* @param {OO.ui.InputWidget} filterInput A filter input that focuses the capsule widget
* @param {Object} config Configuration object
* @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
*/
mw.rcfilters.ui.FilterCapsuleMultiselectWidget = function MwRcfiltersUiFilterCapsuleMultiselectWidget( controller, model, filterInput, config ) {
var title = new OO.ui.LabelWidget( {
label: mw.msg( 'rcfilters-activefilters' ),
classes: [ 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-wrapper-content-title' ]
} ),
$contentWrapper = $( '<div>' )
.addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-wrapper' );
this.$overlay = config.$overlay || this.$element;
// Parent
mw.rcfilters.ui.FilterCapsuleMultiselectWidget.parent.call( this, $.extend( true, {
popup: {
$autoCloseIgnore: filterInput.$element.add( this.$overlay ),
$floatableContainer: filterInput.$element
}
}, config ) );
this.controller = controller;
this.model = model;
this.filterInput = filterInput;
this.isSelecting = false;
this.selected = null;
this.resetButton = new OO.ui.ButtonWidget( {
framed: false,
classes: [ 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-resetButton' ]
} );
this.emptyFilterMessage = new OO.ui.LabelWidget( {
label: mw.msg( 'rcfilters-empty-filter' ),
classes: [ 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-emptyFilters' ]
} );
this.$content.append( this.emptyFilterMessage.$element );
// Events
this.resetButton.connect( this, { click: 'onResetButtonClick' } );
this.resetButton.$element.on( 'mousedown', this.onResetButtonMouseDown.bind( this ) );
this.model.connect( this, {
itemUpdate: 'onModelItemUpdate',
highlightChange: 'onModelHighlightChange'
} );
this.aggregate( { click: 'capsuleItemClick' } );
// Add the filterInput as trigger
this.filterInput.$input
.on( 'focus', this.focus.bind( this ) );
// Build the content
$contentWrapper.append(
title.$element,
$( '<div>' )
.addClass( 'mw-rcfilters-ui-table' )
.append(
// The filter list and button should appear side by side regardless of how
// wide the button is; the button also changes its width depending
// on language and its state, so the safest way to present both side
// by side is with a table layout
$( '<div>' )
.addClass( 'mw-rcfilters-ui-row' )
.append(
this.$content
.addClass( 'mw-rcfilters-ui-cell' )
.addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-cell-filters' ),
$( '<div>' )
.addClass( 'mw-rcfilters-ui-cell' )
.addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-cell-reset' )
.append( this.resetButton.$element )
)
)
);
// Initialize
this.$handle.append( $contentWrapper );
this.$element
.addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget' );
this.reevaluateResetRestoreState();
};
/* Initialization */
OO.inheritClass( mw.rcfilters.ui.FilterCapsuleMultiselectWidget, OO.ui.CapsuleMultiselectWidget );
/* Events */
/**
* @event remove
* @param {string[]} filters Array of names of removed filters
*
* Filters were removed
*/
/* Methods */
/**
* Respond to model itemUpdate event
*
* @param {mw.rcfilters.dm.FilterItem} item Filter item model
*/
mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onModelItemUpdate = function ( item ) {
if (
item.isSelected() ||
(
this.model.isHighlightEnabled() &&
item.isHighlightSupported() &&
item.getHighlightColor()
)
) {
this.addItemByName( item.getName() );
} else {
this.removeItemByName( item.getName() );
}
// Re-evaluate reset state
this.reevaluateResetRestoreState();
};
/**
* Respond to highlightChange event
*
* @param {boolean} isHighlightEnabled Highlight is enabled
*/
mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onModelHighlightChange = function ( isHighlightEnabled ) {
var highlightedItems = this.model.getHighlightedItems();
if ( isHighlightEnabled ) {
// Add capsule widgets
highlightedItems.forEach( function ( filterItem ) {
this.addItemByName( filterItem.getName() );
}.bind( this ) );
} else {
// Remove capsule widgets if they're not selected
highlightedItems.forEach( function ( filterItem ) {
if ( !filterItem.isSelected() ) {
this.removeItemByName( filterItem.getName() );
}
}.bind( this ) );
}
};
/**
* Respond to click event on the reset button
*/
mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onResetButtonClick = function () {
if ( this.model.areCurrentFiltersEmpty() ) {
// Reset to default filters
this.controller.resetToDefaults();
} else {
// Reset to have no filters
this.controller.emptyFilters();
}
};
/**
* Respond to mouse down event on the reset button to prevent the popup from opening
*
* @param {jQuery.Event} e Event
*/
mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onResetButtonMouseDown = function ( e ) {
e.stopPropagation();
};
/**
* Reevaluate the restore state for the widget between setting to defaults and clearing all filters
*/
mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.reevaluateResetRestoreState = function () {
var defaultsAreEmpty = this.model.areDefaultFiltersEmpty(),
currFiltersAreEmpty = this.model.areCurrentFiltersEmpty(),
hideResetButton = currFiltersAreEmpty && defaultsAreEmpty;
this.resetButton.setIcon(
currFiltersAreEmpty ? 'history' : 'trash'
);
this.resetButton.setLabel(
currFiltersAreEmpty ? mw.msg( 'rcfilters-restore-default-filters' ) : ''
);
this.resetButton.setTitle(
currFiltersAreEmpty ? null : mw.msg( 'rcfilters-clear-all-filters' )
);
this.resetButton.toggle( !hideResetButton );
this.emptyFilterMessage.toggle( currFiltersAreEmpty );
};
/**
* Mark an item widget as selected
*
* @param {mw.rcfilters.ui.CapsuleItemWidget} item Capsule widget
*/
mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.select = function ( item ) {
if ( this.selected !== item ) {
// Unselect previous
if ( this.selected ) {
this.selected.toggleSelected( false );
}
// Select new one
this.selected = item;
if ( this.selected ) {
item.toggleSelected( true );
}
}
};
/**
* Reset selection and remove selected states from all items
*/
mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.resetSelection = function () {
if ( this.selected !== null ) {
this.selected = null;
this.getItems().forEach( function ( capsuleWidget ) {
capsuleWidget.toggleSelected( false );
} );
}
};
/**
* @inheritdoc
*/
mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.createItemWidget = function ( data ) {
var item = this.model.getItemByName( data );
if ( !item ) {
return;
}
return new mw.rcfilters.ui.CapsuleItemWidget(
this.controller,
item,
{ $overlay: this.$overlay }
);
};
/**
* Add items by their filter name
*
* @param {string} name Filter name
*/
mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.addItemByName = function ( name ) {
var item = this.model.getItemByName( name );
if ( !item ) {
return;
}
// Check that the item isn't already added
if ( !this.getItemFromData( name ) ) {
this.addItems( [ this.createItemWidget( name ) ] );
}
};
/**
* Remove items by their filter name
*
* @param {string} name Filter name
*/
mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.removeItemByName = function ( name ) {
this.removeItemsFromData( [ name ] );
};
/**
* @inheritdoc
*/
mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.focus = function () {
// Override this method; we don't want to focus on the popup, and we
// don't want to bind the size to the handle.
if ( !this.isDisabled() ) {
this.popup.toggle( true );
this.filterInput.$input.get( 0 ).focus();
}
return this;
};
/**
* @inheritdoc
*/
mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onFocusForPopup = function () {
// HACK can be removed once I21b8cff4048 is merged in oojs-ui
this.focus();
};
/**
* @inheritdoc
*/
mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onKeyDown = function () {};
/**
* @inheritdoc
*/
mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onPopupFocusOut = function () {};
/**
* @inheritdoc
*/
mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.clearInput = function () {
if ( this.filterInput ) {
this.filterInput.setValue( '' );
}
this.menu.toggle( false );
this.menu.selectItem();
this.menu.highlightItem();
};
/**
* @inheritdoc
*/
mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.removeItems = function ( items ) {
// Parent call
mw.rcfilters.ui.FilterCapsuleMultiselectWidget.parent.prototype.removeItems.call( this, items );
// Destroy the item widget when it is removed
// This is done because we re-add items by recreating them, rather than hiding them
// and items include popups, that will just continue to be created and appended
// unnecessarily.
items.forEach( function ( widget ) {
widget.destroy();
} );
};
/**
* Override 'editItem' since it tries to use $input which does
* not exist when a popup is available.
*/
mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.editItem = function () {};
}( mediaWiki, jQuery ) );

View file

@ -0,0 +1,137 @@
( function ( mw ) {
/**
* A floating menu widget for the filter list
*
* @extends OO.ui.FloatingMenuSelectWidget
*
* @constructor
* @param {mw.rcfilters.Controller} controller Controller
* @param {mw.rcfilters.dm.FiltersViewModel} model View model
* @param {Object} [config] Configuration object
* @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
* @cfg {jQuery} [$footer] An optional footer for the menu
*/
mw.rcfilters.ui.FilterFloatingMenuSelectWidget = function MwRcfiltersUiFilterFloatingMenuSelectWidget( controller, model, config ) {
var header;
config = config || {};
this.controller = controller;
this.model = model;
this.inputValue = '';
this.$overlay = config.$overlay || this.$element;
this.$footer = config.$footer;
this.$body = $( '<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.$element
.addClass( 'mw-rcfilters-ui-filterFloatingMenuSelectWidget' )
.append(
this.$body
.append( header.$element, this.$group )
);
if ( this.$footer ) {
this.$element.append(
this.$footer
.addClass( 'mw-rcfilters-ui-filterFloatingMenuSelectWidget-footer' )
);
}
};
/* Initialize */
OO.inheritClass( mw.rcfilters.ui.FilterFloatingMenuSelectWidget, OO.ui.FloatingMenuSelectWidget );
/* Events */
/**
* @event itemVisibilityChange
*
* Item visibility has changed
*/
/* Methods */
/**
* @fires itemVisibilityChange
* @inheritdoc
*/
mw.rcfilters.ui.FilterFloatingMenuSelectWidget.prototype.updateItemVisibility = function () {
var i,
itemWasHighlighted = false,
inputVal = this.$input.val(),
items = this.getItems();
// Since the method hides/shows items, we don't want to
// call it unless the input actually changed
if ( this.inputValue !== inputVal ) {
// Parent method
mw.rcfilters.ui.FilterFloatingMenuSelectWidget.parent.prototype.updateItemVisibility.call( this );
if ( inputVal !== '' ) {
// Highlight the first item in the list
for ( i = 0; i < items.length; i++ ) {
if (
!( items[ i ] instanceof OO.ui.MenuSectionOptionWidget ) &&
items[ i ].isVisible()
) {
itemWasHighlighted = true;
this.highlightItem( items[ i ] );
break;
}
}
}
if ( !itemWasHighlighted ) {
this.highlightItem( null );
}
// Cache value
this.inputValue = inputVal;
this.emit( 'itemVisibilityChange' );
}
};
/**
* Override the item matcher to use the model's match process
*
* @inheritdoc
*/
mw.rcfilters.ui.FilterFloatingMenuSelectWidget.prototype.getItemMatcher = function ( s ) {
var results = this.model.findMatches( s, true );
return function ( item ) {
return results.indexOf( item.getModel() ) > -1;
};
};
/**
* Scroll to the top of the menu
*/
mw.rcfilters.ui.FilterFloatingMenuSelectWidget.prototype.scrollToTop = function () {
this.$body.scrollTop( 0 );
};
}( mediaWiki ) );

View file

@ -1,171 +0,0 @@
( function ( mw, $ ) {
/**
* A group of filters
*
* @extends OO.ui.Widget
* @mixins OO.ui.mixin.GroupWidget
* @mixins OO.ui.mixin.LabelElement
*
* @constructor
* @param {mw.rcfilters.Controller} controller Controller
* @param {mw.rcfilters.dm.FilterGroup} model Filter group model
* @param {Object} config Configuration object
* @cfg {jQuery} [$overlay] Overlay
*/
mw.rcfilters.ui.FilterGroupWidget = function MwRcfiltersUiFilterGroupWidget( controller, model, config ) {
var whatsThisMessages,
$header = $( '<div>' )
.addClass( 'mw-rcfilters-ui-filterGroupWidget-header' ),
$popupContent = $( '<div>' )
.addClass( 'mw-rcfilters-ui-filterGroupWidget-whatsThisButton-popup-content' );
config = config || {};
// Parent
mw.rcfilters.ui.FilterGroupWidget.parent.call( this, config );
this.controller = controller;
this.model = model;
this.filters = {};
this.$overlay = config.$overlay || this.$element;
// Mixin constructors
OO.ui.mixin.GroupWidget.call( this, config );
OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, {
label: this.model.getTitle(),
$label: $( '<div>' )
.addClass( 'mw-rcfilters-ui-filterGroupWidget-header-title' )
} ) );
$header.append( this.$label );
if ( this.model.hasWhatsThis() ) {
whatsThisMessages = this.model.getWhatsThis();
// Create popup
if ( whatsThisMessages.header ) {
$popupContent.append(
( new OO.ui.LabelWidget( {
label: mw.msg( whatsThisMessages.header ),
classes: [ 'mw-rcfilters-ui-filterGroupWidget-whatsThisButton-popup-content-header' ]
} ) ).$element
);
}
if ( whatsThisMessages.body ) {
$popupContent.append(
( new OO.ui.LabelWidget( {
label: mw.msg( whatsThisMessages.body ),
classes: [ 'mw-rcfilters-ui-filterGroupWidget-whatsThisButton-popup-content-body' ]
} ) ).$element
);
}
if ( whatsThisMessages.linkText && whatsThisMessages.url ) {
$popupContent.append(
( new OO.ui.ButtonWidget( {
framed: false,
flags: [ 'progressive' ],
href: whatsThisMessages.url,
label: mw.msg( whatsThisMessages.linkText ),
classes: [ 'mw-rcfilters-ui-filterGroupWidget-whatsThisButton-popup-content-link' ]
} ) ).$element
);
}
// Add button
this.whatsThisButton = new OO.ui.PopupButtonWidget( {
framed: false,
label: mw.msg( 'rcfilters-filterlist-whatsthis' ),
$overlay: this.$overlay,
classes: [ 'mw-rcfilters-ui-filterGroupWidget-whatsThisButton' ],
flags: [ 'progressive' ],
popup: {
padded: false,
align: 'center',
position: 'above',
$content: $popupContent,
classes: [ 'mw-rcfilters-ui-filterGroupWidget-whatsThisButton-popup' ]
}
} );
$header
.append( this.whatsThisButton.$element );
}
// Populate
this.populateFromModel();
this.model.connect( this, { update: 'onModelUpdate' } );
this.$element
.addClass( 'mw-rcfilters-ui-filterGroupWidget' )
.addClass( 'mw-rcfilters-ui-filterGroupWidget-name-' + this.model.getName() )
.append(
$header,
this.$group
.addClass( 'mw-rcfilters-ui-filterGroupWidget-group' )
);
};
/* Initialization */
OO.inheritClass( mw.rcfilters.ui.FilterGroupWidget, OO.ui.Widget );
OO.mixinClass( mw.rcfilters.ui.FilterGroupWidget, OO.ui.mixin.GroupWidget );
OO.mixinClass( mw.rcfilters.ui.FilterGroupWidget, OO.ui.mixin.LabelElement );
/**
* Respond to model update event
*/
mw.rcfilters.ui.FilterGroupWidget.prototype.onModelUpdate = function () {
this.$element.toggleClass(
'mw-rcfilters-ui-filterGroupWidget-active',
this.model.isActive()
);
};
/**
* Get an item widget from its filter name
*
* @param {string} filterName Filter name
* @return {mw.rcfilters.ui.FilterItemWidget} Item widget
*/
mw.rcfilters.ui.FilterGroupWidget.prototype.getItemWidget = function ( filterName ) {
return this.filters[ filterName ];
};
/**
* Populate data from the model
*/
mw.rcfilters.ui.FilterGroupWidget.prototype.populateFromModel = function () {
var widget = this;
this.clearItems();
this.filters = {};
this.addItems(
this.model.getItems().map( function ( filterItem ) {
var groupWidget = new mw.rcfilters.ui.FilterItemWidget(
widget.controller,
filterItem,
{
label: filterItem.getLabel(),
description: filterItem.getDescription(),
$overlay: widget.$overlay
}
);
widget.filters[ filterItem.getName() ] = groupWidget;
return groupWidget;
} )
);
};
/**
* Get the group name
*
* @return {string} Group name
*/
mw.rcfilters.ui.FilterGroupWidget.prototype.getName = function () {
return this.model.getName();
};
}( mediaWiki, jQuery ) );

View file

@ -15,7 +15,7 @@
this.colorPickerWidget = new mw.rcfilters.ui.HighlightColorPickerWidget( controller, model );
// Parent
mw.rcfilters.ui.FilterItemHighlightButton.parent.call( this, $.extend( {}, config, {
mw.rcfilters.ui.FilterItemHighlightButton.parent.call( this, $.extend( true, {}, config, {
icon: 'highlight',
indicator: 'down',
popup: {
@ -39,6 +39,10 @@
// Event
this.model.connect( this, { update: 'onModelUpdate' } );
this.colorPickerWidget.connect( this, { chooseColor: 'onChooseColor' } );
// This lives inside a MenuOptionWidget, which intercepts mousedown
// to select the item. We want to prevent that when we click the highlight
// button
this.$element.on( 'mousedown', function ( e ) { e.stopPropagation(); } );
this.$element
.addClass( 'mw-rcfilters-ui-filterItemHighlightButton' );

View file

@ -0,0 +1,86 @@
( function ( mw, $ ) {
/**
* Menu header for the RCFilters filters menu
*
* @extends OO.ui.Widget
*
* @constructor
* @param {mw.rcfilters.Controller} controller Controller
* @param {mw.rcfilters.dm.FiltersViewModel} model View model
* @param {Object} config Configuration object
* @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
*/
mw.rcfilters.ui.FilterMenuHeaderWidget = function MwRcfiltersUiFilterMenuHeaderWidget( controller, model, config ) {
config = config || {};
this.controller = controller;
this.model = model;
this.$overlay = config.$overlay || this.$element;
// Parent
mw.rcfilters.ui.FilterMenuHeaderWidget.parent.call( this, config );
OO.ui.mixin.LabelElement.call( this, $.extend( {
label: mw.msg( 'rcfilters-filterlist-title' ),
$label: $( '<div>' )
.addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-title' )
}, config ) );
// Highlight button
this.highlightButton = new OO.ui.ToggleButtonWidget( {
icon: 'highlight',
label: mw.message( 'rcfilters-highlightbutton-title' ).text(),
classes: [ 'mw-rcfilters-ui-filterMenuHeaderWidget-hightlightButton' ]
} );
// Events
this.highlightButton
.connect( this, { click: 'onHighlightButtonClick' } );
this.model.connect( this, { highlightChange: 'onModelHighlightChange' } );
// Initialize
this.$element
.addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget' )
.append(
$( '<div>' )
.addClass( 'mw-rcfilters-ui-table' )
.addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header' )
.append(
$( '<div>' )
.addClass( 'mw-rcfilters-ui-row' )
.append(
$( '<div>' )
.addClass( 'mw-rcfilters-ui-cell' )
.addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header-title' )
.append( this.$label ),
$( '<div>' )
.addClass( 'mw-rcfilters-ui-cell' )
.addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header-highlight' )
.append( this.highlightButton.$element )
)
)
);
};
/* Initialization */
OO.inheritClass( mw.rcfilters.ui.FilterMenuHeaderWidget, OO.ui.Widget );
OO.mixinClass( mw.rcfilters.ui.FilterMenuHeaderWidget, OO.ui.mixin.LabelElement );
/* Methods */
/**
* Respond to model highlight change event
*
* @param {boolean} highlightEnabled Highlight is enabled
*/
mw.rcfilters.ui.FilterMenuHeaderWidget.prototype.onModelHighlightChange = function ( highlightEnabled ) {
this.highlightButton.setActive( highlightEnabled );
};
/**
* Respond to highlight button click
*/
mw.rcfilters.ui.FilterMenuHeaderWidget.prototype.onHighlightButtonClick = function () {
this.controller.toggleHighlight();
};
}( mediaWiki, jQuery ) );

View file

@ -1,27 +1,31 @@
( function ( mw, $ ) {
( function ( mw ) {
/**
* A widget representing a single toggle filter
*
* @extends OO.ui.Widget
* @extends OO.ui.MenuOptionWidget
*
* @constructor
* @param {mw.rcfilters.Controller} controller RCFilters controller
* @param {mw.rcfilters.dm.FilterItem} model Filter item model
* @param {Object} config Configuration object
*/
mw.rcfilters.ui.FilterItemWidget = function MwRcfiltersUiFilterItemWidget( controller, model, config ) {
mw.rcfilters.ui.FilterMenuOptionWidget = function MwRcfiltersUiFilterMenuOptionWidget( controller, model, config ) {
var layout,
$label = $( '<div>' )
.addClass( 'mw-rcfilters-ui-filterItemWidget-label' );
.addClass( 'mw-rcfilters-ui-filterMenuOptionWidget-label' );
config = config || {};
// Parent
mw.rcfilters.ui.FilterItemWidget.parent.call( this, config );
this.controller = controller;
this.model = model;
this.selected = false;
// Parent
mw.rcfilters.ui.FilterMenuOptionWidget.parent.call( this, $.extend( {
// Override the 'check' icon that OOUI defines
icon: '',
data: this.model.getName(),
label: this.model.getLabel()
}, config ) );
this.checkboxWidget = new mw.rcfilters.ui.CheckboxInputWidget( {
value: this.model.getName(),
@ -30,13 +34,13 @@
$label.append(
$( '<div>' )
.addClass( 'mw-rcfilters-ui-filterItemWidget-label-title' )
.text( this.model.getLabel() )
.addClass( 'mw-rcfilters-ui-filterMenuOptionWidget-label-title' )
.append( this.$label )
);
if ( this.model.getDescription() ) {
$label.append(
$( '<div>' )
.addClass( 'mw-rcfilters-ui-filterItemWidget-label-desc' )
.addClass( 'mw-rcfilters-ui-filterMenuOptionWidget-label-desc' )
.text( this.model.getDescription() )
);
}
@ -57,12 +61,11 @@
} );
// Event
this.checkboxWidget.connect( this, { userChange: 'onCheckboxChange' } );
this.model.connect( this, { update: 'onModelUpdate' } );
this.model.getGroupModel().connect( this, { update: 'onGroupModelUpdate' } );
this.$element
.addClass( 'mw-rcfilters-ui-filterItemWidget' )
.addClass( 'mw-rcfilters-ui-filterMenuOptionWidget' )
.append(
$( '<div>' )
.addClass( 'mw-rcfilters-ui-table' )
@ -71,10 +74,10 @@
.addClass( 'mw-rcfilters-ui-row' )
.append(
$( '<div>' )
.addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-filterItemWidget-filterCheckbox' )
.addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-filterMenuOptionWidget-filterCheckbox' )
.append( layout.$element ),
$( '<div>' )
.addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-filterItemWidget-highlightButton' )
.addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-filterMenuOptionWidget-highlightButton' )
.append( this.highlightButton.$element )
)
)
@ -83,25 +86,19 @@
/* Initialization */
OO.inheritClass( mw.rcfilters.ui.FilterItemWidget, OO.ui.Widget );
OO.inheritClass( mw.rcfilters.ui.FilterMenuOptionWidget, OO.ui.MenuOptionWidget );
/* Static properties */
// We do our own scrolling to top
mw.rcfilters.ui.FilterMenuOptionWidget.static.scrollIntoViewOnSelect = false;
/* Methods */
/**
* Respond to checkbox change.
* NOTE: This event is emitted both for deliberate user action and for
* a change that the code requests ('setSelected')
*
* @param {boolean} isSelected The checkbox is selected
*/
mw.rcfilters.ui.FilterItemWidget.prototype.onCheckboxChange = function ( isSelected ) {
this.controller.toggleFilterSelect( this.model.getName(), isSelected );
};
/**
* Respond to item model update event
*/
mw.rcfilters.ui.FilterItemWidget.prototype.onModelUpdate = function () {
mw.rcfilters.ui.FilterMenuOptionWidget.prototype.onModelUpdate = function () {
this.checkboxWidget.setSelected( this.model.isSelected() );
this.setCurrentMuteState();
@ -110,31 +107,16 @@
/**
* Respond to item group model update event
*/
mw.rcfilters.ui.FilterItemWidget.prototype.onGroupModelUpdate = function () {
mw.rcfilters.ui.FilterMenuOptionWidget.prototype.onGroupModelUpdate = function () {
this.setCurrentMuteState();
};
/**
* Set selected state on this widget
*
* @param {boolean} [isSelected] Widget is selected
*/
mw.rcfilters.ui.FilterItemWidget.prototype.toggleSelected = function ( isSelected ) {
isSelected = isSelected !== undefined ? isSelected : !this.selected;
if ( this.selected !== isSelected ) {
this.selected = isSelected;
this.$element.toggleClass( 'mw-rcfilters-ui-filterItemWidget-selected', this.selected );
}
};
/**
* Set the current mute state for this item
*/
mw.rcfilters.ui.FilterItemWidget.prototype.setCurrentMuteState = function () {
mw.rcfilters.ui.FilterMenuOptionWidget.prototype.setCurrentMuteState = function () {
this.$element.toggleClass(
'mw-rcfilters-ui-filterItemWidget-muted',
'mw-rcfilters-ui-filterMenuOptionWidget-muted',
this.model.isConflicted() ||
(
// Item is also muted when any of the items in its group is active
@ -154,7 +136,12 @@
*
* @return {string} Filter name
*/
mw.rcfilters.ui.FilterItemWidget.prototype.getName = function () {
mw.rcfilters.ui.FilterMenuOptionWidget.prototype.getName = function () {
return this.model.getName();
};
}( mediaWiki, jQuery ) );
mw.rcfilters.ui.FilterMenuOptionWidget.prototype.getModel = function () {
return this.model;
};
}( mediaWiki ) );

View file

@ -0,0 +1,123 @@
( function ( mw ) {
/**
* A widget representing a menu section for filter groups
*
* @extends OO.ui.MenuSectionOptionWidget
*
* @constructor
* @param {mw.rcfilters.Controller} controller RCFilters controller
* @param {mw.rcfilters.dm.FilterGroup} model Filter group model
* @param {Object} config Configuration object
* @cfg {jQuery} [$overlay] Overlay
*/
mw.rcfilters.ui.FilterMenuSectionOptionWidget = function MwRcfiltersUiFilterMenuSectionOptionWidget( controller, model, config ) {
var whatsThisMessages,
$header = $( '<div>' )
.addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-header' ),
$popupContent = $( '<div>' )
.addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content' );
config = config || {};
this.controller = controller;
this.model = model;
this.$overlay = config.$overlay || this.$element;
// Parent
mw.rcfilters.ui.FilterMenuSectionOptionWidget.parent.call( this, $.extend( {
label: this.model.getTitle(),
$label: $( '<div>' )
.addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-header-title' )
}, config ) );
$header.append( this.$label );
if ( this.model.hasWhatsThis() ) {
whatsThisMessages = this.model.getWhatsThis();
// Create popup
if ( whatsThisMessages.header ) {
$popupContent.append(
( new OO.ui.LabelWidget( {
label: mw.msg( whatsThisMessages.header ),
classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content-header' ]
} ) ).$element
);
}
if ( whatsThisMessages.body ) {
$popupContent.append(
( new OO.ui.LabelWidget( {
label: mw.msg( whatsThisMessages.body ),
classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content-body' ]
} ) ).$element
);
}
if ( whatsThisMessages.linkText && whatsThisMessages.url ) {
$popupContent.append(
( new OO.ui.ButtonWidget( {
framed: false,
flags: [ 'progressive' ],
href: whatsThisMessages.url,
label: mw.msg( whatsThisMessages.linkText ),
classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content-link' ]
} ) ).$element
);
}
// Add button
this.whatsThisButton = new OO.ui.PopupButtonWidget( {
framed: false,
label: mw.msg( 'rcfilters-filterlist-whatsthis' ),
$overlay: this.$overlay,
classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton' ],
flags: [ 'progressive' ],
popup: {
$autoCloseIgnore: this.$element.add( this.$overlay ),
padded: false,
align: 'center',
position: 'above',
$content: $popupContent,
classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup' ]
}
} );
$header
.append( this.whatsThisButton.$element );
}
// Events
this.model.connect( this, { update: 'onModelUpdate' } );
// Initialize
this.$element
.addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget' )
.addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-name-' + this.model.getName() )
.append( $header );
};
/* Initialize */
OO.inheritClass( mw.rcfilters.ui.FilterMenuSectionOptionWidget, OO.ui.MenuSectionOptionWidget );
/* Methods */
/**
* Respond to model update event
*/
mw.rcfilters.ui.FilterMenuSectionOptionWidget.prototype.onModelUpdate = function () {
this.$element.toggleClass(
'mw-rcfilters-ui-filterMenuSectionOptionWidget-active',
this.model.isActive()
);
};
/**
* Get the group name
*
* @return {string} Group name
*/
mw.rcfilters.ui.FilterMenuSectionOptionWidget.prototype.getName = function () {
return this.model.getName();
};
}( mediaWiki ) );

View file

@ -1,9 +1,9 @@
( function ( mw, $ ) {
/**
* Extend OOUI's CapsuleItemWidget to also display a popup on hover.
* Extend OOUI's FilterTagItemWidget to also display a popup on hover.
*
* @class
* @extends OO.ui.CapsuleItemWidget
* @extends OO.ui.FilterTagItemWidget
* @mixins OO.ui.mixin.PopupElement
*
* @constructor
@ -12,24 +12,21 @@
* @param {Object} config Configuration object
* @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
*/
mw.rcfilters.ui.CapsuleItemWidget = function MwRcfiltersUiCapsuleItemWidget( controller, model, config ) {
mw.rcfilters.ui.FilterTagItemWidget = function MwRcfiltersUiFilterTagItemWidget( controller, model, config ) {
// Configuration initialization
config = config || {};
this.controller = controller;
this.model = model;
this.popupLabel = new OO.ui.LabelWidget();
this.$overlay = config.$overlay || this.$element;
this.positioned = false;
this.popupTimeoutShow = null;
this.popupTimeoutHide = null;
// Parent constructor
mw.rcfilters.ui.CapsuleItemWidget.parent.call( this, $.extend( {
mw.rcfilters.ui.FilterTagItemWidget.parent.call( this, $.extend( {
data: this.model.getName(),
label: this.model.getLabel()
}, config ) );
this.$overlay = config.$overlay || this.$element;
this.popupLabel = new OO.ui.LabelWidget();
// Mixin constructors
OO.ui.mixin.PopupElement.call( this, $.extend( {
popup: {
@ -37,15 +34,19 @@
align: 'center',
position: 'above',
$content: $( '<div>' )
.addClass( 'mw-rcfilters-ui-capsuleItemWidget-popup-content' )
.addClass( 'mw-rcfilters-ui-filterTagItemWidget-popup-content' )
.append( this.popupLabel.$element ),
$floatableContainer: this.$element,
classes: [ 'mw-rcfilters-ui-capsuleItemWidget-popup' ]
classes: [ 'mw-rcfilters-ui-filterTagItemWidget-popup' ]
}
}, config ) );
this.positioned = false;
this.popupTimeoutShow = null;
this.popupTimeoutHide = null;
this.$highlight = $( '<div>' )
.addClass( 'mw-rcfilters-ui-capsuleItemWidget-highlight' );
.addClass( 'mw-rcfilters-ui-filterTagItemWidget-highlight' );
// Events
this.model.connect( this, { update: 'onModelUpdate' } );
@ -55,8 +56,6 @@
this.$element
.prepend( this.$highlight )
.attr( 'aria-haspopup', 'true' )
.addClass( 'mw-rcfilters-ui-capsuleItemWidget' )
.on( 'mousedown', this.onMouseDown.bind( this ) )
.on( 'mouseenter', this.onMouseEnter.bind( this ) )
.on( 'mouseleave', this.onMouseLeave.bind( this ) );
@ -64,58 +63,29 @@
this.setHighlightColor();
};
OO.inheritClass( mw.rcfilters.ui.CapsuleItemWidget, OO.ui.CapsuleItemWidget );
OO.mixinClass( mw.rcfilters.ui.CapsuleItemWidget, OO.ui.mixin.PopupElement );
/* Initialization */
OO.inheritClass( mw.rcfilters.ui.FilterTagItemWidget, OO.ui.TagItemWidget );
OO.mixinClass( mw.rcfilters.ui.FilterTagItemWidget, OO.ui.mixin.PopupElement );
/* Methods */
/**
* Respond to model update event
*/
mw.rcfilters.ui.CapsuleItemWidget.prototype.onModelUpdate = function () {
mw.rcfilters.ui.FilterTagItemWidget.prototype.onModelUpdate = function () {
this.setCurrentMuteState();
this.setHighlightColor();
};
/**
* Override mousedown event to prevent its propagation to the parent,
* since the parent (the multiselect widget) focuses the popup when its
* mousedown event is fired.
*
* @param {jQuery.Event} e Event
*/
mw.rcfilters.ui.CapsuleItemWidget.prototype.onMouseDown = function ( e ) {
e.stopPropagation();
};
/**
* Emit a click event when the capsule is clicked so we can aggregate this
* in the parent (the capsule)
*/
mw.rcfilters.ui.CapsuleItemWidget.prototype.onClick = function () {
this.emit( 'click' );
};
/**
* Override the event listening to the item close button click
*/
mw.rcfilters.ui.CapsuleItemWidget.prototype.onCloseClick = function () {
var element = this.getElementGroup();
if ( element && $.isFunction( element.removeItems ) ) {
element.removeItems( [ this ] );
}
// Respond to user removing the filter
this.controller.clearFilter( this.model.getName() );
};
mw.rcfilters.ui.CapsuleItemWidget.prototype.setHighlightColor = function () {
mw.rcfilters.ui.FilterTagItemWidget.prototype.setHighlightColor = function () {
var selectedColor = this.model.isHighlightEnabled() ? this.model.getHighlightColor() : null;
this.$highlight
.attr( 'data-color', selectedColor )
.toggleClass(
'mw-rcfilters-ui-capsuleItemWidget-highlight-highlighted',
'mw-rcfilters-ui-filterTagItemWidget-highlight-highlighted',
!!selectedColor
);
};
@ -123,16 +93,16 @@
/**
* Set the current mute state for this item
*/
mw.rcfilters.ui.CapsuleItemWidget.prototype.setCurrentMuteState = function () {
mw.rcfilters.ui.FilterTagItemWidget.prototype.setCurrentMuteState = function () {
this.$element
.toggleClass(
'mw-rcfilters-ui-capsuleItemWidget-muted',
'mw-rcfilters-ui-filterTagItemWidget-muted',
!this.model.isSelected() ||
this.model.isIncluded() ||
this.model.isFullyCovered()
)
.toggleClass(
'mw-rcfilters-ui-capsuleItemWidget-conflicted',
'mw-rcfilters-ui-filterTagItemWidget-conflicted',
this.model.isSelected() && this.model.isConflicted()
);
};
@ -140,7 +110,7 @@
/**
* Respond to mouse enter event
*/
mw.rcfilters.ui.CapsuleItemWidget.prototype.onMouseEnter = function () {
mw.rcfilters.ui.FilterTagItemWidget.prototype.onMouseEnter = function () {
var labelText = this.model.getStateMessage();
if ( labelText ) {
@ -166,7 +136,7 @@
/**
* Respond to mouse leave event
*/
mw.rcfilters.ui.CapsuleItemWidget.prototype.onMouseLeave = function () {
mw.rcfilters.ui.FilterTagItemWidget.prototype.onMouseLeave = function () {
this.popupTimeoutHide = setTimeout( function () {
this.popup.toggle( false );
}.bind( this ), 250 );
@ -181,20 +151,29 @@
*
* @param {boolean} [isSelected] Widget is selected
*/
mw.rcfilters.ui.CapsuleItemWidget.prototype.toggleSelected = function ( isSelected ) {
mw.rcfilters.ui.FilterTagItemWidget.prototype.toggleSelected = function ( isSelected ) {
isSelected = isSelected !== undefined ? isSelected : !this.selected;
if ( this.selected !== isSelected ) {
this.selected = isSelected;
this.$element.toggleClass( 'mw-rcfilters-ui-capsuleItemWidget-selected', this.selected );
this.$element.toggleClass( 'mw-rcfilters-ui-filterTagItemWidget-selected', this.selected );
}
};
/**
* Get item name
*
* @return {string} Filter name
*/
mw.rcfilters.ui.FilterTagItemWidget.prototype.getName = function () {
return this.model.getName();
};
/**
* Remove and destroy external elements of this widget
*/
mw.rcfilters.ui.CapsuleItemWidget.prototype.destroy = function () {
mw.rcfilters.ui.FilterTagItemWidget.prototype.destroy = function () {
// Destroy the popup
this.popup.$element.detach();

View file

@ -0,0 +1,393 @@
( function ( mw ) {
/**
* List displaying all filter groups
*
* @extends OO.ui.Widget
* @mixins OO.ui.mixin.PendingElement
*
* @constructor
* @param {mw.rcfilters.Controller} controller Controller
* @param {mw.rcfilters.dm.FiltersViewModel} model View model
* @param {Object} config Configuration object
* @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
*/
mw.rcfilters.ui.FilterTagMultiselectWidget = function MwRcfiltersUiFilterTagMultiselectWidget( controller, model, config ) {
var title = new OO.ui.LabelWidget( {
label: mw.msg( 'rcfilters-activefilters' ),
classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-title' ]
} ),
$contentWrapper = $( '<div>' )
.addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper' );
config = config || {};
this.controller = controller;
this.model = model;
this.$overlay = config.$overlay || this.$element;
// Parent
mw.rcfilters.ui.FilterTagMultiselectWidget.parent.call( this, $.extend( true, {
label: mw.msg( 'rcfilters-filterlist-title' ),
placeholder: mw.msg( 'rcfilters-empty-filter' ),
inputPosition: 'outline',
allowArbitrary: false,
allowDisplayInvalidTags: false,
allowReordering: false,
$overlay: this.$overlay,
menu: {
hideWhenOutOfView: false,
hideOnChoose: false,
width: 650,
$footer: $( '<div>' )
.append(
new OO.ui.ButtonWidget( {
framed: false,
icon: 'feedback',
flags: [ 'progressive' ],
label: mw.msg( 'rcfilters-filterlist-feedbacklink' ),
href: 'https://www.mediawiki.org/wiki/Help_talk:New_filters_for_edit_review'
} ).$element
)
},
input: {
icon: 'search',
placeholder: mw.msg( 'rcfilters-search-placeholder' )
}
}, config ) );
this.resetButton = new OO.ui.ButtonWidget( {
framed: false,
classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-resetButton' ]
} );
this.emptyFilterMessage = new OO.ui.LabelWidget( {
label: mw.msg( 'rcfilters-empty-filter' ),
classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-emptyFilters' ]
} );
this.$content.append( this.emptyFilterMessage.$element );
// Events
this.resetButton.connect( this, { click: 'onResetButtonClick' } );
// Stop propagation for mousedown, so that the widget doesn't
// trigger the focus on the input and scrolls up when we click the reset button
this.resetButton.$element.on( 'mousedown', function ( e ) { e.stopPropagation(); } );
this.model.connect( this, {
initialize: 'onModelInitialize',
itemUpdate: 'onModelItemUpdate',
highlightChange: 'onModelHighlightChange'
} );
this.menu.connect( this, { toggle: 'onMenuToggle' } );
// Build the content
$contentWrapper.append(
title.$element,
$( '<div>' )
.addClass( 'mw-rcfilters-ui-table' )
.append(
// The filter list and button should appear side by side regardless of how
// wide the button is; the button also changes its width depending
// on language and its state, so the safest way to present both side
// by side is with a table layout
$( '<div>' )
.addClass( 'mw-rcfilters-ui-row' )
.append(
this.$content
.addClass( 'mw-rcfilters-ui-cell' )
.addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-filters' ),
$( '<div>' )
.addClass( 'mw-rcfilters-ui-cell' )
.addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-reset' )
.append( this.resetButton.$element )
)
)
);
// Initialize
this.$handle.append( $contentWrapper );
this.emptyFilterMessage.toggle( this.isEmpty() );
this.$element
.addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget' );
this.populateFromModel();
this.reevaluateResetRestoreState();
};
/* Initialization */
OO.inheritClass( mw.rcfilters.ui.FilterTagMultiselectWidget, OO.ui.MenuTagMultiselectWidget );
/* Methods */
/**
* Respond to menu toggle
*
* @param {boolean} isVisible Menu is visible
*/
mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onMenuToggle = function ( isVisible ) {
if ( isVisible ) {
mw.hook( 'RcFilters.popup.open' ).fire( this.getMenu().getSelectedItem() );
if ( !this.getMenu().getSelectedItem() ) {
// If there are no selected items, scroll menu to top
// This has to be in a setTimeout so the menu has time
// to be positioned and fixed
setTimeout( function () { this.getMenu().scrollToTop(); }.bind( this ), 0 );
}
} else {
// Clear selection
this.getMenu().selectItem( null );
}
};
/**
* @inheritdoc
*/
mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onInputFocus = function () {
// Parent
mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onInputFocus.call( this );
// Scroll to top
this.scrollToTop( this.$element );
};
/**
* @inheridoc
*/
mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onChangeTags = function () {
// Parent method
mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onChangeTags.call( this );
this.emptyFilterMessage.toggle( this.isEmpty() );
};
/**
* Respond to model initialize event
*/
mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelInitialize = function () {
this.populateFromModel();
};
/**
* Respond to model itemUpdate event
*
* @param {mw.rcfilters.dm.FilterItem} item Filter item model
*/
mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelItemUpdate = function ( item ) {
if (
item.isSelected() ||
(
this.model.isHighlightEnabled() &&
item.isHighlightSupported() &&
item.getHighlightColor()
)
) {
this.addTag( item.getName(), item.getLabel() );
} else {
this.removeTagByData( item.getName() );
}
// Re-evaluate reset state
this.reevaluateResetRestoreState();
};
/**
* @inheritdoc
*/
mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.isAllowedData = function ( data ) {
return (
this.menu.getItemFromData( data ) &&
!this.isDuplicateData( data )
);
};
/**
* @inheritdoc
*/
mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onMenuChoose = function ( item ) {
this.controller.toggleFilterSelect( item.model.getName() );
};
/**
* Respond to highlightChange event
*
* @param {boolean} isHighlightEnabled Highlight is enabled
*/
mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelHighlightChange = function ( isHighlightEnabled ) {
var highlightedItems = this.model.getHighlightedItems();
if ( isHighlightEnabled ) {
// Add capsule widgets
highlightedItems.forEach( function ( filterItem ) {
this.addTag( filterItem.getName(), filterItem.getLabel() );
}.bind( this ) );
} else {
// Remove capsule widgets if they're not selected
highlightedItems.forEach( function ( filterItem ) {
if ( !filterItem.isSelected() ) {
this.removeTagByData( filterItem.getName() );
}
}.bind( this ) );
}
};
/**
* @inheritdoc
*/
mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onTagSelect = function ( tagItem ) {
var widget = this,
menuOption = this.menu.getItemFromData( tagItem.getData() );
// Reset input
this.input.setValue( '' );
// Parent method
mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onTagSelect.call( this, tagItem );
this.menu.selectItem( menuOption );
// Scroll to the item
// We're binding a 'once' to the itemVisibilityChange event
// so this happens when the menu is ready after the items
// are visible again, in case this is done right after the
// user filtered the results
this.getMenu().once(
'itemVisibilityChange',
function () { widget.scrollToTop( menuOption.$element ); }
);
};
/**
* @inheritdoc
*/
mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onTagRemove = function ( tagItem ) {
// Parent method
mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onTagRemove.call( this, tagItem );
this.controller.clearFilter( tagItem.getName() );
};
/**
* Respond to click event on the reset button
*/
mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onResetButtonClick = function () {
if ( this.model.areCurrentFiltersEmpty() ) {
// Reset to default filters
this.controller.resetToDefaults();
} else {
// Reset to have no filters
this.controller.emptyFilters();
}
};
/**
* Reevaluate the restore state for the widget between setting to defaults and clearing all filters
*/
mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.reevaluateResetRestoreState = function () {
var defaultsAreEmpty = this.model.areDefaultFiltersEmpty(),
currFiltersAreEmpty = this.model.areCurrentFiltersEmpty(),
hideResetButton = currFiltersAreEmpty && defaultsAreEmpty;
this.resetButton.setIcon(
currFiltersAreEmpty ? 'history' : 'trash'
);
this.resetButton.setLabel(
currFiltersAreEmpty ? mw.msg( 'rcfilters-restore-default-filters' ) : ''
);
this.resetButton.setTitle(
currFiltersAreEmpty ? null : mw.msg( 'rcfilters-clear-all-filters' )
);
this.resetButton.toggle( !hideResetButton );
this.emptyFilterMessage.toggle( currFiltersAreEmpty );
};
/**
* @inheritdoc
*/
mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.createMenuWidget = function ( menuConfig ) {
return new mw.rcfilters.ui.FilterFloatingMenuSelectWidget(
this.controller,
this.model,
$.extend( {
filterFromInput: true
}, menuConfig )
);
};
/**
* Populate the menu from the model
*/
mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.populateFromModel = function () {
var widget = this,
items = [];
// Reset
this.getMenu().clearItems();
$.each( this.model.getFilterGroups(), function ( groupName, groupModel ) {
items.push(
// Group section
new mw.rcfilters.ui.FilterMenuSectionOptionWidget(
widget.controller,
groupModel,
{
$overlay: widget.$overlay
}
)
);
// Add items
widget.model.getGroupFilters( groupName ).forEach( function ( filterItem ) {
items.push(
new mw.rcfilters.ui.FilterMenuOptionWidget(
widget.controller,
filterItem,
{
$overlay: widget.$overlay
}
)
);
} );
} );
// Add all items to the menu
this.getMenu().addItems( items );
};
/**
* @inheritdoc
*/
mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.createTagItemWidget = function ( data ) {
var filterItem = this.model.getItemByName( data );
if ( filterItem ) {
return new mw.rcfilters.ui.FilterTagItemWidget(
this.controller,
filterItem,
{
$overlay: this.$overlay
}
);
}
};
/**
* Scroll the element to top within its container
*
* @private
* @param {jQuery} $element Element to position
* @param {number} [marginFromTop] When scrolling the entire widget to the top, leave this
* much space (in pixels) above the widget.
*/
mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.scrollToTop = function ( $element, marginFromTop ) {
var container = OO.ui.Element.static.getClosestScrollableContainer( $element[ 0 ], 'y' ),
pos = OO.ui.Element.static.getRelativePosition( $element, $( container ) ),
containerScrollTop = $( container ).is( 'body, html' ) ? 0 : $( container ).scrollTop();
// Scroll to item
$( container ).animate( {
scrollTop: containerScrollTop + pos.top - ( marginFromTop || 0 )
} );
};
}( mediaWiki ) );

View file

@ -13,7 +13,6 @@
* @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
*/
mw.rcfilters.ui.FilterWrapperWidget = function MwRcfiltersUiFilterWrapperWidget( controller, model, config ) {
var $footer = $( '<div>' );
config = config || {};
// Parent
@ -25,188 +24,20 @@
this.model = model;
this.$overlay = config.$overlay || this.$element;
this.filterPopup = new mw.rcfilters.ui.FiltersListWidget(
this.filterTagWidget = new mw.rcfilters.ui.FilterTagMultiselectWidget(
this.controller,
this.model,
{
label: mw.msg( 'rcfilters-filterlist-title' ),
$overlay: this.$overlay
}
{ $overlay: this.$overlay }
);
$footer.append(
new OO.ui.ButtonWidget( {
framed: false,
icon: 'feedback',
flags: [ 'progressive' ],
label: mw.msg( 'rcfilters-filterlist-feedbacklink' ),
href: 'https://www.mediawiki.org/wiki/Help_talk:New_filters_for_edit_review'
} ).$element
);
this.textInput = new OO.ui.TextInputWidget( {
classes: [ 'mw-rcfilters-ui-filterWrapperWidget-search' ],
icon: 'search',
placeholder: mw.msg( 'rcfilters-search-placeholder' )
} );
this.capsule = new mw.rcfilters.ui.FilterCapsuleMultiselectWidget( controller, this.model, this.textInput, {
$overlay: this.$overlay,
popup: {
$content: this.filterPopup.$element,
$footer: $footer,
classes: [ 'mw-rcfilters-ui-filterWrapperWidget-popup' ],
width: 650,
hideWhenOutOfView: false
}
} );
// Events
this.model.connect( this, {
initialize: 'onModelInitialize',
itemUpdate: 'onModelItemUpdate'
} );
this.textInput.connect( this, {
change: 'onTextInputChange',
enter: 'onTextInputEnter'
} );
this.capsule.connect( this, { capsuleItemClick: 'onCapsuleItemClick' } );
this.capsule.popup.connect( this, {
toggle: 'onCapsulePopupToggle',
ready: 'onCapsulePopupReady'
} );
// Initialize
this.$element
.addClass( 'mw-rcfilters-ui-filterWrapperWidget' )
.append( this.capsule.$element, this.textInput.$element );
.append( this.filterTagWidget.$element );
};
/* Initialization */
OO.inheritClass( mw.rcfilters.ui.FilterWrapperWidget, OO.ui.Widget );
OO.mixinClass( mw.rcfilters.ui.FilterWrapperWidget, OO.ui.mixin.PendingElement );
/**
* Respond to capsule item click and make the popup scroll down to the requested item
*
* @param {mw.rcfilters.ui.CapsuleItemWidget} item Clicked item
*/
mw.rcfilters.ui.FilterWrapperWidget.prototype.onCapsuleItemClick = function ( item ) {
var filterName = item.getData(),
// Find the item in the popup
filterWidget = this.filterPopup.getItemWidget( filterName );
// Highlight item
this.filterPopup.select( filterName );
this.capsule.select( item );
this.capsule.popup.toggle( true );
this.scrollToTop( filterWidget.$element );
};
/**
* Respond to capsule popup ready event, fired after the popup is visible, positioned and clipped
*/
mw.rcfilters.ui.FilterWrapperWidget.prototype.onCapsulePopupReady = function () {
mw.hook( 'RcFilters.popup.open' ).fire( this.filterPopup.getSelectedFilter() );
this.scrollToTop( this.capsule.$element, 10 );
if ( !this.filterPopup.getSelectedFilter() ) {
// No selection, scroll the popup list to top
setTimeout( function () { this.capsule.popup.$body.scrollTop( 0 ); }.bind( this ), 0 );
}
};
/**
* Respond to popup toggle event. Reset selection in the list when the popup is closed.
*
* @param {boolean} isVisible Popup is visible
*/
mw.rcfilters.ui.FilterWrapperWidget.prototype.onCapsulePopupToggle = function ( isVisible ) {
if ( !isVisible && !this.textInput.getValue() ) {
// Only reset selection if we are not filtering
this.filterPopup.resetSelection();
this.capsule.resetSelection();
}
};
/**
* Respond to text input change
*
* @param {string} newValue Current value
*/
mw.rcfilters.ui.FilterWrapperWidget.prototype.onTextInputChange = function ( newValue ) {
// Filter the results
this.filterPopup.filter( this.model.findMatches( newValue ) );
if ( !newValue ) {
// If the value is empty, we didn't actually
// filter anything. the filter method will run
// and show all, but then will select the
// top item - but in this case, no selection
// should be made.
this.filterPopup.resetSelection();
}
this.capsule.popup.clip();
};
/**
* Respond to text input enter event
*/
mw.rcfilters.ui.FilterWrapperWidget.prototype.onTextInputEnter = function () {
var filter = this.filterPopup.getSelectedFilter();
// Toggle the filter
if ( filter ) {
this.controller.toggleFilterSelect( filter );
}
};
/**
* Respond to model update event and set up the available filters to choose
* from.
*/
mw.rcfilters.ui.FilterWrapperWidget.prototype.onModelInitialize = function () {
var wrapper = this;
// Add defaults to capsule. We have to do this
// after we added to the capsule menu, since that's
// how the capsule multiselect widget knows which
// object to add
this.model.getItems().forEach( function ( filterItem ) {
if ( filterItem.isSelected() ) {
wrapper.capsule.addItemByName( filterItem.getName() );
}
} );
};
/**
* Respond to item update and reset the selection. This will make it so that
* any actual interaction with the system resets the selection state of any item.
*/
mw.rcfilters.ui.FilterWrapperWidget.prototype.onModelItemUpdate = function () {
if ( !this.textInput.getValue() ) {
this.filterPopup.resetSelection();
}
};
/**
* Scroll the element to top within its container
*
* @private
* @param {jQuery} $element Element to position
* @param {number} [marginFromTop] When scrolling the entire widget to the top, leave this
* much space (in pixels) above the widget.
*/
mw.rcfilters.ui.FilterWrapperWidget.prototype.scrollToTop = function ( $element, marginFromTop ) {
var container = OO.ui.Element.static.getClosestScrollableContainer( $element[ 0 ], 'y' ),
pos = OO.ui.Element.static.getRelativePosition( $element, $( container ) ),
containerScrollTop = $( container ).is( 'body, html' ) ? 0 : $( container ).scrollTop();
// Scroll to item
$( container ).animate( {
scrollTop: containerScrollTop + pos.top - ( marginFromTop || 0 )
} );
};
}( mediaWiki ) );

View file

@ -1,256 +0,0 @@
( function ( mw, $ ) {
/**
* List displaying all filter groups
*
* @extends OO.ui.Widget
* @mixins OO.ui.mixin.GroupWidget
* @mixins OO.ui.mixin.LabelElement
*
* @constructor
* @param {mw.rcfilters.Controller} controller Controller
* @param {mw.rcfilters.dm.FiltersViewModel} model View model
* @param {Object} config Configuration object
*/
mw.rcfilters.ui.FiltersListWidget = function MwRcfiltersUiFiltersListWidget( controller, model, config ) {
config = config || {};
// Parent
mw.rcfilters.ui.FiltersListWidget.parent.call( this, config );
// Mixin constructors
OO.ui.mixin.GroupWidget.call( this, config );
OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, {
$label: $( '<div>' )
.addClass( 'mw-rcfilters-ui-filtersListWidget-title' )
} ) );
this.controller = controller;
this.model = model;
this.$overlay = config.$overlay || this.$element;
this.groups = {};
this.selected = null;
this.highlightButton = new OO.ui.ToggleButtonWidget( {
icon: 'highlight',
label: mw.message( 'rcfilters-highlightbutton-title' ).text(),
classes: [ 'mw-rcfilters-ui-filtersListWidget-hightlightButton' ]
} );
this.noResultsLabel = new OO.ui.LabelWidget( {
label: mw.msg( 'rcfilters-filterlist-noresults' ),
classes: [ 'mw-rcfilters-ui-filtersListWidget-noresults' ]
} );
// Events
this.highlightButton.connect( this, { click: 'onHighlightButtonClick' } );
this.model.connect( this, {
initialize: 'onModelInitialize',
highlightChange: 'onModelHighlightChange'
} );
// Initialize
this.showNoResultsMessage( false );
this.$element
.addClass( 'mw-rcfilters-ui-filtersListWidget' )
.append(
$( '<div>' )
.addClass( 'mw-rcfilters-ui-table' )
.addClass( 'mw-rcfilters-ui-filtersListWidget-header' )
.append(
$( '<div>' )
.addClass( 'mw-rcfilters-ui-row' )
.append(
$( '<div>' )
.addClass( 'mw-rcfilters-ui-cell' )
.addClass( 'mw-rcfilters-ui-filtersListWidget-header-title' )
.append( this.$label ),
$( '<div>' )
.addClass( 'mw-rcfilters-ui-cell' )
.addClass( 'mw-rcfilters-ui-filtersListWidget-header-highlight' )
.append( this.highlightButton.$element )
)
),
// this.$label,
this.$group
.addClass( 'mw-rcfilters-ui-filtersListWidget-group' ),
this.noResultsLabel.$element
);
};
/* Initialization */
OO.inheritClass( mw.rcfilters.ui.FiltersListWidget, OO.ui.Widget );
OO.mixinClass( mw.rcfilters.ui.FiltersListWidget, OO.ui.mixin.GroupWidget );
OO.mixinClass( mw.rcfilters.ui.FiltersListWidget, OO.ui.mixin.LabelElement );
/* Methods */
/**
* Respond to initialize event from the model
*/
mw.rcfilters.ui.FiltersListWidget.prototype.onModelInitialize = function () {
var widget = this;
// Reset
this.clearItems();
this.groups = {};
this.addItems(
Object.keys( this.model.getFilterGroups() ).map( function ( groupName ) {
var groupWidget = new mw.rcfilters.ui.FilterGroupWidget(
widget.controller,
widget.model.getGroup( groupName ),
{
$overlay: widget.$overlay
}
);
widget.groups[ groupName ] = groupWidget;
return groupWidget;
} )
);
};
/**
* Respond to model highlight change event
*
* @param {boolean} highlightEnabled Highlight is enabled
*/
mw.rcfilters.ui.FiltersListWidget.prototype.onModelHighlightChange = function ( highlightEnabled ) {
this.highlightButton.setActive( highlightEnabled );
};
/**
* Respond to highlight button click
*/
mw.rcfilters.ui.FiltersListWidget.prototype.onHighlightButtonClick = function () {
this.controller.toggleHighlight();
};
/**
* Find the filter item widget that corresponds to the item name
*
* @param {string} itemName Filter name
* @return {mw.rcfilters.ui.FilterItemWidget} Filter widget
*/
mw.rcfilters.ui.FiltersListWidget.prototype.getItemWidget = function ( itemName ) {
var filterItem = this.model.getItemByName( itemName ),
// Find the group
groupWidget = this.groups[ filterItem.getGroupName() ];
// Find the item inside the group
return groupWidget.getItemWidget( itemName );
};
/**
* Get the current selection
*
* @return {string|null} Selected filter. Null if none is selected.
*/
mw.rcfilters.ui.FiltersListWidget.prototype.getSelectedFilter = function () {
return this.selected;
};
/**
* Mark an item widget as selected
*
* @param {string} itemName Filter name
*/
mw.rcfilters.ui.FiltersListWidget.prototype.select = function ( itemName ) {
var filterWidget;
if ( this.selected !== itemName ) {
// Unselect previous
if ( this.selected ) {
filterWidget = this.getItemWidget( this.selected );
filterWidget.toggleSelected( false );
}
// Select new one
this.selected = itemName;
if ( this.selected ) {
filterWidget = this.getItemWidget( this.selected );
filterWidget.toggleSelected( true );
}
}
};
/**
* Reset selection and remove selected states from all items
*/
mw.rcfilters.ui.FiltersListWidget.prototype.resetSelection = function () {
if ( this.selected !== null ) {
this.selected = null;
this.getItems().forEach( function ( groupWidget ) {
groupWidget.getItems().forEach( function ( filterItemWidget ) {
filterItemWidget.toggleSelected( false );
} );
} );
}
};
/**
* Switch between showing the 'no results' message for filtering results or the result list.
*
* @param {boolean} showNoResults Show no results message
*/
mw.rcfilters.ui.FiltersListWidget.prototype.showNoResultsMessage = function ( showNoResults ) {
this.noResultsLabel.toggle( !!showNoResults );
this.$group.toggleClass( 'oo-ui-element-hidden', !!showNoResults );
};
/**
* Show only the items matching with the models in the given list
*
* @param {Object} groupItems An object of items to show
* arranged by their group names
*/
mw.rcfilters.ui.FiltersListWidget.prototype.filter = function ( groupItems ) {
var i, j, groupName, itemWidgets, topItem, isVisible,
groupWidgets = this.getItems(),
hasItemWithName = function ( itemArr, name ) {
return !!itemArr.filter( function ( item ) {
return item.getName() === name;
} ).length;
};
this.resetSelection();
if ( $.isEmptyObject( groupItems ) ) {
// No results. Hide everything, show only 'no results'
// message
this.showNoResultsMessage( true );
return;
}
this.showNoResultsMessage( false );
for ( i = 0; i < groupWidgets.length; i++ ) {
groupName = groupWidgets[ i ].getName();
// If this group widget is in the filtered results,
// show it - otherwise, hide it
groupWidgets[ i ].toggle( !!groupItems[ groupName ] );
if ( !groupItems[ groupName ] ) {
// Continue to next group
continue;
}
// We have items to show
itemWidgets = groupWidgets[ i ].getItems();
for ( j = 0; j < itemWidgets.length; j++ ) {
isVisible = hasItemWithName( groupItems[ groupName ], itemWidgets[ j ].getName() );
// Only show items that are in the filtered list
itemWidgets[ j ].toggle( isVisible );
if ( !topItem && isVisible ) {
topItem = itemWidgets[ j ];
}
}
}
// Select the first item
if ( topItem ) {
this.select( topItem.getName() );
}
};
}( mediaWiki, jQuery ) );