1 // Copyright 2007 The Closure Library Authors. All Rights Reserved.
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
7 // http://www.apache.org/licenses/LICENSE-2.0
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS-IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
16 * @fileoverview Base class for containers that host {@link goog.ui.Control}s,
17 * such as menus and toolbars. Provides default keyboard and mouse event
18 * handling and child management, based on a generalized version of
19 * {@link goog.ui.Menu}.
21 * @author attila@google.com (Attila Bodis)
22 * @see ../demos/container.html
24 // TODO(attila): Fix code/logic duplication between this and goog.ui.Control.
25 // TODO(attila): Maybe pull common stuff all the way up into Component...?
27 goog.provide('goog.ui.Container');
28 goog.provide('goog.ui.Container.EventType');
29 goog.provide('goog.ui.Container.Orientation');
31 goog.require('goog.a11y.aria');
32 goog.require('goog.a11y.aria.State');
33 goog.require('goog.asserts');
34 goog.require('goog.dom');
35 goog.require('goog.events.EventType');
36 goog.require('goog.events.KeyCodes');
37 goog.require('goog.events.KeyHandler');
38 goog.require('goog.object');
39 goog.require('goog.style');
40 goog.require('goog.ui.Component');
41 goog.require('goog.ui.ContainerRenderer');
42 goog.require('goog.ui.Control');
47 * Base class for containers. Extends {@link goog.ui.Component} by adding
50 * <li>a {@link goog.events.KeyHandler}, to simplify keyboard handling,
51 * <li>a pluggable <em>renderer</em> framework, to simplify the creation of
52 * containers without the need to subclass this class,
53 * <li>methods to manage child controls hosted in the container,
54 * <li>default mouse and keyboard event handling methods.
56 * @param {?goog.ui.Container.Orientation=} opt_orientation Container
57 * orientation; defaults to {@code VERTICAL}.
58 * @param {goog.ui.ContainerRenderer=} opt_renderer Renderer used to render or
59 * decorate the container; defaults to {@link goog.ui.ContainerRenderer}.
60 * @param {goog.dom.DomHelper=} opt_domHelper DOM helper, used for document
62 * @extends {goog.ui.Component}
65 goog.ui.Container = function(opt_orientation, opt_renderer, opt_domHelper) {
66 goog.ui.Component.call(this, opt_domHelper);
67 this.renderer_ = opt_renderer || goog.ui.ContainerRenderer.getInstance();
68 this.orientation_ = opt_orientation || this.renderer_.getDefaultOrientation();
70 goog.inherits(goog.ui.Container, goog.ui.Component);
71 goog.tagUnsealableClass(goog.ui.Container);
75 * Container-specific events.
78 goog.ui.Container.EventType = {
80 * Dispatched after a goog.ui.Container becomes visible. Non-cancellable.
81 * NOTE(user): This event really shouldn't exist, because the
82 * goog.ui.Component.EventType.SHOW event should behave like this one. But the
83 * SHOW event for containers has been behaving as other components'
84 * BEFORE_SHOW event for a long time, and too much code relies on that old
85 * behavior to fix it now.
87 AFTER_SHOW: 'aftershow',
90 * Dispatched after a goog.ui.Container becomes invisible. Non-cancellable.
92 AFTER_HIDE: 'afterhide'
97 * Container orientation constants.
100 goog.ui.Container.Orientation = {
101 HORIZONTAL: 'horizontal',
107 * Allows an alternative element to be set to receive key events, otherwise
108 * defers to the renderer's element choice.
109 * @type {Element|undefined}
112 goog.ui.Container.prototype.keyEventTarget_ = null;
116 * Keyboard event handler.
117 * @type {goog.events.KeyHandler?}
120 goog.ui.Container.prototype.keyHandler_ = null;
124 * Renderer for the container. Defaults to {@link goog.ui.ContainerRenderer}.
125 * @type {goog.ui.ContainerRenderer?}
128 goog.ui.Container.prototype.renderer_ = null;
132 * Container orientation; determines layout and default keyboard navigation.
133 * @type {?goog.ui.Container.Orientation}
136 goog.ui.Container.prototype.orientation_ = null;
140 * Whether the container is set to be visible. Defaults to true.
144 goog.ui.Container.prototype.visible_ = true;
148 * Whether the container is enabled and reacting to keyboard and mouse events.
153 goog.ui.Container.prototype.enabled_ = true;
157 * Whether the container supports keyboard focus. Defaults to true. Focusable
158 * containers have a {@code tabIndex} and can be navigated to via the keyboard.
162 goog.ui.Container.prototype.focusable_ = true;
166 * The 0-based index of the currently highlighted control in the container
171 goog.ui.Container.prototype.highlightedIndex_ = -1;
175 * The currently open (expanded) control in the container (null if none).
176 * @type {goog.ui.Control?}
179 goog.ui.Container.prototype.openItem_ = null;
183 * Whether the mouse button is held down. Defaults to false. This flag is set
184 * when the user mouses down over the container, and remains set until they
185 * release the mouse button.
189 goog.ui.Container.prototype.mouseButtonPressed_ = false;
193 * Whether focus of child components should be allowed. Only effective if
194 * focusable_ is set to false.
198 goog.ui.Container.prototype.allowFocusableChildren_ = false;
202 * Whether highlighting a child component should also open it.
206 goog.ui.Container.prototype.openFollowsHighlight_ = true;
210 * Map of DOM IDs to child controls. Each key is the DOM ID of a child
211 * control's root element; each value is a reference to the child control
212 * itself. Used for looking up the child control corresponding to a DOM
217 goog.ui.Container.prototype.childElementIdMap_ = null;
220 // Event handler and renderer management.
224 * Returns the DOM element on which the container is listening for keyboard
225 * events (null if none).
226 * @return {Element} Element on which the container is listening for key
229 goog.ui.Container.prototype.getKeyEventTarget = function() {
230 // Delegate to renderer, unless we've set an explicit target.
231 return this.keyEventTarget_ || this.renderer_.getKeyEventTarget(this);
236 * Attaches an element on which to listen for key events.
237 * @param {Element|undefined} element The element to attach, or null/undefined
238 * to attach to the default element.
240 goog.ui.Container.prototype.setKeyEventTarget = function(element) {
241 if (this.focusable_) {
242 var oldTarget = this.getKeyEventTarget();
243 var inDocument = this.isInDocument();
245 this.keyEventTarget_ = element;
246 var newTarget = this.getKeyEventTarget();
249 // Unlisten for events on the old key target. Requires us to reset
250 // key target state temporarily.
251 this.keyEventTarget_ = oldTarget;
252 this.enableFocusHandling_(false);
253 this.keyEventTarget_ = element;
255 // Listen for events on the new key target.
256 this.getKeyHandler().attach(newTarget);
257 this.enableFocusHandling_(true);
260 throw Error('Can\'t set key event target for container ' +
261 'that doesn\'t support keyboard focus!');
267 * Returns the keyboard event handler for this container, lazily created the
268 * first time this method is called. The keyboard event handler listens for
269 * keyboard events on the container's key event target, as determined by its
271 * @return {!goog.events.KeyHandler} Keyboard event handler for this container.
273 goog.ui.Container.prototype.getKeyHandler = function() {
274 return this.keyHandler_ ||
275 (this.keyHandler_ = new goog.events.KeyHandler(this.getKeyEventTarget()));
280 * Returns the renderer used by this container to render itself or to decorate
281 * an existing element.
282 * @return {goog.ui.ContainerRenderer} Renderer used by the container.
284 goog.ui.Container.prototype.getRenderer = function() {
285 return this.renderer_;
290 * Registers the given renderer with the container. Changing renderers after
291 * the container has already been rendered or decorated is an error.
292 * @param {goog.ui.ContainerRenderer} renderer Renderer used by the container.
294 goog.ui.Container.prototype.setRenderer = function(renderer) {
295 if (this.getElement()) {
297 throw Error(goog.ui.Component.Error.ALREADY_RENDERED);
300 this.renderer_ = renderer;
304 // Standard goog.ui.Component implementation.
308 * Creates the container's DOM.
311 goog.ui.Container.prototype.createDom = function() {
312 // Delegate to renderer.
313 this.setElementInternal(this.renderer_.createDom(this));
318 * Returns the DOM element into which child components are to be rendered,
319 * or null if the container itself hasn't been rendered yet. Overrides
320 * {@link goog.ui.Component#getContentElement} by delegating to the renderer.
321 * @return {Element} Element to contain child elements (null if none).
324 goog.ui.Container.prototype.getContentElement = function() {
325 // Delegate to renderer.
326 return this.renderer_.getContentElement(this.getElement());
331 * Returns true if the given element can be decorated by this container.
332 * Overrides {@link goog.ui.Component#canDecorate}.
333 * @param {Element} element Element to decorate.
334 * @return {boolean} True iff the element can be decorated.
337 goog.ui.Container.prototype.canDecorate = function(element) {
338 // Delegate to renderer.
339 return this.renderer_.canDecorate(element);
344 * Decorates the given element with this container. Overrides {@link
345 * goog.ui.Component#decorateInternal}. Considered protected.
346 * @param {Element} element Element to decorate.
349 goog.ui.Container.prototype.decorateInternal = function(element) {
350 // Delegate to renderer.
351 this.setElementInternal(this.renderer_.decorate(this, element));
352 // Check whether the decorated element is explicitly styled to be invisible.
353 if (element.style.display == 'none') {
354 this.visible_ = false;
360 * Configures the container after its DOM has been rendered, and sets up event
361 * handling. Overrides {@link goog.ui.Component#enterDocument}.
364 goog.ui.Container.prototype.enterDocument = function() {
365 goog.ui.Container.superClass_.enterDocument.call(this);
367 this.forEachChild(function(child) {
368 if (child.isInDocument()) {
369 this.registerChildId_(child);
373 var elem = this.getElement();
375 // Call the renderer's initializeDom method to initialize the container's DOM.
376 this.renderer_.initializeDom(this);
378 // Initialize visibility (opt_force = true, so we don't dispatch events).
379 this.setVisible(this.visible_, true);
381 // Handle events dispatched by child controls.
383 listen(this, goog.ui.Component.EventType.ENTER,
384 this.handleEnterItem).
385 listen(this, goog.ui.Component.EventType.HIGHLIGHT,
386 this.handleHighlightItem).
387 listen(this, goog.ui.Component.EventType.UNHIGHLIGHT,
388 this.handleUnHighlightItem).
389 listen(this, goog.ui.Component.EventType.OPEN, this.handleOpenItem).
390 listen(this, goog.ui.Component.EventType.CLOSE, this.handleCloseItem).
392 // Handle mouse events.
393 listen(elem, goog.events.EventType.MOUSEDOWN, this.handleMouseDown).
394 listen(goog.dom.getOwnerDocument(elem), goog.events.EventType.MOUSEUP,
395 this.handleDocumentMouseUp).
397 // Handle mouse events on behalf of controls in the container.
399 goog.events.EventType.MOUSEDOWN,
400 goog.events.EventType.MOUSEUP,
401 goog.events.EventType.MOUSEOVER,
402 goog.events.EventType.MOUSEOUT,
403 goog.events.EventType.CONTEXTMENU
404 ], this.handleChildMouseEvents);
406 // If the container is focusable, set up keyboard event handling.
407 if (this.isFocusable()) {
408 this.enableFocusHandling_(true);
414 * Sets up listening for events applicable to focusable containers.
415 * @param {boolean} enable Whether to enable or disable focus handling.
418 goog.ui.Container.prototype.enableFocusHandling_ = function(enable) {
419 var handler = this.getHandler();
420 var keyTarget = this.getKeyEventTarget();
423 listen(keyTarget, goog.events.EventType.FOCUS, this.handleFocus).
424 listen(keyTarget, goog.events.EventType.BLUR, this.handleBlur).
425 listen(this.getKeyHandler(), goog.events.KeyHandler.EventType.KEY,
426 this.handleKeyEvent);
429 unlisten(keyTarget, goog.events.EventType.FOCUS, this.handleFocus).
430 unlisten(keyTarget, goog.events.EventType.BLUR, this.handleBlur).
431 unlisten(this.getKeyHandler(), goog.events.KeyHandler.EventType.KEY,
432 this.handleKeyEvent);
438 * Cleans up the container before its DOM is removed from the document, and
439 * removes event handlers. Overrides {@link goog.ui.Component#exitDocument}.
442 goog.ui.Container.prototype.exitDocument = function() {
443 // {@link #setHighlightedIndex} has to be called before
444 // {@link goog.ui.Component#exitDocument}, otherwise it has no effect.
445 this.setHighlightedIndex(-1);
447 if (this.openItem_) {
448 this.openItem_.setOpen(false);
451 this.mouseButtonPressed_ = false;
453 goog.ui.Container.superClass_.exitDocument.call(this);
458 goog.ui.Container.prototype.disposeInternal = function() {
459 goog.ui.Container.superClass_.disposeInternal.call(this);
461 if (this.keyHandler_) {
462 this.keyHandler_.dispose();
463 this.keyHandler_ = null;
466 this.keyEventTarget_ = null;
467 this.childElementIdMap_ = null;
468 this.openItem_ = null;
469 this.renderer_ = null;
473 // Default event handlers.
477 * Handles ENTER events raised by child controls when they are navigated to.
478 * @param {goog.events.Event} e ENTER event to handle.
479 * @return {boolean} Whether to prevent handleMouseOver from handling
482 goog.ui.Container.prototype.handleEnterItem = function(e) {
483 // Allow the Control to highlight itself.
489 * Handles HIGHLIGHT events dispatched by items in the container when
490 * they are highlighted.
491 * @param {goog.events.Event} e Highlight event to handle.
493 goog.ui.Container.prototype.handleHighlightItem = function(e) {
494 var index = this.indexOfChild(/** @type {goog.ui.Control} */ (e.target));
495 if (index > -1 && index != this.highlightedIndex_) {
496 var item = this.getHighlighted();
498 // Un-highlight previously highlighted item.
499 item.setHighlighted(false);
502 this.highlightedIndex_ = index;
503 item = this.getHighlighted();
505 if (this.isMouseButtonPressed()) {
506 // Activate item when mouse button is pressed, to allow MacOS-style
507 // dragging to choose menu items. Although this should only truly
508 // happen if the highlight is due to mouse movements, there is little
509 // harm in doing it for keyboard or programmatic highlights.
510 item.setActive(true);
513 // Update open item if open item needs follow highlight.
514 if (this.openFollowsHighlight_ &&
515 this.openItem_ && item != this.openItem_) {
516 if (item.isSupportedState(goog.ui.Component.State.OPENED)) {
519 this.openItem_.setOpen(false);
524 var element = this.getElement();
525 goog.asserts.assert(element,
526 'The DOM element for the container cannot be null.');
527 if (e.target.getElement() != null) {
528 goog.a11y.aria.setState(element,
529 goog.a11y.aria.State.ACTIVEDESCENDANT,
530 e.target.getElement().id);
536 * Handles UNHIGHLIGHT events dispatched by items in the container when
537 * they are unhighlighted.
538 * @param {goog.events.Event} e Unhighlight event to handle.
540 goog.ui.Container.prototype.handleUnHighlightItem = function(e) {
541 if (e.target == this.getHighlighted()) {
542 this.highlightedIndex_ = -1;
544 var element = this.getElement();
545 goog.asserts.assert(element,
546 'The DOM element for the container cannot be null.');
547 // Setting certain ARIA attributes to empty strings is problematic.
548 // Just remove the attribute instead.
549 goog.a11y.aria.removeState(element, goog.a11y.aria.State.ACTIVEDESCENDANT);
554 * Handles OPEN events dispatched by items in the container when they are
556 * @param {goog.events.Event} e Open event to handle.
558 goog.ui.Container.prototype.handleOpenItem = function(e) {
559 var item = /** @type {goog.ui.Control} */ (e.target);
560 if (item && item != this.openItem_ && item.getParent() == this) {
561 if (this.openItem_) {
562 this.openItem_.setOpen(false);
564 this.openItem_ = item;
570 * Handles CLOSE events dispatched by items in the container when they are
572 * @param {goog.events.Event} e Close event to handle.
574 goog.ui.Container.prototype.handleCloseItem = function(e) {
575 if (e.target == this.openItem_) {
576 this.openItem_ = null;
582 * Handles mousedown events over the container. The default implementation
583 * sets the "mouse button pressed" flag and, if the container is focusable,
584 * grabs keyboard focus.
585 * @param {goog.events.BrowserEvent} e Mousedown event to handle.
587 goog.ui.Container.prototype.handleMouseDown = function(e) {
589 this.setMouseButtonPressed(true);
592 var keyTarget = this.getKeyEventTarget();
593 if (keyTarget && goog.dom.isFocusableTabIndex(keyTarget)) {
594 // The container is configured to receive keyboard focus.
597 // The control isn't configured to receive keyboard focus; prevent it
598 // from stealing focus or destroying the selection.
605 * Handles mouseup events over the document. The default implementation
606 * clears the "mouse button pressed" flag.
607 * @param {goog.events.BrowserEvent} e Mouseup event to handle.
609 goog.ui.Container.prototype.handleDocumentMouseUp = function(e) {
610 this.setMouseButtonPressed(false);
615 * Handles mouse events originating from nodes belonging to the controls hosted
616 * in the container. Locates the child control based on the DOM node that
617 * dispatched the event, and forwards the event to the control for handling.
618 * @param {goog.events.BrowserEvent} e Mouse event to handle.
620 goog.ui.Container.prototype.handleChildMouseEvents = function(e) {
621 var control = this.getOwnerControl(/** @type {Node} */ (e.target));
623 // Child control identified; forward the event.
625 case goog.events.EventType.MOUSEDOWN:
626 control.handleMouseDown(e);
628 case goog.events.EventType.MOUSEUP:
629 control.handleMouseUp(e);
631 case goog.events.EventType.MOUSEOVER:
632 control.handleMouseOver(e);
634 case goog.events.EventType.MOUSEOUT:
635 control.handleMouseOut(e);
637 case goog.events.EventType.CONTEXTMENU:
638 control.handleContextMenu(e);
646 * Returns the child control that owns the given DOM node, or null if no such
648 * @param {Node} node DOM node whose owner is to be returned.
649 * @return {goog.ui.Control?} Control hosted in the container to which the node
650 * belongs (if found).
653 goog.ui.Container.prototype.getOwnerControl = function(node) {
654 // Ensure that this container actually has child controls before
655 // looking up the owner.
656 if (this.childElementIdMap_) {
657 var elem = this.getElement();
658 // See http://b/2964418 . IE9 appears to evaluate '!=' incorrectly, so
659 // using '!==' instead.
660 // TODO(user): Possibly revert this change if/when IE9 fixes the issue.
661 while (node && node !== elem) {
663 if (id in this.childElementIdMap_) {
664 return this.childElementIdMap_[id];
666 node = node.parentNode;
674 * Handles focus events raised when the container's key event target receives
676 * @param {goog.events.BrowserEvent} e Focus event to handle.
678 goog.ui.Container.prototype.handleFocus = function(e) {
679 // No-op in the base class.
684 * Handles blur events raised when the container's key event target loses
685 * keyboard focus. The default implementation clears the highlight index.
686 * @param {goog.events.BrowserEvent} e Blur event to handle.
688 goog.ui.Container.prototype.handleBlur = function(e) {
689 this.setHighlightedIndex(-1);
690 this.setMouseButtonPressed(false);
691 // If the container loses focus, and one of its children is open, close it.
692 if (this.openItem_) {
693 this.openItem_.setOpen(false);
699 * Attempts to handle a keyboard event, if the control is enabled, by calling
700 * {@link handleKeyEventInternal}. Considered protected; should only be used
701 * within this package and by subclasses.
702 * @param {goog.events.KeyEvent} e Key event to handle.
703 * @return {boolean} Whether the key event was handled.
705 goog.ui.Container.prototype.handleKeyEvent = function(e) {
706 if (this.isEnabled() && this.isVisible() &&
707 (this.getChildCount() != 0 || this.keyEventTarget_) &&
708 this.handleKeyEventInternal(e)) {
718 * Attempts to handle a keyboard event; returns true if the event was handled,
719 * false otherwise. If the container is enabled, and a child is highlighted,
720 * calls the child control's {@code handleKeyEvent} method to give the control
721 * a chance to handle the event first.
722 * @param {goog.events.KeyEvent} e Key event to handle.
723 * @return {boolean} Whether the event was handled by the container (or one of
726 goog.ui.Container.prototype.handleKeyEventInternal = function(e) {
727 // Give the highlighted control the chance to handle the key event.
728 var highlighted = this.getHighlighted();
729 if (highlighted && typeof highlighted.handleKeyEvent == 'function' &&
730 highlighted.handleKeyEvent(e)) {
734 // Give the open control the chance to handle the key event.
735 if (this.openItem_ && this.openItem_ != highlighted &&
736 typeof this.openItem_.handleKeyEvent == 'function' &&
737 this.openItem_.handleKeyEvent(e)) {
741 // Do not handle the key event if any modifier key is pressed.
742 if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) {
746 // Either nothing is highlighted, or the highlighted control didn't handle
747 // the key event, so attempt to handle it here.
749 case goog.events.KeyCodes.ESC:
750 if (this.isFocusable()) {
751 this.getKeyEventTarget().blur();
757 case goog.events.KeyCodes.HOME:
758 this.highlightFirst();
761 case goog.events.KeyCodes.END:
762 this.highlightLast();
765 case goog.events.KeyCodes.UP:
766 if (this.orientation_ == goog.ui.Container.Orientation.VERTICAL) {
767 this.highlightPrevious();
773 case goog.events.KeyCodes.LEFT:
774 if (this.orientation_ == goog.ui.Container.Orientation.HORIZONTAL) {
775 if (this.isRightToLeft()) {
776 this.highlightNext();
778 this.highlightPrevious();
785 case goog.events.KeyCodes.DOWN:
786 if (this.orientation_ == goog.ui.Container.Orientation.VERTICAL) {
787 this.highlightNext();
793 case goog.events.KeyCodes.RIGHT:
794 if (this.orientation_ == goog.ui.Container.Orientation.HORIZONTAL) {
795 if (this.isRightToLeft()) {
796 this.highlightPrevious();
798 this.highlightNext();
813 // Child component management.
817 * Creates a DOM ID for the child control and registers it to an internal
818 * hash table to be able to find it fast by id.
819 * @param {goog.ui.Component} child The child control. Its root element has
823 goog.ui.Container.prototype.registerChildId_ = function(child) {
824 // Map the DOM ID of the control's root element to the control itself.
825 var childElem = child.getElement();
827 // If the control's root element doesn't have a DOM ID assign one.
828 var id = childElem.id || (childElem.id = child.getId());
830 // Lazily create the child element ID map on first use.
831 if (!this.childElementIdMap_) {
832 this.childElementIdMap_ = {};
834 this.childElementIdMap_[id] = child;
839 * Adds the specified control as the last child of this container. See
840 * {@link goog.ui.Container#addChildAt} for detailed semantics.
841 * @param {goog.ui.Component} child The new child control.
842 * @param {boolean=} opt_render Whether the new child should be rendered
843 * immediately after being added (defaults to false).
846 goog.ui.Container.prototype.addChild = function(child, opt_render) {
847 goog.asserts.assertInstanceof(child, goog.ui.Control,
848 'The child of a container must be a control');
849 goog.ui.Container.superClass_.addChild.call(this, child, opt_render);
854 * Overrides {@link goog.ui.Container#getChild} to make it clear that it
855 * only returns {@link goog.ui.Control}s.
856 * @param {string} id Child component ID.
857 * @return {goog.ui.Control} The child with the given ID; null if none.
860 goog.ui.Container.prototype.getChild;
864 * Overrides {@link goog.ui.Container#getChildAt} to make it clear that it
865 * only returns {@link goog.ui.Control}s.
866 * @param {number} index 0-based index.
867 * @return {goog.ui.Control} The child with the given ID; null if none.
870 goog.ui.Container.prototype.getChildAt;
874 * Adds the control as a child of this container at the given 0-based index.
875 * Overrides {@link goog.ui.Component#addChildAt} by also updating the
876 * container's highlight index. Since {@link goog.ui.Component#addChild} uses
877 * {@link #addChildAt} internally, we only need to override this method.
878 * @param {goog.ui.Component} control New child.
879 * @param {number} index Index at which the new child is to be added.
880 * @param {boolean=} opt_render Whether the new child should be rendered
881 * immediately after being added (defaults to false).
884 goog.ui.Container.prototype.addChildAt = function(control, index, opt_render) {
885 // Make sure the child control dispatches HIGHLIGHT, UNHIGHLIGHT, OPEN, and
886 // CLOSE events, and that it doesn't steal keyboard focus.
887 control.setDispatchTransitionEvents(goog.ui.Component.State.HOVER, true);
888 control.setDispatchTransitionEvents(goog.ui.Component.State.OPENED, true);
889 if (this.isFocusable() || !this.isFocusableChildrenAllowed()) {
890 control.setSupportedState(goog.ui.Component.State.FOCUSED, false);
893 // Disable mouse event handling by child controls.
894 control.setHandleMouseEvents(false);
896 // Let the superclass implementation do the work.
897 goog.ui.Container.superClass_.addChildAt.call(this, control, index,
900 if (control.isInDocument() && this.isInDocument()) {
901 this.registerChildId_(control);
904 // Update the highlight index, if needed.
905 if (index <= this.highlightedIndex_) {
906 this.highlightedIndex_++;
912 * Removes a child control. Overrides {@link goog.ui.Component#removeChild} by
913 * updating the highlight index. Since {@link goog.ui.Component#removeChildAt}
914 * uses {@link #removeChild} internally, we only need to override this method.
915 * @param {string|goog.ui.Component} control The ID of the child to remove, or
916 * the control itself.
917 * @param {boolean=} opt_unrender Whether to call {@code exitDocument} on the
918 * removed control, and detach its DOM from the document (defaults to
920 * @return {goog.ui.Control} The removed control, if any.
923 goog.ui.Container.prototype.removeChild = function(control, opt_unrender) {
924 control = goog.isString(control) ? this.getChild(control) : control;
927 var index = this.indexOfChild(control);
929 if (index == this.highlightedIndex_) {
930 control.setHighlighted(false);
931 this.highlightedIndex_ = -1;
932 } else if (index < this.highlightedIndex_) {
933 this.highlightedIndex_--;
937 // Remove the mapping from the child element ID map.
938 var childElem = control.getElement();
939 if (childElem && childElem.id && this.childElementIdMap_) {
940 goog.object.remove(this.childElementIdMap_, childElem.id);
944 control = /** @type {goog.ui.Control} */ (
945 goog.ui.Container.superClass_.removeChild.call(this, control,
948 // Re-enable mouse event handling (in case the control is reused elsewhere).
949 control.setHandleMouseEvents(true);
955 // Container state management.
959 * Returns the container's orientation.
960 * @return {?goog.ui.Container.Orientation} Container orientation.
962 goog.ui.Container.prototype.getOrientation = function() {
963 return this.orientation_;
968 * Sets the container's orientation.
969 * @param {goog.ui.Container.Orientation} orientation Container orientation.
971 // TODO(attila): Do we need to support containers with dynamic orientation?
972 goog.ui.Container.prototype.setOrientation = function(orientation) {
973 if (this.getElement()) {
975 throw Error(goog.ui.Component.Error.ALREADY_RENDERED);
978 this.orientation_ = orientation;
983 * Returns true if the container's visibility is set to visible, false if
984 * it is set to hidden. A container that is set to hidden is guaranteed
985 * to be hidden from the user, but the reverse isn't necessarily true.
986 * A container may be set to visible but can otherwise be obscured by another
987 * element, rendered off-screen, or hidden using direct CSS manipulation.
988 * @return {boolean} Whether the container is set to be visible.
990 goog.ui.Container.prototype.isVisible = function() {
991 return this.visible_;
996 * Shows or hides the container. Does nothing if the container already has
997 * the requested visibility. Otherwise, dispatches a SHOW or HIDE event as
998 * appropriate, giving listeners a chance to prevent the visibility change.
999 * @param {boolean} visible Whether to show or hide the container.
1000 * @param {boolean=} opt_force If true, doesn't check whether the container
1001 * already has the requested visibility, and doesn't dispatch any events.
1002 * @return {boolean} Whether the visibility was changed.
1004 goog.ui.Container.prototype.setVisible = function(visible, opt_force) {
1005 if (opt_force || (this.visible_ != visible && this.dispatchEvent(visible ?
1006 goog.ui.Component.EventType.SHOW : goog.ui.Component.EventType.HIDE))) {
1007 this.visible_ = visible;
1009 var elem = this.getElement();
1011 goog.style.setElementShown(elem, visible);
1012 if (this.isFocusable()) {
1013 // Enable keyboard access only for enabled & visible containers.
1014 this.renderer_.enableTabIndex(this.getKeyEventTarget(),
1015 this.enabled_ && this.visible_);
1018 this.dispatchEvent(this.visible_ ?
1019 goog.ui.Container.EventType.AFTER_SHOW :
1020 goog.ui.Container.EventType.AFTER_HIDE);
1032 * Returns true if the container is enabled, false otherwise.
1033 * @return {boolean} Whether the container is enabled.
1035 goog.ui.Container.prototype.isEnabled = function() {
1036 return this.enabled_;
1041 * Enables/disables the container based on the {@code enable} argument.
1042 * Dispatches an {@code ENABLED} or {@code DISABLED} event prior to changing
1043 * the container's state, which may be caught and canceled to prevent the
1044 * container from changing state. Also enables/disables child controls.
1045 * @param {boolean} enable Whether to enable or disable the container.
1047 goog.ui.Container.prototype.setEnabled = function(enable) {
1048 if (this.enabled_ != enable && this.dispatchEvent(enable ?
1049 goog.ui.Component.EventType.ENABLE :
1050 goog.ui.Component.EventType.DISABLE)) {
1052 // Flag the container as enabled first, then update children. This is
1053 // because controls can't be enabled if their parent is disabled.
1054 this.enabled_ = true;
1055 this.forEachChild(function(child) {
1056 // Enable child control unless it is flagged.
1057 if (child.wasDisabled) {
1058 delete child.wasDisabled;
1060 child.setEnabled(true);
1064 // Disable children first, then flag the container as disabled. This is
1065 // because controls can't be disabled if their parent is already disabled.
1066 this.forEachChild(function(child) {
1067 // Disable child control, or flag it if it's already disabled.
1068 if (child.isEnabled()) {
1069 child.setEnabled(false);
1071 child.wasDisabled = true;
1074 this.enabled_ = false;
1075 this.setMouseButtonPressed(false);
1078 if (this.isFocusable()) {
1079 // Enable keyboard access only for enabled & visible components.
1080 this.renderer_.enableTabIndex(this.getKeyEventTarget(),
1081 enable && this.visible_);
1088 * Returns true if the container is focusable, false otherwise. The default
1089 * is true. Focusable containers always have a tab index and allocate a key
1090 * handler to handle keyboard events while focused.
1091 * @return {boolean} Whether the component is focusable.
1093 goog.ui.Container.prototype.isFocusable = function() {
1094 return this.focusable_;
1099 * Sets whether the container is focusable. The default is true. Focusable
1100 * containers always have a tab index and allocate a key handler to handle
1101 * keyboard events while focused.
1102 * @param {boolean} focusable Whether the component is to be focusable.
1104 goog.ui.Container.prototype.setFocusable = function(focusable) {
1105 if (focusable != this.focusable_ && this.isInDocument()) {
1106 this.enableFocusHandling_(focusable);
1108 this.focusable_ = focusable;
1109 if (this.enabled_ && this.visible_) {
1110 this.renderer_.enableTabIndex(this.getKeyEventTarget(), focusable);
1116 * Returns true if the container allows children to be focusable, false
1117 * otherwise. Only effective if the container is not focusable.
1118 * @return {boolean} Whether children should be focusable.
1120 goog.ui.Container.prototype.isFocusableChildrenAllowed = function() {
1121 return this.allowFocusableChildren_;
1126 * Sets whether the container allows children to be focusable, false
1127 * otherwise. Only effective if the container is not focusable.
1128 * @param {boolean} focusable Whether the children should be focusable.
1130 goog.ui.Container.prototype.setFocusableChildrenAllowed = function(focusable) {
1131 this.allowFocusableChildren_ = focusable;
1136 * @return {boolean} Whether highlighting a child component should also open it.
1138 goog.ui.Container.prototype.isOpenFollowsHighlight = function() {
1139 return this.openFollowsHighlight_;
1144 * Sets whether highlighting a child component should also open it.
1145 * @param {boolean} follow Whether highlighting a child component also opens it.
1147 goog.ui.Container.prototype.setOpenFollowsHighlight = function(follow) {
1148 this.openFollowsHighlight_ = follow;
1152 // Highlight management.
1156 * Returns the index of the currently highlighted item (-1 if none).
1157 * @return {number} Index of the currently highlighted item.
1159 goog.ui.Container.prototype.getHighlightedIndex = function() {
1160 return this.highlightedIndex_;
1165 * Highlights the item at the given 0-based index (if any). If another item
1166 * was previously highlighted, it is un-highlighted.
1167 * @param {number} index Index of item to highlight (-1 removes the current
1170 goog.ui.Container.prototype.setHighlightedIndex = function(index) {
1171 var child = this.getChildAt(index);
1173 child.setHighlighted(true);
1174 } else if (this.highlightedIndex_ > -1) {
1175 this.getHighlighted().setHighlighted(false);
1181 * Highlights the given item if it exists and is a child of the container;
1182 * otherwise un-highlights the currently highlighted item.
1183 * @param {goog.ui.Control} item Item to highlight.
1185 goog.ui.Container.prototype.setHighlighted = function(item) {
1186 this.setHighlightedIndex(this.indexOfChild(item));
1191 * Returns the currently highlighted item (if any).
1192 * @return {goog.ui.Control?} Highlighted item (null if none).
1194 goog.ui.Container.prototype.getHighlighted = function() {
1195 return this.getChildAt(this.highlightedIndex_);
1200 * Highlights the first highlightable item in the container
1202 goog.ui.Container.prototype.highlightFirst = function() {
1203 this.highlightHelper(function(index, max) {
1204 return (index + 1) % max;
1205 }, this.getChildCount() - 1);
1210 * Highlights the last highlightable item in the container.
1212 goog.ui.Container.prototype.highlightLast = function() {
1213 this.highlightHelper(function(index, max) {
1215 return index < 0 ? max - 1 : index;
1221 * Highlights the next highlightable item (or the first if nothing is currently
1224 goog.ui.Container.prototype.highlightNext = function() {
1225 this.highlightHelper(function(index, max) {
1226 return (index + 1) % max;
1227 }, this.highlightedIndex_);
1232 * Highlights the previous highlightable item (or the last if nothing is
1233 * currently highlighted).
1235 goog.ui.Container.prototype.highlightPrevious = function() {
1236 this.highlightHelper(function(index, max) {
1238 return index < 0 ? max - 1 : index;
1239 }, this.highlightedIndex_);
1244 * Helper function that manages the details of moving the highlight among
1245 * child controls in response to keyboard events.
1246 * @param {function(number, number) : number} fn Function that accepts the
1247 * current and maximum indices, and returns the next index to check.
1248 * @param {number} startIndex Start index.
1249 * @return {boolean} Whether the highlight has changed.
1252 goog.ui.Container.prototype.highlightHelper = function(fn, startIndex) {
1253 // If the start index is -1 (meaning there's nothing currently highlighted),
1254 // try starting from the currently open item, if any.
1255 var curIndex = startIndex < 0 ?
1256 this.indexOfChild(this.openItem_) : startIndex;
1257 var numItems = this.getChildCount();
1259 curIndex = fn.call(this, curIndex, numItems);
1261 while (visited <= numItems) {
1262 var control = this.getChildAt(curIndex);
1263 if (control && this.canHighlightItem(control)) {
1264 this.setHighlightedIndexFromKeyEvent(curIndex);
1268 curIndex = fn.call(this, curIndex, numItems);
1275 * Returns whether the given item can be highlighted.
1276 * @param {goog.ui.Control} item The item to check.
1277 * @return {boolean} Whether the item can be highlighted.
1280 goog.ui.Container.prototype.canHighlightItem = function(item) {
1281 return item.isVisible() && item.isEnabled() &&
1282 item.isSupportedState(goog.ui.Component.State.HOVER);
1287 * Helper method that sets the highlighted index to the given index in response
1288 * to a keyboard event. The base class implementation simply calls the
1289 * {@link #setHighlightedIndex} method, but subclasses can override this
1290 * behavior as needed.
1291 * @param {number} index Index of item to highlight.
1294 goog.ui.Container.prototype.setHighlightedIndexFromKeyEvent = function(index) {
1295 this.setHighlightedIndex(index);
1300 * Returns the currently open (expanded) control in the container (null if
1302 * @return {goog.ui.Control?} The currently open control.
1304 goog.ui.Container.prototype.getOpenItem = function() {
1305 return this.openItem_;
1310 * Returns true if the mouse button is pressed, false otherwise.
1311 * @return {boolean} Whether the mouse button is pressed.
1313 goog.ui.Container.prototype.isMouseButtonPressed = function() {
1314 return this.mouseButtonPressed_;
1319 * Sets or clears the "mouse button pressed" flag.
1320 * @param {boolean} pressed Whether the mouse button is presed.
1322 goog.ui.Container.prototype.setMouseButtonPressed = function(pressed) {
1323 this.mouseButtonPressed_ = pressed;