wikimedia/mediawiki-core

View on GitHub
resources/src/mediawiki.rcfilters/ui/FilterTagMultiselectWidget.js

Summary

Maintainability
F
3 days
Test Coverage
var ViewSwitchWidget = require( './ViewSwitchWidget.js' ),
    SaveFiltersPopupButtonWidget = require( './SaveFiltersPopupButtonWidget.js' ),
    MenuSelectWidget = require( './MenuSelectWidget.js' ),
    FilterTagItemWidget = require( './FilterTagItemWidget.js' ),
    FilterTagMultiselectWidget;

/**
 * List displaying all filter groups.
 *
 * @class mw.rcfilters.ui.FilterTagMultiselectWidget
 * @ignore
 * @extends OO.ui.MenuTagMultiselectWidget
 * @mixes OO.ui.mixin.PendingElement
 *
 * @param {mw.rcfilters.Controller} controller Controller
 * @param {mw.rcfilters.dm.FiltersViewModel} model View model
 * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
 * @param {Object} config Configuration object
 * @param {jQuery} [config.$overlay] A jQuery object serving as overlay for popups
 * @param {jQuery} [config.$wrapper] A jQuery object for the wrapper of the general
 *  system. If not given, falls back to this widget's $element
 * @param {boolean} [config.collapsed] Filter area is collapsed
 */
