const ViewSwitchWidget = require( './ViewSwitchWidget.js' ),
SaveFiltersPopupButtonWidget = require( './SaveFiltersPopupButtonWidget.js' ),
MenuSelectWidget = require( './MenuSelectWidget.js' ),
FilterTagItemWidget = require( './FilterTagItemWidget.js' );
/**
* List displaying all filter groups.
*
* @class mw.rcfilters.ui.FilterTagMultiselectWidget
* @ignore
* @extends OO.ui.MenuTagMultiselectWidget
* @mixes OO.ui.mixin.PendingElement
*
* @param {mw.rcfilters.Controller} controller Controller
* @param {mw.rcfilters.dm.FiltersViewModel} model View model
* @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
* @param {Object} config Configuration object
* @param {jQuery} [config.$overlay] A jQuery object serving as overlay for popups
* @param {jQuery} [config.$wrapper] A jQuery object for the wrapper of the general
* system. If not given, falls back to this widget's $element
* @param {boolean} [config.collapsed] Filter area is collapsed
*/
const FilterTagMultiselectWidget = function MwRcfiltersUiFilterTagMultiselectWidget( controller, model, savedQueriesModel, config ) {
const 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.queriesModel = savedQueriesModel;
this.$overlay = config.$overlay || this.$element;
this.$wrapper = config.$wrapper || this.$element;
this.matchingQuery = null;
this.currentView = this.model.getCurrentView();
this.collapsed = false;
this.isMobile = config.isMobile;
// Has to be before the parent constructor, because the parent constructor may call setValue()
// which causes the onChangeTags handler to run (T245073)
this.emptyFilterMessage = new OO.ui.LabelWidget( {
label: mw.msg( 'rcfilters-empty-filter' ),
classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-emptyFilters' ]
} );
// Parent
FilterTagMultiselectWidget.super.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: {
// Our filtering is done through the model
filterFromInput: false,
hideWhenOutOfView: false,
hideOnChoose: false,
// Only set width and footers for desktop
isMobile: this.isMobile,
width: 650,
footers: [
{
name: 'viewSelect',
sticky: false,
// View select menu, appears on default view only
$element: $( '<div>' )
.append( new ViewSwitchWidget( this.controller, this.model ).$element ),
views: [ 'default' ]
}
]
},
/**
* In the presence of an onscreen keyboard (i.e. isMobile) the filter input should act as a button
* rather than a text input. Mobile screens are too small to accommodate both an
* onscreen keyboard and a popup-menu, so readyOnly is set to disable the keyboard.
* A different icon and shorter message is used for mobile as well. (See T224655 for details).
*
* @ignore
*/
input: {
icon: this.isMobile ? 'funnel' : 'menu',
placeholder: this.isMobile ? mw.msg( 'rcfilters-search-placeholder-mobile' ) : mw.msg( 'rcfilters-search-placeholder' ),
readOnly: !!this.isMobile,
classes: [ 'oo-ui-tagMultiselectWidget-input' ]
}
}, config ) );
this.input.$input.attr( 'aria-label', mw.msg( 'rcfilters-search-placeholder' ) );
this.savedQueryTitle = new OO.ui.LabelWidget( {
label: '',
classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-savedQueryTitle' ]
} );
this.resetButton = new OO.ui.ButtonWidget( {
framed: false,
classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-resetButton' ]
} );
this.hideShowButton = new OO.ui.ButtonWidget( {
framed: false,
flags: [ 'progressive' ],
classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-hideshowButton' ]
} );
this.toggleCollapsed( !!config.collapsed );
if ( !mw.user.isAnon() ) {
this.saveQueryButton = new SaveFiltersPopupButtonWidget(
this.controller,
this.queriesModel,
{
$overlay: this.$overlay
}
);
this.saveQueryButton.$element.on( 'mousedown', ( e ) => {
e.stopPropagation();
} );
this.saveQueryButton.connect( this, {
click: 'onSaveQueryButtonClick',
saveCurrent: 'setSavedQueryVisibility'
} );
this.queriesModel.connect( this, {
itemUpdate: 'onSavedQueriesItemUpdate',
initialize: 'onSavedQueriesInitialize',
default: 'reevaluateResetRestoreState'
} );
}
this.$content.append( this.emptyFilterMessage.$element );
// Events
this.resetButton.connect( this, { click: 'onResetButtonClick' } );
this.hideShowButton.connect( this, { click: 'onHideShowButtonClick' } );
// 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', ( e ) => {
e.stopPropagation();
} );
this.hideShowButton.$element.on( 'mousedown', ( e ) => {
e.stopPropagation();
} );
this.model.connect( this, {
initialize: 'onModelInitialize',
update: 'onModelUpdate',
searchChange: this.isMobile ? function () {} : 'onModelSearchChange',
itemUpdate: 'onModelItemUpdate',
highlightChange: 'onModelHighlightChange'
} );
if ( !this.isMobile ) {
this.input.connect( this, { change: 'onInputChange' } );
}
// 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
const $rcFiltersRow = $( '<div>' )
.addClass( 'mw-rcfilters-ui-row' )
.append(
this.$content
.addClass( 'mw-rcfilters-ui-cell' )
.addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-filters' )
);
if ( !mw.user.isAnon() ) {
$rcFiltersRow.append(
$( '<div>' )
.addClass( 'mw-rcfilters-ui-cell' )
.addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-save' )
.append( this.saveQueryButton.$element )
);
}
// Add a selector at the right of the input
this.viewsSelectWidget = this.createViewsSelectWidget();
// change the layout of the viewsSelectWidget
this.restructureViewsSelectWidget();
// Event
this.viewsSelectWidget.aggregate( { click: 'buttonClick' } );
this.viewsSelectWidget.connect( this, { buttonClick: 'onViewsSelectWidgetButtonClick' } );
$rcFiltersRow.append(
$( '<div>' )
.addClass( 'mw-rcfilters-ui-cell' )
.addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-reset' )
.append( this.resetButton.$element )
);
// Build the content
$contentWrapper.append(
$( '<div>' )
.addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top' )
.append(
$( '<div>' )
.addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top-title' )
.append( title.$element ),
$( '<div>' )
.addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top-queryName' )
.append( this.savedQueryTitle.$element ),
$( '<div>' )
.addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top-hideshow' )
.append(
this.hideShowButton.$element
)
),
$( '<div>' )
.addClass( 'mw-rcfilters-ui-table' )
.addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-filters' )
.append( $rcFiltersRow )
);
// Initialize
this.$handle.append( $contentWrapper );
this.emptyFilterMessage.toggle( this.isEmpty() );
this.savedQueryTitle.toggle( false );
this.$element
.addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget' );
if ( this.isMobile ) {
this.$element
.addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-mobile' );
}
this.reevaluateResetRestoreState();
};
/* Initialization */
OO.inheritClass( FilterTagMultiselectWidget, OO.ui.MenuTagMultiselectWidget );
/* Methods */
/**
* Create a OOUI ButtonGroupWidget. The buttons are framed and have additional CSS
* classes applied on mobile.
*
* @return {OO.ui.ButtonGroupWidget}
*/
FilterTagMultiselectWidget.prototype.createViewsSelectWidget = function () {
const viewsSelectWidget = new OO.ui.ButtonGroupWidget( {
classes: this.isMobile ?
[
'mw-rcfilters-ui-table',
'mw-rcfilters-ui-filterTagMultiselectWidget-mobile-view'
] :
[
'mw-rcfilters-ui-filterTagMultiselectWidget-views-select-widget'
],
items: [
new OO.ui.ButtonWidget( {
framed: !!this.isMobile,
data: 'namespaces',
icon: 'article',
label: mw.msg( 'namespaces' ),
title: mw.msg( 'rcfilters-view-namespaces-tooltip' ),
classes: this.isMobile ? [ 'mw-rcfilters-ui-cell' ] : []
} ),
new OO.ui.ButtonWidget( {
framed: !!this.isMobile,
data: 'tags',
icon: 'tag',
label: mw.msg( 'tags-title' ),
title: mw.msg( 'rcfilters-view-tags-tooltip' ),
classes: this.isMobile ? [ 'mw-rcfilters-ui-cell' ] : []
} )
]
} );
viewsSelectWidget.items.forEach( ( item ) => {
item.$button.attr( 'aria-label', item.title );
} );
return viewsSelectWidget;
};
/**
* Rearrange the DOM structure of the viewsSelectWiget so that on the namespace & tags buttons
* are at the right of the input on desktop, and below the input on mobile.
*/
FilterTagMultiselectWidget.prototype.restructureViewsSelectWidget = function () {
if ( this.isMobile ) {
// On mobile, append the search input and the extra buttons below the search input.
this.$element.append(
$( '<div>' )
.addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-input' )
.append( this.input.$element )
.append( this.viewsSelectWidget.$element )
);
} else {
// On desktop, rearrange the UI so the select widget is at the right of the input
this.$element.append(
$( '<div>' )
.addClass( 'mw-rcfilters-ui-table' )
.append(
$( '<div>' )
.addClass( 'mw-rcfilters-ui-row' )
.addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views' )
.append(
$( '<div>' )
.addClass( 'mw-rcfilters-ui-cell' )
.addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-input' )
.append( this.input.$element ),
$( '<div>' )
.addClass( 'mw-rcfilters-ui-cell' )
.addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-select' )
.append( this.viewsSelectWidget.$element )
)
)
);
}
};
/**
* Respond to button click event
*
* @param {OO.ui.ButtonWidget} buttonWidget Clicked widget
*/
FilterTagMultiselectWidget.prototype.onViewsSelectWidgetButtonClick = function ( buttonWidget ) {
this.controller.switchView( buttonWidget.getData() );
this.focus();
};
/**
* Respond to model search change event
*
* @param {string} value Search value
*/
FilterTagMultiselectWidget.prototype.onModelSearchChange = function ( value ) {
this.input.setValue( value );
};
/**
* Respond to input change event
*
* @param {string} value Value of the input
*/
FilterTagMultiselectWidget.prototype.onInputChange = function ( value ) {
this.controller.setSearch( value );
};
/**
* Respond to query button click
*/
FilterTagMultiselectWidget.prototype.onSaveQueryButtonClick = function () {
this.getMenu().toggle( false );
};
/**
* Respond to save query model initialization
*/
FilterTagMultiselectWidget.prototype.onSavedQueriesInitialize = function () {
this.setSavedQueryVisibility();
};
/**
* Respond to save query item change. Mainly this is done to update the label in case
* a query item has been edited
*
* @param {mw.rcfilters.dm.SavedQueryItemModel} item Saved query item
*/
FilterTagMultiselectWidget.prototype.onSavedQueriesItemUpdate = function ( item ) {
if ( this.matchingQuery === item ) {
// This means we just edited the item that is currently matched
this.savedQueryTitle.setLabel( item.getLabel() );
}
};
/**
* Respond to menu toggle
*
* @param {boolean} isVisible Menu is visible
*/
FilterTagMultiselectWidget.prototype.onMenuToggle = function ( isVisible ) {
const scrollToElement = this.isMobile ? this.input.$input : this.$element;
// Parent
FilterTagMultiselectWidget.super.prototype.onMenuToggle.call( this );
if ( isVisible ) {
if ( !this.isMobile ) {
this.focus();
}
/**
* Fires when the RCFilters tag multi selector menu is toggled.
*
* @event ~'RcFilters.popup.open'
* @memberof Hooks
*/
mw.hook( 'RcFilters.popup.open' ).fire();
if ( !this.getMenu().findSelectedItem() ) {
// 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(
() => {
this.getMenu().scrollToTop();
}
);
}
// Only scroll to top of the viewport if:
// - The widget is more than 20px from the top
// - The widget is not above the top of the viewport (do not scroll downwards)
// (This isn't represented because >20 is, anyways and always, bigger than 0)
this.scrollToTop( scrollToElement, 0, { min: 20, max: Infinity } );
} else {
// Clear selection
this.selectTag( null );
// Clear the search
this.controller.setSearch( '' );
this.blur();
}
if ( this.isMobile ) {
this.input.setIcon( isVisible ? 'close' : 'funnel' );
} else {
this.input.setIcon( isVisible ? 'search' : 'menu' );
}
};
/**
* @inheritdoc
*/
FilterTagMultiselectWidget.prototype.onInputFocus = function () {
// treat the input as a menu toggle rather than a text field on mobile
if ( this.isMobile ) {
this.input.$input.trigger( 'blur' );
this.getMenu().toggle();
} else {
// Parent
FilterTagMultiselectWidget.super.prototype.onInputFocus.call( this );
}
};
/**
* @inheritdoc
*/
FilterTagMultiselectWidget.prototype.doInputEscape = function () {
// Parent
FilterTagMultiselectWidget.super.prototype.doInputEscape.call( this );
// Blur the input
this.input.$input.trigger( 'blur' );
};
/**
* @inheritdoc
*/
FilterTagMultiselectWidget.prototype.onMouseDown = function ( e ) {
if ( !this.collapsed && !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
this.menu.toggle();
return false;
}
};
/**
* @inheritdoc
*/
FilterTagMultiselectWidget.prototype.onChangeTags = function () {
// If initialized, call parent method.
if ( this.controller.isInitialized() ) {
FilterTagMultiselectWidget.super.prototype.onChangeTags.call( this );
}
this.emptyFilterMessage.toggle( this.isEmpty() );
};
/**
* Respond to model initialize event
*/
FilterTagMultiselectWidget.prototype.onModelInitialize = function () {
this.setSavedQueryVisibility();
};
/**
* Respond to model update event
*/
FilterTagMultiselectWidget.prototype.onModelUpdate = function () {
this.updateElementsForView();
};
/**
* Update the elements in the widget to the current view
*/
FilterTagMultiselectWidget.prototype.updateElementsForView = function () {
let inputValue = this.input.getValue().trim();
const view = this.model.getCurrentView(),
inputView = this.model.getViewByTrigger( inputValue.slice( 0, 1 ) );
if ( inputView !== 'default' ) {
// We have a prefix already, remove it
inputValue = inputValue.slice( 1 );
}
if ( inputView !== view ) {
// Add the correct prefix
inputValue = this.model.getViewTrigger( view ) + inputValue;
}
// Update input
this.input.setValue( inputValue );
if ( this.currentView !== view ) {
this.scrollToTop( this.$element );
this.currentView = view;
}
};
/**
* Set the visibility of the saved query button
*/
FilterTagMultiselectWidget.prototype.setSavedQueryVisibility = function () {
if ( mw.user.isAnon() ) {
return;
}
this.matchingQuery = this.controller.findQueryMatchingCurrentState();
this.savedQueryTitle.setLabel(
this.matchingQuery ? this.matchingQuery.getLabel() : ''
);
this.savedQueryTitle.toggle( !!this.matchingQuery );
this.saveQueryButton.setDisabled( !!this.matchingQuery );
this.saveQueryButton.setTitle( !this.matchingQuery ?
mw.msg( 'rcfilters-savedqueries-add-new-title' ) :
mw.msg( 'rcfilters-savedqueries-already-saved' ) );
if ( this.matchingQuery ) {
this.emphasize();
}
};
/**
* Respond to model itemUpdate event
* fixme: when a new state is applied to the model this function is called 60+ times in a row
*
* @param {mw.rcfilters.dm.FilterItem} item Filter item model
*/
FilterTagMultiselectWidget.prototype.onModelItemUpdate = function ( item ) {
if ( !item.getGroupModel().isHidden() ) {
if (
item.isSelected() ||
(
this.model.isHighlightEnabled() &&
item.getHighlightColor()
)
) {
this.addTag( item.getName(), item.getLabel() );
} else {
// Only attempt to remove the tag if we can find an item for it (T198140, T198231)
if ( this.findItemFromData( item.getName() ) !== null ) {
this.removeTagByData( item.getName() );
}
}
}
this.setSavedQueryVisibility();
// Re-evaluate reset state
this.reevaluateResetRestoreState();
};
/**
* @inheritdoc
*/
FilterTagMultiselectWidget.prototype.isAllowedData = function ( data ) {
return (
this.model.getItemByName( data ) &&
!this.isDuplicateData( data )
);
};
/**
* @inheritdoc
*/
FilterTagMultiselectWidget.prototype.onMenuChoose = function ( item ) {
this.controller.toggleFilterSelect( item.model.getName() );
// Select the tag if it exists, or reset selection otherwise
this.selectTag( this.findItemFromData( item.model.getName() ) );
if ( !this.isMobile ) {
this.focus();
}
};
/**
* Respond to highlightChange event
*
* @param {boolean} isHighlightEnabled Highlight is enabled
*/
FilterTagMultiselectWidget.prototype.onModelHighlightChange = function ( isHighlightEnabled ) {
const highlightedItems = this.model.getHighlightedItems();
if ( isHighlightEnabled ) {
// Add capsule widgets
highlightedItems.forEach( ( filterItem ) => {
this.addTag( filterItem.getName(), filterItem.getLabel() );
} );
} else {
// Remove capsule widgets if they're not selected
highlightedItems.forEach( ( filterItem ) => {
if ( !filterItem.isSelected() ) {
// Only attempt to remove the tag if we can find an item for it (T198140, T198231)
if ( this.findItemFromData( filterItem.getName() ) !== null ) {
this.removeTagByData( filterItem.getName() );
}
}
} );
}
this.setSavedQueryVisibility();
};
/**
* @inheritdoc
*/
FilterTagMultiselectWidget.prototype.onTagSelect = function ( tagItem ) {
const menuOption = this.menu.getItemFromModel( tagItem.getModel() );
this.menu.setUserSelecting( true );
// Parent method
FilterTagMultiselectWidget.super.prototype.onTagSelect.call( this, tagItem );
// Switch view
this.controller.resetSearchForView( tagItem.getView() );
this.selectTag( tagItem );
this.scrollToTop( menuOption.$element );
this.menu.setUserSelecting( false );
};
/**
* Select a tag by reference. This is what OO.ui.SelectWidget is doing.
* If no items are given, reset selection from all.
*
* @param {mw.rcfilters.ui.FilterTagItemWidget} [item] Tag to select,
* omit to deselect all
*/
FilterTagMultiselectWidget.prototype.selectTag = function ( item ) {
let i, len, selected;
for ( i = 0, len = this.items.length; i < len; i++ ) {
selected = this.items[ i ] === item;
if ( this.items[ i ].isSelected() !== selected ) {
this.items[ i ].toggleSelected( selected );
}
}
};
/**
* @inheritdoc
*/
FilterTagMultiselectWidget.prototype.onTagRemove = function ( tagItem ) {
// Parent method
FilterTagMultiselectWidget.super.prototype.onTagRemove.call( this, tagItem );
this.controller.clearFilter( tagItem.getName() );
tagItem.destroy();
};
/**
* Respond to click event on the reset button
*/
FilterTagMultiselectWidget.prototype.onResetButtonClick = function () {
if ( this.model.areVisibleFiltersEmpty() ) {
// Reset to default filters
this.controller.resetToDefaults();
} else {
// Reset to have no filters
this.controller.emptyFilters();
}
};
/**
* Respond to hide/show button click
*/
FilterTagMultiselectWidget.prototype.onHideShowButtonClick = function () {
this.toggleCollapsed();
};
/**
* Toggle the collapsed state of the filters widget
*
* @param {boolean} isCollapsed Widget is collapsed
*/
FilterTagMultiselectWidget.prototype.toggleCollapsed = function ( isCollapsed ) {
isCollapsed = isCollapsed === undefined ? !this.collapsed : !!isCollapsed;
this.collapsed = isCollapsed;
if ( isCollapsed ) {
// If we are collapsing, close the menu, in case it was open
// We should make sure the menu closes before the rest of the elements
// are hidden, otherwise there is an unknown error in jQuery as ooui
// sets and unsets properties on the input (which is hidden at that point)
this.menu.toggle( false );
}
this.input.setDisabled( isCollapsed );
this.hideShowButton.setLabel( mw.msg(
isCollapsed ? 'rcfilters-activefilters-show' : 'rcfilters-activefilters-hide'
) );
this.hideShowButton.setTitle( mw.msg(
isCollapsed ? 'rcfilters-activefilters-show-tooltip' : 'rcfilters-activefilters-hide-tooltip'
) );
// Toggle the wrapper class, so we have min height values correctly throughout
this.$wrapper.toggleClass( 'mw-rcfilters-collapsed', isCollapsed );
// Save the state
this.controller.updateCollapsedState( isCollapsed );
};
/**
* Reevaluate the restore state for the widget between setting to defaults and clearing all filters
*/
FilterTagMultiselectWidget.prototype.reevaluateResetRestoreState = function () {
const defaultsAreEmpty = this.controller.areDefaultsEmpty(),
currFiltersAreEmpty = this.model.areVisibleFiltersEmpty(),
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
*/
FilterTagMultiselectWidget.prototype.createMenuWidget = function ( menuConfig ) {
return new MenuSelectWidget(
this.controller,
this.model,
menuConfig
);
};
/**
* @inheritdoc
*/
FilterTagMultiselectWidget.prototype.createTagItemWidget = function ( data ) {
const filterItem = this.model.getItemByName( data );
if ( filterItem ) {
return new FilterTagItemWidget(
this.controller,
this.model,
this.model.getInvertModel( filterItem.getGroupModel().getView() ),
filterItem,
{
$overlay: this.$overlay
}
);
}
};
FilterTagMultiselectWidget.prototype.emphasize = function () {
if (
// eslint-disable-next-line no-jquery/no-class-state
!this.$handle.hasClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' )
) {
this.$handle
.addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-emphasize' )
.addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' );
setTimeout( () => {
this.$handle
.removeClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-emphasize' );
setTimeout( () => {
this.$handle
.removeClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' );
}, 1000 );
}, 500 );
}
};
/**
* Scroll the element to top within its container
*
* @private
* @param {jQuery} $element Element to position
* @param {number} [marginFromTop=0] When scrolling the entire widget to the top, leave this
* much space (in pixels) above the widget.
* @param {Object} [threshold] Minimum distance from the top of the element to scroll at all
* @param {number} [threshold.min] Minimum distance above the element
* @param {number} [threshold.max] Minimum distance below the element
*/
FilterTagMultiselectWidget.prototype.scrollToTop = function ( $element, marginFromTop, threshold ) {
const container = OO.ui.Element.static.getClosestScrollableContainer( $element[ 0 ], 'y' ),
pos = OO.ui.Element.static.getRelativePosition( $element, $( container ) ),
containerScrollTop = $( container ).scrollTop(),
effectiveScrollTop = $( container ).is( 'body, html' ) ? 0 : containerScrollTop,
newScrollTop = effectiveScrollTop + pos.top - ( marginFromTop || 0 );
// Scroll to item
if (
threshold === undefined ||
(
(
threshold.min === undefined ||
newScrollTop - containerScrollTop >= threshold.min
) &&
(
threshold.max === undefined ||
newScrollTop - containerScrollTop <= threshold.max
)
)
) {
$( container ).animate( {
scrollTop: newScrollTop
} );
}
};
module.exports = FilterTagMultiselectWidget;