]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / resources / src / mediawiki.rcfilters / ui / mw.rcfilters.ui.FilterTagMultiselectWidget.js
1 ( function ( mw ) {
2         /**
3          * List displaying all filter groups
4          *
5          * @class
6          * @extends OO.ui.MenuTagMultiselectWidget
7          * @mixins OO.ui.mixin.PendingElement
8          *
9          * @constructor
10          * @param {mw.rcfilters.Controller} controller Controller
11          * @param {mw.rcfilters.dm.FiltersViewModel} model View model
12          * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
13          * @param {Object} config Configuration object
14          * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
15          */
16         mw.rcfilters.ui.FilterTagMultiselectWidget = function MwRcfiltersUiFilterTagMultiselectWidget( controller, model, savedQueriesModel, config ) {
17                 var rcFiltersRow,
18                         title = new OO.ui.LabelWidget( {
19                                 label: mw.msg( 'rcfilters-activefilters' ),
20                                 classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-title' ]
21                         } ),
22                         $contentWrapper = $( '<div>' )
23                                 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper' );
24
25                 config = config || {};
26
27                 this.controller = controller;
28                 this.model = model;
29                 this.queriesModel = savedQueriesModel;
30                 this.$overlay = config.$overlay || this.$element;
31                 this.matchingQuery = null;
32                 this.currentView = this.model.getCurrentView();
33
34                 // Parent
35                 mw.rcfilters.ui.FilterTagMultiselectWidget.parent.call( this, $.extend( true, {
36                         label: mw.msg( 'rcfilters-filterlist-title' ),
37                         placeholder: mw.msg( 'rcfilters-empty-filter' ),
38                         inputPosition: 'outline',
39                         allowArbitrary: false,
40                         allowDisplayInvalidTags: false,
41                         allowReordering: false,
42                         $overlay: this.$overlay,
43                         menu: {
44                                 hideWhenOutOfView: false,
45                                 hideOnChoose: false,
46                                 width: 650,
47                                 footers: [
48                                         {
49                                                 name: 'viewSelect',
50                                                 sticky: false,
51                                                 // View select menu, appears on default view only
52                                                 $element: $( '<div>' )
53                                                         .append( new mw.rcfilters.ui.ViewSwitchWidget( this.controller, this.model ).$element ),
54                                                 views: [ 'default' ]
55                                         },
56                                         {
57                                                 name: 'feedback',
58                                                 // Feedback footer, appears on all views
59                                                 $element: $( '<div>' )
60                                                         .append(
61                                                                 new OO.ui.ButtonWidget( {
62                                                                         framed: false,
63                                                                         icon: 'feedback',
64                                                                         flags: [ 'progressive' ],
65                                                                         label: mw.msg( 'rcfilters-filterlist-feedbacklink' ),
66                                                                         href: 'https://www.mediawiki.org/wiki/Help_talk:New_filters_for_edit_review'
67                                                                 } ).$element
68                                                         )
69                                         }
70                                 ]
71                         },
72                         input: {
73                                 icon: 'menu',
74                                 placeholder: mw.msg( 'rcfilters-search-placeholder' )
75                         }
76                 }, config ) );
77
78                 this.savedQueryTitle = new OO.ui.LabelWidget( {
79                         label: '',
80                         classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-savedQueryTitle' ]
81                 } );
82
83                 this.resetButton = new OO.ui.ButtonWidget( {
84                         framed: false,
85                         classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-resetButton' ]
86                 } );
87
88                 if ( !mw.user.isAnon() ) {
89                         this.saveQueryButton = new mw.rcfilters.ui.SaveFiltersPopupButtonWidget(
90                                 this.controller,
91                                 this.queriesModel
92                         );
93
94                         this.saveQueryButton.$element.on( 'mousedown', function ( e ) { e.stopPropagation(); } );
95
96                         this.saveQueryButton.connect( this, {
97                                 click: 'onSaveQueryButtonClick',
98                                 saveCurrent: 'setSavedQueryVisibility'
99                         } );
100                         this.queriesModel.connect( this, {
101                                 itemUpdate: 'onSavedQueriesItemUpdate',
102                                 initialize: 'onSavedQueriesInitialize',
103                                 'default': 'reevaluateResetRestoreState'
104                         } );
105                 }
106
107                 this.emptyFilterMessage = new OO.ui.LabelWidget( {
108                         label: mw.msg( 'rcfilters-empty-filter' ),
109                         classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-emptyFilters' ]
110                 } );
111                 this.$content.append( this.emptyFilterMessage.$element );
112
113                 // Events
114                 this.resetButton.connect( this, { click: 'onResetButtonClick' } );
115                 // Stop propagation for mousedown, so that the widget doesn't
116                 // trigger the focus on the input and scrolls up when we click the reset button
117                 this.resetButton.$element.on( 'mousedown', function ( e ) { e.stopPropagation(); } );
118                 this.model.connect( this, {
119                         initialize: 'onModelInitialize',
120                         update: 'onModelUpdate',
121                         itemUpdate: 'onModelItemUpdate',
122                         highlightChange: 'onModelHighlightChange'
123                 } );
124                 this.input.connect( this, { change: 'onInputChange' } );
125
126                 // The filter list and button should appear side by side regardless of how
127                 // wide the button is; the button also changes its width depending
128                 // on language and its state, so the safest way to present both side
129                 // by side is with a table layout
130                 rcFiltersRow = $( '<div>' )
131                         .addClass( 'mw-rcfilters-ui-row' )
132                         .append(
133                                 this.$content
134                                         .addClass( 'mw-rcfilters-ui-cell' )
135                                         .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-filters' )
136                         );
137
138                 if ( !mw.user.isAnon() ) {
139                         rcFiltersRow.append(
140                                 $( '<div>' )
141                                         .addClass( 'mw-rcfilters-ui-cell' )
142                                         .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-save' )
143                                         .append( this.saveQueryButton.$element )
144                         );
145                 }
146
147                 // Add a selector at the right of the input
148                 this.viewsSelectWidget = new OO.ui.ButtonSelectWidget( {
149                         classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-views-select-widget' ],
150                         items: [
151                                 new OO.ui.ButtonOptionWidget( {
152                                         framed: false,
153                                         data: '',
154                                         disabled: true,
155                                         classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-views-select-widget-label' ],
156                                         label: mw.msg( 'rcfilters-view-advanced-filters-label' )
157                                 } ),
158                                 new OO.ui.ButtonOptionWidget( {
159                                         framed: false,
160                                         data: 'namespaces',
161                                         icon: 'article',
162                                         title: mw.msg( 'rcfilters-view-namespaces-tooltip' )
163                                 } ),
164                                 new OO.ui.ButtonOptionWidget( {
165                                         framed: false,
166                                         data: 'tags',
167                                         icon: 'tag',
168                                         title: mw.msg( 'rcfilters-view-tags-tooltip' )
169                                 } )
170                         ]
171                 } );
172
173                 // Rearrange the UI so the select widget is at the right of the input
174                 this.$element.append(
175                         $( '<div>' )
176                                 .addClass( 'mw-rcfilters-ui-table' )
177                                 .append(
178                                         $( '<div>' )
179                                                 .addClass( 'mw-rcfilters-ui-row' )
180                                                 .append(
181                                                         $( '<div>' )
182                                                                 .addClass( 'mw-rcfilters-ui-cell' )
183                                                                 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-input' )
184                                                                 .append( this.input.$element ),
185                                                         $( '<div>' )
186                                                                 .addClass( 'mw-rcfilters-ui-cell' )
187                                                                 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-select' )
188                                                                 .append( this.viewsSelectWidget.$element )
189                                                 )
190                                 )
191                 );
192
193                 // Event
194                 this.viewsSelectWidget.connect( this, { choose: 'onViewsSelectWidgetChoose' } );
195
196                 rcFiltersRow.append(
197                         $( '<div>' )
198                                 .addClass( 'mw-rcfilters-ui-cell' )
199                                 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-reset' )
200                                 .append( this.resetButton.$element )
201                 );
202
203                 // Build the content
204                 $contentWrapper.append(
205                         title.$element,
206                         this.savedQueryTitle.$element,
207                         $( '<div>' )
208                                 .addClass( 'mw-rcfilters-ui-table' )
209                                 .append(
210                                         rcFiltersRow
211                                 )
212                 );
213
214                 // Initialize
215                 this.$handle.append( $contentWrapper );
216                 this.emptyFilterMessage.toggle( this.isEmpty() );
217                 this.savedQueryTitle.toggle( false );
218
219                 this.$element
220                         .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget' );
221
222                 this.reevaluateResetRestoreState();
223         };
224
225         /* Initialization */
226
227         OO.inheritClass( mw.rcfilters.ui.FilterTagMultiselectWidget, OO.ui.MenuTagMultiselectWidget );
228
229         /* Methods */
230
231         /**
232          * Respond to view select widget choose event
233          *
234          * @param {OO.ui.ButtonOptionWidget} buttonOptionWidget Chosen widget
235          */
236         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onViewsSelectWidgetChoose = function ( buttonOptionWidget ) {
237                 this.controller.switchView( buttonOptionWidget.getData() );
238                 this.viewsSelectWidget.selectItem( null );
239                 this.focus();
240         };
241
242         /**
243          * Respond to input change event
244          *
245          * @param {string} value Value of the input
246          */
247         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onInputChange = function ( value ) {
248                 var view;
249
250                 value = value.trim();
251
252                 view = this.model.getViewByTrigger( value.substr( 0, 1 ) );
253
254                 this.controller.switchView( view );
255         };
256         /**
257          * Respond to query button click
258          */
259         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onSaveQueryButtonClick = function () {
260                 this.getMenu().toggle( false );
261         };
262
263         /**
264          * Respond to save query model initialization
265          */
266         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onSavedQueriesInitialize = function () {
267                 this.setSavedQueryVisibility();
268         };
269
270         /**
271          * Respond to save query item change. Mainly this is done to update the label in case
272          * a query item has been edited
273          *
274          * @param {mw.rcfilters.dm.SavedQueryItemModel} item Saved query item
275          */
276         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onSavedQueriesItemUpdate = function ( item ) {
277                 if ( this.matchingQuery === item ) {
278                         // This means we just edited the item that is currently matched
279                         this.savedQueryTitle.setLabel( item.getLabel() );
280                 }
281         };
282
283         /**
284          * Respond to menu toggle
285          *
286          * @param {boolean} isVisible Menu is visible
287          */
288         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onMenuToggle = function ( isVisible ) {
289                 // Parent
290                 mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onMenuToggle.call( this );
291
292                 if ( isVisible ) {
293                         mw.hook( 'RcFilters.popup.open' ).fire();
294
295                         if ( !this.getMenu().getSelectedItem() ) {
296                                 // If there are no selected items, scroll menu to top
297                                 // This has to be in a setTimeout so the menu has time
298                                 // to be positioned and fixed
299                                 setTimeout( function () { this.getMenu().scrollToTop(); }.bind( this ), 0 );
300                         }
301                 } else {
302                         // Clear selection
303                         this.selectTag( null );
304
305                         // Clear input if the only thing in the input is the prefix
306                         if (
307                                 this.input.getValue().trim() === this.model.getViewTrigger( this.model.getCurrentView() )
308                         ) {
309                                 // Clear the input
310                                 this.input.setValue( '' );
311                         }
312
313                         // Log filter grouping
314                         this.controller.trackFilterGroupings( 'filtermenu' );
315                 }
316
317                 this.input.setIcon( isVisible ? 'search' : 'menu' );
318         };
319
320         /**
321          * @inheritdoc
322          */
323         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onInputFocus = function () {
324                 // Parent
325                 mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onInputFocus.call( this );
326
327                 // Only scroll to top of the viewport if:
328                 // - The widget is more than 20px from the top
329                 // - The widget is not above the top of the viewport (do not scroll downwards)
330                 //   (This isn't represented because >20 is, anyways and always, bigger than 0)
331                 this.scrollToTop( this.$element, 0, { min: 20, max: Infinity } );
332         };
333
334         /**
335          * @inheritdoc
336          */
337         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.doInputEscape = function () {
338                 // Parent
339                 mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.doInputEscape.call( this );
340
341                 // Blur the input
342                 this.input.$input.blur();
343         };
344
345         /**
346          * @inheritdoc
347          */
348         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onChangeTags = function () {
349                 // Parent method
350                 mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onChangeTags.call( this );
351
352                 this.emptyFilterMessage.toggle( this.isEmpty() );
353         };
354
355         /**
356          * Respond to model initialize event
357          */
358         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelInitialize = function () {
359                 this.setSavedQueryVisibility();
360         };
361
362         /**
363          * Respond to model update event
364          */
365         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelUpdate = function () {
366                 this.updateElementsForView();
367         };
368
369         /**
370          * Update the elements in the widget to the current view
371          */
372         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.updateElementsForView = function () {
373                 var view = this.model.getCurrentView(),
374                         inputValue = this.input.getValue().trim(),
375                         inputView = this.model.getViewByTrigger( inputValue.substr( 0, 1 ) );
376
377                 if ( inputView !== 'default' ) {
378                         // We have a prefix already, remove it
379                         inputValue = inputValue.substr( 1 );
380                 }
381
382                 if ( inputView !== view ) {
383                         // Add the correct prefix
384                         inputValue = this.model.getViewTrigger( view ) + inputValue;
385                 }
386
387                 // Update input
388                 this.input.setValue( inputValue );
389
390                 if ( this.currentView !== view ) {
391                         this.scrollToTop( this.$element );
392                         this.currentView = view;
393                 }
394         };
395
396         /**
397          * Set the visibility of the saved query button
398          */
399         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.setSavedQueryVisibility = function () {
400                 if ( mw.user.isAnon() ) {
401                         return;
402                 }
403
404                 this.matchingQuery = this.controller.findQueryMatchingCurrentState();
405
406                 this.savedQueryTitle.setLabel(
407                         this.matchingQuery ? this.matchingQuery.getLabel() : ''
408                 );
409                 this.savedQueryTitle.toggle( !!this.matchingQuery );
410                 this.saveQueryButton.toggle( !this.matchingQuery );
411
412                 if ( this.matchingQuery ) {
413                         this.emphasize();
414                 }
415         };
416
417         /**
418          * Respond to model itemUpdate event
419          *
420          * @param {mw.rcfilters.dm.FilterItem} item Filter item model
421          */
422         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelItemUpdate = function ( item ) {
423                 if ( item.getGroupModel().isHidden() ) {
424                         return;
425                 }
426
427                 if (
428                         item.isSelected() ||
429                         (
430                                 this.model.isHighlightEnabled() &&
431                                 item.isHighlightSupported() &&
432                                 item.getHighlightColor()
433                         )
434                 ) {
435                         this.addTag( item.getName(), item.getLabel() );
436                 } else {
437                         this.removeTagByData( item.getName() );
438                 }
439
440                 this.setSavedQueryVisibility();
441
442                 // Re-evaluate reset state
443                 this.reevaluateResetRestoreState();
444         };
445
446         /**
447          * @inheritdoc
448          */
449         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.isAllowedData = function ( data ) {
450                 return (
451                         this.model.getItemByName( data ) &&
452                         !this.isDuplicateData( data )
453                 );
454         };
455
456         /**
457          * @inheritdoc
458          */
459         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onMenuChoose = function ( item ) {
460                 this.controller.toggleFilterSelect( item.model.getName() );
461
462                 // Select the tag if it exists, or reset selection otherwise
463                 this.selectTag( this.getItemFromData( item.model.getName() ) );
464
465                 this.focus();
466         };
467
468         /**
469          * Respond to highlightChange event
470          *
471          * @param {boolean} isHighlightEnabled Highlight is enabled
472          */
473         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelHighlightChange = function ( isHighlightEnabled ) {
474                 var highlightedItems = this.model.getHighlightedItems();
475
476                 if ( isHighlightEnabled ) {
477                         // Add capsule widgets
478                         highlightedItems.forEach( function ( filterItem ) {
479                                 this.addTag( filterItem.getName(), filterItem.getLabel() );
480                         }.bind( this ) );
481                 } else {
482                         // Remove capsule widgets if they're not selected
483                         highlightedItems.forEach( function ( filterItem ) {
484                                 if ( !filterItem.isSelected() ) {
485                                         this.removeTagByData( filterItem.getName() );
486                                 }
487                         }.bind( this ) );
488                 }
489         };
490
491         /**
492          * @inheritdoc
493          */
494         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onTagSelect = function ( tagItem ) {
495                 var widget = this,
496                         menuOption = this.menu.getItemFromModel( tagItem.getModel() ),
497                         oldInputValue = this.input.getValue().trim();
498
499                 this.menu.setUserSelecting( true );
500
501                 // Reset input
502                 this.input.setValue( '' );
503
504                 // Switch view
505                 this.controller.switchView( tagItem.getView() );
506
507                 // Parent method
508                 mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onTagSelect.call( this, tagItem );
509
510                 this.menu.selectItem( menuOption );
511                 this.selectTag( tagItem );
512
513                 // Scroll to the item
514                 if ( this.model.removeViewTriggers( oldInputValue ) ) {
515                         // We're binding a 'once' to the itemVisibilityChange event
516                         // so this happens when the menu is ready after the items
517                         // are visible again, in case this is done right after the
518                         // user filtered the results
519                         this.getMenu().once(
520                                 'itemVisibilityChange',
521                                 function () {
522                                         widget.scrollToTop( menuOption.$element );
523                                         widget.menu.setUserSelecting( false );
524                                 }
525                         );
526                 } else {
527                         this.scrollToTop( menuOption.$element );
528                         this.menu.setUserSelecting( false );
529                 }
530
531         };
532
533         /**
534          * Select a tag by reference. This is what OO.ui.SelectWidget is doing.
535          * If no items are given, reset selection from all.
536          *
537          * @param {mw.rcfilters.ui.FilterTagItemWidget} [item] Tag to select,
538          *  omit to deselect all
539          */
540         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.selectTag = function ( item ) {
541                 var i, len, selected;
542
543                 for ( i = 0, len = this.items.length; i < len; i++ ) {
544                         selected = this.items[ i ] === item;
545                         if ( this.items[ i ].isSelected() !== selected ) {
546                                 this.items[ i ].toggleSelected( selected );
547                         }
548                 }
549         };
550         /**
551          * @inheritdoc
552          */
553         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onTagRemove = function ( tagItem ) {
554                 // Parent method
555                 mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onTagRemove.call( this, tagItem );
556
557                 this.controller.clearFilter( tagItem.getName() );
558
559                 tagItem.destroy();
560         };
561
562         /**
563          * Respond to click event on the reset button
564          */
565         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onResetButtonClick = function () {
566                 if ( this.model.areCurrentFiltersEmpty() ) {
567                         // Reset to default filters
568                         this.controller.resetToDefaults();
569                 } else {
570                         // Reset to have no filters
571                         this.controller.emptyFilters();
572                 }
573         };
574
575         /**
576          * Reevaluate the restore state for the widget between setting to defaults and clearing all filters
577          */
578         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.reevaluateResetRestoreState = function () {
579                 var defaultsAreEmpty = this.controller.areDefaultsEmpty(),
580                         currFiltersAreEmpty = this.model.areCurrentFiltersEmpty(),
581                         hideResetButton = currFiltersAreEmpty && defaultsAreEmpty;
582
583                 this.resetButton.setIcon(
584                         currFiltersAreEmpty ? 'history' : 'trash'
585                 );
586
587                 this.resetButton.setLabel(
588                         currFiltersAreEmpty ? mw.msg( 'rcfilters-restore-default-filters' ) : ''
589                 );
590                 this.resetButton.setTitle(
591                         currFiltersAreEmpty ? null : mw.msg( 'rcfilters-clear-all-filters' )
592                 );
593
594                 this.resetButton.toggle( !hideResetButton );
595                 this.emptyFilterMessage.toggle( currFiltersAreEmpty );
596         };
597
598         /**
599          * @inheritdoc
600          */
601         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.createMenuWidget = function ( menuConfig ) {
602                 return new mw.rcfilters.ui.MenuSelectWidget(
603                         this.controller,
604                         this.model,
605                         $.extend( {
606                                 filterFromInput: true
607                         }, menuConfig )
608                 );
609         };
610
611         /**
612          * @inheritdoc
613          */
614         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.createTagItemWidget = function ( data ) {
615                 var filterItem = this.model.getItemByName( data );
616
617                 if ( filterItem ) {
618                         return new mw.rcfilters.ui.FilterTagItemWidget(
619                                 this.controller,
620                                 filterItem,
621                                 {
622                                         $overlay: this.$overlay
623                                 }
624                         );
625                 }
626         };
627
628         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.emphasize = function () {
629                 if (
630                         !this.$handle.hasClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' )
631                 ) {
632                         this.$handle
633                                 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-emphasize' )
634                                 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' );
635
636                         setTimeout( function () {
637                                 this.$handle
638                                         .removeClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-emphasize' );
639
640                                 setTimeout( function () {
641                                         this.$handle
642                                                 .removeClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' );
643                                 }.bind( this ), 1000 );
644                         }.bind( this ), 500 );
645
646                 }
647         };
648         /**
649          * Scroll the element to top within its container
650          *
651          * @private
652          * @param {jQuery} $element Element to position
653          * @param {number} [marginFromTop=0] When scrolling the entire widget to the top, leave this
654          *  much space (in pixels) above the widget.
655          * @param {Object} [threshold] Minimum distance from the top of the element to scroll at all
656          * @param {number} [threshold.min] Minimum distance above the element
657          * @param {number} [threshold.max] Minimum distance below the element
658          */
659         mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.scrollToTop = function ( $element, marginFromTop, threshold ) {
660                 var container = OO.ui.Element.static.getClosestScrollableContainer( $element[ 0 ], 'y' ),
661                         pos = OO.ui.Element.static.getRelativePosition( $element, $( container ) ),
662                         containerScrollTop = $( container ).scrollTop(),
663                         effectiveScrollTop = $( container ).is( 'body, html' ) ? 0 : containerScrollTop,
664                         newScrollTop = effectiveScrollTop + pos.top - ( marginFromTop || 0 );
665
666                 // Scroll to item
667                 if (
668                         threshold === undefined ||
669                         (
670                                 (
671                                         threshold.min === undefined ||
672                                         newScrollTop - containerScrollTop >= threshold.min
673                                 ) &&
674                                 (
675                                         threshold.max === undefined ||
676                                         newScrollTop - containerScrollTop <= threshold.max
677                                 )
678                         )
679                 ) {
680                         $( container ).animate( {
681                                 scrollTop: newScrollTop
682                         } );
683                 }
684         };
685 }( mediaWiki ) );