FilterTagMultiselectWidget = function MwRcfiltersUiFilterTagMultiselectWidget( controller, model, savedQueriesModel, config ) {
    var $rcFiltersRow,
        title = new OO.ui.LabelWidget( {
            label: mw.msg( 'rcfilters-activefilters' ),
            classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-title' ]
        } ),
        $contentWrapper = $( '<div>' )
            .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper' );

    config = config || {};

    this.controller = controller;
    this.model = model;
    this.queriesModel = savedQueriesModel;
    this.$overlay = config.$overlay || this.$element;
    this.$wrapper = config.$wrapper || this.$element;
    this.matchingQuery = null;
    this.currentView = this.model.getCurrentView();
    this.collapsed = false;
    this.isMobile = config.isMobile;

    // Has to be before the parent constructor, because the parent constructor may call setValue()
    // which causes the onChangeTags handler to run (T245073)
    this.emptyFilterMessage = new OO.ui.LabelWidget( {
        label: mw.msg( 'rcfilters-empty-filter' ),
        classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-emptyFilters' ]
    } );

    // Parent
    FilterTagMultiselectWidget.super.call( this, $.extend( true, {
        label: mw.msg( 'rcfilters-filterlist-title' ),
        placeholder: mw.msg( 'rcfilters-empty-filter' ),
        inputPosition: 'outline',
        allowArbitrary: false,
        allowDisplayInvalidTags: false,
        allowReordering: false,
        $overlay: this.$overlay,
        menu: {
            // Our filtering is done through the model
            filterFromInput: false,
            hideWhenOutOfView: false,
            hideOnChoose: false,
            // Only set width and footers for desktop
            isMobile: this.isMobile,
            width: 650,
            footers: [
                {
                    name: 'viewSelect',
                    sticky: false,
                    // View select menu, appears on default view only
                    $element: $( '<div>' )
                        .append( new ViewSwitchWidget( this.controller, this.model ).$element ),
                    views: [ 'default' ]
                }
            ]
        },
        /**
         * In the presence of an onscreen keyboard (i.e. isMobile) the filter input should act as a button
         * rather than a text input. Mobile screens are too small to accommodate both an
         * onscreen keyboard and a popup-menu, so readyOnly is set to disable the keyboard.
         * A different icon and shorter message is used for mobile as well. (See T224655 for details).
         * @ignore
         */
        input: {
            icon: this.isMobile ? 'funnel' : 'menu',
            placeholder: this.isMobile ? mw.msg( 'rcfilters-search-placeholder-mobile' ) : mw.msg( 'rcfilters-search-placeholder' ),
            readOnly: !!this.isMobile,
            classes: [ 'oo-ui-tagMultiselectWidget-input' ]
        }
    }, config ) );

    this.input.$input.attr( 'aria-label', mw.msg( 'rcfilters-search-placeholder' ) );

    this.savedQueryTitle = new OO.ui.LabelWidget( {
        label: '',
        classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-savedQueryTitle' ]
    } );

    this.resetButton = new OO.ui.ButtonWidget( {
        framed: false,
        classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-resetButton' ]
    } );

    this.hideShowButton = new OO.ui.ButtonWidget( {
        framed: false,
        flags: [ 'progressive' ],
        classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-hideshowButton' ]
    } );
    this.toggleCollapsed( !!config.collapsed );

    if ( !mw.user.isAnon() ) {
        this.saveQueryButton = new SaveFiltersPopupButtonWidget(
            this.controller,
            this.queriesModel,
            {
                $overlay: this.$overlay
            }
        );

        this.saveQueryButton.$element.on( 'mousedown', function ( e ) {
            e.stopPropagation();
        } );

        this.saveQueryButton.connect( this, {
            click: 'onSaveQueryButtonClick',
            saveCurrent: 'setSavedQueryVisibility'
        } );
        this.queriesModel.connect( this, {
            itemUpdate: 'onSavedQueriesItemUpdate',
            initialize: 'onSavedQueriesInitialize',
            default: 'reevaluateResetRestoreState'
        } );
    }

    this.$content.append( this.emptyFilterMessage.$element );

    // Events
    this.resetButton.connect( this, { click: 'onResetButtonClick' } );
    this.hideShowButton.connect( this, { click: 'onHideShowButtonClick' } );
    // Stop propagation for mousedown, so that the widget doesn't
    // trigger the focus on the input and scrolls up when we click the reset button
    this.resetButton.$element.on( 'mousedown', function ( e ) {
        e.stopPropagation();
    } );
    this.hideShowButton.$element.on( 'mousedown', function ( e ) {
        e.stopPropagation();
    } );
    this.model.connect( this, {
        initialize: 'onModelInitialize',
        update: 'onModelUpdate',
        searchChange: this.isMobile ? function () {} : 'onModelSearchChange',
        itemUpdate: 'onModelItemUpdate',
        highlightChange: 'onModelHighlightChange'
    } );

    if ( !this.isMobile ) {
        this.input.connect( this, { change: 'onInputChange' } );
    }

    // The filter list and button should appear side by side regardless of how
    // wide the button is; the button also changes its width depending
    // on language and its state, so the safest way to present both side
    // by side is with a table layout
    $rcFiltersRow = $( '<div>' )
        .addClass( 'mw-rcfilters-ui-row' )
        .append(
            this.$content
                .addClass( 'mw-rcfilters-ui-cell' )
                .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-filters' )
        );

    if ( !mw.user.isAnon() ) {
        $rcFiltersRow.append(
            $( '<div>' )
                .addClass( 'mw-rcfilters-ui-cell' )
                .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-save' )
                .append( this.saveQueryButton.$element )
        );
    }

    // Add a selector at the right of the input
    this.viewsSelectWidget = this.createViewsSelectWidget();

    // change the layout of the viewsSelectWidget
    this.restructureViewsSelectWidget();

    // Event
    this.viewsSelectWidget.aggregate( { click: 'buttonClick' } );
    this.viewsSelectWidget.connect( this, { buttonClick: 'onViewsSelectWidgetButtonClick' } );

    $rcFiltersRow.append(
        $( '<div>' )
            .addClass( 'mw-rcfilters-ui-cell' )
            .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-reset' )
            .append( this.resetButton.$element )
    );

    // Build the content
    $contentWrapper.append(
        $( '<div>' )
            .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top' )
            .append(
                $( '<div>' )
                    .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top-title' )
                    .append( title.$element ),
                $( '<div>' )
                    .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top-queryName' )
                    .append( this.savedQueryTitle.$element ),
                $( '<div>' )
                    .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top-hideshow' )
                    .append(
                        this.hideShowButton.$element
                    )
            ),
        $( '<div>' )
            .addClass( 'mw-rcfilters-ui-table' )
            .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-filters' )
            .append( $rcFiltersRow )
    );

    // Initialize
    this.$handle.append( $contentWrapper );
    this.emptyFilterMessage.toggle( this.isEmpty() );
    this.savedQueryTitle.toggle( false );

    this.$element
        .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget' );

    if ( this.isMobile ) {
        this.$element
            .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-mobile' );
    }

    this.reevaluateResetRestoreState();
};

