Merge "resourceloader: Move mw.libs and mw.widgets from startup to mediawiki.base"
[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
388 var scrollToElement = this.isMobile ? this.input.$input : this.$element;
389
390 // Parent
391 FilterTagMultiselectWidget.parent.prototype.onMenuToggle.call( this );
392
393 if ( isVisible ) {
394 if ( !this.isMobile ) {
395 this.focus();
396 }
397
398 mw.hook( 'RcFilters.popup.open' ).fire();
399
400 if ( !this.getMenu().findSelectedItem() ) {
401 // If there are no selected items, scroll menu to top
402 // This has to be in a setTimeout so the menu has time
403 // to be positioned and fixed
404 setTimeout(
405 function () {
406 this.getMenu().scrollToTop();
407 }.bind( this )
408 );
409 }
410
411 // Only scroll to top of the viewport if:
412 // - The widget is more than 20px from the top
413 // - The widget is not above the top of the viewport (do not scroll downwards)
414 // (This isn't represented because >20 is, anyways and always, bigger than 0)
415 this.scrollToTop( scrollToElement, 0, { min: 20, max: Infinity } );
416
417 } else {
418 // Clear selection
419 this.selectTag( null );
420
421 // Clear the search
422 this.controller.setSearch( '' );
423
424 // Log filter grouping
425 this.controller.trackFilterGroupings( 'filtermenu' );
426
427 this.blur();
428 }
429
430 if ( this.isMobile ) {
431 this.input.setIcon( isVisible ? 'close' : 'funnel' );
432 } else {
433 this.input.setIcon( isVisible ? 'search' : 'menu' );
434 }
435 };
436
437 /**
438 * @inheritdoc
439 */
440 FilterTagMultiselectWidget.prototype.onInputFocus = function () {
441
442 // treat the input as a menu toggle rather than a text field on mobile
443 if ( this.isMobile ) {
444 this.input.$input.trigger( 'blur' );
445 this.getMenu().toggle();
446 } else {
447 // Parent
448 FilterTagMultiselectWidget.parent.prototype.onInputFocus.call( this );
449 }
450 };
451
452 /**
453 * @inheritdoc
454 */
455 FilterTagMultiselectWidget.prototype.doInputEscape = function () {
456 // Parent
457 FilterTagMultiselectWidget.parent.prototype.doInputEscape.call( this );
458
459 // Blur the input
460 this.input.$input.trigger( 'blur' );
461 };
462
463 /**
464 * @inheritdoc
465 */
466 FilterTagMultiselectWidget.prototype.onMouseDown = function ( e ) {
467 if ( !this.collapsed && !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
468 this.menu.toggle();
469
470 return false;
471 }
472 };
473
474 /**
475 * @inheritdoc
476 */
477 FilterTagMultiselectWidget.prototype.onChangeTags = function () {
478 // If initialized, call parent method.
479 if ( this.controller.isInitialized() ) {
480 FilterTagMultiselectWidget.parent.prototype.onChangeTags.call( this );
481 }
482
483 this.emptyFilterMessage.toggle( this.isEmpty() );
484 };
485
486 /**
487 * Respond to model initialize event
488 */
489 FilterTagMultiselectWidget.prototype.onModelInitialize = function () {
490 this.setSavedQueryVisibility();
491 };
492
493 /**
494 * Respond to model update event
495 */
496 FilterTagMultiselectWidget.prototype.onModelUpdate = function () {
497 this.updateElementsForView();
498 };
499
500 /**
501 * Update the elements in the widget to the current view
502 */
503 FilterTagMultiselectWidget.prototype.updateElementsForView = function () {
504 var view = this.model.getCurrentView(),
505 inputValue = this.input.getValue().trim(),
506 inputView = this.model.getViewByTrigger( inputValue.substr( 0, 1 ) );
507
508 if ( inputView !== 'default' ) {
509 // We have a prefix already, remove it
510 inputValue = inputValue.substr( 1 );
511 }
512
513 if ( inputView !== view ) {
514 // Add the correct prefix
515 inputValue = this.model.getViewTrigger( view ) + inputValue;
516 }
517
518 // Update input
519 this.input.setValue( inputValue );
520
521 if ( this.currentView !== view ) {
522 this.scrollToTop( this.$element );
523 this.currentView = view;
524 }
525 };
526
527 /**
528 * Set the visibility of the saved query button
529 */
530 FilterTagMultiselectWidget.prototype.setSavedQueryVisibility = function () {
531 if ( mw.user.isAnon() ) {
532 return;
533 }
534
535 this.matchingQuery = this.controller.findQueryMatchingCurrentState();
536
537 this.savedQueryTitle.setLabel(
538 this.matchingQuery ? this.matchingQuery.getLabel() : ''
539 );
540 this.savedQueryTitle.toggle( !!this.matchingQuery );
541 this.saveQueryButton.setDisabled( !!this.matchingQuery );
542 this.saveQueryButton.setTitle( !this.matchingQuery ?
543 mw.msg( 'rcfilters-savedqueries-add-new-title' ) :
544 mw.msg( 'rcfilters-savedqueries-already-saved' ) );
545
546 if ( this.matchingQuery ) {
547 this.emphasize();
548 }
549 };
550
551 /**
552 * Respond to model itemUpdate event
553 * fixme: when a new state is applied to the model this function is called 60+ times in a row
554 *
555 * @param {mw.rcfilters.dm.FilterItem} item Filter item model
556 */
557 FilterTagMultiselectWidget.prototype.onModelItemUpdate = function ( item ) {
558 if ( !item.getGroupModel().isHidden() ) {
559 if (
560 item.isSelected() ||
561 (
562 this.model.isHighlightEnabled() &&
563 item.getHighlightColor()
564 )
565 ) {
566 this.addTag( item.getName(), item.getLabel() );
567 } else {
568 // Only attempt to remove the tag if we can find an item for it (T198140, T198231)
569 if ( this.findItemFromData( item.getName() ) !== null ) {
570 this.removeTagByData( item.getName() );
571 }
572 }
573 }
574
575 this.setSavedQueryVisibility();
576
577 // Re-evaluate reset state
578 this.reevaluateResetRestoreState();
579 };
580
581 /**
582 * @inheritdoc
583 */
584 FilterTagMultiselectWidget.prototype.isAllowedData = function ( data ) {
585 return (
586 this.model.getItemByName( data ) &&
587 !this.isDuplicateData( data )
588 );
589 };
590
591 /**
592 * @inheritdoc
593 */
594 FilterTagMultiselectWidget.prototype.onMenuChoose = function ( item ) {
595 this.controller.toggleFilterSelect( item.model.getName() );
596
597 // Select the tag if it exists, or reset selection otherwise
598 this.selectTag( this.findItemFromData( item.model.getName() ) );
599
600 if ( !this.isMobile ) {
601 this.focus();
602 }
603
604 };
605
606 /**
607 * Respond to highlightChange event
608 *
609 * @param {boolean} isHighlightEnabled Highlight is enabled
610 */
611 FilterTagMultiselectWidget.prototype.onModelHighlightChange = function ( isHighlightEnabled ) {
612 var highlightedItems = this.model.getHighlightedItems();
613
614 if ( isHighlightEnabled ) {
615 // Add capsule widgets
616 highlightedItems.forEach( function ( filterItem ) {
617 this.addTag( filterItem.getName(), filterItem.getLabel() );
618 }.bind( this ) );
619 } else {
620 // Remove capsule widgets if they're not selected
621 highlightedItems.forEach( function ( filterItem ) {
622 if ( !filterItem.isSelected() ) {
623 // Only attempt to remove the tag if we can find an item for it (T198140, T198231)
624 if ( this.findItemFromData( filterItem.getName() ) !== null ) {
625 this.removeTagByData( filterItem.getName() );
626 }
627 }
628 }.bind( this ) );
629 }
630
631 this.setSavedQueryVisibility();
632 };
633
634 /**
635 * @inheritdoc
636 */
637 FilterTagMultiselectWidget.prototype.onTagSelect = function ( tagItem ) {
638 var menuOption = this.menu.getItemFromModel( tagItem.getModel() );
639
640 this.menu.setUserSelecting( true );
641 // Parent method
642 FilterTagMultiselectWidget.parent.prototype.onTagSelect.call( this, tagItem );
643
644 // Switch view
645 this.controller.resetSearchForView( tagItem.getView() );
646
647 this.selectTag( tagItem );
648 this.scrollToTop( menuOption.$element );
649
650 this.menu.setUserSelecting( false );
651 };
652
653 /**
654 * Select a tag by reference. This is what OO.ui.SelectWidget is doing.
655 * If no items are given, reset selection from all.
656 *
657 * @param {mw.rcfilters.ui.FilterTagItemWidget} [item] Tag to select,
658 * omit to deselect all
659 */
660 FilterTagMultiselectWidget.prototype.selectTag = function ( item ) {
661 var i, len, selected;
662
663 for ( i = 0, len = this.items.length; i < len; i++ ) {
664 selected = this.items[ i ] === item;
665 if ( this.items[ i ].isSelected() !== selected ) {
666 this.items[ i ].toggleSelected( selected );
667 }
668 }
669 };
670 /**
671 * @inheritdoc
672 */
673 FilterTagMultiselectWidget.prototype.onTagRemove = function ( tagItem ) {
674 // Parent method
675 FilterTagMultiselectWidget.parent.prototype.onTagRemove.call( this, tagItem );
676
677 this.controller.clearFilter( tagItem.getName() );
678
679 tagItem.destroy();
680 };
681
682 /**
683 * Respond to click event on the reset button
684 */
685 FilterTagMultiselectWidget.prototype.onResetButtonClick = function () {
686 if ( this.model.areVisibleFiltersEmpty() ) {
687 // Reset to default filters
688 this.controller.resetToDefaults();
689 } else {
690 // Reset to have no filters
691 this.controller.emptyFilters();
692 }
693 };
694
695 /**
696 * Respond to hide/show button click
697 */
698 FilterTagMultiselectWidget.prototype.onHideShowButtonClick = function () {
699 this.toggleCollapsed();
700 };
701
702 /**
703 * Toggle the collapsed state of the filters widget
704 *
705 * @param {boolean} isCollapsed Widget is collapsed
706 */
707 FilterTagMultiselectWidget.prototype.toggleCollapsed = function ( isCollapsed ) {
708 isCollapsed = isCollapsed === undefined ? !this.collapsed : !!isCollapsed;
709
710 this.collapsed = isCollapsed;
711
712 if ( isCollapsed ) {
713 // If we are collapsing, close the menu, in case it was open
714 // We should make sure the menu closes before the rest of the elements
715 // are hidden, otherwise there is an unknown error in jQuery as ooui
716 // sets and unsets properties on the input (which is hidden at that point)
717 this.menu.toggle( false );
718 }
719 this.input.setDisabled( isCollapsed );
720 this.hideShowButton.setLabel( mw.msg(
721 isCollapsed ? 'rcfilters-activefilters-show' : 'rcfilters-activefilters-hide'
722 ) );
723 this.hideShowButton.setTitle( mw.msg(
724 isCollapsed ? 'rcfilters-activefilters-show-tooltip' : 'rcfilters-activefilters-hide-tooltip'
725 ) );
726
727 // Toggle the wrapper class, so we have min height values correctly throughout
728 this.$wrapper.toggleClass( 'mw-rcfilters-collapsed', isCollapsed );
729
730 // Save the state
731 this.controller.updateCollapsedState( isCollapsed );
732 };
733
734 /**
735 * Reevaluate the restore state for the widget between setting to defaults and clearing all filters
736 */
737 FilterTagMultiselectWidget.prototype.reevaluateResetRestoreState = function () {
738 var defaultsAreEmpty = this.controller.areDefaultsEmpty(),
739 currFiltersAreEmpty = this.model.areVisibleFiltersEmpty(),
740 hideResetButton = currFiltersAreEmpty && defaultsAreEmpty;
741
742 this.resetButton.setIcon(
743 currFiltersAreEmpty ? 'history' : 'trash'
744 );
745
746 this.resetButton.setLabel(
747 currFiltersAreEmpty ? mw.msg( 'rcfilters-restore-default-filters' ) : ''
748 );
749 this.resetButton.setTitle(
750 currFiltersAreEmpty ? null : mw.msg( 'rcfilters-clear-all-filters' )
751 );
752
753 this.resetButton.toggle( !hideResetButton );
754 this.emptyFilterMessage.toggle( currFiltersAreEmpty );
755 };
756
757 /**
758 * @inheritdoc
759 */
760 FilterTagMultiselectWidget.prototype.createMenuWidget = function ( menuConfig ) {
761 return new MenuSelectWidget(
762 this.controller,
763 this.model,
764 menuConfig
765 );
766 };
767
768 /**
769 * @inheritdoc
770 */
771 FilterTagMultiselectWidget.prototype.createTagItemWidget = function ( data ) {
772 var filterItem = this.model.getItemByName( data );
773
774 if ( filterItem ) {
775 return new FilterTagItemWidget(
776 this.controller,
777 this.model,
778 this.model.getInvertModel(),
779 filterItem,
780 {
781 $overlay: this.$overlay
782 }
783 );
784 }
785 };
786
787 FilterTagMultiselectWidget.prototype.emphasize = function () {
788 if (
789 // eslint-disable-next-line no-jquery/no-class-state
790 !this.$handle.hasClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' )
791 ) {
792 this.$handle
793 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-emphasize' )
794 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' );
795
796 setTimeout( function () {
797 this.$handle
798 .removeClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-emphasize' );
799
800 setTimeout( function () {
801 this.$handle
802 .removeClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' );
803 }.bind( this ), 1000 );
804 }.bind( this ), 500 );
805
806 }
807 };
808 /**
809 * Scroll the element to top within its container
810 *
811 * @private
812 * @param {jQuery} $element Element to position
813 * @param {number} [marginFromTop=0] When scrolling the entire widget to the top, leave this
814 * much space (in pixels) above the widget.
815 * @param {Object} [threshold] Minimum distance from the top of the element to scroll at all
816 * @param {number} [threshold.min] Minimum distance above the element
817 * @param {number} [threshold.max] Minimum distance below the element
818 */
819 FilterTagMultiselectWidget.prototype.scrollToTop = function ( $element, marginFromTop, threshold ) {
820 var container = OO.ui.Element.static.getClosestScrollableContainer( $element[ 0 ], 'y' ),
821 pos = OO.ui.Element.static.getRelativePosition( $element, $( container ) ),
822 containerScrollTop = $( container ).scrollTop(),
823 effectiveScrollTop = $( container ).is( 'body, html' ) ? 0 : containerScrollTop,
824 newScrollTop = effectiveScrollTop + pos.top - ( marginFromTop || 0 );
825
826 // Scroll to item
827 if (
828 threshold === undefined ||
829 (
830 (
831 threshold.min === undefined ||
832 newScrollTop - containerScrollTop >= threshold.min
833 ) &&
834 (
835 threshold.max === undefined ||
836 newScrollTop - containerScrollTop <= threshold.max
837 )
838 )
839 ) {
840 $( container ).animate( {
841 scrollTop: newScrollTop
842 } );
843 }
844 };
845
846 module.exports = FilterTagMultiselectWidget;