Path: blob/trunk/third_party/closure/goog/ui/container.js
4049 views
/**1* @license2* Copyright The Closure Library Authors.3* SPDX-License-Identifier: Apache-2.04*/56/**7* @fileoverview Base class for containers that host {@link goog.ui.Control}s,8* such as menus and toolbars. Provides default keyboard and mouse event9* handling and child management, based on a generalized version of10* {@link goog.ui.Menu}.11*12* @see ../demos/container.html13*/14// TODO(attila): Fix code/logic duplication between this and goog.ui.Control.15// TODO(attila): Maybe pull common stuff all the way up into Component...?1617goog.provide('goog.ui.Container');18goog.provide('goog.ui.Container.EventType');19goog.provide('goog.ui.Container.Orientation');2021goog.require('goog.a11y.aria');22goog.require('goog.a11y.aria.State');23goog.require('goog.asserts');24goog.require('goog.dom');25goog.require('goog.events.EventType');26goog.require('goog.events.KeyCodes');27goog.require('goog.events.KeyHandler');28goog.require('goog.object');29goog.require('goog.style');30goog.require('goog.ui.Component');31goog.require('goog.ui.ComponentUtil');32goog.require('goog.ui.ContainerRenderer');33goog.require('goog.ui.Control');34goog.requireType('goog.events.BrowserEvent');35goog.requireType('goog.events.Event');36goog.requireType('goog.events.KeyEvent');37383940/**41* Base class for containers. Extends {@link goog.ui.Component} by adding42* the following:43* <ul>44* <li>a {@link goog.events.KeyHandler}, to simplify keyboard handling,45* <li>a pluggable <em>renderer</em> framework, to simplify the creation of46* containers without the need to subclass this class,47* <li>methods to manage child controls hosted in the container,48* <li>default mouse and keyboard event handling methods.49* </ul>50* @param {?goog.ui.Container.Orientation=} opt_orientation Container51* orientation; defaults to `VERTICAL`.52* @param {goog.ui.ContainerRenderer=} opt_renderer Renderer used to render or53* decorate the container; defaults to {@link goog.ui.ContainerRenderer}.54* @param {goog.dom.DomHelper=} opt_domHelper DOM helper, used for document55* interaction.56* @extends {goog.ui.Component}57* @constructor58*/59goog.ui.Container = function(opt_orientation, opt_renderer, opt_domHelper) {60'use strict';61goog.ui.Component.call(this, opt_domHelper);62this.renderer_ = opt_renderer || goog.ui.ContainerRenderer.getInstance();63this.orientation_ = opt_orientation || this.renderer_.getDefaultOrientation();64};65goog.inherits(goog.ui.Container, goog.ui.Component);666768/**69* Container-specific events.70* @enum {string}71*/72goog.ui.Container.EventType = {73/**74* Dispatched after a goog.ui.Container becomes visible. Non-cancellable.75* NOTE(user): This event really shouldn't exist, because the76* goog.ui.Component.EventType.SHOW event should behave like this one. But the77* SHOW event for containers has been behaving as other components'78* BEFORE_SHOW event for a long time, and too much code relies on that old79* behavior to fix it now.80*/81AFTER_SHOW: 'aftershow',8283/**84* Dispatched after a goog.ui.Container becomes invisible. Non-cancellable.85*/86AFTER_HIDE: 'afterhide'87};888990/**91* Container orientation constants.92* @enum {string}93*/94goog.ui.Container.Orientation = {95HORIZONTAL: 'horizontal',96VERTICAL: 'vertical'97};9899100/**101* Allows an alternative element to be set to receive key events, otherwise102* defers to the renderer's element choice.103* @type {?Element|undefined}104* @private105*/106goog.ui.Container.prototype.keyEventTarget_ = null;107108109/**110* Keyboard event handler.111* @type {goog.events.KeyHandler?}112* @private113*/114goog.ui.Container.prototype.keyHandler_ = null;115116117/**118* Renderer for the container. Defaults to {@link goog.ui.ContainerRenderer}.119* @type {goog.ui.ContainerRenderer?}120* @private121*/122goog.ui.Container.prototype.renderer_ = null;123124125/**126* Container orientation; determines layout and default keyboard navigation.127* @type {?goog.ui.Container.Orientation}128* @private129*/130goog.ui.Container.prototype.orientation_ = null;131132133/**134* Whether the container is set to be visible. Defaults to true.135* @type {boolean}136* @private137*/138goog.ui.Container.prototype.visible_ = true;139140141/**142* Whether the container is enabled and reacting to keyboard and mouse events.143* Defaults to true.144* @type {boolean}145* @private146*/147goog.ui.Container.prototype.enabled_ = true;148149150/**151* Whether the container supports keyboard focus. Defaults to true. Focusable152* containers have a `tabIndex` and can be navigated to via the keyboard.153* @type {boolean}154* @private155*/156goog.ui.Container.prototype.focusable_ = true;157158159/**160* The 0-based index of the currently highlighted control in the container161* (-1 if none).162* @type {number}163* @private164*/165goog.ui.Container.prototype.highlightedIndex_ = -1;166167168/**169* The currently open (expanded) control in the container (null if none).170* @type {goog.ui.Control?}171* @private172*/173goog.ui.Container.prototype.openItem_ = null;174175176/**177* Whether the mouse button is held down. Defaults to false. This flag is set178* when the user mouses down over the container, and remains set until they179* release the mouse button.180* @type {boolean}181* @private182*/183goog.ui.Container.prototype.mouseButtonPressed_ = false;184185186/**187* Whether focus of child components should be allowed. Only effective if188* focusable_ is set to false.189* @type {boolean}190* @private191*/192goog.ui.Container.prototype.allowFocusableChildren_ = false;193194195/**196* Whether highlighting a child component should also open it.197* @type {boolean}198* @private199*/200goog.ui.Container.prototype.openFollowsHighlight_ = true;201202203/**204* Map of DOM IDs to child controls. Each key is the DOM ID of a child205* control's root element; each value is a reference to the child control206* itself. Used for looking up the child control corresponding to a DOM207* node in O(1) time.208* @type {?Object}209* @private210*/211goog.ui.Container.prototype.childElementIdMap_ = null;212213214// Event handler and renderer management.215216217/**218* Returns the DOM element on which the container is listening for keyboard219* events (null if none).220* @return {Element} Element on which the container is listening for key221* events.222*/223goog.ui.Container.prototype.getKeyEventTarget = function() {224'use strict';225// Delegate to renderer, unless we've set an explicit target.226return this.keyEventTarget_ || this.renderer_.getKeyEventTarget(this);227};228229230/**231* Attaches an element on which to listen for key events.232* @param {Element|undefined} element The element to attach, or null/undefined233* to attach to the default element.234*/235goog.ui.Container.prototype.setKeyEventTarget = function(element) {236'use strict';237if (this.focusable_) {238var oldTarget = this.getKeyEventTarget();239var inDocument = this.isInDocument();240241this.keyEventTarget_ = element;242var newTarget = this.getKeyEventTarget();243244if (inDocument) {245// Unlisten for events on the old key target. Requires us to reset246// key target state temporarily.247this.keyEventTarget_ = oldTarget;248this.enableFocusHandling_(false);249this.keyEventTarget_ = element;250251// Listen for events on the new key target.252this.getKeyHandler().attach(newTarget);253this.enableFocusHandling_(true);254}255} else {256throw new Error(257'Can\'t set key event target for container ' +258'that doesn\'t support keyboard focus!');259}260};261262263/**264* Returns the keyboard event handler for this container, lazily created the265* first time this method is called. The keyboard event handler listens for266* keyboard events on the container's key event target, as determined by its267* renderer.268* @return {!goog.events.KeyHandler} Keyboard event handler for this container.269*/270goog.ui.Container.prototype.getKeyHandler = function() {271'use strict';272return this.keyHandler_ ||273(this.keyHandler_ = new goog.events.KeyHandler(this.getKeyEventTarget()));274};275276277/**278* Returns the renderer used by this container to render itself or to decorate279* an existing element.280* @return {goog.ui.ContainerRenderer} Renderer used by the container.281*/282goog.ui.Container.prototype.getRenderer = function() {283'use strict';284return this.renderer_;285};286287288/**289* Registers the given renderer with the container. Changing renderers after290* the container has already been rendered or decorated is an error.291* @param {goog.ui.ContainerRenderer} renderer Renderer used by the container.292*/293goog.ui.Container.prototype.setRenderer = function(renderer) {294'use strict';295if (this.getElement()) {296// Too late.297throw new Error(goog.ui.Component.Error.ALREADY_RENDERED);298}299300this.renderer_ = renderer;301};302303304// Standard goog.ui.Component implementation.305306307/**308* Creates the container's DOM.309* @override310*/311goog.ui.Container.prototype.createDom = function() {312'use strict';313// Delegate to renderer.314this.setElementInternal(this.renderer_.createDom(this));315};316317318/**319* Returns the DOM element into which child components are to be rendered,320* or null if the container itself hasn't been rendered yet. Overrides321* {@link goog.ui.Component#getContentElement} by delegating to the renderer.322* @return {Element} Element to contain child elements (null if none).323* @override324*/325goog.ui.Container.prototype.getContentElement = function() {326'use strict';327// Delegate to renderer.328return this.renderer_.getContentElement(this.getElement());329};330331332/**333* Returns true if the given element can be decorated by this container.334* Overrides {@link goog.ui.Component#canDecorate}.335* @param {Element} element Element to decorate.336* @return {boolean} True iff the element can be decorated.337* @override338*/339goog.ui.Container.prototype.canDecorate = function(element) {340'use strict';341// Delegate to renderer.342return this.renderer_.canDecorate(element);343};344345346/**347* Decorates the given element with this container. Overrides {@link348* goog.ui.Component#decorateInternal}. Considered protected.349* @param {Element} element Element to decorate.350* @override351*/352goog.ui.Container.prototype.decorateInternal = function(element) {353'use strict';354// Delegate to renderer.355this.setElementInternal(this.renderer_.decorate(this, element));356// Check whether the decorated element is explicitly styled to be invisible.357if (element.style.display == 'none') {358this.visible_ = false;359}360};361362363/**364* Configures the container after its DOM has been rendered, and sets up event365* handling. Overrides {@link goog.ui.Component#enterDocument}.366* @override367* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration368*/369goog.ui.Container.prototype.enterDocument = function() {370'use strict';371goog.ui.Container.superClass_.enterDocument.call(this);372373this.forEachChild(function(child) {374'use strict';375if (child.isInDocument()) {376this.registerChildId_(child);377}378}, this);379380var elem = this.getElement();381382// Call the renderer's initializeDom method to initialize the container's DOM.383this.renderer_.initializeDom(this);384385// Initialize visibility (opt_force = true, so we don't dispatch events).386this.setVisible(this.visible_, true);387388var MouseEventType = goog.ui.ComponentUtil.getMouseEventType(this);389390// Handle events dispatched by child controls.391this.getHandler()392.listen(this, goog.ui.Component.EventType.ENTER, this.handleEnterItem)393.listen(394this, goog.ui.Component.EventType.HIGHLIGHT, this.handleHighlightItem)395.listen(396this, goog.ui.Component.EventType.UNHIGHLIGHT,397this.handleUnHighlightItem)398.listen(this, goog.ui.Component.EventType.OPEN, this.handleOpenItem)399.listen(this, goog.ui.Component.EventType.CLOSE, this.handleCloseItem)400401// Handle mouse events.402.listen(elem, MouseEventType.MOUSEDOWN, this.handleMouseDown)403.listen(404goog.dom.getOwnerDocument(elem),405[MouseEventType.MOUSEUP, MouseEventType.MOUSECANCEL],406this.handleDocumentMouseUp)407408// Handle mouse events on behalf of controls in the container.409.listen(410elem,411[412MouseEventType.MOUSEDOWN, MouseEventType.MOUSEUP,413MouseEventType.MOUSECANCEL, goog.events.EventType.MOUSEOVER,414goog.events.EventType.MOUSEOUT, goog.events.EventType.CONTEXTMENU415],416this.handleChildMouseEvents);417418if (this.pointerEventsEnabled()) {419// Prevent pointer events from capturing the target element so they behave420// more like mouse events.421this.getHandler().listen(422elem, goog.events.EventType.GOTPOINTERCAPTURE,423this.preventPointerCapture_);424}425426// If the container is focusable, set up keyboard event handling.427if (this.isFocusable()) {428this.enableFocusHandling_(true);429}430};431432433/**434* @param {!goog.events.BrowserEvent} e Event to handle.435* @private436*/437goog.ui.Container.prototype.preventPointerCapture_ = function(e) {438'use strict';439var elem = /** @type {!Element} */ (e.target);440if (!!elem.releasePointerCapture) {441elem.releasePointerCapture(e.pointerId);442}443};444445446/**447* Sets up listening for events applicable to focusable containers.448* @param {boolean} enable Whether to enable or disable focus handling.449* @private450*/451goog.ui.Container.prototype.enableFocusHandling_ = function(enable) {452'use strict';453var handler = this.getHandler();454var keyTarget = this.getKeyEventTarget();455if (enable) {456handler.listen(keyTarget, goog.events.EventType.FOCUS, this.handleFocus)457.listen(keyTarget, goog.events.EventType.BLUR, this.handleBlur)458.listen(459this.getKeyHandler(), goog.events.KeyHandler.EventType.KEY,460this.handleKeyEvent);461} else {462handler.unlisten(keyTarget, goog.events.EventType.FOCUS, this.handleFocus)463.unlisten(keyTarget, goog.events.EventType.BLUR, this.handleBlur)464.unlisten(465this.getKeyHandler(), goog.events.KeyHandler.EventType.KEY,466this.handleKeyEvent);467}468};469470471/**472* Cleans up the container before its DOM is removed from the document, and473* removes event handlers. Overrides {@link goog.ui.Component#exitDocument}.474* @override475*/476goog.ui.Container.prototype.exitDocument = function() {477'use strict';478// {@link #setHighlightedIndex} has to be called before479// {@link goog.ui.Component#exitDocument}, otherwise it has no effect.480this.setHighlightedIndex(-1);481482if (this.openItem_) {483this.openItem_.setOpen(false);484}485486this.mouseButtonPressed_ = false;487488goog.ui.Container.superClass_.exitDocument.call(this);489};490491492/** @override */493goog.ui.Container.prototype.disposeInternal = function() {494'use strict';495goog.ui.Container.superClass_.disposeInternal.call(this);496497if (this.keyHandler_) {498this.keyHandler_.dispose();499this.keyHandler_ = null;500}501502this.keyEventTarget_ = null;503this.childElementIdMap_ = null;504this.openItem_ = null;505this.renderer_ = null;506};507508509// Default event handlers.510511512/**513* Handles ENTER events raised by child controls when they are navigated to.514* @param {goog.events.Event} e ENTER event to handle.515* @return {boolean} Whether to prevent handleMouseOver from handling516* the event.517*/518goog.ui.Container.prototype.handleEnterItem = function(e) {519'use strict';520// Allow the Control to highlight itself.521return true;522};523524525/**526* Handles HIGHLIGHT events dispatched by items in the container when527* they are highlighted.528* @param {goog.events.Event} e Highlight event to handle.529* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration530*/531goog.ui.Container.prototype.handleHighlightItem = function(e) {532'use strict';533var index = this.indexOfChild(/** @type {goog.ui.Control} */ (e.target));534if (index > -1 && index != this.highlightedIndex_) {535var item = this.getHighlighted();536if (item) {537// Un-highlight previously highlighted item.538item.setHighlighted(false);539}540541this.highlightedIndex_ = index;542item = this.getHighlighted();543544if (this.isMouseButtonPressed()) {545// Activate item when mouse button is pressed, to allow MacOS-style546// dragging to choose menu items. Although this should only truly547// happen if the highlight is due to mouse movements, there is little548// harm in doing it for keyboard or programmatic highlights.549item.setActive(true);550}551552// Update open item if open item needs follow highlight.553if (this.openFollowsHighlight_ && this.openItem_ &&554item != this.openItem_) {555if (item.isSupportedState(goog.ui.Component.State.OPENED)) {556item.setOpen(true);557} else {558this.openItem_.setOpen(false);559}560}561}562563var element = this.getElement();564goog.asserts.assert(565element, 'The DOM element for the container cannot be null.');566if (e.target.getElement() != null) {567goog.a11y.aria.setState(568element, goog.a11y.aria.State.ACTIVEDESCENDANT,569e.target.getElement().id);570}571};572573574/**575* Handles UNHIGHLIGHT events dispatched by items in the container when576* they are unhighlighted.577* @param {goog.events.Event} e Unhighlight event to handle.578*/579goog.ui.Container.prototype.handleUnHighlightItem = function(e) {580'use strict';581if (e.target == this.getHighlighted()) {582this.highlightedIndex_ = -1;583}584var element = this.getElement();585goog.asserts.assert(586element, 'The DOM element for the container cannot be null.');587// Setting certain ARIA attributes to empty strings is problematic.588// Just remove the attribute instead.589goog.a11y.aria.removeState(element, goog.a11y.aria.State.ACTIVEDESCENDANT);590};591592593/**594* Handles OPEN events dispatched by items in the container when they are595* opened.596* @param {goog.events.Event} e Open event to handle.597*/598goog.ui.Container.prototype.handleOpenItem = function(e) {599'use strict';600var item = /** @type {goog.ui.Control} */ (e.target);601if (item && item != this.openItem_ && item.getParent() == this) {602if (this.openItem_) {603this.openItem_.setOpen(false);604}605this.openItem_ = item;606}607};608609610/**611* Handles CLOSE events dispatched by items in the container when they are612* closed.613* @param {goog.events.Event} e Close event to handle.614* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration615*/616goog.ui.Container.prototype.handleCloseItem = function(e) {617'use strict';618if (e.target == this.openItem_) {619this.openItem_ = null;620}621622var element = this.getElement();623var targetEl = e.target.getElement();624// Set the active descendant to the menu item when its submenu is closed and625// it is still highlighted. This can sometimes be called when the menuitem is626// unhighlighted because the focus moved elsewhere, do nothing at that point.627if (element && e.target.isHighlighted() && targetEl) {628goog.a11y.aria.setActiveDescendant(element, targetEl);629}630};631632633/**634* Handles mousedown events over the container. The default implementation635* sets the "mouse button pressed" flag and, if the container is focusable,636* grabs keyboard focus.637* @param {goog.events.BrowserEvent} e Mousedown event to handle.638*/639goog.ui.Container.prototype.handleMouseDown = function(e) {640'use strict';641if (this.enabled_) {642this.setMouseButtonPressed(true);643}644645var keyTarget = this.getKeyEventTarget();646if (keyTarget && goog.dom.isFocusableTabIndex(keyTarget)) {647// The container is configured to receive keyboard focus.648keyTarget.focus();649} else {650// The control isn't configured to receive keyboard focus; prevent it651// from stealing focus or destroying the selection.652e.preventDefault();653}654};655656657/**658* Handles mouseup events over the document. The default implementation659* clears the "mouse button pressed" flag.660* @param {goog.events.BrowserEvent} e Mouseup event to handle.661*/662goog.ui.Container.prototype.handleDocumentMouseUp = function(e) {663'use strict';664this.setMouseButtonPressed(false);665};666667668/**669* Handles mouse events originating from nodes belonging to the controls hosted670* in the container. Locates the child control based on the DOM node that671* dispatched the event, and forwards the event to the control for handling.672* @param {goog.events.BrowserEvent} e Mouse event to handle.673* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration674*/675goog.ui.Container.prototype.handleChildMouseEvents = function(e) {676'use strict';677var MouseEventType = goog.ui.ComponentUtil.getMouseEventType(this);678679var control = this.getOwnerControl(/** @type {Node} */ (e.target));680if (control) {681// Child control identified; forward the event.682switch (e.type) {683case MouseEventType.MOUSEDOWN:684control.handleMouseDown(e);685break;686case MouseEventType.MOUSEUP:687case MouseEventType.MOUSECANCEL:688control.handleMouseUp(e);689break;690case goog.events.EventType.MOUSEOVER:691control.handleMouseOver(e);692break;693case goog.events.EventType.MOUSEOUT:694control.handleMouseOut(e);695break;696case goog.events.EventType.CONTEXTMENU:697control.handleContextMenu(e);698break;699}700}701};702703704/**705* Returns the child control that owns the given DOM node, or null if no such706* control is found.707* @param {Node} node DOM node whose owner is to be returned.708* @return {goog.ui.Control?} Control hosted in the container to which the node709* belongs (if found).710* @protected711* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration712*/713goog.ui.Container.prototype.getOwnerControl = function(node) {714'use strict';715// Ensure that this container actually has child controls before716// looking up the owner.717if (this.childElementIdMap_) {718var elem = this.getElement();719// See http://b/2964418 . IE9 appears to evaluate '!=' incorrectly, so720// using '!==' instead.721// TODO(user): Possibly revert this change if/when IE9 fixes the issue.722while (node && node !== elem) {723var id = node.id;724if (id in this.childElementIdMap_) {725return this.childElementIdMap_[id];726}727node = node.parentNode;728}729}730return null;731};732733734/**735* Handles focus events raised when the container's key event target receives736* keyboard focus.737* @param {goog.events.BrowserEvent} e Focus event to handle.738*/739goog.ui.Container.prototype.handleFocus = function(e) {740// No-op in the base class.741};742743744/**745* Handles blur events raised when the container's key event target loses746* keyboard focus. The default implementation clears the highlight index.747* @param {goog.events.BrowserEvent} e Blur event to handle.748*/749goog.ui.Container.prototype.handleBlur = function(e) {750'use strict';751this.setHighlightedIndex(-1);752this.setMouseButtonPressed(false);753// If the container loses focus, and one of its children is open, close it.754if (this.openItem_) {755this.openItem_.setOpen(false);756}757};758759760/**761* Attempts to handle a keyboard event, if the control is enabled, by calling762* {@link handleKeyEventInternal}. Considered protected; should only be used763* within this package and by subclasses.764* @param {goog.events.KeyEvent} e Key event to handle.765* @return {boolean} Whether the key event was handled.766*/767goog.ui.Container.prototype.handleKeyEvent = function(e) {768'use strict';769if (this.isEnabled() && this.isVisible() &&770(this.getChildCount() != 0 || this.keyEventTarget_) &&771this.handleKeyEventInternal(e)) {772e.preventDefault();773e.stopPropagation();774return true;775}776return false;777};778779780/**781* Attempts to handle a keyboard event; returns true if the event was handled,782* false otherwise. If the container is enabled, and a child is highlighted,783* calls the child control's `handleKeyEvent` method to give the control784* a chance to handle the event first.785* @param {goog.events.KeyEvent} e Key event to handle.786* @return {boolean} Whether the event was handled by the container (or one of787* its children).788*/789goog.ui.Container.prototype.handleKeyEventInternal = function(e) {790'use strict';791// Give the highlighted control the chance to handle the key event.792var highlighted = this.getHighlighted();793if (highlighted && typeof highlighted.handleKeyEvent == 'function' &&794highlighted.handleKeyEvent(e)) {795return true;796}797798// Give the open control the chance to handle the key event.799if (this.openItem_ && this.openItem_ != highlighted &&800typeof this.openItem_.handleKeyEvent == 'function' &&801this.openItem_.handleKeyEvent(e)) {802return true;803}804805// Do not handle the key event if any modifier key is pressed.806if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) {807return false;808}809810// Either nothing is highlighted, or the highlighted control didn't handle811// the key event, so attempt to handle it here.812switch (e.keyCode) {813case goog.events.KeyCodes.ESC:814if (this.isFocusable()) {815this.getKeyEventTarget().blur();816} else {817return false;818}819break;820821case goog.events.KeyCodes.HOME:822this.highlightFirst();823break;824825case goog.events.KeyCodes.END:826this.highlightLast();827break;828829case goog.events.KeyCodes.UP:830if (this.orientation_ == goog.ui.Container.Orientation.VERTICAL) {831this.highlightPrevious();832} else {833return false;834}835break;836837case goog.events.KeyCodes.LEFT:838if (this.orientation_ == goog.ui.Container.Orientation.HORIZONTAL) {839if (this.isRightToLeft()) {840this.highlightNext();841} else {842this.highlightPrevious();843}844} else {845return false;846}847break;848849case goog.events.KeyCodes.DOWN:850if (this.orientation_ == goog.ui.Container.Orientation.VERTICAL) {851this.highlightNext();852} else {853return false;854}855break;856857case goog.events.KeyCodes.RIGHT:858if (this.orientation_ == goog.ui.Container.Orientation.HORIZONTAL) {859if (this.isRightToLeft()) {860this.highlightPrevious();861} else {862this.highlightNext();863}864} else {865return false;866}867break;868869default:870return false;871}872873return true;874};875876877// Child component management.878879880/**881* Creates a DOM ID for the child control and registers it to an internal882* hash table to be able to find it fast by id.883* @param {goog.ui.Component} child The child control. Its root element has884* to be created yet.885* @private886*/887goog.ui.Container.prototype.registerChildId_ = function(child) {888'use strict';889// Map the DOM ID of the control's root element to the control itself.890var childElem = child.getElement();891892// If the control's root element doesn't have a DOM ID assign one.893var id = childElem.id || (childElem.id = child.getId());894895// Lazily create the child element ID map on first use.896if (!this.childElementIdMap_) {897this.childElementIdMap_ = {};898}899this.childElementIdMap_[id] = child;900};901902903/**904* Adds the specified control as the last child of this container. See905* {@link goog.ui.Container#addChildAt} for detailed semantics.906* @param {goog.ui.Component} child The new child control.907* @param {boolean=} opt_render Whether the new child should be rendered908* immediately after being added (defaults to false).909* @override910*/911goog.ui.Container.prototype.addChild = function(child, opt_render) {912'use strict';913goog.asserts.assertInstanceof(914child, goog.ui.Control, 'The child of a container must be a control');915goog.ui.Container.superClass_.addChild.call(this, child, opt_render);916};917918919/**920* Overrides {@link goog.ui.Container#getChild} to make it clear that it921* only returns {@link goog.ui.Control}s.922* @param {string} id Child component ID.923* @return {goog.ui.Control} The child with the given ID; null if none.924* @override925*/926goog.ui.Container.prototype.getChild;927928929/**930* Overrides {@link goog.ui.Container#getChildAt} to make it clear that it931* only returns {@link goog.ui.Control}s.932* @param {number} index 0-based index.933* @return {goog.ui.Control} The child with the given ID; null if none.934* @override935*/936goog.ui.Container.prototype.getChildAt;937938939/**940* Adds the control as a child of this container at the given 0-based index.941* Overrides {@link goog.ui.Component#addChildAt} by also updating the942* container's highlight index. Since {@link goog.ui.Component#addChild} uses943* {@link #addChildAt} internally, we only need to override this method.944* @param {goog.ui.Component} control New child.945* @param {number} index Index at which the new child is to be added.946* @param {boolean=} opt_render Whether the new child should be rendered947* immediately after being added (defaults to false).948* @override949*/950goog.ui.Container.prototype.addChildAt = function(control, index, opt_render) {951'use strict';952goog.asserts.assertInstanceof(control, goog.ui.Control);953954// Make sure the child control dispatches HIGHLIGHT, UNHIGHLIGHT, OPEN, and955// CLOSE events, and that it doesn't steal keyboard focus.956control.setDispatchTransitionEvents(goog.ui.Component.State.HOVER, true);957control.setDispatchTransitionEvents(goog.ui.Component.State.OPENED, true);958if (this.isFocusable() || !this.isFocusableChildrenAllowed()) {959control.setSupportedState(goog.ui.Component.State.FOCUSED, false);960}961962// Disable mouse event handling by child controls.963control.setHandleMouseEvents(false);964965var srcIndex =966(control.getParent() == this) ? this.indexOfChild(control) : -1;967968// Let the superclass implementation do the work.969goog.ui.Container.superClass_.addChildAt.call(970this, control, index, opt_render);971972if (control.isInDocument() && this.isInDocument()) {973this.registerChildId_(control);974}975976this.updateHighlightedIndex_(srcIndex, index);977};978979980/**981* Updates the highlighted index when children are added or moved.982* @param {number} fromIndex Index of the child before it was moved, or -1 if983* the child was added.984* @param {number} toIndex Index of the child after it was moved or added.985* @private986*/987goog.ui.Container.prototype.updateHighlightedIndex_ = function(988fromIndex, toIndex) {989'use strict';990if (fromIndex == -1) {991fromIndex = this.getChildCount();992}993if (fromIndex == this.highlightedIndex_) {994// The highlighted element itself was moved.995this.highlightedIndex_ = Math.min(this.getChildCount() - 1, toIndex);996} else if (997fromIndex > this.highlightedIndex_ && toIndex <= this.highlightedIndex_) {998// The control was added or moved behind the highlighted index.999this.highlightedIndex_++;1000} else if (1001fromIndex < this.highlightedIndex_ && toIndex > this.highlightedIndex_) {1002// The control was moved from before to behind the highlighted index.1003this.highlightedIndex_--;1004}1005};100610071008/**1009* Removes a child control. Overrides {@link goog.ui.Component#removeChild} by1010* updating the highlight index. Since {@link goog.ui.Component#removeChildAt}1011* uses {@link #removeChild} internally, we only need to override this method.1012* @param {string|goog.ui.Component} control The ID of the child to remove, or1013* the control itself.1014* @param {boolean=} opt_unrender Whether to call `exitDocument` on the1015* removed control, and detach its DOM from the document (defaults to1016* false).1017* @return {?goog.ui.Control} The removed control, if any.1018* @override1019*/1020goog.ui.Container.prototype.removeChild = function(control, opt_unrender) {1021'use strict';1022control = (typeof control === 'string') ? this.getChild(control) : control;1023goog.asserts.assertInstanceof(control, goog.ui.Control);10241025if (control) {1026var index = this.indexOfChild(control);1027if (index != -1) {1028if (index == this.highlightedIndex_) {1029control.setHighlighted(false);1030this.highlightedIndex_ = -1;1031} else if (index < this.highlightedIndex_) {1032this.highlightedIndex_--;1033}1034}10351036// Remove the mapping from the child element ID map.1037var childElem = control.getElement();1038if (childElem && childElem.id && this.childElementIdMap_) {1039goog.object.remove(this.childElementIdMap_, childElem.id);1040}1041}10421043control = /** @type {!goog.ui.Control} */ (1044goog.ui.Container.superClass_.removeChild.call(1045this, control, opt_unrender));10461047// Re-enable mouse event handling (in case the control is reused elsewhere).1048control.setHandleMouseEvents(true);10491050return control;1051};105210531054// Container state management.105510561057/**1058* Returns the container's orientation.1059* @return {?goog.ui.Container.Orientation} Container orientation.1060*/1061goog.ui.Container.prototype.getOrientation = function() {1062'use strict';1063return this.orientation_;1064};106510661067/**1068* Sets the container's orientation.1069* @param {goog.ui.Container.Orientation} orientation Container orientation.1070*/1071// TODO(attila): Do we need to support containers with dynamic orientation?1072goog.ui.Container.prototype.setOrientation = function(orientation) {1073'use strict';1074if (this.getElement()) {1075// Too late.1076throw new Error(goog.ui.Component.Error.ALREADY_RENDERED);1077}10781079this.orientation_ = orientation;1080};108110821083/**1084* Returns true if the container's visibility is set to visible, false if1085* it is set to hidden. A container that is set to hidden is guaranteed1086* to be hidden from the user, but the reverse isn't necessarily true.1087* A container may be set to visible but can otherwise be obscured by another1088* element, rendered off-screen, or hidden using direct CSS manipulation.1089* @return {boolean} Whether the container is set to be visible.1090*/1091goog.ui.Container.prototype.isVisible = function() {1092'use strict';1093return this.visible_;1094};109510961097/**1098* Shows or hides the container. Does nothing if the container already has1099* the requested visibility. Otherwise, dispatches a SHOW or HIDE event as1100* appropriate, giving listeners a chance to prevent the visibility change.1101* @param {boolean} visible Whether to show or hide the container.1102* @param {boolean=} opt_force If true, doesn't check whether the container1103* already has the requested visibility, and doesn't dispatch any events.1104* @return {boolean} Whether the visibility was changed.1105*/1106goog.ui.Container.prototype.setVisible = function(visible, opt_force) {1107'use strict';1108if (opt_force ||1109(this.visible_ != visible &&1110this.dispatchEvent(1111visible ? goog.ui.Component.EventType.SHOW :1112goog.ui.Component.EventType.HIDE))) {1113this.visible_ = visible;11141115var elem = this.getElement();1116if (elem) {1117goog.style.setElementShown(elem, visible);1118if (this.isFocusable()) {1119// Enable keyboard access only for enabled & visible containers.1120this.renderer_.enableTabIndex(1121this.getKeyEventTarget(), this.enabled_ && this.visible_);1122}1123if (!opt_force) {1124this.dispatchEvent(1125this.visible_ ? goog.ui.Container.EventType.AFTER_SHOW :1126goog.ui.Container.EventType.AFTER_HIDE);1127}1128}11291130return true;1131}11321133return false;1134};113511361137/**1138* Returns true if the container is enabled, false otherwise.1139* @return {boolean} Whether the container is enabled.1140*/1141goog.ui.Container.prototype.isEnabled = function() {1142'use strict';1143return this.enabled_;1144};114511461147/**1148* Enables/disables the container based on the `enable` argument.1149* Dispatches an `ENABLED` or `DISABLED` event prior to changing1150* the container's state, which may be caught and canceled to prevent the1151* container from changing state. Also enables/disables child controls.1152* @param {boolean} enable Whether to enable or disable the container.1153*/1154goog.ui.Container.prototype.setEnabled = function(enable) {1155'use strict';1156if (this.enabled_ != enable &&1157this.dispatchEvent(1158enable ? goog.ui.Component.EventType.ENABLE :1159goog.ui.Component.EventType.DISABLE)) {1160if (enable) {1161// Flag the container as enabled first, then update children. This is1162// because controls can't be enabled if their parent is disabled.1163this.enabled_ = true;1164this.forEachChild(function(child) {1165'use strict';1166// Enable child control unless it is flagged.1167if (child.wasDisabled) {1168delete child.wasDisabled;1169} else {1170child.setEnabled(true);1171}1172});1173} else {1174// Disable children first, then flag the container as disabled. This is1175// because controls can't be disabled if their parent is already disabled.1176this.forEachChild(function(child) {1177'use strict';1178// Disable child control, or flag it if it's already disabled.1179if (child.isEnabled()) {1180child.setEnabled(false);1181} else {1182child.wasDisabled = true;1183}1184});1185this.enabled_ = false;1186this.setMouseButtonPressed(false);1187}11881189if (this.isFocusable()) {1190// Enable keyboard access only for enabled & visible components.1191this.renderer_.enableTabIndex(1192this.getKeyEventTarget(), enable && this.visible_);1193}1194}1195};119611971198/**1199* Returns true if the container is focusable, false otherwise. The default1200* is true. Focusable containers always have a tab index and allocate a key1201* handler to handle keyboard events while focused.1202* @return {boolean} Whether the component is focusable.1203*/1204goog.ui.Container.prototype.isFocusable = function() {1205'use strict';1206return this.focusable_;1207};120812091210/**1211* Sets whether the container is focusable. The default is true. Focusable1212* containers always have a tab index and allocate a key handler to handle1213* keyboard events while focused.1214* @param {boolean} focusable Whether the component is to be focusable.1215*/1216goog.ui.Container.prototype.setFocusable = function(focusable) {1217'use strict';1218if (focusable != this.focusable_ && this.isInDocument()) {1219this.enableFocusHandling_(focusable);1220}1221this.focusable_ = focusable;1222if (this.enabled_ && this.visible_) {1223this.renderer_.enableTabIndex(this.getKeyEventTarget(), focusable);1224}1225};122612271228/**1229* Returns true if the container allows children to be focusable, false1230* otherwise. Only effective if the container is not focusable.1231* @return {boolean} Whether children should be focusable.1232*/1233goog.ui.Container.prototype.isFocusableChildrenAllowed = function() {1234'use strict';1235return this.allowFocusableChildren_;1236};123712381239/**1240* Sets whether the container allows children to be focusable, false1241* otherwise. Only effective if the container is not focusable.1242* @param {boolean} focusable Whether the children should be focusable.1243*/1244goog.ui.Container.prototype.setFocusableChildrenAllowed = function(focusable) {1245'use strict';1246this.allowFocusableChildren_ = focusable;1247};124812491250/**1251* @return {boolean} Whether highlighting a child component should also open it.1252*/1253goog.ui.Container.prototype.isOpenFollowsHighlight = function() {1254'use strict';1255return this.openFollowsHighlight_;1256};125712581259/**1260* Sets whether highlighting a child component should also open it.1261* @param {boolean} follow Whether highlighting a child component also opens it.1262*/1263goog.ui.Container.prototype.setOpenFollowsHighlight = function(follow) {1264'use strict';1265this.openFollowsHighlight_ = follow;1266};126712681269// Highlight management.127012711272/**1273* Returns the index of the currently highlighted item (-1 if none).1274* @return {number} Index of the currently highlighted item.1275*/1276goog.ui.Container.prototype.getHighlightedIndex = function() {1277'use strict';1278return this.highlightedIndex_;1279};128012811282/**1283* Highlights the item at the given 0-based index (if any). If another item1284* was previously highlighted, it is un-highlighted.1285* @param {number} index Index of item to highlight (-1 removes the current1286* highlight).1287*/1288goog.ui.Container.prototype.setHighlightedIndex = function(index) {1289'use strict';1290var child = this.getChildAt(index);1291if (child) {1292child.setHighlighted(true);1293} else if (this.highlightedIndex_ > -1) {1294this.getHighlighted().setHighlighted(false);1295}1296};129712981299/**1300* Highlights the given item if it exists and is a child of the container;1301* otherwise un-highlights the currently highlighted item.1302* @param {goog.ui.Control} item Item to highlight.1303*/1304goog.ui.Container.prototype.setHighlighted = function(item) {1305'use strict';1306this.setHighlightedIndex(this.indexOfChild(item));1307};130813091310/**1311* Returns the currently highlighted item (if any).1312* @return {goog.ui.Control?} Highlighted item (null if none).1313*/1314goog.ui.Container.prototype.getHighlighted = function() {1315'use strict';1316return this.getChildAt(this.highlightedIndex_);1317};131813191320/**1321* Highlights the first highlightable item in the container1322*/1323goog.ui.Container.prototype.highlightFirst = function() {1324'use strict';1325this.highlightHelper(function(index, max) {1326'use strict';1327return (index + 1) % max;1328}, this.getChildCount() - 1);1329};133013311332/**1333* Highlights the last highlightable item in the container.1334*/1335goog.ui.Container.prototype.highlightLast = function() {1336'use strict';1337this.highlightHelper(function(index, max) {1338'use strict';1339index--;1340return index < 0 ? max - 1 : index;1341}, 0);1342};134313441345/**1346* Highlights the next highlightable item (or the first if nothing is currently1347* highlighted).1348*/1349goog.ui.Container.prototype.highlightNext = function() {1350'use strict';1351this.highlightHelper(function(index, max) {1352'use strict';1353return (index + 1) % max;1354}, this.highlightedIndex_);1355};135613571358/**1359* Highlights the previous highlightable item (or the last if nothing is1360* currently highlighted).1361*/1362goog.ui.Container.prototype.highlightPrevious = function() {1363'use strict';1364this.highlightHelper(function(index, max) {1365'use strict';1366index--;1367return index < 0 ? max - 1 : index;1368}, this.highlightedIndex_);1369};137013711372/**1373* Helper function that manages the details of moving the highlight among1374* child controls in response to keyboard events.1375* @param {function(this: goog.ui.Container, number, number) : number} fn1376* Function that accepts the current and maximum indices, and returns the1377* next index to check.1378* @param {number} startIndex Start index.1379* @return {boolean} Whether the highlight has changed.1380* @protected1381*/1382goog.ui.Container.prototype.highlightHelper = function(fn, startIndex) {1383'use strict';1384// If the start index is -1 (meaning there's nothing currently highlighted),1385// try starting from the currently open item, if any.1386var curIndex =1387startIndex < 0 ? this.indexOfChild(this.openItem_) : startIndex;1388var numItems = this.getChildCount();13891390curIndex = fn.call(this, curIndex, numItems);1391var visited = 0;1392while (visited <= numItems) {1393var control = this.getChildAt(curIndex);1394if (control && this.canHighlightItem(control)) {1395this.setHighlightedIndexFromKeyEvent(curIndex);1396return true;1397}1398visited++;1399curIndex = fn.call(this, curIndex, numItems);1400}1401return false;1402};140314041405/**1406* Returns whether the given item can be highlighted.1407* @param {goog.ui.Control} item The item to check.1408* @return {boolean} Whether the item can be highlighted.1409* @protected1410*/1411goog.ui.Container.prototype.canHighlightItem = function(item) {1412'use strict';1413return item.isVisible() && item.isEnabled() &&1414item.isSupportedState(goog.ui.Component.State.HOVER);1415};141614171418/**1419* Helper method that sets the highlighted index to the given index in response1420* to a keyboard event. The base class implementation simply calls the1421* {@link #setHighlightedIndex} method, but subclasses can override this1422* behavior as needed.1423* @param {number} index Index of item to highlight.1424* @protected1425*/1426goog.ui.Container.prototype.setHighlightedIndexFromKeyEvent = function(index) {1427'use strict';1428this.setHighlightedIndex(index);1429};143014311432/**1433* Returns the currently open (expanded) control in the container (null if1434* none).1435* @return {goog.ui.Control?} The currently open control.1436*/1437goog.ui.Container.prototype.getOpenItem = function() {1438'use strict';1439return this.openItem_;1440};144114421443/**1444* Returns true if the mouse button is pressed, false otherwise.1445* @return {boolean} Whether the mouse button is pressed.1446*/1447goog.ui.Container.prototype.isMouseButtonPressed = function() {1448'use strict';1449return this.mouseButtonPressed_;1450};145114521453/**1454* Sets or clears the "mouse button pressed" flag.1455* @param {boolean} pressed Whether the mouse button is presed.1456*/1457goog.ui.Container.prototype.setMouseButtonPressed = function(pressed) {1458'use strict';1459this.mouseButtonPressed_ = pressed;1460};146114621463