/* Initialization */

OO.inheritClass( FilterTagMultiselectWidget, OO.ui.MenuTagMultiselectWidget );

/* Methods */

/**
 * Create a OOUI ButtonGroupWidget. The buttons are framed and have additional CSS
 * classes applied on mobile.
 *
 * @return {OO.ui.ButtonGroupWidget}
 */
FilterTagMultiselectWidget.prototype.createViewsSelectWidget = function () {
    var viewsSelectWidget = new OO.ui.ButtonGroupWidget( {
        classes: this.isMobile ?
            [
                'mw-rcfilters-ui-table',
                'mw-rcfilters-ui-filterTagMultiselectWidget-mobile-view'
            ] :
            [
                'mw-rcfilters-ui-filterTagMultiselectWidget-views-select-widget'
            ],
        items: [
            new OO.ui.ButtonWidget( {
                framed: !!this.isMobile,
                data: 'namespaces',
                icon: 'article',
                label: mw.msg( 'namespaces' ),
                title: mw.msg( 'rcfilters-view-namespaces-tooltip' ),
                classes: this.isMobile ? [ 'mw-rcfilters-ui-cell' ] : []
            } ),
            new OO.ui.ButtonWidget( {
                framed: !!this.isMobile,
                data: 'tags',
                icon: 'tag',
                label: mw.msg( 'tags-title' ),
                title: mw.msg( 'rcfilters-view-tags-tooltip' ),
                classes: this.isMobile ? [ 'mw-rcfilters-ui-cell' ] : []
            } )
        ]
    } );

    viewsSelectWidget.items.forEach( function ( item ) {
        item.$button.attr( 'aria-label', item.title );
    } );

    return viewsSelectWidget;
};

/**
 * Rearrange the DOM structure of the viewsSelectWiget so that on the namespace & tags buttons
 * are at the right of the input on desktop, and below the input on mobile.
 */
FilterTagMultiselectWidget.prototype.restructureViewsSelectWidget = function () {
    if ( this.isMobile ) {
        // On mobile, append the search input and the extra buttons below the search input.
        this.$element.append(
            $( '<div>' )
                .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-input' )
                .append( this.input.$element )
                .append( this.viewsSelectWidget.$element )
        );
    } else {
        // On desktop, rearrange the UI so the select widget is at the right of the input
        this.$element.append(
            $( '<div>' )
                .addClass( 'mw-rcfilters-ui-table' )
                .append(
                    $( '<div>' )
                        .addClass( 'mw-rcfilters-ui-row' )
                        .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views' )
                        .append(
                            $( '<div>' )
                                .addClass( 'mw-rcfilters-ui-cell' )
                                .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-input' )
                                .append( this.input.$element ),
                            $( '<div>' )
                                .addClass( 'mw-rcfilters-ui-cell' )
                                .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-select' )
                                .append( this.viewsSelectWidget.$element )
                        )
                )
        );
    }
};

/**
 * Respond to button click event
 *
 * @param {OO.ui.ButtonWidget} buttonWidget Clicked widget
 */
FilterTagMultiselectWidget.prototype.onViewsSelectWidgetButtonClick = function ( buttonWidget ) {
    this.controller.switchView( buttonWidget.getData() );
    this.focus();
};

