( function ( mw ) { /** * List displaying all filter groups * * @class * @extends OO.ui.MenuTagMultiselectWidget * @mixins OO.ui.mixin.PendingElement * * @constructor * @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 * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups */ mw.rcfilters.ui.FilterTagMultiselectWidget = function MwRcfiltersUiFilterTagMultiselectWidget( controller, model, savedQueriesModel, config ) { var rcFiltersRow, areSavedQueriesEnabled = mw.config.get( 'wgStructuredChangeFiltersEnableSaving' ), title = new OO.ui.LabelWidget( { label: mw.msg( 'rcfilters-activefilters' ), classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-title' ] } ), $contentWrapper = $( '
' ) .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper' ); config = config || {}; this.controller = controller; this.model = model; this.queriesModel = savedQueriesModel; this.$overlay = config.$overlay || this.$element; this.matchingQuery = null; this.areSavedQueriesEnabled = areSavedQueriesEnabled; // Parent mw.rcfilters.ui.FilterTagMultiselectWidget.parent.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: { hideWhenOutOfView: false, hideOnChoose: false, width: 650, footers: [ { name: 'viewSelect', sticky: false, // View select menu, appears on default view only $element: $( '
' ) .append( new mw.rcfilters.ui.ViewSwitchWidget( this.controller, this.model ).$element ), views: [ 'default' ] }, { name: 'feedback', // Feedback footer, appears on all views $element: $( '
' ) .append( new OO.ui.ButtonWidget( { framed: false, icon: 'feedback', flags: [ 'progressive' ], label: mw.msg( 'rcfilters-filterlist-feedbacklink' ), href: 'https://www.mediawiki.org/wiki/Help_talk:New_filters_for_edit_review' } ).$element ) } ] }, input: { icon: 'menu', placeholder: mw.msg( 'rcfilters-search-placeholder' ) } }, config ) ); 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' ] } ); if ( areSavedQueriesEnabled ) { this.saveQueryButton = new mw.rcfilters.ui.SaveFiltersPopupButtonWidget( this.controller, this.queriesModel ); this.saveQueryButton.$element.on( 'mousedown', function ( e ) { e.stopPropagation(); } ); this.saveQueryButton.connect( this, { click: 'onSaveQueryButtonClick', saveCurrent: 'setSavedQueryVisibility' } ); } this.emptyFilterMessage = new OO.ui.LabelWidget( { label: mw.msg( 'rcfilters-empty-filter' ), classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-emptyFilters' ] } ); this.$content.append( this.emptyFilterMessage.$element ); // Events this.resetButton.connect( this, { click: 'onResetButtonClick' } ); // 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.model.connect( this, { initialize: 'onModelInitialize', update: 'onModelUpdate', itemUpdate: 'onModelItemUpdate', highlightChange: 'onModelHighlightChange' } ); this.input.connect( this, { change: 'onInputChange' } ); this.queriesModel.connect( this, { itemUpdate: 'onSavedQueriesItemUpdate' } ); // 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 = $( '
' ) .addClass( 'mw-rcfilters-ui-row' ) .append( this.$content .addClass( 'mw-rcfilters-ui-cell' ) .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-filters' ) ); if ( areSavedQueriesEnabled ) { rcFiltersRow.append( $( '
' ) .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 = new OO.ui.ButtonSelectWidget( { classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-views-select-widget' ], items: [ new OO.ui.ButtonOptionWidget( { framed: false, data: 'namespaces', icon: 'article', title: mw.msg( 'rcfilters-view-namespaces-tooltip' ) } ), new OO.ui.ButtonOptionWidget( { framed: false, data: 'tags', icon: 'tag', title: mw.msg( 'rcfilters-view-tags-tooltip' ) } ) ] } ); // Rearrange the UI so the select widget is at the right of the input this.$element.append( $( '
' ) .addClass( 'mw-rcfilters-ui-table' ) .append( $( '
' ) .addClass( 'mw-rcfilters-ui-row' ) .append( $( '
' ) .addClass( 'mw-rcfilters-ui-cell' ) .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-input' ) .append( this.input.$element ), $( '
' ) .addClass( 'mw-rcfilters-ui-cell' ) .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-select' ) .append( this.viewsSelectWidget.$element ) ) ) ); // Event this.viewsSelectWidget.connect( this, { choose: 'onViewsSelectWidgetChoose' } ); rcFiltersRow.append( $( '
' ) .addClass( 'mw-rcfilters-ui-cell' ) .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-reset' ) .append( this.resetButton.$element ) ); // Build the content $contentWrapper.append( title.$element, this.savedQueryTitle.$element, $( '
' ) .addClass( 'mw-rcfilters-ui-table' ) .append( rcFiltersRow ) ); // Initialize this.$handle.append( $contentWrapper ); this.emptyFilterMessage.toggle( this.isEmpty() ); this.savedQueryTitle.toggle( false ); this.$element .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget' ); this.reevaluateResetRestoreState(); }; /* Initialization */ OO.inheritClass( mw.rcfilters.ui.FilterTagMultiselectWidget, OO.ui.MenuTagMultiselectWidget ); /* Methods */ /** * Respond to view select widget choose event * * @param {OO.ui.ButtonOptionWidget} buttonOptionWidget Chosen widget */ mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onViewsSelectWidgetChoose = function ( buttonOptionWidget ) { this.controller.switchView( buttonOptionWidget.getData() ); this.viewsSelectWidget.selectItem( null ); this.focus(); }; /** * Respond to input change event * * @param {string} value Value of the input */ mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onInputChange = function ( value ) { var view; value = value.trim(); view = this.model.getViewByTrigger( value.substr( 0, 1 ) ); this.controller.switchView( view ); }; /** * Respond to query button click */ mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onSaveQueryButtonClick = function () { this.getMenu().toggle( false ); }; /** * 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 */ mw.rcfilters.ui.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 */ mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onMenuToggle = function ( isVisible ) { // Parent mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onMenuToggle.call( this ); if ( isVisible ) { mw.hook( 'RcFilters.popup.open' ).fire(); if ( !this.getMenu().getSelectedItem() ) { // 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 ), 0 ); } } else { // Clear selection this.selectTag( null ); // Clear input if the only thing in the input is the prefix if ( this.input.getValue().trim() === this.model.getViewTrigger( this.model.getCurrentView() ) ) { // Clear the input this.input.setValue( '' ); } // Log filter grouping this.controller.trackFilterGroupings( 'filtermenu' ); } this.input.setIcon( isVisible ? 'search' : 'menu' ); }; /** * @inheritdoc */ mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onInputFocus = function () { // Parent mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onInputFocus.call( this ); // Scroll to top this.scrollToTop( this.$element ); }; /** * @inheritdoc */ mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.doInputEscape = function () { // Parent mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.doInputEscape.call( this ); // Blur the input this.input.$input.blur(); }; /** * @inheritdoc */ mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onChangeTags = function () { // Parent method mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onChangeTags.call( this ); this.emptyFilterMessage.toggle( this.isEmpty() ); }; /** * Respond to model initialize event */ mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelInitialize = function () { this.setSavedQueryVisibility(); }; /** * Respond to model update event */ mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelUpdate = function () { this.updateElementsForView(); }; /** * Update the elements in the widget to the current view */ mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.updateElementsForView = function () { var view = this.model.getCurrentView(), inputValue = this.input.getValue().trim(), inputView = this.model.getViewByTrigger( inputValue.substr( 0, 1 ) ); if ( inputView !== 'default' ) { // We have a prefix already, remove it inputValue = inputValue.substr( 1 ); } if ( inputView !== view ) { // Add the correct prefix inputValue = this.model.getViewTrigger( view ) + inputValue; } // Update input this.input.setValue( inputValue ); }; /** * Set the visibility of the saved query button */ mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.setSavedQueryVisibility = function () { if ( this.areSavedQueriesEnabled ) { this.matchingQuery = this.controller.findQueryMatchingCurrentState(); this.savedQueryTitle.setLabel( this.matchingQuery ? this.matchingQuery.getLabel() : '' ); this.savedQueryTitle.toggle( !!this.matchingQuery ); this.saveQueryButton.toggle( !this.isEmpty() && !this.matchingQuery ); if ( this.matchingQuery ) { this.emphasize(); } } }; /** * Respond to model itemUpdate event * * @param {mw.rcfilters.dm.FilterItem} item Filter item model */ mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelItemUpdate = function ( item ) { if ( item.getGroupModel().isHidden() ) { return; } if ( item.isSelected() || ( this.model.isHighlightEnabled() && item.isHighlightSupported() && item.getHighlightColor() ) ) { this.addTag( item.getName(), item.getLabel() ); } else { this.removeTagByData( item.getName() ); } this.setSavedQueryVisibility(); // Re-evaluate reset state this.reevaluateResetRestoreState(); }; /** * @inheritdoc */ mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.isAllowedData = function ( data ) { return ( this.model.getItemByName( data ) && !this.isDuplicateData( data ) ); }; /** * @inheritdoc */ mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onMenuChoose = function ( item ) { this.controller.toggleFilterSelect( item.model.getName() ); // Select the tag if it exists, or reset selection otherwise this.selectTag( this.getItemFromData( item.model.getName() ) ); this.focus(); }; /** * Respond to highlightChange event * * @param {boolean} isHighlightEnabled Highlight is enabled */ mw.rcfilters.ui.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() ) { this.removeTagByData( filterItem.getName() ); } }.bind( this ) ); } }; /** * @inheritdoc */ mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onTagSelect = function ( tagItem ) { var widget = this, menuOption = this.menu.getItemFromModel( tagItem.getModel() ), oldInputValue = this.input.getValue().trim(); this.menu.setUserSelecting( true ); // Reset input this.input.setValue( '' ); // Switch view this.controller.switchView( tagItem.getView() ); // Parent method mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onTagSelect.call( this, tagItem ); this.menu.selectItem( menuOption ); this.selectTag( tagItem ); // Scroll to the item if ( this.model.removeViewTriggers( oldInputValue ) ) { // We're binding a 'once' to the itemVisibilityChange event // so this happens when the menu is ready after the items // are visible again, in case this is done right after the // user filtered the results this.getMenu().once( 'itemVisibilityChange', function () { widget.scrollToTop( menuOption.$element ); widget.menu.setUserSelecting( false ); } ); } else { 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 */ mw.rcfilters.ui.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 */ mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onTagRemove = function ( tagItem ) { // Parent method mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onTagRemove.call( this, tagItem ); this.controller.clearFilter( tagItem.getName() ); tagItem.destroy(); }; /** * Respond to click event on the reset button */ mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onResetButtonClick = function () { if ( this.model.areCurrentFiltersEmpty() ) { // Reset to default filters this.controller.resetToDefaults(); } else { // Reset to have no filters this.controller.emptyFilters(); } }; /** * Reevaluate the restore state for the widget between setting to defaults and clearing all filters */ mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.reevaluateResetRestoreState = function () { var defaultsAreEmpty = this.model.areDefaultFiltersEmpty(), currFiltersAreEmpty = this.model.areCurrentFiltersEmpty(), 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 */ mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.createMenuWidget = function ( menuConfig ) { return new mw.rcfilters.ui.MenuSelectWidget( this.controller, this.model, $.extend( { filterFromInput: true }, menuConfig ) ); }; /** * @inheritdoc */ mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.createTagItemWidget = function ( data ) { var filterItem = this.model.getItemByName( data ); if ( filterItem ) { return new mw.rcfilters.ui.FilterTagItemWidget( this.controller, filterItem, { $overlay: this.$overlay } ); } }; mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.emphasize = function () { if ( !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] When scrolling the entire widget to the top, leave this * much space (in pixels) above the widget. */ mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.scrollToTop = function ( $element, marginFromTop ) { var container = OO.ui.Element.static.getClosestScrollableContainer( $element[ 0 ], 'y' ), pos = OO.ui.Element.static.getRelativePosition( $element, $( container ) ), containerScrollTop = $( container ).is( 'body, html' ) ? 0 : $( container ).scrollTop(); // Scroll to item $( container ).animate( { scrollTop: containerScrollTop + pos.top - ( marginFromTop || 0 ) } ); }; }( mediaWiki ) );