mediawiki-extensions-Visual.../modules/ve/ui/widgets/ve.ui.MenuWidget.js
Rob Moen 61c708ef1c Affordances for MenuWidget to be optionally focusable.
* ve.ui.MenuWidget.js
MenuWidget no longer creates an embeded input element by default.
In the case of no configured input element, we bind the keydown
handler to window with addEventListner while using the useCapture
flag.  This nicely prevents elements lower in the dom from triggering
( document node ) Supported in IE9 and above and all modern browsers.

* ve.ui.ListAction.js
Since MenuWidget is no longer stealing focus from the surface,
we no longer need to restore focus after a list item conversion.
This is the end goal, as browsers like Chrome like to scroll to
the top of elements that gain focus.

Bug: 50792
Change-Id: I5b6969bca1a58b040708f8ac9d3dc8b07ddf9e6b
2013-07-09 12:53:35 -07:00

225 lines
4.6 KiB
JavaScript

/*!
* VisualEditor UserInterface MenuWidget class.
*
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Create an ve.ui.MenuWidget object.
*
* @class
* @extends ve.ui.SelectWidget
*
* @constructor
* @param {Object} [config] Config options
* @cfg {ve.ui.InputWidget} [input] Input to bind keyboard handlers to
*/
ve.ui.MenuWidget = function VeUiMenuWidget( config ) {
// Config intialization
config = config || {};
// Parent constructor
ve.ui.SelectWidget.call( this, config );
// Properties
this.newItems = [];
this.$input = config.input ? config.input.$input : null;
this.$previousFocus = null;
this.isolated = !config.input;
this.visible = false;
this.keydownHandler = ve.bind( this.onKeyDown, this );
// Initialization
this.$.hide().addClass( 've-ui-menuWidget' );
};
/* Inheritance */
ve.inheritClass( ve.ui.MenuWidget, ve.ui.SelectWidget );
/* Methods */
/**
* Handles key down events.
*
* @method
* @param {jQuery.Event} e Key down event
*/
ve.ui.MenuWidget.prototype.onKeyDown = function ( e ) {
var handled = false,
highlightItem = this.getHighlightedItem();
if ( !this.disabled && this.visible ) {
switch ( e.keyCode ) {
case ve.Keys.ENTER:
this.selectItem( highlightItem );
handled = true;
break;
case ve.Keys.UP:
this.highlightItem( this.getRelativeSelectableItem( highlightItem, -1 ) );
handled = true;
break;
case ve.Keys.DOWN:
this.highlightItem( this.getRelativeSelectableItem( highlightItem, 1 ) );
handled = true;
break;
case ve.Keys.ESCAPE:
if ( highlightItem ) {
highlightItem.setHighlighted( false );
}
this.hide();
handled = true;
break;
}
if ( handled ) {
e.preventDefault();
e.stopPropagation();
return false;
}
}
};
/**
* Check if the menu is visible.
*
* @method
* @returns {boolean} Menu is visible
*/
ve.ui.MenuWidget.prototype.isVisible = function () {
return this.visible;
};
/**
* Bind keydown listener
*
* @method
*/
ve.ui.MenuWidget.prototype.bindKeydownListener = function () {
if ( this.$input ) {
this.$input.on( 'keydown', this.keydownHandler );
} else {
// Capture menu navigation keys
window.addEventListener( 'keydown', this.keydownHandler, true );
}
};
/**
* Unbind keydown listener
*
* @method
*/
ve.ui.MenuWidget.prototype.unbindKeydownListener = function () {
if ( this.$input ) {
this.$input.off( 'keydown' );
} else {
window.removeEventListener( 'keydown', this.keydownHandler, true );
}
};
/**
* Select an item.
*
* The menu will stay open if an item is silently selected.
*
* @method
* @param {ve.ui.OptionWidget} [item] Item to select, omit to deselect all
* @param {boolean} [silent=false] Update UI only, do not emit `select` event
* @chainable
*/
ve.ui.MenuWidget.prototype.selectItem = function ( item, silent ) {
if ( !this.disabled && !silent ) {
if ( item ) {
this.disabled = true;
item.flash( ve.bind( function () {
this.hide();
this.disabled = false;
}, this ) );
} else {
this.hide();
}
}
ve.ui.SelectWidget.prototype.selectItem.call( this, item, silent );
return this;
};
/**
* Add items.
*
* Adding an existing item (by value) will move it.
*
* @method
* @param {ve.ui.MenuItemWidget[]} items Items to add
* @param {number} [index] Index to insert items after
* @chainable
*/
ve.ui.MenuWidget.prototype.addItems = function ( items, index ) {
var i, len, item;
ve.ui.SelectWidget.prototype.addItems.call( this, items, index );
for ( i = 0, len = items.length; i < len; i++ ) {
item = items[i];
if ( this.visible ) {
// Defer fitting label until
item.fitLabel();
} else {
this.newItems.push( item );
}
}
return this;
};
/**
* Show the menu.
*
* @method
* @chainable
*/
ve.ui.MenuWidget.prototype.show = function () {
var i, len;
if ( this.items.length ) {
this.$.show();
this.visible = true;
this.bindKeydownListener();
// Change focus to enable keyboard navigation
if ( this.isolated && this.$input && !this.$input.is( ':focus' ) ) {
this.$previousFocus = this.$$( ':focus' );
this.$input.focus();
}
if ( this.newItems.length ) {
for ( i = 0, len = this.newItems.length; i < len; i++ ) {
this.newItems[i].fitLabel();
}
this.newItems = [];
}
}
return this;
};
/**
* Hide the menu.
*
* @method
* @chainable
*/
ve.ui.MenuWidget.prototype.hide = function () {
this.$.hide();
this.visible = false;
this.unbindKeydownListener();
if ( this.isolated && this.$previousFocus ) {
this.$previousFocus.focus();
this.$previousFocus = null;
}
return this;
};