/**
 * Respond to model search change event
 *
 * @param {string} value Search value
 */
FilterTagMultiselectWidget.prototype.onModelSearchChange = function ( value ) {
    this.input.setValue( value );
};

/**
 * Respond to input change event
 *
 * @param {string} value Value of the input
 */
FilterTagMultiselectWidget.prototype.onInputChange = function ( value ) {
    this.controller.setSearch( value );
};

/**
 * Respond to query button click
 */
FilterTagMultiselectWidget.prototype.onSaveQueryButtonClick = function () {
    this.getMenu().toggle( false );
};

/**
 * Respond to save query model initialization
 */
FilterTagMultiselectWidget.prototype.onSavedQueriesInitialize = function () {
    this.setSavedQueryVisibility();
};

/**
 * Respond to save query item change. Mainly this is done to update the label in case
 * a query item has been edited
 *
 * @param {mw.rcfilters.dm.SavedQueryItemModel} item Saved query item
 */
FilterTagMultiselectWidget.prototype.onSavedQueriesItemUpdate = function ( item ) {
    if ( this.matchingQuery === item ) {
        // This means we just edited the item that is currently matched
        this.savedQueryTitle.setLabel( item.getLabel() );
    }
};

/**
 * Respond to menu toggle
 *
 * @param {boolean} isVisible Menu is visible
 */
FilterTagMultiselectWidget.prototype.onMenuToggle = function ( isVisible ) {

    var scrollToElement = this.isMobile ? this.input.$input : this.$element;

    // Parent
    FilterTagMultiselectWidget.super.prototype.onMenuToggle.call( this );

    if ( isVisible ) {
        if ( !this.isMobile ) {
            this.focus();
        }

        /**
         * Fires when the RCFilters tag multi selector menu is toggled.
         *
         * @event ~'RcFilters.popup.open'
         * @memberof Hooks
         */
        mw.hook( 'RcFilters.popup.open' ).fire();

        if ( !this.getMenu().findSelectedItem() ) {
            // If there are no selected items, scroll menu to top
            // This has to be in a setTimeout so the menu has time
            // to be positioned and fixed
            setTimeout(
                function () {
                    this.getMenu().scrollToTop();
                }.bind( this )
            );
        }

        // Only scroll to top of the viewport if:
        // - The widget is more than 20px from the top
        // - The widget is not above the top of the viewport (do not scroll downwards)
        //   (This isn't represented because >20 is, anyways and always, bigger than 0)
        this.scrollToTop( scrollToElement, 0, { min: 20, max: Infinity } );

    } else {
        // Clear selection
        this.selectTag( null );

        // Clear the search
        this.controller.setSearch( '' );

        this.blur();
    }

    if ( this.isMobile ) {
        this.input.setIcon( isVisible ? 'close' : 'funnel' );
    } else {
        this.input.setIcon( isVisible ? 'search' : 'menu' );
    }
};

/**
 * @inheritdoc
 */
FilterTagMultiselectWidget.prototype.onInputFocus = function () {

    // treat the input as a menu toggle rather than a text field on mobile
    if ( this.isMobile ) {
        this.input.$input.trigger( 'blur' );
        this.getMenu().toggle();
    } else {
        // Parent
        FilterTagMultiselectWidget.super.prototype.onInputFocus.call( this );
    }
};

/**
 * @inheritdoc
 */
FilterTagMultiselectWidget.prototype.doInputEscape = function () {
    // Parent
    FilterTagMultiselectWidget.super.prototype.doInputEscape.call( this );

    // Blur the input
    this.input.$input.trigger( 'blur' );
};

/**
 * @inheritdoc
 */
FilterTagMultiselectWidget.prototype.onMouseDown = function ( e ) {
    if ( !this.collapsed && !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
        this.menu.toggle();

        return false;
    }
};

/**
 * @inheritdoc
 */
