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 var scrollToElement
= this.isMobile
? this.input
.$input
: this.$element
;
391 FilterTagMultiselectWidget
.parent
.prototype.onMenuToggle
.call( this );
394 if ( !this.isMobile
) {
398 mw
.hook( 'RcFilters.popup.open' ).fire();
400 if ( !this.getMenu().findSelectedItem() ) {
401 // If there are no selected items, scroll menu to top
402 // This has to be in a setTimeout so the menu has time
403 // to be positioned and fixed
406 this.getMenu().scrollToTop();
411 // Only scroll to top of the viewport if:
412 // - The widget is more than 20px from the top
413 // - The widget is not above the top of the viewport (do not scroll downwards)
414 // (This isn't represented because >20 is, anyways and always, bigger than 0)
415 this.scrollToTop( scrollToElement
, 0, { min
: 20, max
: Infinity
} );
419 this.selectTag( null );
422 this.controller
.setSearch( '' );
424 // Log filter grouping
425 this.controller
.trackFilterGroupings( 'filtermenu' );
430 if ( this.isMobile
) {
431 this.input
.setIcon( isVisible
? 'close' : 'funnel' );
433 this.input
.setIcon( isVisible
? 'search' : 'menu' );
440 FilterTagMultiselectWidget
.prototype.onInputFocus = function () {
442 // treat the input as a menu toggle rather than a text field on mobile
443 if ( this.isMobile
) {
444 this.input
.$input
.trigger( 'blur' );
445 this.getMenu().toggle();
448 FilterTagMultiselectWidget
.parent
.prototype.onInputFocus
.call( this );
455 FilterTagMultiselectWidget
.prototype.doInputEscape = function () {
457 FilterTagMultiselectWidget
.parent
.prototype.doInputEscape
.call( this );
460 this.input
.$input
.trigger( 'blur' );
466 FilterTagMultiselectWidget
.prototype.onMouseDown = function ( e
) {
467 if ( !this.collapsed
&& !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
477 FilterTagMultiselectWidget
.prototype.onChangeTags = function () {
478 // If initialized, call parent method.
479 if ( this.controller
.isInitialized() ) {
480 FilterTagMultiselectWidget
.parent
.prototype.onChangeTags
.call( this );
483 this.emptyFilterMessage
.toggle( this.isEmpty() );
487 * Respond to model initialize event
489 FilterTagMultiselectWidget
.prototype.onModelInitialize = function () {
490 this.setSavedQueryVisibility();
494 * Respond to model update event
496 FilterTagMultiselectWidget
.prototype.onModelUpdate = function () {
497 this.updateElementsForView();
501 * Update the elements in the widget to the current view
503 FilterTagMultiselectWidget
.prototype.updateElementsForView = function () {
504 var view
= this.model
.getCurrentView(),
505 inputValue
= this.input
.getValue().trim(),
506 inputView
= this.model
.getViewByTrigger( inputValue
.substr( 0, 1 ) );
508 if ( inputView
!== 'default' ) {
509 // We have a prefix already, remove it
510 inputValue
= inputValue
.substr( 1 );
513 if ( inputView
!== view
) {
514 // Add the correct prefix
515 inputValue
= this.model
.getViewTrigger( view
) + inputValue
;
519 this.input
.setValue( inputValue
);
521 if ( this.currentView
!== view
) {
522 this.scrollToTop( this.$element
);
523 this.currentView
= view
;
528 * Set the visibility of the saved query button
530 FilterTagMultiselectWidget
.prototype.setSavedQueryVisibility = function () {
531 if ( mw
.user
.isAnon() ) {
535 this.matchingQuery
= this.controller
.findQueryMatchingCurrentState();
537 this.savedQueryTitle
.setLabel(
538 this.matchingQuery
? this.matchingQuery
.getLabel() : ''
540 this.savedQueryTitle
.toggle( !!this.matchingQuery
);
541 this.saveQueryButton
.setDisabled( !!this.matchingQuery
);
542 this.saveQueryButton
.setTitle( !this.matchingQuery
?
543 mw
.msg( 'rcfilters-savedqueries-add-new-title' ) :
544 mw
.msg( 'rcfilters-savedqueries-already-saved' ) );
546 if ( this.matchingQuery
) {
552 * Respond to model itemUpdate event
553 * fixme: when a new state is applied to the model this function is called 60+ times in a row
555 * @param {mw.rcfilters.dm.FilterItem} item Filter item model
557 FilterTagMultiselectWidget
.prototype.onModelItemUpdate = function ( item
) {
558 if ( !item
.getGroupModel().isHidden() ) {
562 this.model
.isHighlightEnabled() &&
563 item
.getHighlightColor()
566 this.addTag( item
.getName(), item
.getLabel() );
568 // Only attempt to remove the tag if we can find an item for it (T198140, T198231)
569 if ( this.findItemFromData( item
.getName() ) !== null ) {
570 this.removeTagByData( item
.getName() );
575 this.setSavedQueryVisibility();
577 // Re-evaluate reset state
578 this.reevaluateResetRestoreState();
584 FilterTagMultiselectWidget
.prototype.isAllowedData = function ( data
) {
586 this.model
.getItemByName( data
) &&
587 !this.isDuplicateData( data
)
594 FilterTagMultiselectWidget
.prototype.onMenuChoose = function ( item
) {
595 this.controller
.toggleFilterSelect( item
.model
.getName() );
597 // Select the tag if it exists, or reset selection otherwise
598 this.selectTag( this.findItemFromData( item
.model
.getName() ) );
600 if ( !this.isMobile
) {
607 * Respond to highlightChange event
609 * @param {boolean} isHighlightEnabled Highlight is enabled
611 FilterTagMultiselectWidget
.prototype.onModelHighlightChange = function ( isHighlightEnabled
) {
612 var highlightedItems
= this.model
.getHighlightedItems();
614 if ( isHighlightEnabled
) {
615 // Add capsule widgets
616 highlightedItems
.forEach( function ( filterItem
) {
617 this.addTag( filterItem
.getName(), filterItem
.getLabel() );
620 // Remove capsule widgets if they're not selected
621 highlightedItems
.forEach( function ( filterItem
) {
622 if ( !filterItem
.isSelected() ) {
623 // Only attempt to remove the tag if we can find an item for it (T198140, T198231)
624 if ( this.findItemFromData( filterItem
.getName() ) !== null ) {
625 this.removeTagByData( filterItem
.getName() );
631 this.setSavedQueryVisibility();
637 FilterTagMultiselectWidget
.prototype.onTagSelect = function ( tagItem
) {
638 var menuOption
= this.menu
.getItemFromModel( tagItem
.getModel() );
640 this.menu
.setUserSelecting( true );
642 FilterTagMultiselectWidget
.parent
.prototype.onTagSelect
.call( this, tagItem
);
645 this.controller
.resetSearchForView( tagItem
.getView() );
647 this.selectTag( tagItem
);
648 this.scrollToTop( menuOption
.$element
);
650 this.menu
.setUserSelecting( false );
654 * Select a tag by reference. This is what OO.ui.SelectWidget is doing.
655 * If no items are given, reset selection from all.
657 * @param {mw.rcfilters.ui.FilterTagItemWidget} [item] Tag to select,
658 * omit to deselect all
660 FilterTagMultiselectWidget
.prototype.selectTag = function ( item
) {
661 var i
, len
, selected
;
663 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
664 selected
= this.items
[ i
] === item
;
665 if ( this.items
[ i
].isSelected() !== selected
) {
666 this.items
[ i
].toggleSelected( selected
);
673 FilterTagMultiselectWidget
.prototype.onTagRemove = function ( tagItem
) {
675 FilterTagMultiselectWidget
.parent
.prototype.onTagRemove
.call( this, tagItem
);
677 this.controller
.clearFilter( tagItem
.getName() );
683 * Respond to click event on the reset button
685 FilterTagMultiselectWidget
.prototype.onResetButtonClick = function () {
686 if ( this.model
.areVisibleFiltersEmpty() ) {
687 // Reset to default filters
688 this.controller
.resetToDefaults();
690 // Reset to have no filters
691 this.controller
.emptyFilters();
696 * Respond to hide/show button click
698 FilterTagMultiselectWidget
.prototype.onHideShowButtonClick = function () {
699 this.toggleCollapsed();
703 * Toggle the collapsed state of the filters widget
705 * @param {boolean} isCollapsed Widget is collapsed
707 FilterTagMultiselectWidget
.prototype.toggleCollapsed = function ( isCollapsed
) {
708 isCollapsed
= isCollapsed
=== undefined ? !this.collapsed
: !!isCollapsed
;
710 this.collapsed
= isCollapsed
;
713 // If we are collapsing, close the menu, in case it was open
714 // We should make sure the menu closes before the rest of the elements
715 // are hidden, otherwise there is an unknown error in jQuery as ooui
716 // sets and unsets properties on the input (which is hidden at that point)
717 this.menu
.toggle( false );
719 this.input
.setDisabled( isCollapsed
);
720 this.hideShowButton
.setLabel( mw
.msg(
721 isCollapsed
? 'rcfilters-activefilters-show' : 'rcfilters-activefilters-hide'
723 this.hideShowButton
.setTitle( mw
.msg(
724 isCollapsed
? 'rcfilters-activefilters-show-tooltip' : 'rcfilters-activefilters-hide-tooltip'
727 // Toggle the wrapper class, so we have min height values correctly throughout
728 this.$wrapper
.toggleClass( 'mw-rcfilters-collapsed', isCollapsed
);
731 this.controller
.updateCollapsedState( isCollapsed
);
735 * Reevaluate the restore state for the widget between setting to defaults and clearing all filters
737 FilterTagMultiselectWidget
.prototype.reevaluateResetRestoreState = function () {
738 var defaultsAreEmpty
= this.controller
.areDefaultsEmpty(),
739 currFiltersAreEmpty
= this.model
.areVisibleFiltersEmpty(),
740 hideResetButton
= currFiltersAreEmpty
&& defaultsAreEmpty
;
742 this.resetButton
.setIcon(
743 currFiltersAreEmpty
? 'history' : 'trash'
746 this.resetButton
.setLabel(
747 currFiltersAreEmpty
? mw
.msg( 'rcfilters-restore-default-filters' ) : ''
749 this.resetButton
.setTitle(
750 currFiltersAreEmpty
? null : mw
.msg( 'rcfilters-clear-all-filters' )
753 this.resetButton
.toggle( !hideResetButton
);
754 this.emptyFilterMessage
.toggle( currFiltersAreEmpty
);
760 FilterTagMultiselectWidget
.prototype.createMenuWidget = function ( menuConfig
) {
761 return new MenuSelectWidget(
771 FilterTagMultiselectWidget
.prototype.createTagItemWidget = function ( data
) {
772 var filterItem
= this.model
.getItemByName( data
);
775 return new FilterTagItemWidget(
778 this.model
.getInvertModel(),
781 $overlay
: this.$overlay
787 FilterTagMultiselectWidget
.prototype.emphasize = function () {
789 // eslint-disable-next-line no-jquery/no-class-state
790 !this.$handle
.hasClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' )
793 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-emphasize' )
794 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' );
796 setTimeout( function () {
798 .removeClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-emphasize' );
800 setTimeout( function () {
802 .removeClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' );
803 }.bind( this ), 1000 );
804 }.bind( this ), 500 );
809 * Scroll the element to top within its container
812 * @param {jQuery} $element Element to position
813 * @param {number} [marginFromTop=0] When scrolling the entire widget to the top, leave this
814 * much space (in pixels) above the widget.
815 * @param {Object} [threshold] Minimum distance from the top of the element to scroll at all
816 * @param {number} [threshold.min] Minimum distance above the element
817 * @param {number} [threshold.max] Minimum distance below the element
819 FilterTagMultiselectWidget
.prototype.scrollToTop = function ( $element
, marginFromTop
, threshold
) {
820 var container
= OO
.ui
.Element
.static.getClosestScrollableContainer( $element
[ 0 ], 'y' ),
821 pos
= OO
.ui
.Element
.static.getRelativePosition( $element
, $( container
) ),
822 containerScrollTop
= $( container
).scrollTop(),
823 effectiveScrollTop
= $( container
).is( 'body, html' ) ? 0 : containerScrollTop
,
824 newScrollTop
= effectiveScrollTop
+ pos
.top
- ( marginFromTop
|| 0 );
828 threshold
=== undefined ||
831 threshold
.min
=== undefined ||
832 newScrollTop
- containerScrollTop
>= threshold
.min
835 threshold
.max
=== undefined ||
836 newScrollTop
- containerScrollTop
<= threshold
.max
840 $( container
).animate( {
841 scrollTop
: newScrollTop
846 module
.exports
= FilterTagMultiselectWidget
;