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