FilterTagMultiselectWidget.prototype.onChangeTags = function () {
    // If initialized, call parent method.
    if ( this.controller.isInitialized() ) {
        FilterTagMultiselectWidget.super.prototype.onChangeTags.call( this );
    }

    this.emptyFilterMessage.toggle( this.isEmpty() );
};

/**
 * Respond to model initialize event
 */
FilterTagMultiselectWidget.prototype.onModelInitialize = function () {
    this.setSavedQueryVisibility();
};

/**
 * Respond to model update event
 */
FilterTagMultiselectWidget.prototype.onModelUpdate = function () {
    this.updateElementsForView();
};

/**
 * Update the elements in the widget to the current view
 */
FilterTagMultiselectWidget.prototype.updateElementsForView = function () {
    var view = this.model.getCurrentView(),
        inputValue = this.input.getValue().trim(),
        inputView = this.model.getViewByTrigger( inputValue.slice( 0, 1 ) );

    if ( inputView !== 'default' ) {
        // We have a prefix already, remove it
        inputValue = inputValue.slice( 1 );
    }

    if ( inputView !== view ) {
        // Add the correct prefix
        inputValue = this.model.getViewTrigger( view ) + inputValue;
    }

    // Update input
    this.input.setValue( inputValue );

    if ( this.currentView !== view ) {
        this.scrollToTop( this.$element );
        this.currentView = view;
    }
};

/**
 * Set the visibility of the saved query button
 */
FilterTagMultiselectWidget.prototype.setSavedQueryVisibility = function () {
    if ( mw.user.isAnon() ) {
        return;
    }

    this.matchingQuery = this.controller.findQueryMatchingCurrentState();

    this.savedQueryTitle.setLabel(
        this.matchingQuery ? this.matchingQuery.getLabel() : ''
    );
    this.savedQueryTitle.toggle( !!this.matchingQuery );
    this.saveQueryButton.setDisabled( !!this.matchingQuery );
    this.saveQueryButton.setTitle( !this.matchingQuery ?
        mw.msg( 'rcfilters-savedqueries-add-new-title' ) :
        mw.msg( 'rcfilters-savedqueries-already-saved' ) );

    if ( this.matchingQuery ) {
        this.emphasize();
    }
};

/**
 * Respond to model itemUpdate event
 * fixme: when a new state is applied to the model this function is called 60+ times in a row
 *
 * @param {mw.rcfilters.dm.FilterItem} item Filter item model
 */
FilterTagMultiselectWidget.prototype.onModelItemUpdate = function ( item ) {
    if ( !item.getGroupModel().isHidden() ) {
        if (
            item.isSelected() ||
            (
                this.model.isHighlightEnabled() &&
                item.getHighlightColor()
            )
        ) {
            this.addTag( item.getName(), item.getLabel() );
        } else {
            // Only attempt to remove the tag if we can find an item for it (T198140, T198231)
            if ( this.findItemFromData( item.getName() ) !== null ) {
                this.removeTagByData( item.getName() );
            }
        }
    }

    this.setSavedQueryVisibility();

    // Re-evaluate reset state
    this.reevaluateResetRestoreState();
};

/**
 * @inheritdoc
 */
FilterTagMultiselectWidget.prototype.isAllowedData = function ( data ) {
    return (
        this.model.getItemByName( data ) &&
        !this.isDuplicateData( data )
    );
};

/**
 * @inheritdoc
 */
FilterTagMultiselectWidget.prototype.onMenuChoose = function ( item ) {
    this.controller.toggleFilterSelect( item.model.getName() );

    // Select the tag if it exists, or reset selection otherwise
    this.selectTag( this.findItemFromData( item.model.getName() ) );

    if ( !this.isMobile ) {
        this.focus();
    }

};

/**
 * Respond to highlightChange event
 *
 * @param {boolean} isHighlightEnabled Highlight is enabled
 */
