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