1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372 |
- // Copyright 2007 The Closure Library Authors. All Rights Reserved.
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS-IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- /**
- * @fileoverview Base class for containers that host {@link goog.ui.Control}s,
- * such as menus and toolbars. Provides default keyboard and mouse event
- * handling and child management, based on a generalized version of
- * {@link goog.ui.Menu}.
- *
- * @author attila@google.com (Attila Bodis)
- * @see ../demos/container.html
- */
- // TODO(attila): Fix code/logic duplication between this and goog.ui.Control.
- // TODO(attila): Maybe pull common stuff all the way up into Component...?
- goog.provide('goog.ui.Container');
- goog.provide('goog.ui.Container.EventType');
- goog.provide('goog.ui.Container.Orientation');
- goog.require('goog.a11y.aria');
- goog.require('goog.a11y.aria.State');
- goog.require('goog.asserts');
- goog.require('goog.dom');
- goog.require('goog.events.EventType');
- goog.require('goog.events.KeyCodes');
- goog.require('goog.events.KeyHandler');
- goog.require('goog.object');
- goog.require('goog.style');
- goog.require('goog.ui.Component');
- goog.require('goog.ui.ContainerRenderer');
- goog.require('goog.ui.Control');
- /**
- * Base class for containers. Extends {@link goog.ui.Component} by adding
- * the following:
- * <ul>
- * <li>a {@link goog.events.KeyHandler}, to simplify keyboard handling,
- * <li>a pluggable <em>renderer</em> framework, to simplify the creation of
- * containers without the need to subclass this class,
- * <li>methods to manage child controls hosted in the container,
- * <li>default mouse and keyboard event handling methods.
- * </ul>
- * @param {?goog.ui.Container.Orientation=} opt_orientation Container
- * orientation; defaults to {@code VERTICAL}.
- * @param {goog.ui.ContainerRenderer=} opt_renderer Renderer used to render or
- * decorate the container; defaults to {@link goog.ui.ContainerRenderer}.
- * @param {goog.dom.DomHelper=} opt_domHelper DOM helper, used for document
- * interaction.
- * @extends {goog.ui.Component}
- * @constructor
- */
- goog.ui.Container = function(opt_orientation, opt_renderer, opt_domHelper) {
- goog.ui.Component.call(this, opt_domHelper);
- this.renderer_ = opt_renderer || goog.ui.ContainerRenderer.getInstance();
- this.orientation_ = opt_orientation || this.renderer_.getDefaultOrientation();
- };
- goog.inherits(goog.ui.Container, goog.ui.Component);
- goog.tagUnsealableClass(goog.ui.Container);
- /**
- * Container-specific events.
- * @enum {string}
- */
- goog.ui.Container.EventType = {
- /**
- * Dispatched after a goog.ui.Container becomes visible. Non-cancellable.
- * NOTE(user): This event really shouldn't exist, because the
- * goog.ui.Component.EventType.SHOW event should behave like this one. But the
- * SHOW event for containers has been behaving as other components'
- * BEFORE_SHOW event for a long time, and too much code relies on that old
- * behavior to fix it now.
- */
- AFTER_SHOW: 'aftershow',
- /**
- * Dispatched after a goog.ui.Container becomes invisible. Non-cancellable.
- */
- AFTER_HIDE: 'afterhide'
- };
- /**
- * Container orientation constants.
- * @enum {string}
- */
- goog.ui.Container.Orientation = {
- HORIZONTAL: 'horizontal',
- VERTICAL: 'vertical'
- };
- /**
- * Allows an alternative element to be set to receive key events, otherwise
- * defers to the renderer's element choice.
- * @type {Element|undefined}
- * @private
- */
- goog.ui.Container.prototype.keyEventTarget_ = null;
- /**
- * Keyboard event handler.
- * @type {goog.events.KeyHandler?}
- * @private
- */
- goog.ui.Container.prototype.keyHandler_ = null;
- /**
- * Renderer for the container. Defaults to {@link goog.ui.ContainerRenderer}.
- * @type {goog.ui.ContainerRenderer?}
- * @private
- */
- goog.ui.Container.prototype.renderer_ = null;
- /**
- * Container orientation; determines layout and default keyboard navigation.
- * @type {?goog.ui.Container.Orientation}
- * @private
- */
- goog.ui.Container.prototype.orientation_ = null;
- /**
- * Whether the container is set to be visible. Defaults to true.
- * @type {boolean}
- * @private
- */
- goog.ui.Container.prototype.visible_ = true;
- /**
- * Whether the container is enabled and reacting to keyboard and mouse events.
- * Defaults to true.
- * @type {boolean}
- * @private
- */
- goog.ui.Container.prototype.enabled_ = true;
- /**
- * Whether the container supports keyboard focus. Defaults to true. Focusable
- * containers have a {@code tabIndex} and can be navigated to via the keyboard.
- * @type {boolean}
- * @private
- */
- goog.ui.Container.prototype.focusable_ = true;
- /**
- * The 0-based index of the currently highlighted control in the container
- * (-1 if none).
- * @type {number}
- * @private
- */
- goog.ui.Container.prototype.highlightedIndex_ = -1;
- /**
- * The currently open (expanded) control in the container (null if none).
- * @type {goog.ui.Control?}
- * @private
- */
- goog.ui.Container.prototype.openItem_ = null;
- /**
- * Whether the mouse button is held down. Defaults to false. This flag is set
- * when the user mouses down over the container, and remains set until they
- * release the mouse button.
- * @type {boolean}
- * @private
- */
- goog.ui.Container.prototype.mouseButtonPressed_ = false;
- /**
- * Whether focus of child components should be allowed. Only effective if
- * focusable_ is set to false.
- * @type {boolean}
- * @private
- */
- goog.ui.Container.prototype.allowFocusableChildren_ = false;
- /**
- * Whether highlighting a child component should also open it.
- * @type {boolean}
- * @private
- */
- goog.ui.Container.prototype.openFollowsHighlight_ = true;
- /**
- * Map of DOM IDs to child controls. Each key is the DOM ID of a child
- * control's root element; each value is a reference to the child control
- * itself. Used for looking up the child control corresponding to a DOM
- * node in O(1) time.
- * @type {Object}
- * @private
- */
- goog.ui.Container.prototype.childElementIdMap_ = null;
- // Event handler and renderer management.
- /**
- * Returns the DOM element on which the container is listening for keyboard
- * events (null if none).
- * @return {Element} Element on which the container is listening for key
- * events.
- */
- goog.ui.Container.prototype.getKeyEventTarget = function() {
- // Delegate to renderer, unless we've set an explicit target.
- return this.keyEventTarget_ || this.renderer_.getKeyEventTarget(this);
- };
- /**
- * Attaches an element on which to listen for key events.
- * @param {Element|undefined} element The element to attach, or null/undefined
- * to attach to the default element.
- */
- goog.ui.Container.prototype.setKeyEventTarget = function(element) {
- if (this.focusable_) {
- var oldTarget = this.getKeyEventTarget();
- var inDocument = this.isInDocument();
- this.keyEventTarget_ = element;
- var newTarget = this.getKeyEventTarget();
- if (inDocument) {
- // Unlisten for events on the old key target. Requires us to reset
- // key target state temporarily.
- this.keyEventTarget_ = oldTarget;
- this.enableFocusHandling_(false);
- this.keyEventTarget_ = element;
- // Listen for events on the new key target.
- this.getKeyHandler().attach(newTarget);
- this.enableFocusHandling_(true);
- }
- } else {
- throw Error(
- 'Can\'t set key event target for container ' +
- 'that doesn\'t support keyboard focus!');
- }
- };
- /**
- * Returns the keyboard event handler for this container, lazily created the
- * first time this method is called. The keyboard event handler listens for
- * keyboard events on the container's key event target, as determined by its
- * renderer.
- * @return {!goog.events.KeyHandler} Keyboard event handler for this container.
- */
- goog.ui.Container.prototype.getKeyHandler = function() {
- return this.keyHandler_ ||
- (this.keyHandler_ = new goog.events.KeyHandler(this.getKeyEventTarget()));
- };
- /**
- * Returns the renderer used by this container to render itself or to decorate
- * an existing element.
- * @return {goog.ui.ContainerRenderer} Renderer used by the container.
- */
- goog.ui.Container.prototype.getRenderer = function() {
- return this.renderer_;
- };
- /**
- * Registers the given renderer with the container. Changing renderers after
- * the container has already been rendered or decorated is an error.
- * @param {goog.ui.ContainerRenderer} renderer Renderer used by the container.
- */
- goog.ui.Container.prototype.setRenderer = function(renderer) {
- if (this.getElement()) {
- // Too late.
- throw Error(goog.ui.Component.Error.ALREADY_RENDERED);
- }
- this.renderer_ = renderer;
- };
- // Standard goog.ui.Component implementation.
- /**
- * Creates the container's DOM.
- * @override
- */
- goog.ui.Container.prototype.createDom = function() {
- // Delegate to renderer.
- this.setElementInternal(this.renderer_.createDom(this));
- };
- /**
- * Returns the DOM element into which child components are to be rendered,
- * or null if the container itself hasn't been rendered yet. Overrides
- * {@link goog.ui.Component#getContentElement} by delegating to the renderer.
- * @return {Element} Element to contain child elements (null if none).
- * @override
- */
- goog.ui.Container.prototype.getContentElement = function() {
- // Delegate to renderer.
- return this.renderer_.getContentElement(this.getElement());
- };
- /**
- * Returns true if the given element can be decorated by this container.
- * Overrides {@link goog.ui.Component#canDecorate}.
- * @param {Element} element Element to decorate.
- * @return {boolean} True iff the element can be decorated.
- * @override
- */
- goog.ui.Container.prototype.canDecorate = function(element) {
- // Delegate to renderer.
- return this.renderer_.canDecorate(element);
- };
- /**
- * Decorates the given element with this container. Overrides {@link
- * goog.ui.Component#decorateInternal}. Considered protected.
- * @param {Element} element Element to decorate.
- * @override
- */
- goog.ui.Container.prototype.decorateInternal = function(element) {
- // Delegate to renderer.
- this.setElementInternal(this.renderer_.decorate(this, element));
- // Check whether the decorated element is explicitly styled to be invisible.
- if (element.style.display == 'none') {
- this.visible_ = false;
- }
- };
- /**
- * Configures the container after its DOM has been rendered, and sets up event
- * handling. Overrides {@link goog.ui.Component#enterDocument}.
- * @override
- */
- goog.ui.Container.prototype.enterDocument = function() {
- goog.ui.Container.superClass_.enterDocument.call(this);
- this.forEachChild(function(child) {
- if (child.isInDocument()) {
- this.registerChildId_(child);
- }
- }, this);
- var elem = this.getElement();
- // Call the renderer's initializeDom method to initialize the container's DOM.
- this.renderer_.initializeDom(this);
- // Initialize visibility (opt_force = true, so we don't dispatch events).
- this.setVisible(this.visible_, true);
- // Handle events dispatched by child controls.
- this.getHandler()
- .listen(this, goog.ui.Component.EventType.ENTER, this.handleEnterItem)
- .listen(
- this, goog.ui.Component.EventType.HIGHLIGHT, this.handleHighlightItem)
- .listen(
- this, goog.ui.Component.EventType.UNHIGHLIGHT,
- this.handleUnHighlightItem)
- .listen(this, goog.ui.Component.EventType.OPEN, this.handleOpenItem)
- .listen(this, goog.ui.Component.EventType.CLOSE, this.handleCloseItem)
- .
- // Handle mouse events.
- listen(elem, goog.events.EventType.MOUSEDOWN, this.handleMouseDown)
- .listen(
- goog.dom.getOwnerDocument(elem), goog.events.EventType.MOUSEUP,
- this.handleDocumentMouseUp)
- .
- // Handle mouse events on behalf of controls in the container.
- listen(
- elem,
- [
- goog.events.EventType.MOUSEDOWN, goog.events.EventType.MOUSEUP,
- goog.events.EventType.MOUSEOVER, goog.events.EventType.MOUSEOUT,
- goog.events.EventType.CONTEXTMENU
- ],
- this.handleChildMouseEvents);
- // If the container is focusable, set up keyboard event handling.
- if (this.isFocusable()) {
- this.enableFocusHandling_(true);
- }
- };
- /**
- * Sets up listening for events applicable to focusable containers.
- * @param {boolean} enable Whether to enable or disable focus handling.
- * @private
- */
- goog.ui.Container.prototype.enableFocusHandling_ = function(enable) {
- var handler = this.getHandler();
- var keyTarget = this.getKeyEventTarget();
- if (enable) {
- handler.listen(keyTarget, goog.events.EventType.FOCUS, this.handleFocus)
- .listen(keyTarget, goog.events.EventType.BLUR, this.handleBlur)
- .listen(
- this.getKeyHandler(), goog.events.KeyHandler.EventType.KEY,
- this.handleKeyEvent);
- } else {
- handler.unlisten(keyTarget, goog.events.EventType.FOCUS, this.handleFocus)
- .unlisten(keyTarget, goog.events.EventType.BLUR, this.handleBlur)
- .unlisten(
- this.getKeyHandler(), goog.events.KeyHandler.EventType.KEY,
- this.handleKeyEvent);
- }
- };
- /**
- * Cleans up the container before its DOM is removed from the document, and
- * removes event handlers. Overrides {@link goog.ui.Component#exitDocument}.
- * @override
- */
- goog.ui.Container.prototype.exitDocument = function() {
- // {@link #setHighlightedIndex} has to be called before
- // {@link goog.ui.Component#exitDocument}, otherwise it has no effect.
- this.setHighlightedIndex(-1);
- if (this.openItem_) {
- this.openItem_.setOpen(false);
- }
- this.mouseButtonPressed_ = false;
- goog.ui.Container.superClass_.exitDocument.call(this);
- };
- /** @override */
- goog.ui.Container.prototype.disposeInternal = function() {
- goog.ui.Container.superClass_.disposeInternal.call(this);
- if (this.keyHandler_) {
- this.keyHandler_.dispose();
- this.keyHandler_ = null;
- }
- this.keyEventTarget_ = null;
- this.childElementIdMap_ = null;
- this.openItem_ = null;
- this.renderer_ = null;
- };
- // Default event handlers.
- /**
- * Handles ENTER events raised by child controls when they are navigated to.
- * @param {goog.events.Event} e ENTER event to handle.
- * @return {boolean} Whether to prevent handleMouseOver from handling
- * the event.
- */
- goog.ui.Container.prototype.handleEnterItem = function(e) {
- // Allow the Control to highlight itself.
- return true;
- };
- /**
- * Handles HIGHLIGHT events dispatched by items in the container when
- * they are highlighted.
- * @param {goog.events.Event} e Highlight event to handle.
- */
- goog.ui.Container.prototype.handleHighlightItem = function(e) {
- var index = this.indexOfChild(/** @type {goog.ui.Control} */ (e.target));
- if (index > -1 && index != this.highlightedIndex_) {
- var item = this.getHighlighted();
- if (item) {
- // Un-highlight previously highlighted item.
- item.setHighlighted(false);
- }
- this.highlightedIndex_ = index;
- item = this.getHighlighted();
- if (this.isMouseButtonPressed()) {
- // Activate item when mouse button is pressed, to allow MacOS-style
- // dragging to choose menu items. Although this should only truly
- // happen if the highlight is due to mouse movements, there is little
- // harm in doing it for keyboard or programmatic highlights.
- item.setActive(true);
- }
- // Update open item if open item needs follow highlight.
- if (this.openFollowsHighlight_ && this.openItem_ &&
- item != this.openItem_) {
- if (item.isSupportedState(goog.ui.Component.State.OPENED)) {
- item.setOpen(true);
- } else {
- this.openItem_.setOpen(false);
- }
- }
- }
- var element = this.getElement();
- goog.asserts.assert(
- element, 'The DOM element for the container cannot be null.');
- if (e.target.getElement() != null) {
- goog.a11y.aria.setState(
- element, goog.a11y.aria.State.ACTIVEDESCENDANT,
- e.target.getElement().id);
- }
- };
- /**
- * Handles UNHIGHLIGHT events dispatched by items in the container when
- * they are unhighlighted.
- * @param {goog.events.Event} e Unhighlight event to handle.
- */
- goog.ui.Container.prototype.handleUnHighlightItem = function(e) {
- if (e.target == this.getHighlighted()) {
- this.highlightedIndex_ = -1;
- }
- var element = this.getElement();
- goog.asserts.assert(
- element, 'The DOM element for the container cannot be null.');
- // Setting certain ARIA attributes to empty strings is problematic.
- // Just remove the attribute instead.
- goog.a11y.aria.removeState(element, goog.a11y.aria.State.ACTIVEDESCENDANT);
- };
- /**
- * Handles OPEN events dispatched by items in the container when they are
- * opened.
- * @param {goog.events.Event} e Open event to handle.
- */
- goog.ui.Container.prototype.handleOpenItem = function(e) {
- var item = /** @type {goog.ui.Control} */ (e.target);
- if (item && item != this.openItem_ && item.getParent() == this) {
- if (this.openItem_) {
- this.openItem_.setOpen(false);
- }
- this.openItem_ = item;
- }
- };
- /**
- * Handles CLOSE events dispatched by items in the container when they are
- * closed.
- * @param {goog.events.Event} e Close event to handle.
- */
- goog.ui.Container.prototype.handleCloseItem = function(e) {
- if (e.target == this.openItem_) {
- this.openItem_ = null;
- }
- var element = this.getElement();
- var targetEl = e.target.getElement();
- // Set the active descendant to the menu item when its submenu is closed and
- // it is still highlighted. This can sometimes be called when the menuitem is
- // unhighlighted because the focus moved elsewhere, do nothing at that point.
- if (element && e.target.isHighlighted() && targetEl) {
- goog.a11y.aria.setActiveDescendant(element, targetEl);
- }
- };
- /**
- * Handles mousedown events over the container. The default implementation
- * sets the "mouse button pressed" flag and, if the container is focusable,
- * grabs keyboard focus.
- * @param {goog.events.BrowserEvent} e Mousedown event to handle.
- */
- goog.ui.Container.prototype.handleMouseDown = function(e) {
- if (this.enabled_) {
- this.setMouseButtonPressed(true);
- }
- var keyTarget = this.getKeyEventTarget();
- if (keyTarget && goog.dom.isFocusableTabIndex(keyTarget)) {
- // The container is configured to receive keyboard focus.
- keyTarget.focus();
- } else {
- // The control isn't configured to receive keyboard focus; prevent it
- // from stealing focus or destroying the selection.
- e.preventDefault();
- }
- };
- /**
- * Handles mouseup events over the document. The default implementation
- * clears the "mouse button pressed" flag.
- * @param {goog.events.BrowserEvent} e Mouseup event to handle.
- */
- goog.ui.Container.prototype.handleDocumentMouseUp = function(e) {
- this.setMouseButtonPressed(false);
- };
- /**
- * Handles mouse events originating from nodes belonging to the controls hosted
- * in the container. Locates the child control based on the DOM node that
- * dispatched the event, and forwards the event to the control for handling.
- * @param {goog.events.BrowserEvent} e Mouse event to handle.
- */
- goog.ui.Container.prototype.handleChildMouseEvents = function(e) {
- var control = this.getOwnerControl(/** @type {Node} */ (e.target));
- if (control) {
- // Child control identified; forward the event.
- switch (e.type) {
- case goog.events.EventType.MOUSEDOWN:
- control.handleMouseDown(e);
- break;
- case goog.events.EventType.MOUSEUP:
- control.handleMouseUp(e);
- break;
- case goog.events.EventType.MOUSEOVER:
- control.handleMouseOver(e);
- break;
- case goog.events.EventType.MOUSEOUT:
- control.handleMouseOut(e);
- break;
- case goog.events.EventType.CONTEXTMENU:
- control.handleContextMenu(e);
- break;
- }
- }
- };
- /**
- * Returns the child control that owns the given DOM node, or null if no such
- * control is found.
- * @param {Node} node DOM node whose owner is to be returned.
- * @return {goog.ui.Control?} Control hosted in the container to which the node
- * belongs (if found).
- * @protected
- */
- goog.ui.Container.prototype.getOwnerControl = function(node) {
- // Ensure that this container actually has child controls before
- // looking up the owner.
- if (this.childElementIdMap_) {
- var elem = this.getElement();
- // See http://b/2964418 . IE9 appears to evaluate '!=' incorrectly, so
- // using '!==' instead.
- // TODO(user): Possibly revert this change if/when IE9 fixes the issue.
- while (node && node !== elem) {
- var id = node.id;
- if (id in this.childElementIdMap_) {
- return this.childElementIdMap_[id];
- }
- node = node.parentNode;
- }
- }
- return null;
- };
- /**
- * Handles focus events raised when the container's key event target receives
- * keyboard focus.
- * @param {goog.events.BrowserEvent} e Focus event to handle.
- */
- goog.ui.Container.prototype.handleFocus = function(e) {
- // No-op in the base class.
- };
- /**
- * Handles blur events raised when the container's key event target loses
- * keyboard focus. The default implementation clears the highlight index.
- * @param {goog.events.BrowserEvent} e Blur event to handle.
- */
- goog.ui.Container.prototype.handleBlur = function(e) {
- this.setHighlightedIndex(-1);
- this.setMouseButtonPressed(false);
- // If the container loses focus, and one of its children is open, close it.
- if (this.openItem_) {
- this.openItem_.setOpen(false);
- }
- };
- /**
- * Attempts to handle a keyboard event, if the control is enabled, by calling
- * {@link handleKeyEventInternal}. Considered protected; should only be used
- * within this package and by subclasses.
- * @param {goog.events.KeyEvent} e Key event to handle.
- * @return {boolean} Whether the key event was handled.
- */
- goog.ui.Container.prototype.handleKeyEvent = function(e) {
- if (this.isEnabled() && this.isVisible() &&
- (this.getChildCount() != 0 || this.keyEventTarget_) &&
- this.handleKeyEventInternal(e)) {
- e.preventDefault();
- e.stopPropagation();
- return true;
- }
- return false;
- };
- /**
- * Attempts to handle a keyboard event; returns true if the event was handled,
- * false otherwise. If the container is enabled, and a child is highlighted,
- * calls the child control's {@code handleKeyEvent} method to give the control
- * a chance to handle the event first.
- * @param {goog.events.KeyEvent} e Key event to handle.
- * @return {boolean} Whether the event was handled by the container (or one of
- * its children).
- */
- goog.ui.Container.prototype.handleKeyEventInternal = function(e) {
- // Give the highlighted control the chance to handle the key event.
- var highlighted = this.getHighlighted();
- if (highlighted && typeof highlighted.handleKeyEvent == 'function' &&
- highlighted.handleKeyEvent(e)) {
- return true;
- }
- // Give the open control the chance to handle the key event.
- if (this.openItem_ && this.openItem_ != highlighted &&
- typeof this.openItem_.handleKeyEvent == 'function' &&
- this.openItem_.handleKeyEvent(e)) {
- return true;
- }
- // Do not handle the key event if any modifier key is pressed.
- if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) {
- return false;
- }
- // Either nothing is highlighted, or the highlighted control didn't handle
- // the key event, so attempt to handle it here.
- switch (e.keyCode) {
- case goog.events.KeyCodes.ESC:
- if (this.isFocusable()) {
- this.getKeyEventTarget().blur();
- } else {
- return false;
- }
- break;
- case goog.events.KeyCodes.HOME:
- this.highlightFirst();
- break;
- case goog.events.KeyCodes.END:
- this.highlightLast();
- break;
- case goog.events.KeyCodes.UP:
- if (this.orientation_ == goog.ui.Container.Orientation.VERTICAL) {
- this.highlightPrevious();
- } else {
- return false;
- }
- break;
- case goog.events.KeyCodes.LEFT:
- if (this.orientation_ == goog.ui.Container.Orientation.HORIZONTAL) {
- if (this.isRightToLeft()) {
- this.highlightNext();
- } else {
- this.highlightPrevious();
- }
- } else {
- return false;
- }
- break;
- case goog.events.KeyCodes.DOWN:
- if (this.orientation_ == goog.ui.Container.Orientation.VERTICAL) {
- this.highlightNext();
- } else {
- return false;
- }
- break;
- case goog.events.KeyCodes.RIGHT:
- if (this.orientation_ == goog.ui.Container.Orientation.HORIZONTAL) {
- if (this.isRightToLeft()) {
- this.highlightPrevious();
- } else {
- this.highlightNext();
- }
- } else {
- return false;
- }
- break;
- default:
- return false;
- }
- return true;
- };
- // Child component management.
- /**
- * Creates a DOM ID for the child control and registers it to an internal
- * hash table to be able to find it fast by id.
- * @param {goog.ui.Component} child The child control. Its root element has
- * to be created yet.
- * @private
- */
- goog.ui.Container.prototype.registerChildId_ = function(child) {
- // Map the DOM ID of the control's root element to the control itself.
- var childElem = child.getElement();
- // If the control's root element doesn't have a DOM ID assign one.
- var id = childElem.id || (childElem.id = child.getId());
- // Lazily create the child element ID map on first use.
- if (!this.childElementIdMap_) {
- this.childElementIdMap_ = {};
- }
- this.childElementIdMap_[id] = child;
- };
- /**
- * Adds the specified control as the last child of this container. See
- * {@link goog.ui.Container#addChildAt} for detailed semantics.
- * @param {goog.ui.Component} child The new child control.
- * @param {boolean=} opt_render Whether the new child should be rendered
- * immediately after being added (defaults to false).
- * @override
- */
- goog.ui.Container.prototype.addChild = function(child, opt_render) {
- goog.asserts.assertInstanceof(
- child, goog.ui.Control, 'The child of a container must be a control');
- goog.ui.Container.superClass_.addChild.call(this, child, opt_render);
- };
- /**
- * Overrides {@link goog.ui.Container#getChild} to make it clear that it
- * only returns {@link goog.ui.Control}s.
- * @param {string} id Child component ID.
- * @return {goog.ui.Control} The child with the given ID; null if none.
- * @override
- */
- goog.ui.Container.prototype.getChild;
- /**
- * Overrides {@link goog.ui.Container#getChildAt} to make it clear that it
- * only returns {@link goog.ui.Control}s.
- * @param {number} index 0-based index.
- * @return {goog.ui.Control} The child with the given ID; null if none.
- * @override
- */
- goog.ui.Container.prototype.getChildAt;
- /**
- * Adds the control as a child of this container at the given 0-based index.
- * Overrides {@link goog.ui.Component#addChildAt} by also updating the
- * container's highlight index. Since {@link goog.ui.Component#addChild} uses
- * {@link #addChildAt} internally, we only need to override this method.
- * @param {goog.ui.Component} control New child.
- * @param {number} index Index at which the new child is to be added.
- * @param {boolean=} opt_render Whether the new child should be rendered
- * immediately after being added (defaults to false).
- * @override
- */
- goog.ui.Container.prototype.addChildAt = function(control, index, opt_render) {
- goog.asserts.assertInstanceof(control, goog.ui.Control);
- // Make sure the child control dispatches HIGHLIGHT, UNHIGHLIGHT, OPEN, and
- // CLOSE events, and that it doesn't steal keyboard focus.
- control.setDispatchTransitionEvents(goog.ui.Component.State.HOVER, true);
- control.setDispatchTransitionEvents(goog.ui.Component.State.OPENED, true);
- if (this.isFocusable() || !this.isFocusableChildrenAllowed()) {
- control.setSupportedState(goog.ui.Component.State.FOCUSED, false);
- }
- // Disable mouse event handling by child controls.
- control.setHandleMouseEvents(false);
- var srcIndex =
- (control.getParent() == this) ? this.indexOfChild(control) : -1;
- // Let the superclass implementation do the work.
- goog.ui.Container.superClass_.addChildAt.call(
- this, control, index, opt_render);
- if (control.isInDocument() && this.isInDocument()) {
- this.registerChildId_(control);
- }
- this.updateHighlightedIndex_(srcIndex, index);
- };
- /**
- * Updates the highlighted index when children are added or moved.
- * @param {number} fromIndex Index of the child before it was moved, or -1 if
- * the child was added.
- * @param {number} toIndex Index of the child after it was moved or added.
- * @private
- */
- goog.ui.Container.prototype.updateHighlightedIndex_ = function(
- fromIndex, toIndex) {
- if (fromIndex == -1) {
- fromIndex = this.getChildCount();
- }
- if (fromIndex == this.highlightedIndex_) {
- // The highlighted element itself was moved.
- this.highlightedIndex_ = Math.min(this.getChildCount() - 1, toIndex);
- } else if (
- fromIndex > this.highlightedIndex_ && toIndex <= this.highlightedIndex_) {
- // The control was added or moved behind the highlighted index.
- this.highlightedIndex_++;
- } else if (
- fromIndex < this.highlightedIndex_ && toIndex > this.highlightedIndex_) {
- // The control was moved from before to behind the highlighted index.
- this.highlightedIndex_--;
- }
- };
- /**
- * Removes a child control. Overrides {@link goog.ui.Component#removeChild} by
- * updating the highlight index. Since {@link goog.ui.Component#removeChildAt}
- * uses {@link #removeChild} internally, we only need to override this method.
- * @param {string|goog.ui.Component} control The ID of the child to remove, or
- * the control itself.
- * @param {boolean=} opt_unrender Whether to call {@code exitDocument} on the
- * removed control, and detach its DOM from the document (defaults to
- * false).
- * @return {goog.ui.Control} The removed control, if any.
- * @override
- */
- goog.ui.Container.prototype.removeChild = function(control, opt_unrender) {
- control = goog.isString(control) ? this.getChild(control) : control;
- goog.asserts.assertInstanceof(control, goog.ui.Control);
- if (control) {
- var index = this.indexOfChild(control);
- if (index != -1) {
- if (index == this.highlightedIndex_) {
- control.setHighlighted(false);
- this.highlightedIndex_ = -1;
- } else if (index < this.highlightedIndex_) {
- this.highlightedIndex_--;
- }
- }
- // Remove the mapping from the child element ID map.
- var childElem = control.getElement();
- if (childElem && childElem.id && this.childElementIdMap_) {
- goog.object.remove(this.childElementIdMap_, childElem.id);
- }
- }
- control = /** @type {!goog.ui.Control} */ (
- goog.ui.Container.superClass_.removeChild.call(
- this, control, opt_unrender));
- // Re-enable mouse event handling (in case the control is reused elsewhere).
- control.setHandleMouseEvents(true);
- return control;
- };
- // Container state management.
- /**
- * Returns the container's orientation.
- * @return {?goog.ui.Container.Orientation} Container orientation.
- */
- goog.ui.Container.prototype.getOrientation = function() {
- return this.orientation_;
- };
- /**
- * Sets the container's orientation.
- * @param {goog.ui.Container.Orientation} orientation Container orientation.
- */
- // TODO(attila): Do we need to support containers with dynamic orientation?
- goog.ui.Container.prototype.setOrientation = function(orientation) {
- if (this.getElement()) {
- // Too late.
- throw Error(goog.ui.Component.Error.ALREADY_RENDERED);
- }
- this.orientation_ = orientation;
- };
- /**
- * Returns true if the container's visibility is set to visible, false if
- * it is set to hidden. A container that is set to hidden is guaranteed
- * to be hidden from the user, but the reverse isn't necessarily true.
- * A container may be set to visible but can otherwise be obscured by another
- * element, rendered off-screen, or hidden using direct CSS manipulation.
- * @return {boolean} Whether the container is set to be visible.
- */
- goog.ui.Container.prototype.isVisible = function() {
- return this.visible_;
- };
- /**
- * Shows or hides the container. Does nothing if the container already has
- * the requested visibility. Otherwise, dispatches a SHOW or HIDE event as
- * appropriate, giving listeners a chance to prevent the visibility change.
- * @param {boolean} visible Whether to show or hide the container.
- * @param {boolean=} opt_force If true, doesn't check whether the container
- * already has the requested visibility, and doesn't dispatch any events.
- * @return {boolean} Whether the visibility was changed.
- */
- goog.ui.Container.prototype.setVisible = function(visible, opt_force) {
- if (opt_force || (this.visible_ != visible &&
- this.dispatchEvent(
- visible ? goog.ui.Component.EventType.SHOW :
- goog.ui.Component.EventType.HIDE))) {
- this.visible_ = visible;
- var elem = this.getElement();
- if (elem) {
- goog.style.setElementShown(elem, visible);
- if (this.isFocusable()) {
- // Enable keyboard access only for enabled & visible containers.
- this.renderer_.enableTabIndex(
- this.getKeyEventTarget(), this.enabled_ && this.visible_);
- }
- if (!opt_force) {
- this.dispatchEvent(
- this.visible_ ? goog.ui.Container.EventType.AFTER_SHOW :
- goog.ui.Container.EventType.AFTER_HIDE);
- }
- }
- return true;
- }
- return false;
- };
- /**
- * Returns true if the container is enabled, false otherwise.
- * @return {boolean} Whether the container is enabled.
- */
- goog.ui.Container.prototype.isEnabled = function() {
- return this.enabled_;
- };
- /**
- * Enables/disables the container based on the {@code enable} argument.
- * Dispatches an {@code ENABLED} or {@code DISABLED} event prior to changing
- * the container's state, which may be caught and canceled to prevent the
- * container from changing state. Also enables/disables child controls.
- * @param {boolean} enable Whether to enable or disable the container.
- */
- goog.ui.Container.prototype.setEnabled = function(enable) {
- if (this.enabled_ != enable &&
- this.dispatchEvent(
- enable ? goog.ui.Component.EventType.ENABLE :
- goog.ui.Component.EventType.DISABLE)) {
- if (enable) {
- // Flag the container as enabled first, then update children. This is
- // because controls can't be enabled if their parent is disabled.
- this.enabled_ = true;
- this.forEachChild(function(child) {
- // Enable child control unless it is flagged.
- if (child.wasDisabled) {
- delete child.wasDisabled;
- } else {
- child.setEnabled(true);
- }
- });
- } else {
- // Disable children first, then flag the container as disabled. This is
- // because controls can't be disabled if their parent is already disabled.
- this.forEachChild(function(child) {
- // Disable child control, or flag it if it's already disabled.
- if (child.isEnabled()) {
- child.setEnabled(false);
- } else {
- child.wasDisabled = true;
- }
- });
- this.enabled_ = false;
- this.setMouseButtonPressed(false);
- }
- if (this.isFocusable()) {
- // Enable keyboard access only for enabled & visible components.
- this.renderer_.enableTabIndex(
- this.getKeyEventTarget(), enable && this.visible_);
- }
- }
- };
- /**
- * Returns true if the container is focusable, false otherwise. The default
- * is true. Focusable containers always have a tab index and allocate a key
- * handler to handle keyboard events while focused.
- * @return {boolean} Whether the component is focusable.
- */
- goog.ui.Container.prototype.isFocusable = function() {
- return this.focusable_;
- };
- /**
- * Sets whether the container is focusable. The default is true. Focusable
- * containers always have a tab index and allocate a key handler to handle
- * keyboard events while focused.
- * @param {boolean} focusable Whether the component is to be focusable.
- */
- goog.ui.Container.prototype.setFocusable = function(focusable) {
- if (focusable != this.focusable_ && this.isInDocument()) {
- this.enableFocusHandling_(focusable);
- }
- this.focusable_ = focusable;
- if (this.enabled_ && this.visible_) {
- this.renderer_.enableTabIndex(this.getKeyEventTarget(), focusable);
- }
- };
- /**
- * Returns true if the container allows children to be focusable, false
- * otherwise. Only effective if the container is not focusable.
- * @return {boolean} Whether children should be focusable.
- */
- goog.ui.Container.prototype.isFocusableChildrenAllowed = function() {
- return this.allowFocusableChildren_;
- };
- /**
- * Sets whether the container allows children to be focusable, false
- * otherwise. Only effective if the container is not focusable.
- * @param {boolean} focusable Whether the children should be focusable.
- */
- goog.ui.Container.prototype.setFocusableChildrenAllowed = function(focusable) {
- this.allowFocusableChildren_ = focusable;
- };
- /**
- * @return {boolean} Whether highlighting a child component should also open it.
- */
- goog.ui.Container.prototype.isOpenFollowsHighlight = function() {
- return this.openFollowsHighlight_;
- };
- /**
- * Sets whether highlighting a child component should also open it.
- * @param {boolean} follow Whether highlighting a child component also opens it.
- */
- goog.ui.Container.prototype.setOpenFollowsHighlight = function(follow) {
- this.openFollowsHighlight_ = follow;
- };
- // Highlight management.
- /**
- * Returns the index of the currently highlighted item (-1 if none).
- * @return {number} Index of the currently highlighted item.
- */
- goog.ui.Container.prototype.getHighlightedIndex = function() {
- return this.highlightedIndex_;
- };
- /**
- * Highlights the item at the given 0-based index (if any). If another item
- * was previously highlighted, it is un-highlighted.
- * @param {number} index Index of item to highlight (-1 removes the current
- * highlight).
- */
- goog.ui.Container.prototype.setHighlightedIndex = function(index) {
- var child = this.getChildAt(index);
- if (child) {
- child.setHighlighted(true);
- } else if (this.highlightedIndex_ > -1) {
- this.getHighlighted().setHighlighted(false);
- }
- };
- /**
- * Highlights the given item if it exists and is a child of the container;
- * otherwise un-highlights the currently highlighted item.
- * @param {goog.ui.Control} item Item to highlight.
- */
- goog.ui.Container.prototype.setHighlighted = function(item) {
- this.setHighlightedIndex(this.indexOfChild(item));
- };
- /**
- * Returns the currently highlighted item (if any).
- * @return {goog.ui.Control?} Highlighted item (null if none).
- */
- goog.ui.Container.prototype.getHighlighted = function() {
- return this.getChildAt(this.highlightedIndex_);
- };
- /**
- * Highlights the first highlightable item in the container
- */
- goog.ui.Container.prototype.highlightFirst = function() {
- this.highlightHelper(function(index, max) {
- return (index + 1) % max;
- }, this.getChildCount() - 1);
- };
- /**
- * Highlights the last highlightable item in the container.
- */
- goog.ui.Container.prototype.highlightLast = function() {
- this.highlightHelper(function(index, max) {
- index--;
- return index < 0 ? max - 1 : index;
- }, 0);
- };
- /**
- * Highlights the next highlightable item (or the first if nothing is currently
- * highlighted).
- */
- goog.ui.Container.prototype.highlightNext = function() {
- this.highlightHelper(function(index, max) {
- return (index + 1) % max;
- }, this.highlightedIndex_);
- };
- /**
- * Highlights the previous highlightable item (or the last if nothing is
- * currently highlighted).
- */
- goog.ui.Container.prototype.highlightPrevious = function() {
- this.highlightHelper(function(index, max) {
- index--;
- return index < 0 ? max - 1 : index;
- }, this.highlightedIndex_);
- };
- /**
- * Helper function that manages the details of moving the highlight among
- * child controls in response to keyboard events.
- * @param {function(this: goog.ui.Container, number, number) : number} fn
- * Function that accepts the current and maximum indices, and returns the
- * next index to check.
- * @param {number} startIndex Start index.
- * @return {boolean} Whether the highlight has changed.
- * @protected
- */
- goog.ui.Container.prototype.highlightHelper = function(fn, startIndex) {
- // If the start index is -1 (meaning there's nothing currently highlighted),
- // try starting from the currently open item, if any.
- var curIndex =
- startIndex < 0 ? this.indexOfChild(this.openItem_) : startIndex;
- var numItems = this.getChildCount();
- curIndex = fn.call(this, curIndex, numItems);
- var visited = 0;
- while (visited <= numItems) {
- var control = this.getChildAt(curIndex);
- if (control && this.canHighlightItem(control)) {
- this.setHighlightedIndexFromKeyEvent(curIndex);
- return true;
- }
- visited++;
- curIndex = fn.call(this, curIndex, numItems);
- }
- return false;
- };
- /**
- * Returns whether the given item can be highlighted.
- * @param {goog.ui.Control} item The item to check.
- * @return {boolean} Whether the item can be highlighted.
- * @protected
- */
- goog.ui.Container.prototype.canHighlightItem = function(item) {
- return item.isVisible() && item.isEnabled() &&
- item.isSupportedState(goog.ui.Component.State.HOVER);
- };
- /**
- * Helper method that sets the highlighted index to the given index in response
- * to a keyboard event. The base class implementation simply calls the
- * {@link #setHighlightedIndex} method, but subclasses can override this
- * behavior as needed.
- * @param {number} index Index of item to highlight.
- * @protected
- */
- goog.ui.Container.prototype.setHighlightedIndexFromKeyEvent = function(index) {
- this.setHighlightedIndex(index);
- };
- /**
- * Returns the currently open (expanded) control in the container (null if
- * none).
- * @return {goog.ui.Control?} The currently open control.
- */
- goog.ui.Container.prototype.getOpenItem = function() {
- return this.openItem_;
- };
- /**
- * Returns true if the mouse button is pressed, false otherwise.
- * @return {boolean} Whether the mouse button is pressed.
- */
- goog.ui.Container.prototype.isMouseButtonPressed = function() {
- return this.mouseButtonPressed_;
- };
- /**
- * Sets or clears the "mouse button pressed" flag.
- * @param {boolean} pressed Whether the mouse button is presed.
- */
- goog.ui.Container.prototype.setMouseButtonPressed = function(pressed) {
- this.mouseButtonPressed_ = pressed;
- };
|