FilterTagMultiselectWidget.prototype.onModelHighlightChange = function ( isHighlightEnabled ) {
    var highlightedItems = this.model.getHighlightedItems();

    if ( isHighlightEnabled ) {
        // Add capsule widgets
        highlightedItems.forEach( function ( filterItem ) {
            this.addTag( filterItem.getName(), filterItem.getLabel() );
        }.bind( this ) );
    } else {
        // Remove capsule widgets if they're not selected
        highlightedItems.forEach( function ( filterItem ) {
            if ( !filterItem.isSelected() ) {
                // Only attempt to remove the tag if we can find an item for it (T198140, T198231)
                if ( this.findItemFromData( filterItem.getName() ) !== null ) {
                    this.removeTagByData( filterItem.getName() );
                }
            }
        }.bind( this ) );
    }

    this.setSavedQueryVisibility();
};

/**
 * @inheritdoc
 */
FilterTagMultiselectWidget.prototype.onTagSelect = function ( tagItem ) {
    var menuOption = this.menu.getItemFromModel( tagItem.getModel() );

    this.menu.setUserSelecting( true );
    // Parent method
    FilterTagMultiselectWidget.super.prototype.onTagSelect.call( this, tagItem );

    // Switch view
    this.controller.resetSearchForView( tagItem.getView() );

    this.selectTag( tagItem );
    this.scrollToTop( menuOption.$element );

    this.menu.setUserSelecting( false );
};

/**
 * Select a tag by reference. This is what OO.ui.SelectWidget is doing.
 * If no items are given, reset selection from all.
 *
 * @param {mw.rcfilters.ui.FilterTagItemWidget} [item] Tag to select,
 *  omit to deselect all
 */
FilterTagMultiselectWidget.prototype.selectTag = function ( item ) {
    var i, len, selected;

    for ( i = 0, len = this.items.length; i < len; i++ ) {
        selected = this.items[ i ] === item;
        if ( this.items[ i ].isSelected() !== selected ) {
            this.items[ i ].toggleSelected( selected );
        }
    }
};
/**
 * @inheritdoc
 */
FilterTagMultiselectWidget.prototype.onTagRemove = function ( tagItem ) {
    // Parent method
    FilterTagMultiselectWidget.super.prototype.onTagRemove.call( this, tagItem );

    this.controller.clearFilter( tagItem.getName() );

    tagItem.destroy();
};

/**
 * Respond to click event on the reset button
 */
FilterTagMultiselectWidget.prototype.onResetButtonClick = function () {
    if ( this.model.areVisibleFiltersEmpty() ) {
        // Reset to default filters
        this.controller.resetToDefaults();
    } else {
        // Reset to have no filters
        this.controller.emptyFilters();
    }
};

/**
 * Respond to hide/show button click
 */
FilterTagMultiselectWidget.prototype.onHideShowButtonClick = function () {
    this.toggleCollapsed();
};

/**
 * Toggle the collapsed state of the filters widget
 *
 * @param {boolean} isCollapsed Widget is collapsed
 */
FilterTagMultiselectWidget.prototype.toggleCollapsed = function ( isCollapsed ) {
    isCollapsed = isCollapsed === undefined ? !this.collapsed : !!isCollapsed;

    this.collapsed = isCollapsed;

    if ( isCollapsed ) {
        // If we are collapsing, close the menu, in case it was open
        // We should make sure the menu closes before the rest of the elements
        // are hidden, otherwise there is an unknown error in jQuery as ooui
        // sets and unsets properties on the input (which is hidden at that point)
        this.menu.toggle( false );
    }
    this.input.setDisabled( isCollapsed );
    this.hideShowButton.setLabel( mw.msg(
        isCollapsed ? 'rcfilters-activefilters-show' : 'rcfilters-activefilters-hide'
    ) );
    this.hideShowButton.setTitle( mw.msg(
        isCollapsed ? 'rcfilters-activefilters-show-tooltip' : 'rcfilters-activefilters-hide-tooltip'
    ) );

    // Toggle the wrapper class, so we have min height values correctly throughout
    this.$wrapper.toggleClass( 'mw-rcfilters-collapsed', isCollapsed );

    // Save the state
    this.controller.updateCollapsedState( isCollapsed );
};

