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