1 var ViewSwitchWidget
= require( './ViewSwitchWidget.js' ),
2 SaveFiltersPopupButtonWidget
= require( './SaveFiltersPopupButtonWidget.js' ),
3 MenuSelectWidget
= require( './MenuSelectWidget.js' ),
4 FilterTagItemWidget
= require( './FilterTagItemWidget.js' ),
5 FilterTagMultiselectWidget
;
8 * List displaying all filter groups
10 * @class mw.rcfilters.ui.FilterTagMultiselectWidget
11 * @extends OO.ui.MenuTagMultiselectWidget
12 * @mixins OO.ui.mixin.PendingElement
15 * @param {mw.rcfilters.Controller} controller Controller
16 * @param {mw.rcfilters.dm.FiltersViewModel} model View model
17 * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
18 * @param {Object} config Configuration object
19 * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
20 * @cfg {jQuery} [$wrapper] A jQuery object for the wrapper of the general
21 * system. If not given, falls back to this widget's $element
22 * @cfg {boolean} [collapsed] Filter area is collapsed
24 FilterTagMultiselectWidget
= function MwRcfiltersUiFilterTagMultiselectWidget( controller
, model
, savedQueriesModel
, config
) {
26 title
= new OO
.ui
.LabelWidget( {
27 label
: mw
.msg( 'rcfilters-activefilters' ),
28 classes
: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-title' ]
30 $contentWrapper
= $( '<div>' )
31 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper' );
33 config
= config
|| {};
35 this.controller
= controller
;
37 this.queriesModel
= savedQueriesModel
;
38 this.$overlay
= config
.$overlay
|| this.$element
;
39 this.$wrapper
= config
.$wrapper
|| this.$element
;
40 this.matchingQuery
= null;
41 this.currentView
= this.model
.getCurrentView();
42 this.collapsed
= false;
43 this.isMobile
= config
.isMobile
;
46 FilterTagMultiselectWidget
.parent
.call( this, $.extend( true, {
47 label
: mw
.msg( 'rcfilters-filterlist-title' ),
48 placeholder
: mw
.msg( 'rcfilters-empty-filter' ),
49 inputPosition
: 'outline',
50 allowArbitrary
: false,
51 allowDisplayInvalidTags
: false,
52 allowReordering
: false,
53 $overlay
: this.$overlay
,
55 // Our filtering is done through the model
56 filterFromInput
: false,
57 hideWhenOutOfView
: false,
59 // Only set width and footers for desktop
60 isMobile
: this.isMobile
,
66 // View select menu, appears on default view only
67 $element
: $( '<div>' )
68 .append( new ViewSwitchWidget( this.controller
, this.model
).$element
),
73 // Feedback footer, appears on all views
74 $element
: $( '<div>' )
76 new OO
.ui
.ButtonWidget( {
79 flags
: [ 'progressive' ],
80 label
: mw
.msg( 'rcfilters-filterlist-feedbacklink' ),
81 href
: 'https://www.mediawiki.org/wiki/Help_talk:New_filters_for_edit_review'
88 * In the presence of an onscreen keyboard (i.e. isMobile) the filter input should act as a button
89 * rather than a text input. Mobile screens are too small to accommodate both an
90 * onscreen keyboard and a popup-menu, so readyOnly is set to disable the keyboard.
91 * A different icon and shorter message is used for mobile as well. (See T224655 for details).
94 icon
: this.isMobile
? 'funnel' : 'menu',
95 placeholder
: this.isMobile
? mw
.msg( 'rcfilters-search-placeholder-mobile' ) : mw
.msg( 'rcfilters-search-placeholder' ),
96 readOnly
: !!this.isMobile
,
97 classes
: [ 'oo-ui-tagMultiselectWidget-input' ]
101 this.savedQueryTitle
= new OO
.ui
.LabelWidget( {
103 classes
: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-savedQueryTitle' ]
106 this.resetButton
= new OO
.ui
.ButtonWidget( {
108 classes
: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-resetButton' ]
111 this.hideShowButton
= new OO
.ui
.ButtonWidget( {
113 flags
: [ 'progressive' ],
114 classes
: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-hideshowButton' ]
116 this.toggleCollapsed( !!config
.collapsed
);
118 if ( !mw
.user
.isAnon() ) {
119 this.saveQueryButton
= new SaveFiltersPopupButtonWidget(
123 $overlay
: this.$overlay
127 this.saveQueryButton
.$element
.on( 'mousedown', function ( e
) {
131 this.saveQueryButton
.connect( this, {
132 click
: 'onSaveQueryButtonClick',
133 saveCurrent
: 'setSavedQueryVisibility'
135 this.queriesModel
.connect( this, {
136 itemUpdate
: 'onSavedQueriesItemUpdate',
137 initialize
: 'onSavedQueriesInitialize',
138 default: 'reevaluateResetRestoreState'
142 this.emptyFilterMessage
= new OO
.ui
.LabelWidget( {
143 label
: mw
.msg( 'rcfilters-empty-filter' ),
144 classes
: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-emptyFilters' ]
146 this.$content
.append( this.emptyFilterMessage
.$element
);
149 this.resetButton
.connect( this, { click
: 'onResetButtonClick' } );
150 this.hideShowButton
.connect( this, { click
: 'onHideShowButtonClick' } );
151 // Stop propagation for mousedown, so that the widget doesn't
152 // trigger the focus on the input and scrolls up when we click the reset button
153 this.resetButton
.$element
.on( 'mousedown', function ( e
) {
156 this.hideShowButton
.$element
.on( 'mousedown', function ( e
) {
159 this.model
.connect( this, {
160 initialize
: 'onModelInitialize',
161 update
: 'onModelUpdate',
162 searchChange
: this.isMobile
? function () {} : 'onModelSearchChange',
163 itemUpdate
: 'onModelItemUpdate',
164 highlightChange
: 'onModelHighlightChange'
167 if ( !this.isMobile
) {
168 this.input
.connect( this, { change
: 'onInputChange' } );
171 // The filter list and button should appear side by side regardless of how
172 // wide the button is; the button also changes its width depending
173 // on language and its state, so the safest way to present both side
174 // by side is with a table layout
175 rcFiltersRow
= $( '<div>' )
176 .addClass( 'mw-rcfilters-ui-row' )
179 .addClass( 'mw-rcfilters-ui-cell' )
180 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-filters' )
183 if ( !mw
.user
.isAnon() ) {
186 .addClass( 'mw-rcfilters-ui-cell' )
187 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-save' )
188 .append( this.saveQueryButton
.$element
)
192 // Add a selector at the right of the input
193 this.viewsSelectWidget
= this.createViewsSelectWidget();
195 // change the layout of the viewsSelectWidget
196 this.restructureViewsSelectWidget();
199 this.viewsSelectWidget
.connect( this, { choose
: 'onViewsSelectWidgetChoose' } );
203 .addClass( 'mw-rcfilters-ui-cell' )
204 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-reset' )
205 .append( this.resetButton
.$element
)
209 $contentWrapper
.append(
211 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top' )
214 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top-title' )
215 .append( title
.$element
),
217 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top-queryName' )
218 .append( this.savedQueryTitle
.$element
),
220 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top-hideshow' )
222 this.hideShowButton
.$element
226 .addClass( 'mw-rcfilters-ui-table' )
227 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-filters' )
228 .append( rcFiltersRow
)
232 this.$handle
.append( $contentWrapper
);
233 this.emptyFilterMessage
.toggle( this.isEmpty() );
234 this.savedQueryTitle
.toggle( false );
237 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget' );
239 if ( this.isMobile
) {
241 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-mobile' );
244 this.reevaluateResetRestoreState();
249 OO
.inheritClass( FilterTagMultiselectWidget
, OO
.ui
.MenuTagMultiselectWidget
);
254 * Create a OOUI ButtonSelectWidget. The buttons are framed and have additional CSS
255 * classes applied on mobile.
256 * @return {OO.ui.ButtonSelectWidget}
258 FilterTagMultiselectWidget
.prototype.createViewsSelectWidget = function () {
259 return new OO
.ui
.ButtonSelectWidget( {
260 classes
: this.isMobile
?
262 'mw-rcfilters-ui-table',
263 'mw-rcfilters-ui-filterTagMultiselectWidget-mobile-view'
266 'mw-rcfilters-ui-filterTagMultiselectWidget-views-select-widget'
269 new OO
.ui
.ButtonOptionWidget( {
270 framed
: !!this.isMobile
,
273 label
: mw
.msg( 'namespaces' ),
274 classes
: this.isMobile
? [ 'mw-rcfilters-ui-cell' ] : []
276 new OO
.ui
.ButtonOptionWidget( {
277 framed
: !!this.isMobile
,
280 label
: mw
.msg( 'tags-title' ),
281 title
: mw
.msg( 'rcfilters-view-tags-tooltip' ),
282 classes
: this.isMobile
? [ 'mw-rcfilters-ui-cell' ] : []
289 * Rearrange the DOM structure of the viewsSelectWiget so that on the namespace & tags buttons
290 * are at the right of the input on desktop, and below the input on mobile.
292 FilterTagMultiselectWidget
.prototype.restructureViewsSelectWidget = function () {
293 if ( this.isMobile
) {
294 // On mobile, append the search input and the extra buttons below the search input.
295 this.$element
.append(
297 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-input' )
298 .append( this.input
.$element
)
299 .append( this.viewsSelectWidget
.$element
)
302 // On desktop, rearrange the UI so the select widget is at the right of the input
303 this.$element
.append(
305 .addClass( 'mw-rcfilters-ui-table' )
308 .addClass( 'mw-rcfilters-ui-row' )
309 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views' )
312 .addClass( 'mw-rcfilters-ui-cell' )
313 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-input' )
314 .append( this.input
.$element
),
316 .addClass( 'mw-rcfilters-ui-cell' )
317 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-select' )
318 .append( this.viewsSelectWidget
.$element
)
326 * Respond to view select widget choose event
328 * @param {OO.ui.ButtonOptionWidget} buttonOptionWidget Chosen widget
330 FilterTagMultiselectWidget
.prototype.onViewsSelectWidgetChoose = function ( buttonOptionWidget
) {
331 this.controller
.switchView( buttonOptionWidget
.getData() );
332 this.viewsSelectWidget
.selectItem( null );
337 * Respond to model search change event
339 * @param {string} value Search value
341 FilterTagMultiselectWidget
.prototype.onModelSearchChange = function ( value
) {
342 this.input
.setValue( value
);
346 * Respond to input change event
348 * @param {string} value Value of the input
350 FilterTagMultiselectWidget
.prototype.onInputChange = function ( value
) {
351 this.controller
.setSearch( value
);
355 * Respond to query button click
357 FilterTagMultiselectWidget
.prototype.onSaveQueryButtonClick = function () {
358 this.getMenu().toggle( false );
362 * Respond to save query model initialization
364 FilterTagMultiselectWidget
.prototype.onSavedQueriesInitialize = function () {
365 this.setSavedQueryVisibility();
369 * Respond to save query item change. Mainly this is done to update the label in case
370 * a query item has been edited
372 * @param {mw.rcfilters.dm.SavedQueryItemModel} item Saved query item
374 FilterTagMultiselectWidget
.prototype.onSavedQueriesItemUpdate = function ( item
) {
375 if ( this.matchingQuery
=== item
) {
376 // This means we just edited the item that is currently matched
377 this.savedQueryTitle
.setLabel( item
.getLabel() );
382 * Respond to menu toggle
384 * @param {boolean} isVisible Menu is visible
386 FilterTagMultiselectWidget
.prototype.onMenuToggle = function ( isVisible
) {
388 FilterTagMultiselectWidget
.parent
.prototype.onMenuToggle
.call( this );
391 if ( !this.isMobile
) {
395 mw
.hook( 'RcFilters.popup.open' ).fire();
397 if ( !this.getMenu().findSelectedItem() ) {
398 // If there are no selected items, scroll menu to top
399 // This has to be in a setTimeout so the menu has time
400 // to be positioned and fixed
403 this.getMenu().scrollToTop();
409 this.selectTag( null );
412 this.controller
.setSearch( '' );
414 // Log filter grouping
415 this.controller
.trackFilterGroupings( 'filtermenu' );
420 if ( this.isMobile
) {
421 this.input
.setIcon( isVisible
? 'close' : 'funnel' );
423 this.input
.setIcon( isVisible
? 'search' : 'menu' );
430 FilterTagMultiselectWidget
.prototype.onInputFocus = function () {
431 var scrollToElement
= this.isMobile
? this.input
.$input
: this.$element
;
433 // treat the input as a menu toggle rather than a text field on mobile
434 if ( this.isMobile
) {
435 this.input
.$input
.trigger( 'blur' );
436 this.getMenu().toggle();
439 FilterTagMultiselectWidget
.parent
.prototype.onInputFocus
.call( this );
442 // Only scroll to top of the viewport if:
443 // - The widget is more than 20px from the top
444 // - The widget is not above the top of the viewport (do not scroll downwards)
445 // (This isn't represented because >20 is, anyways and always, bigger than 0)
446 this.scrollToTop( scrollToElement
, 0, { min
: 20, max
: Infinity
} );
452 FilterTagMultiselectWidget
.prototype.doInputEscape = function () {
454 FilterTagMultiselectWidget
.parent
.prototype.doInputEscape
.call( this );
457 this.input
.$input
.trigger( 'blur' );
463 FilterTagMultiselectWidget
.prototype.onMouseDown = function ( e
) {
464 if ( !this.collapsed
&& !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
474 FilterTagMultiselectWidget
.prototype.onChangeTags = function () {
475 // If initialized, call parent method.
476 if ( this.controller
.isInitialized() ) {
477 FilterTagMultiselectWidget
.parent
.prototype.onChangeTags
.call( this );
480 this.emptyFilterMessage
.toggle( this.isEmpty() );
484 * Respond to model initialize event
486 FilterTagMultiselectWidget
.prototype.onModelInitialize = function () {
487 this.setSavedQueryVisibility();
491 * Respond to model update event
493 FilterTagMultiselectWidget
.prototype.onModelUpdate = function () {
494 this.updateElementsForView();
498 * Update the elements in the widget to the current view
500 FilterTagMultiselectWidget
.prototype.updateElementsForView = function () {
501 var view
= this.model
.getCurrentView(),
502 inputValue
= this.input
.getValue().trim(),
503 inputView
= this.model
.getViewByTrigger( inputValue
.substr( 0, 1 ) );
505 if ( inputView
!== 'default' ) {
506 // We have a prefix already, remove it
507 inputValue
= inputValue
.substr( 1 );
510 if ( inputView
!== view
) {
511 // Add the correct prefix
512 inputValue
= this.model
.getViewTrigger( view
) + inputValue
;
516 this.input
.setValue( inputValue
);
518 if ( this.currentView
!== view
) {
519 this.scrollToTop( this.$element
);
520 this.currentView
= view
;
525 * Set the visibility of the saved query button
527 FilterTagMultiselectWidget
.prototype.setSavedQueryVisibility = function () {
528 if ( mw
.user
.isAnon() ) {
532 this.matchingQuery
= this.controller
.findQueryMatchingCurrentState();
534 this.savedQueryTitle
.setLabel(
535 this.matchingQuery
? this.matchingQuery
.getLabel() : ''
537 this.savedQueryTitle
.toggle( !!this.matchingQuery
);
538 this.saveQueryButton
.setDisabled( !!this.matchingQuery
);
539 this.saveQueryButton
.setTitle( !this.matchingQuery
?
540 mw
.msg( 'rcfilters-savedqueries-add-new-title' ) :
541 mw
.msg( 'rcfilters-savedqueries-already-saved' ) );
543 if ( this.matchingQuery
) {
549 * Respond to model itemUpdate event
550 * fixme: when a new state is applied to the model this function is called 60+ times in a row
552 * @param {mw.rcfilters.dm.FilterItem} item Filter item model
554 FilterTagMultiselectWidget
.prototype.onModelItemUpdate = function ( item
) {
555 if ( !item
.getGroupModel().isHidden() ) {
559 this.model
.isHighlightEnabled() &&
560 item
.getHighlightColor()
563 this.addTag( item
.getName(), item
.getLabel() );
565 // Only attempt to remove the tag if we can find an item for it (T198140, T198231)
566 if ( this.findItemFromData( item
.getName() ) !== null ) {
567 this.removeTagByData( item
.getName() );
572 this.setSavedQueryVisibility();
574 // Re-evaluate reset state
575 this.reevaluateResetRestoreState();
581 FilterTagMultiselectWidget
.prototype.isAllowedData = function ( data
) {
583 this.model
.getItemByName( data
) &&
584 !this.isDuplicateData( data
)
591 FilterTagMultiselectWidget
.prototype.onMenuChoose = function ( item
) {
592 this.controller
.toggleFilterSelect( item
.model
.getName() );
594 // Select the tag if it exists, or reset selection otherwise
595 this.selectTag( this.findItemFromData( item
.model
.getName() ) );
597 if ( !this.isMobile
) {
604 * Respond to highlightChange event
606 * @param {boolean} isHighlightEnabled Highlight is enabled
608 FilterTagMultiselectWidget
.prototype.onModelHighlightChange = function ( isHighlightEnabled
) {
609 var highlightedItems
= this.model
.getHighlightedItems();
611 if ( isHighlightEnabled
) {
612 // Add capsule widgets
613 highlightedItems
.forEach( function ( filterItem
) {
614 this.addTag( filterItem
.getName(), filterItem
.getLabel() );
617 // Remove capsule widgets if they're not selected
618 highlightedItems
.forEach( function ( filterItem
) {
619 if ( !filterItem
.isSelected() ) {
620 // Only attempt to remove the tag if we can find an item for it (T198140, T198231)
621 if ( this.findItemFromData( filterItem
.getName() ) !== null ) {
622 this.removeTagByData( filterItem
.getName() );
628 this.setSavedQueryVisibility();
634 FilterTagMultiselectWidget
.prototype.onTagSelect = function ( tagItem
) {
635 var menuOption
= this.menu
.getItemFromModel( tagItem
.getModel() );
637 this.menu
.setUserSelecting( true );
639 FilterTagMultiselectWidget
.parent
.prototype.onTagSelect
.call( this, tagItem
);
642 this.controller
.resetSearchForView( tagItem
.getView() );
644 this.selectTag( tagItem
);
645 this.scrollToTop( menuOption
.$element
);
647 this.menu
.setUserSelecting( false );
651 * Select a tag by reference. This is what OO.ui.SelectWidget is doing.
652 * If no items are given, reset selection from all.
654 * @param {mw.rcfilters.ui.FilterTagItemWidget} [item] Tag to select,
655 * omit to deselect all
657 FilterTagMultiselectWidget
.prototype.selectTag = function ( item
) {
658 var i
, len
, selected
;
660 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
661 selected
= this.items
[ i
] === item
;
662 if ( this.items
[ i
].isSelected() !== selected
) {
663 this.items
[ i
].toggleSelected( selected
);
670 FilterTagMultiselectWidget
.prototype.onTagRemove = function ( tagItem
) {
672 FilterTagMultiselectWidget
.parent
.prototype.onTagRemove
.call( this, tagItem
);
674 this.controller
.clearFilter( tagItem
.getName() );
680 * Respond to click event on the reset button
682 FilterTagMultiselectWidget
.prototype.onResetButtonClick = function () {
683 if ( this.model
.areVisibleFiltersEmpty() ) {
684 // Reset to default filters
685 this.controller
.resetToDefaults();
687 // Reset to have no filters
688 this.controller
.emptyFilters();
693 * Respond to hide/show button click
695 FilterTagMultiselectWidget
.prototype.onHideShowButtonClick = function () {
696 this.toggleCollapsed();
700 * Toggle the collapsed state of the filters widget
702 * @param {boolean} isCollapsed Widget is collapsed
704 FilterTagMultiselectWidget
.prototype.toggleCollapsed = function ( isCollapsed
) {
705 isCollapsed
= isCollapsed
=== undefined ? !this.collapsed
: !!isCollapsed
;
707 this.collapsed
= isCollapsed
;
710 // If we are collapsing, close the menu, in case it was open
711 // We should make sure the menu closes before the rest of the elements
712 // are hidden, otherwise there is an unknown error in jQuery as ooui
713 // sets and unsets properties on the input (which is hidden at that point)
714 this.menu
.toggle( false );
716 this.input
.setDisabled( isCollapsed
);
717 this.hideShowButton
.setLabel( mw
.msg(
718 isCollapsed
? 'rcfilters-activefilters-show' : 'rcfilters-activefilters-hide'
720 this.hideShowButton
.setTitle( mw
.msg(
721 isCollapsed
? 'rcfilters-activefilters-show-tooltip' : 'rcfilters-activefilters-hide-tooltip'
724 // Toggle the wrapper class, so we have min height values correctly throughout
725 this.$wrapper
.toggleClass( 'mw-rcfilters-collapsed', isCollapsed
);
728 this.controller
.updateCollapsedState( isCollapsed
);
732 * Reevaluate the restore state for the widget between setting to defaults and clearing all filters
734 FilterTagMultiselectWidget
.prototype.reevaluateResetRestoreState = function () {
735 var defaultsAreEmpty
= this.controller
.areDefaultsEmpty(),
736 currFiltersAreEmpty
= this.model
.areVisibleFiltersEmpty(),
737 hideResetButton
= currFiltersAreEmpty
&& defaultsAreEmpty
;
739 this.resetButton
.setIcon(
740 currFiltersAreEmpty
? 'history' : 'trash'
743 this.resetButton
.setLabel(
744 currFiltersAreEmpty
? mw
.msg( 'rcfilters-restore-default-filters' ) : ''
746 this.resetButton
.setTitle(
747 currFiltersAreEmpty
? null : mw
.msg( 'rcfilters-clear-all-filters' )
750 this.resetButton
.toggle( !hideResetButton
);
751 this.emptyFilterMessage
.toggle( currFiltersAreEmpty
);
757 FilterTagMultiselectWidget
.prototype.createMenuWidget = function ( menuConfig
) {
758 return new MenuSelectWidget(
768 FilterTagMultiselectWidget
.prototype.createTagItemWidget = function ( data
) {
769 var filterItem
= this.model
.getItemByName( data
);
772 return new FilterTagItemWidget(
775 this.model
.getInvertModel(),
778 $overlay
: this.$overlay
784 FilterTagMultiselectWidget
.prototype.emphasize = function () {
786 // eslint-disable-next-line no-jquery/no-class-state
787 !this.$handle
.hasClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' )
790 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-emphasize' )
791 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' );
793 setTimeout( function () {
795 .removeClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-emphasize' );
797 setTimeout( function () {
799 .removeClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' );
800 }.bind( this ), 1000 );
801 }.bind( this ), 500 );
806 * Scroll the element to top within its container
809 * @param {jQuery} $element Element to position
810 * @param {number} [marginFromTop=0] When scrolling the entire widget to the top, leave this
811 * much space (in pixels) above the widget.
812 * @param {Object} [threshold] Minimum distance from the top of the element to scroll at all
813 * @param {number} [threshold.min] Minimum distance above the element
814 * @param {number} [threshold.max] Minimum distance below the element
816 FilterTagMultiselectWidget
.prototype.scrollToTop = function ( $element
, marginFromTop
, threshold
) {
817 var container
= OO
.ui
.Element
.static.getClosestScrollableContainer( $element
[ 0 ], 'y' ),
818 pos
= OO
.ui
.Element
.static.getRelativePosition( $element
, $( container
) ),
819 containerScrollTop
= $( container
).scrollTop(),
820 effectiveScrollTop
= $( container
).is( 'body, html' ) ? 0 : containerScrollTop
,
821 newScrollTop
= effectiveScrollTop
+ pos
.top
- ( marginFromTop
|| 0 );
825 threshold
=== undefined ||
828 threshold
.min
=== undefined ||
829 newScrollTop
- containerScrollTop
>= threshold
.min
832 threshold
.max
=== undefined ||
833 newScrollTop
- containerScrollTop
<= threshold
.max
837 $( container
).animate( {
838 scrollTop
: newScrollTop
843 module
.exports
= FilterTagMultiselectWidget
;