/**
 * Reevaluate the restore state for the widget between setting to defaults and clearing all filters
 */
FilterTagMultiselectWidget.prototype.reevaluateResetRestoreState = function () {
    var defaultsAreEmpty = this.controller.areDefaultsEmpty(),
        currFiltersAreEmpty = this.model.areVisibleFiltersEmpty(),
        hideResetButton = currFiltersAreEmpty && defaultsAreEmpty;

    this.resetButton.setIcon(
        currFiltersAreEmpty ? 'history' : 'trash'
    );

    this.resetButton.setLabel(
        currFiltersAreEmpty ? mw.msg( 'rcfilters-restore-default-filters' ) : ''
    );
    this.resetButton.setTitle(
        currFiltersAreEmpty ? null : mw.msg( 'rcfilters-clear-all-filters' )
    );

    this.resetButton.toggle( !hideResetButton );
    this.emptyFilterMessage.toggle( currFiltersAreEmpty );
};

/**
 * @inheritdoc
 */
FilterTagMultiselectWidget.prototype.createMenuWidget = function ( menuConfig ) {
    return new MenuSelectWidget(
        this.controller,
        this.model,
        menuConfig
    );
};

/**
 * @inheritdoc
 */
FilterTagMultiselectWidget.prototype.createTagItemWidget = function ( data ) {
    var filterItem = this.model.getItemByName( data );

    if ( filterItem ) {
        return new FilterTagItemWidget(
            this.controller,
            this.model,
            this.model.getInvertModel( filterItem.getGroupModel().getView() ),
            filterItem,
            {
                $overlay: this.$overlay
            }
        );
    }
};

FilterTagMultiselectWidget.prototype.emphasize = function () {
    if (
        // eslint-disable-next-line no-jquery/no-class-state
        !this.$handle.hasClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' )
    ) {
        this.$handle
            .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-emphasize' )
            .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' );

        setTimeout( function () {
            this.$handle
                .removeClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-emphasize' );

            setTimeout( function () {
                this.$handle
                    .removeClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' );
            }.bind( this ), 1000 );
        }.bind( this ), 500 );

    }
};
/**
 * Scroll the element to top within its container
 *
 * @private
 * @param {jQuery} $element Element to position
 * @param {number} [marginFromTop=0] When scrolling the entire widget to the top, leave this
 *  much space (in pixels) above the widget.
 * @param {Object} [threshold] Minimum distance from the top of the element to scroll at all
 * @param {number} [threshold.min] Minimum distance above the element
 * @param {number} [threshold.max] Minimum distance below the element
 */
FilterTagMultiselectWidget.prototype.scrollToTop = function ( $element, marginFromTop, threshold ) {
    var container = OO.ui.Element.static.getClosestScrollableContainer( $element[ 0 ], 'y' ),
        pos = OO.ui.Element.static.getRelativePosition( $element, $( container ) ),
        containerScrollTop = $( container ).scrollTop(),
        effectiveScrollTop = $( container ).is( 'body, html' ) ? 0 : containerScrollTop,
        newScrollTop = effectiveScrollTop + pos.top - ( marginFromTop || 0 );

    // Scroll to item
    if (
        threshold === undefined ||
        (
            (
                threshold.min === undefined ||
                newScrollTop - containerScrollTop >= threshold.min
            ) &&
            (
                threshold.max === undefined ||
                newScrollTop - containerScrollTop <= threshold.max
            )
        )
    ) {
        $( container ).animate( {
            scrollTop: newScrollTop
        } );
    }
};

module.exports = FilterTagMultiselectWidget;