/** * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget. * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxWidget ComboBoxWidget}, * and {@link OO.ui.LookupElement LookupElement} for examples of widgets that contain menus. * MenuSelectWidgets themselves are not instantiated directly, rather subclassed * and customized to be opened, closed, and displayed as needed. * * By default, menus are clipped to the visible viewport and are not visible when a user presses the * mouse outside the menu. * * Menus also have support for keyboard interaction: * * - Enter/Return key: choose and select a menu option * - Up-arrow key: highlight the previous menu option * - Down-arrow key: highlight the next menu option * - Esc key: hide the menu * * Please see the [OOjs UI documentation on MediaWiki][1] for more information. * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options * * @class * @extends OO.ui.SelectWidget * @mixins OO.ui.ClippableElement * * @constructor * @param {Object} [config] Configuration options * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match * the text the user types. This config is used by {@link OO.ui.ComboBoxWidget ComboBoxWidget} * and {@link OO.ui.LookupElement LookupElement} * @cfg {OO.ui.Widget} [widget] Widget associated with the menu’s active state. If the user clicks the mouse * anywhere on the page outside of this widget, the menu is hidden. * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu. */ OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) { // Configuration initialization config = config || {}; // Parent constructor OO.ui.MenuSelectWidget.super.call( this, config ); // Mixin constructors OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) ); // Properties this.newItems = null; this.autoHide = config.autoHide === undefined || !!config.autoHide; this.$input = config.input ? config.input.$input : null; this.$widget = config.widget ? config.widget.$element : null; this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this ); // Initialization this.$element .addClass( 'oo-ui-menuSelectWidget' ) .attr( 'role', 'menu' ); // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods // that reference properties not initialized at that time of parent class construction // TODO: Find a better way to handle post-constructor setup this.visible = false; this.$element.addClass( 'oo-ui-element-hidden' ); }; /* Setup */ OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget ); OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.ClippableElement ); /* Methods */ /** * Handles document mouse down events. * * @protected * @param {jQuery.Event} e Key down event */ OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) { if ( !OO.ui.contains( this.$element[ 0 ], e.target, true ) && ( !this.$widget || !OO.ui.contains( this.$widget[ 0 ], e.target, true ) ) ) { this.toggle( false ); } }; /** * @inheritdoc */ OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) { var currentItem = this.getHighlightedItem() || this.getSelectedItem(); if ( !this.isDisabled() && this.isVisible() ) { switch ( e.keyCode ) { case OO.ui.Keys.LEFT: case OO.ui.Keys.RIGHT: // Do nothing if a text field is associated, arrow keys will be handled natively if ( !this.$input ) { OO.ui.MenuSelectWidget.super.prototype.onKeyDown.call( this, e ); } break; case OO.ui.Keys.ESCAPE: case OO.ui.Keys.TAB: if ( currentItem ) { currentItem.setHighlighted( false ); } this.toggle( false ); // Don't prevent tabbing away, prevent defocusing if ( e.keyCode === OO.ui.Keys.ESCAPE ) { e.preventDefault(); e.stopPropagation(); } break; default: OO.ui.MenuSelectWidget.super.prototype.onKeyDown.call( this, e ); return; } } }; /** * @inheritdoc */ OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () { if ( this.$input ) { this.$input.on( 'keydown', this.onKeyDownHandler ); } else { OO.ui.MenuSelectWidget.super.prototype.bindKeyDownListener.call( this ); } }; /** * @inheritdoc */ OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () { if ( this.$input ) { this.$input.off( 'keydown', this.onKeyDownHandler ); } else { OO.ui.MenuSelectWidget.super.prototype.unbindKeyDownListener.call( this ); } }; /** * Choose an item. * * When a user chooses an item, the menu is closed. * * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method. * @param {OO.ui.OptionWidget} item Item to choose * @chainable */ OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) { OO.ui.MenuSelectWidget.super.prototype.chooseItem.call( this, item ); this.toggle( false ); return this; }; /** * @inheritdoc */ OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) { var i, len, item; // Parent method OO.ui.MenuSelectWidget.super.prototype.addItems.call( this, items, index ); // Auto-initialize if ( !this.newItems ) { this.newItems = []; } for ( i = 0, len = items.length; i < len; i++ ) { item = items[ i ]; if ( this.isVisible() ) { // Defer fitting label until item has been attached item.fitLabel(); } else { this.newItems.push( item ); } } // Reevaluate clipping this.clip(); return this; }; /** * @inheritdoc */ OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) { // Parent method OO.ui.MenuSelectWidget.super.prototype.removeItems.call( this, items ); // Reevaluate clipping this.clip(); return this; }; /** * @inheritdoc */ OO.ui.MenuSelectWidget.prototype.clearItems = function () { // Parent method OO.ui.MenuSelectWidget.super.prototype.clearItems.call( this ); // Reevaluate clipping this.clip(); return this; }; /** * @inheritdoc */ OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) { visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length; var i, len, change = visible !== this.isVisible(); // Parent method OO.ui.MenuSelectWidget.super.prototype.toggle.call( this, visible ); if ( change ) { if ( visible ) { this.bindKeyDownListener(); if ( this.newItems && this.newItems.length ) { for ( i = 0, len = this.newItems.length; i < len; i++ ) { this.newItems[ i ].fitLabel(); } this.newItems = null; } this.toggleClipping( true ); // Auto-hide if ( this.autoHide ) { this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler, true ); } } else { this.unbindKeyDownListener(); this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler, true ); this.toggleClipping( false ); } } return this; };