Path: blob/trunk/third_party/closure/goog/ui/menu.js
4506 views
/**1* @license2* Copyright The Closure Library Authors.3* SPDX-License-Identifier: Apache-2.04*/56/**7* @fileoverview A base menu class that supports key and mouse events. The menu8* can be bound to an existing HTML structure or can generate its own DOM.9*10* To decorate, the menu should be bound to an element containing children11* with the classname 'goog-menuitem'. HRs will be classed as separators.12*13* Decorate Example:14* <div id="menu" class="goog-menu" tabIndex="0">15* <div class="goog-menuitem">Google</div>16* <div class="goog-menuitem">Yahoo</div>17* <div class="goog-menuitem">MSN</div>18* <hr>19* <div class="goog-menuitem">New...</div>20* </div>21* <script>22*23* var menu = new goog.ui.Menu();24* menu.decorate(goog.dom.getElement('menu'));25*26* TESTED=FireFox 2.0, IE6, Opera 9, Chrome.27* TODO(user): Key handling is flaky in Opera and Chrome28* TODO(user): Rename all references of "item" to child since menu is29* essentially very generic and could, in theory, host a date or color picker.30*31* @see ../demos/menu.html32* @see ../demos/menus.html33*/3435goog.provide('goog.ui.Menu');36goog.provide('goog.ui.Menu.EventType');3738goog.require('goog.dom.TagName');39goog.require('goog.math.Coordinate');40goog.require('goog.string');41goog.require('goog.style');42goog.require('goog.ui.Component.EventType');43goog.require('goog.ui.Component.State');44goog.require('goog.ui.Container');45goog.require('goog.ui.Container.Orientation');46goog.require('goog.ui.MenuHeader');47goog.require('goog.ui.MenuItem');48goog.require('goog.ui.MenuRenderer');49goog.require('goog.ui.MenuSeparator');50goog.requireType('goog.dom.DomHelper');51goog.requireType('goog.events.Event');5253// The dependencies MenuHeader, MenuItem, and MenuSeparator are implicit.54// There are no references in the code, but we need to load these55// classes before goog.ui.Menu.56575859// TODO(robbyw): Reverse constructor argument order for consistency.60/**61* A basic menu class.62* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.63* @param {goog.ui.MenuRenderer=} opt_renderer Renderer used to render or64* decorate the container; defaults to {@link goog.ui.MenuRenderer}.65* @constructor66* @extends {goog.ui.Container}67*/68goog.ui.Menu = function(opt_domHelper, opt_renderer) {69'use strict';70goog.ui.Container.call(71this, goog.ui.Container.Orientation.VERTICAL,72opt_renderer || goog.ui.MenuRenderer.getInstance(), opt_domHelper);7374// Unlike Containers, Menus aren't keyboard-accessible by default. This line75// preserves backwards compatibility with code that depends on menus not76// receiving focus - e.g. `goog.ui.MenuButton`.77this.setFocusable(false);78};79goog.inherits(goog.ui.Menu, goog.ui.Container);808182// TODO(robbyw): Remove this and all references to it.83// Please ensure that BEFORE_SHOW behavior is not disrupted as a result.84/**85* Event types dispatched by the menu.86* @enum {string}87* @deprecated Use goog.ui.Component.EventType.88*/89goog.ui.Menu.EventType = {90/** Dispatched before the menu becomes visible */91BEFORE_SHOW: goog.ui.Component.EventType.BEFORE_SHOW,9293/** Dispatched when the menu is shown */94SHOW: goog.ui.Component.EventType.SHOW,9596/** Dispatched before the menu becomes hidden */97BEFORE_HIDE: goog.ui.Component.EventType.HIDE,9899/** Dispatched when the menu is hidden */100HIDE: goog.ui.Component.EventType.HIDE101};102103104// TODO(robbyw): Remove this and all references to it.105/**106* CSS class for menus.107* @type {string}108* @deprecated Use goog.ui.MenuRenderer.CSS_CLASS.109*/110goog.ui.Menu.CSS_CLASS = goog.ui.MenuRenderer.CSS_CLASS;111112113/**114* Coordinates of the mousedown event that caused this menu to be made visible.115* Used to prevent the consequent mouseup event due to a simple click from116* activating a menu item immediately. Considered protected; should only be used117* within this package or by subclasses.118* @type {goog.math.Coordinate|undefined}119*/120goog.ui.Menu.prototype.openingCoords;121122123/**124* Whether the menu can move the focus to its key event target when it is125* shown. Default = true126* @type {boolean}127* @private128*/129goog.ui.Menu.prototype.allowAutoFocus_ = true;130131132/**133* Whether the menu should use windows style behavior and allow disabled menu134* items to be highlighted (though not selectable). Defaults to false135* @type {boolean}136* @private137*/138goog.ui.Menu.prototype.allowHighlightDisabled_ = false;139140141/**142* Returns the CSS class applied to menu elements, also used as the prefix for143* derived styles, if any. Subclasses should override this method as needed.144* Considered protected.145* @return {string} The CSS class applied to menu elements.146* @protected147* @deprecated Use getRenderer().getCssClass().148*/149goog.ui.Menu.prototype.getCssClass = function() {150'use strict';151return this.getRenderer().getCssClass();152};153154155/**156* Returns whether the provided element is to be considered inside the menu for157* purposes such as dismissing the menu on an event. This is so submenus can158* make use of elements outside their own DOM.159* @param {Element} element The element to test for.160* @return {boolean} Whether the provided element is to be considered inside161* the menu.162* @suppress {strictMissingProperties} Added to tighten compiler checks163*/164goog.ui.Menu.prototype.containsElement = function(element) {165'use strict';166if (this.getRenderer().containsElement(this, element)) {167return true;168}169170for (var i = 0, count = this.getChildCount(); i < count; i++) {171var child = this.getChildAt(i);172if (typeof child.containsElement == 'function' &&173child.containsElement(element)) {174return true;175}176}177178return false;179};180181182/**183* Adds a new menu item at the end of the menu.184* @param {goog.ui.MenuHeader|goog.ui.MenuItem|goog.ui.MenuSeparator} item Menu185* item to add to the menu.186* @deprecated Use {@link #addChild} instead, with true for the second argument.187*/188goog.ui.Menu.prototype.addItem = function(item) {189'use strict';190this.addChild(item, true);191};192193194/**195* Adds a new menu item at a specific index in the menu.196* @param {goog.ui.MenuHeader|goog.ui.MenuItem|goog.ui.MenuSeparator} item Menu197* item to add to the menu.198* @param {number} n Index at which to insert the menu item.199* @deprecated Use {@link #addChildAt} instead, with true for the third200* argument.201*/202goog.ui.Menu.prototype.addItemAt = function(item, n) {203'use strict';204this.addChildAt(item, n, true);205};206207208/**209* Removes an item from the menu and disposes of it.210* @param {goog.ui.MenuHeader|goog.ui.MenuItem|goog.ui.MenuSeparator} item The211* menu item to remove.212* @deprecated Use {@link #removeChild} instead.213*/214goog.ui.Menu.prototype.removeItem = function(item) {215'use strict';216var removedChild = this.removeChild(item, true);217if (removedChild) {218removedChild.dispose();219}220};221222223/**224* Removes a menu item at a given index in the menu and disposes of it.225* @param {number} n Index of item.226* @deprecated Use {@link #removeChildAt} instead.227*/228goog.ui.Menu.prototype.removeItemAt = function(n) {229'use strict';230var removedChild = this.removeChildAt(n, true);231if (removedChild) {232removedChild.dispose();233}234};235236237/**238* Returns a reference to the menu item at a given index.239* @param {number} n Index of menu item.240* @return {goog.ui.MenuHeader|goog.ui.MenuItem|goog.ui.MenuSeparator|null}241* Reference to the menu item.242* @deprecated Use {@link #getChildAt} instead.243*/244goog.ui.Menu.prototype.getItemAt = function(n) {245'use strict';246return /** @type {goog.ui.MenuItem?} */ (this.getChildAt(n));247};248249250/**251* Returns the number of items in the menu (including separators).252* @return {number} The number of items in the menu.253* @deprecated Use {@link #getChildCount} instead.254*/255goog.ui.Menu.prototype.getItemCount = function() {256'use strict';257return this.getChildCount();258};259260261/**262* Returns an array containing the menu items contained in the menu.263* @return {!Array<goog.ui.MenuItem>} An array of menu items.264* @deprecated Use getChildAt, forEachChild, and getChildCount.265*/266goog.ui.Menu.prototype.getItems = function() {267'use strict';268// TODO(user): Remove reference to getItems and instead use getChildAt,269// forEachChild, and getChildCount270var children = [];271this.forEachChild(function(child) {272'use strict';273children.push(child);274});275return children;276};277278279/**280* Sets the position of the menu relative to the view port.281* @param {number|goog.math.Coordinate} x Left position or coordinate obj.282* @param {number=} opt_y Top position.283*/284goog.ui.Menu.prototype.setPosition = function(x, opt_y) {285'use strict';286// NOTE(user): It is necessary to temporarily set the display from none, so287// that the position gets set correctly.288var visible = this.isVisible();289if (!visible) {290goog.style.setElementShown(this.getElement(), true);291}292goog.style.setPageOffset(this.getElement(), x, opt_y);293if (!visible) {294goog.style.setElementShown(this.getElement(), false);295}296};297298299/**300* Gets the page offset of the menu, or null if the menu isn't visible301* @return {goog.math.Coordinate?} Object holding the x-y coordinates of the302* menu or null if the menu is not visible.303*/304goog.ui.Menu.prototype.getPosition = function() {305'use strict';306return this.isVisible() ? goog.style.getPageOffset(this.getElement()) : null;307};308309310/**311* Sets whether the menu can automatically move focus to its key event target312* when it is set to visible.313* @param {boolean} allow Whether the menu can automatically move focus to its314* key event target when it is set to visible.315*/316goog.ui.Menu.prototype.setAllowAutoFocus = function(allow) {317'use strict';318this.allowAutoFocus_ = allow;319if (allow) {320this.setFocusable(true);321}322};323324325/**326* @return {boolean} Whether the menu can automatically move focus to its key327* event target when it is set to visible.328*/329goog.ui.Menu.prototype.getAllowAutoFocus = function() {330'use strict';331return this.allowAutoFocus_;332};333334335/**336* Sets whether the menu will highlight disabled menu items or skip to the next337* active item.338* @param {boolean} allow Whether the menu will highlight disabled menu items or339* skip to the next active item.340*/341goog.ui.Menu.prototype.setAllowHighlightDisabled = function(allow) {342'use strict';343this.allowHighlightDisabled_ = allow;344};345346347/**348* @return {boolean} Whether the menu will highlight disabled menu items or skip349* to the next active item.350*/351goog.ui.Menu.prototype.getAllowHighlightDisabled = function() {352'use strict';353return this.allowHighlightDisabled_;354};355356357/**358* @override359* @param {boolean} show Whether to show or hide the menu.360* @param {boolean=} opt_force If true, doesn't check whether the menu361* already has the requested visibility, and doesn't dispatch any events.362* @param {goog.events.Event=} opt_e Mousedown event that caused this menu to363* be made visible (ignored if show is false).364*/365goog.ui.Menu.prototype.setVisible = function(show, opt_force, opt_e) {366'use strict';367var visibilityChanged =368goog.ui.Menu.superClass_.setVisible.call(this, show, opt_force);369if (visibilityChanged && show && this.isInDocument() &&370this.allowAutoFocus_) {371this.getKeyEventTarget().focus();372}373if (show && opt_e && typeof opt_e.clientX === 'number') {374/** @suppress {strictMissingProperties} Added to tighten compiler checks */375this.openingCoords = new goog.math.Coordinate(opt_e.clientX, opt_e.clientY);376} else {377this.openingCoords = null;378}379return visibilityChanged;380};381382383/** @override */384goog.ui.Menu.prototype.handleEnterItem = function(e) {385'use strict';386if (this.allowAutoFocus_) {387this.getKeyEventTarget().focus();388}389390return goog.ui.Menu.superClass_.handleEnterItem.call(this, e);391};392393394/**395* Highlights the next item that begins with the specified string. If no396* (other) item begins with the given string, the selection is unchanged.397* @param {string} charStr The prefix to match.398* @return {boolean} Whether a matching prefix was found.399*/400goog.ui.Menu.prototype.highlightNextPrefix = function(charStr) {401'use strict';402var re = new RegExp('^' + goog.string.regExpEscape(charStr), 'i');403return this.highlightHelper(function(index, max) {404'use strict';405// Index is >= -1 because it is set to -1 when nothing is selected.406var start = index < 0 ? 0 : index;407var wrapped = false;408409// We always start looking from one after the current, because we410// keep the current selection only as a last resort. This makes the411// loop a little awkward in the case where there is no current412// selection, as we need to stop somewhere but can't just stop413// when index == start, which is why we need the 'wrapped' flag.414do {415++index;416if (index == max) {417index = 0;418wrapped = true;419}420var name = this.getChildAt(index).getCaption();421if (name && name.match(re)) {422return index;423}424} while (!wrapped || index != start);425return this.getHighlightedIndex();426}, this.getHighlightedIndex());427};428429430/** @override */431goog.ui.Menu.prototype.canHighlightItem = function(item) {432'use strict';433return (this.allowHighlightDisabled_ || item.isEnabled()) &&434item.isVisible() && item.isSupportedState(goog.ui.Component.State.HOVER);435};436437438/** @override */439goog.ui.Menu.prototype.decorateInternal = function(element) {440'use strict';441this.decorateContent(element);442goog.ui.Menu.superClass_.decorateInternal.call(this, element);443};444445446/** @override */447goog.ui.Menu.prototype.handleKeyEventInternal = function(e) {448'use strict';449var handled = goog.ui.Menu.base(this, 'handleKeyEventInternal', e);450if (!handled) {451// Loop through all child components, and for each menu item call its452// key event handler so that keyboard mnemonics can be handled.453this.forEachChild(function(menuItem) {454'use strict';455if (!handled && menuItem.getMnemonic &&456menuItem.getMnemonic() == e.keyCode) {457if (this.isEnabled()) {458this.setHighlighted(menuItem);459}460// We still delegate to handleKeyEvent, so that it can handle461// enabled/disabled state.462handled = menuItem.handleKeyEvent(e);463}464}, this);465}466return handled;467};468469470/** @override */471goog.ui.Menu.prototype.setHighlightedIndex = function(index) {472'use strict';473goog.ui.Menu.base(this, 'setHighlightedIndex', index);474475// Bring the highlighted item into view. This has no effect if the menu is not476// scrollable.477var child = this.getChildAt(index);478if (child) {479goog.style.scrollIntoContainerView(child.getElement(), this.getElement());480}481};482483484/**485* Decorate menu items located in any descendant node which as been explicitly486* marked as a 'content' node.487* @param {Element} element Element to decorate.488* @protected489*/490goog.ui.Menu.prototype.decorateContent = function(element) {491'use strict';492var renderer = this.getRenderer();493var contentElements = this.getDomHelper().getElementsByTagNameAndClass(494goog.dom.TagName.DIV, goog.getCssName(renderer.getCssClass(), 'content'),495element);496497// Some versions of IE do not like it when you access this nodeList498// with invalid indices. See499// http://code.google.com/p/closure-library/issues/detail?id=373500var length = contentElements.length;501for (var i = 0; i < length; i++) {502renderer.decorateChildren(this, contentElements[i]);503}504};505506507