Path: blob/trunk/third_party/closure/goog/ui/submenu.js
4050 views
/**1* @license2* Copyright The Closure Library Authors.3* SPDX-License-Identifier: Apache-2.04*/56/**7* @fileoverview A class representing menu items that open a submenu.8* @see goog.ui.Menu9*10* @see ../demos/submenus.html11* @see ../demos/submenus2.html12*/1314goog.provide('goog.ui.SubMenu');1516goog.require('goog.Timer');17goog.require('goog.asserts');18goog.require('goog.dom');19goog.require('goog.dom.classlist');20goog.require('goog.events.KeyCodes');21goog.require('goog.positioning.AnchoredViewportPosition');22goog.require('goog.positioning.Corner');23goog.require('goog.style');24goog.require('goog.ui.Component');25goog.require('goog.ui.Menu');26goog.require('goog.ui.MenuItem');27goog.require('goog.ui.SubMenuRenderer');28goog.require('goog.ui.registry');29goog.requireType('goog.events.BrowserEvent');30goog.requireType('goog.events.Event');31goog.requireType('goog.events.KeyEvent');32goog.requireType('goog.ui.ControlContent');33goog.requireType('goog.ui.MenuHeader');34goog.requireType('goog.ui.MenuItemRenderer');35goog.requireType('goog.ui.MenuSeparator');36373839/**40* Class representing a submenu that can be added as an item to other menus.41*42* @param {goog.ui.ControlContent} content Text caption or DOM structure to43* display as the content of the submenu (use to add icons or styling to44* menus).45* @param {*=} opt_model Data/model associated with the menu item.46* @param {goog.dom.DomHelper=} opt_domHelper Optional dom helper used for dom47* interactions.48* @param {goog.ui.MenuItemRenderer=} opt_renderer Renderer used to render or49* decorate the component; defaults to {@link goog.ui.SubMenuRenderer}.50* @constructor51* @extends {goog.ui.MenuItem}52*/53goog.ui.SubMenu = function(content, opt_model, opt_domHelper, opt_renderer) {54'use strict';55goog.ui.MenuItem.call(56this, content, opt_model, opt_domHelper,57opt_renderer || goog.ui.SubMenuRenderer.getInstance());58};59goog.inherits(goog.ui.SubMenu, goog.ui.MenuItem);606162/**63* The delay before opening the sub menu in milliseconds.64* @type {number}65*/66goog.ui.SubMenu.MENU_DELAY_MS = 218;676869/**70* Timer used to dismiss the submenu when the item becomes unhighlighted.71* @type {?number}72* @private73*/74goog.ui.SubMenu.prototype.dismissTimer_ = null;757677/**78* Timer used to show the submenu on mouseover.79* @type {?number}80* @private81*/82goog.ui.SubMenu.prototype.showTimer_ = null;838485/**86* Whether the submenu believes the menu is visible.87* @type {boolean}88* @private89*/90goog.ui.SubMenu.prototype.menuIsVisible_ = false;919293/**94* The lazily created sub menu.95* @type {goog.ui.Menu?}96* @private97*/98goog.ui.SubMenu.prototype.subMenu_ = null;99100101/**102* Whether or not the sub-menu was set explicitly.103* @type {boolean}104* @private105*/106goog.ui.SubMenu.prototype.externalSubMenu_ = false;107108109/**110* Whether or not to align the submenu at the end of the parent menu.111* If true, the menu expands to the right in LTR languages and to the left112* in RTL langauges.113* @type {boolean}114* @private115*/116goog.ui.SubMenu.prototype.alignToEnd_ = true;117118119/**120* Whether the position of this submenu may be adjusted to fit121* the visible area, as in {@link goog.ui.Popup.positionAtCoordinate}.122* @type {boolean}123* @private124*/125goog.ui.SubMenu.prototype.isPositionAdjustable_ = false;126127128/** @override */129goog.ui.SubMenu.prototype.enterDocument = function() {130'use strict';131goog.ui.SubMenu.superClass_.enterDocument.call(this);132133this.getHandler().listen(134this.getParent(), goog.ui.Component.EventType.HIDE, this.onParentHidden_);135136if (this.subMenu_) {137this.setMenuListenersEnabled_(this.subMenu_, true);138}139};140141142/** @override */143goog.ui.SubMenu.prototype.exitDocument = function() {144'use strict';145this.getHandler().unlisten(146this.getParent(), goog.ui.Component.EventType.HIDE, this.onParentHidden_);147148if (this.subMenu_) {149this.setMenuListenersEnabled_(this.subMenu_, false);150if (!this.externalSubMenu_) {151this.subMenu_.exitDocument();152goog.dom.removeNode(this.subMenu_.getElement());153}154}155156goog.ui.SubMenu.superClass_.exitDocument.call(this);157};158159160/** @override */161goog.ui.SubMenu.prototype.disposeInternal = function() {162'use strict';163if (this.subMenu_ && !this.externalSubMenu_) {164this.subMenu_.dispose();165}166this.subMenu_ = null;167goog.ui.SubMenu.superClass_.disposeInternal.call(this);168};169170171/**172* @override173* Dismisses the submenu on a delay, with the result that the user needs less174* accuracy when moving to submenus. Alternate implementations could use175* geometry instead of a timer.176* @param {boolean} highlight Whether item should be highlighted.177* @param {boolean=} opt_btnPressed Whether the mouse button is held down.178*/179goog.ui.SubMenu.prototype.setHighlighted = function(highlight, opt_btnPressed) {180'use strict';181goog.ui.SubMenu.superClass_.setHighlighted.call(this, highlight);182183if (opt_btnPressed) {184this.getMenu().setMouseButtonPressed(true);185}186187if (!highlight) {188if (this.dismissTimer_) {189goog.Timer.clear(this.dismissTimer_);190}191this.dismissTimer_ =192goog.Timer.callOnce(this.dismissSubMenu, this.getMenuDelay(), this);193}194};195196197/**198* Show the submenu and ensure that all siblings are hidden.199*/200goog.ui.SubMenu.prototype.showSubMenu = function() {201'use strict';202// Only show the menu if this item is still selected. This is called on a203// timeout, so make sure our parent still exists.204var parent = this.getParent();205if (parent && parent.getHighlighted() == this) {206this.setSubMenuVisible_(true);207this.dismissSiblings_();208}209};210211212/**213* Dismisses the menu and all further submenus.214*/215goog.ui.SubMenu.prototype.dismissSubMenu = function() {216'use strict';217// Because setHighlighted calls this function on a timeout, we need to make218// sure that the sub menu hasn't been disposed when we come back.219var subMenu = this.subMenu_;220if (subMenu && subMenu.getParent() == this) {221this.setSubMenuVisible_(false);222subMenu.forEachChild(function(child) {223'use strict';224if (typeof child.dismissSubMenu == 'function') {225child.dismissSubMenu();226}227});228}229};230231232/**233* Clears the show and hide timers for the sub menu.234*/235goog.ui.SubMenu.prototype.clearTimers = function() {236'use strict';237if (this.dismissTimer_) {238goog.Timer.clear(this.dismissTimer_);239}240if (this.showTimer_) {241goog.Timer.clear(this.showTimer_);242}243};244245246/**247* Sets the menu item to be visible or invisible.248* @param {boolean} visible Whether to show or hide the component.249* @param {boolean=} opt_force If true, doesn't check whether the component250* already has the requested visibility, and doesn't dispatch any events.251* @return {boolean} Whether the visibility was changed.252* @override253*/254goog.ui.SubMenu.prototype.setVisible = function(visible, opt_force) {255'use strict';256var visibilityChanged =257goog.ui.SubMenu.superClass_.setVisible.call(this, visible, opt_force);258// For menus that allow menu items to be hidden (i.e. ComboBox) ensure that259// the submenu is hidden.260if (visibilityChanged && !this.isVisible()) {261this.dismissSubMenu();262}263return visibilityChanged;264};265266267/**268* Dismiss all the sub menus of sibling menu items.269* @private270*/271goog.ui.SubMenu.prototype.dismissSiblings_ = function() {272'use strict';273this.getParent().forEachChild(function(child) {274'use strict';275if (child != this && typeof child.dismissSubMenu == 'function') {276child.dismissSubMenu();277child.clearTimers();278}279}, this);280};281282283/**284* Handles a key event that is passed to the menu item from its parent because285* it is highlighted. If the arrow keys or enter key is pressed the sub menu286* takes control and delegates further key events to its menu until it is287* dismissed.288* @param {goog.events.KeyEvent} e A key event.289* @return {boolean} Whether the event was handled.290* @override291*/292goog.ui.SubMenu.prototype.handleKeyEvent = function(e) {293'use strict';294var keyCode = e.keyCode;295var arrowOpenKeyCode = this.isRightToLeft() ? goog.events.KeyCodes.LEFT :296goog.events.KeyCodes.RIGHT;297var closeKeyCode = this.isRightToLeft() ? goog.events.KeyCodes.RIGHT :298goog.events.KeyCodes.LEFT;299300if (!this.menuIsVisible_) {301// Menu item doesn't have keyboard control and the correct key was pressed.302// So open take keyboard control and open the sub menu.303if (this.isEnabled() &&304(keyCode == arrowOpenKeyCode || keyCode == goog.events.KeyCodes.ENTER ||305keyCode == this.getMnemonic())) {306this.showSubMenu();307this.getMenu().highlightFirst();308this.clearTimers();309310// The menu item doesn't currently care about the key events so let the311// parent menu handle them accordingly .312} else {313return false;314}315316// Menu item has control, so let its menu try to handle the keys (this may317// in turn be handled by sub-sub menus).318} else if (this.getMenu().handleKeyEvent(e)) {319// Nothing to do320321// The menu has control and the key hasn't yet been handled, on left arrow322// we turn off key control.323} else if (keyCode == closeKeyCode) {324this.dismissSubMenu();325326} else {327// Submenu didn't handle the key so let the parent decide what to do.328return false;329}330331e.preventDefault();332return true;333};334335336/**337* Listens to the sub menus items and ensures that this menu item is selected338* while dismissing the others. This handles the case when the user mouses339* over other items on their way to the sub menu.340* @param {goog.events.Event} e Enter event to handle.341* @private342*/343goog.ui.SubMenu.prototype.onChildEnter_ = function(e) {344'use strict';345if (this.subMenu_.getParent() == this) {346this.clearTimers();347this.getParentEventTarget().setHighlighted(this);348this.dismissSiblings_();349}350};351352353/**354* Listens to the parent menu's hide event and ensures that all submenus are355* hidden at the same time.356* @param {goog.events.Event} e The event.357* @private358*/359goog.ui.SubMenu.prototype.onParentHidden_ = function(e) {360'use strict';361// Ignore propagated events362if (e.target == this.getParentEventTarget()) {363// TODO(user): Using an event for this is expensive. Consider having a364// generalized interface that the parent menu calls on its children when365// it is hidden.366this.dismissSubMenu();367this.clearTimers();368}369};370371372/**373* @override374* Sets a timer to show the submenu and then dispatches an ENTER event to the375* parent menu.376* @param {goog.events.BrowserEvent} e Mouse event to handle.377*/378goog.ui.SubMenu.prototype.handleMouseOver = function(e) {379'use strict';380if (this.isEnabled()) {381this.clearTimers();382this.showTimer_ =383goog.Timer.callOnce(this.showSubMenu, this.getMenuDelay(), this);384}385goog.ui.SubMenu.superClass_.handleMouseOver.call(this, e);386};387388389/**390* Returns the delay before opening or closing the menu in milliseconds.391* @return {number}392* @protected393*/394goog.ui.SubMenu.prototype.getMenuDelay = function() {395'use strict';396return goog.ui.SubMenu.MENU_DELAY_MS;397};398399400/**401* Overrides the default mouseup event handler, so that the ACTION isn't402* dispatched for the submenu itself, instead the submenu is shown instantly.403* @param {goog.events.Event} e The browser event.404* @return {boolean} True if the action was allowed to proceed, false otherwise.405* @override406*/407goog.ui.SubMenu.prototype.performActionInternal = function(e) {408'use strict';409this.clearTimers();410var shouldHandleClick =411this.isSupportedState(goog.ui.Component.State.SELECTED) ||412this.isSupportedState(goog.ui.Component.State.CHECKED);413if (shouldHandleClick) {414return goog.ui.SubMenu.superClass_.performActionInternal.call(this, e);415} else {416this.showSubMenu();417return true;418}419};420421422/**423* Sets the visiblility of the sub menu.424* @param {boolean} visible Whether to show menu.425* @private426*/427goog.ui.SubMenu.prototype.setSubMenuVisible_ = function(visible) {428'use strict';429// Unhighlighting the menuitems if closing the menu so the event handlers can430// determine the correct state.431if (!visible && this.getMenu()) {432this.getMenu().setHighlightedIndex(-1);433}434435// Dispatch OPEN event before calling getMenu(), so we can create the menu436// lazily on first access.437this.dispatchEvent(438goog.ui.Component.getStateTransitionEvent(439goog.ui.Component.State.OPENED, visible));440var subMenu = this.getMenu();441if (visible != this.menuIsVisible_) {442goog.dom.classlist.enable(443goog.asserts.assert(this.getElement()),444goog.getCssName('goog-submenu-open'), visible);445}446if (visible != subMenu.isVisible()) {447if (visible) {448// Lazy-render menu when first shown, if needed.449if (!subMenu.isInDocument()) {450subMenu.render();451}452subMenu.setHighlightedIndex(-1);453}454subMenu.setVisible(visible);455// We must position after the menu is visible, otherwise positioning logic456// breaks in RTL.457if (visible) {458this.positionSubMenu();459}460}461this.menuIsVisible_ = visible;462};463464465/**466* Attaches or detaches menu event listeners to/from the given menu. Called467* each time a menu is attached to or detached from the submenu.468* @param {goog.ui.Menu} menu Menu on which to listen for events.469* @param {boolean} attach Whether to attach or detach event listeners.470* @private471*/472goog.ui.SubMenu.prototype.setMenuListenersEnabled_ = function(menu, attach) {473'use strict';474var handler = this.getHandler();475var method = attach ? handler.listen : handler.unlisten;476method.call(477handler, menu, goog.ui.Component.EventType.ENTER, this.onChildEnter_);478};479480481/**482* Sets whether the submenu is aligned at the end of the parent menu.483* @param {boolean} alignToEnd True to align to end, false to align to start.484*/485goog.ui.SubMenu.prototype.setAlignToEnd = function(alignToEnd) {486'use strict';487if (alignToEnd != this.alignToEnd_) {488this.alignToEnd_ = alignToEnd;489if (this.isInDocument()) {490// Completely re-render the widget.491var oldElement = this.getElement();492this.exitDocument();493494if (oldElement.nextSibling) {495this.renderBefore(/** @type {!Element} */ (oldElement.nextSibling));496} else {497this.render(/** @type {Element} */ (oldElement.parentNode));498}499}500}501};502503504/**505* Determines whether the submenu is aligned at the end of the parent menu.506* @return {boolean} True if aligned to the end (the default), false if507* aligned to the start.508*/509goog.ui.SubMenu.prototype.isAlignedToEnd = function() {510'use strict';511return this.alignToEnd_;512};513514515/**516* Positions the submenu. This method should be called if the sub menu is517* opened and the menu element's size changes (e.g., when adding/removing items518* to an opened sub menu).519*/520goog.ui.SubMenu.prototype.positionSubMenu = function() {521'use strict';522var position = new goog.positioning.AnchoredViewportPosition(523this.getElement(),524this.isAlignedToEnd() ? goog.positioning.Corner.TOP_END :525goog.positioning.Corner.TOP_START,526this.isPositionAdjustable_);527528// TODO(user): Clean up popup code and have this be a one line call529var subMenu = this.getMenu();530var el = subMenu.getElement();531if (!subMenu.isVisible()) {532el.style.visibility = 'hidden';533goog.style.setElementShown(el, true);534}535536position.reposition(537el, this.isAlignedToEnd() ? goog.positioning.Corner.TOP_START :538goog.positioning.Corner.TOP_END);539540if (!subMenu.isVisible()) {541goog.style.setElementShown(el, false);542el.style.visibility = 'visible';543}544};545546547// Methods delegated to sub-menu but accessible here for convinience548549550/**551* Adds a new menu item at the end of the menu.552* @param {goog.ui.MenuHeader|goog.ui.MenuItem|goog.ui.MenuSeparator} item Menu553* item to add to the menu.554*/555goog.ui.SubMenu.prototype.addItem = function(item) {556'use strict';557this.getMenu().addChild(item, true);558};559560561/**562* Adds a new menu item at a specific index in the menu.563* @param {goog.ui.MenuHeader|goog.ui.MenuItem|goog.ui.MenuSeparator} item Menu564* item to add to the menu.565* @param {number} n Index at which to insert the menu item.566*/567goog.ui.SubMenu.prototype.addItemAt = function(item, n) {568'use strict';569this.getMenu().addChildAt(item, n, true);570};571572573/**574* Removes an item from the menu and disposes it.575* @param {goog.ui.MenuItem} item The menu item to remove.576*/577goog.ui.SubMenu.prototype.removeItem = function(item) {578'use strict';579var child = this.getMenu().removeChild(item, true);580if (child) {581child.dispose();582}583};584585586/**587* Removes a menu item at a given index in the menu and disposes it.588* @param {number} n Index of item.589*/590goog.ui.SubMenu.prototype.removeItemAt = function(n) {591'use strict';592var child = this.getMenu().removeChildAt(n, true);593if (child) {594child.dispose();595}596};597598599/**600* Returns a reference to the menu item at a given index.601* @param {number} n Index of menu item.602* @return {goog.ui.Component} Reference to the menu item.603*/604goog.ui.SubMenu.prototype.getItemAt = function(n) {605'use strict';606return this.getMenu().getChildAt(n);607};608609610/**611* Returns the number of items in the sub menu (including separators).612* @return {number} The number of items in the menu.613*/614goog.ui.SubMenu.prototype.getItemCount = function() {615'use strict';616return this.getMenu().getChildCount();617};618619620/**621* Returns the menu items contained in the sub menu.622* @return {!Array<!goog.ui.MenuItem>} An array of menu items.623* @deprecated Use getItemAt/getItemCount instead.624*/625goog.ui.SubMenu.prototype.getItems = function() {626'use strict';627return this.getMenu().getItems();628};629630631/**632* Gets a reference to the submenu's actual menu.633* @return {!goog.ui.Menu} Reference to the object representing the sub menu.634*/635goog.ui.SubMenu.prototype.getMenu = function() {636'use strict';637if (!this.subMenu_) {638this.setMenu(639new goog.ui.Menu(this.getDomHelper()), /* opt_internal */ true);640} else if (this.externalSubMenu_ && this.subMenu_.getParent() != this) {641// Since it is possible for the same popup menu to be attached to multiple642// submenus, we need to ensure that it has the correct parent event target.643this.subMenu_.setParent(this);644}645// Always create the menu DOM, for backward compatibility.646if (!this.subMenu_.getElement()) {647this.subMenu_.createDom();648}649return this.subMenu_;650};651652653/**654* Sets the submenu to a specific menu.655* @param {goog.ui.Menu} menu The menu to show when this item is selected.656* @param {boolean=} opt_internal Whether this menu is an "internal" menu, and657* should be disposed of when this object is disposed of.658*/659goog.ui.SubMenu.prototype.setMenu = function(menu, opt_internal) {660'use strict';661var oldMenu = this.subMenu_;662if (menu != oldMenu) {663if (oldMenu) {664this.dismissSubMenu();665if (this.isInDocument()) {666this.setMenuListenersEnabled_(oldMenu, false);667}668}669670this.subMenu_ = menu;671this.externalSubMenu_ = !opt_internal;672673if (menu) {674menu.setParent(this);675// There's no need to dispatch a HIDE event during submenu construction.676menu.setVisible(false, /* opt_force */ true);677menu.setAllowAutoFocus(false);678menu.setFocusable(false);679if (this.isInDocument()) {680this.setMenuListenersEnabled_(menu, true);681}682}683}684};685686687/**688* Returns true if the provided element is to be considered inside the menu for689* purposes such as dismissing the menu on an event. This is so submenus can690* make use of elements outside their own DOM.691* @param {Element} element The element to test for.692* @return {boolean} Whether or not the provided element is contained.693*/694goog.ui.SubMenu.prototype.containsElement = function(element) {695'use strict';696return this.getMenu().containsElement(element);697};698699700/**701* @param {boolean} isAdjustable Whether this submenu is adjustable.702*/703goog.ui.SubMenu.prototype.setPositionAdjustable = function(isAdjustable) {704'use strict';705this.isPositionAdjustable_ = !!isAdjustable;706};707708709/**710* @return {boolean} Whether this submenu is adjustable.711*/712goog.ui.SubMenu.prototype.isPositionAdjustable = function() {713'use strict';714return this.isPositionAdjustable_;715};716717718// Register a decorator factory function for goog.ui.SubMenus.719goog.ui.registry.setDecoratorByClassName(720goog.getCssName('goog-submenu'), function() {721'use strict';722return new goog.ui.SubMenu(null);723});724725726