Upstream version 11.40.271.0
[platform/framework/web/crosswalk.git] / src / third_party / google_input_tools / third_party / closure_library / closure / goog / ui / container.js
1 // Copyright 2007 The Closure Library Authors. All Rights Reserved.
2 //
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
6 //
7 //      http://www.apache.org/licenses/LICENSE-2.0
8 //
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.
14
15 /**
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}.
20  *
21  * @author attila@google.com (Attila Bodis)
22  * @see ../demos/container.html
23  */
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...?
26
27 goog.provide('goog.ui.Container');
28 goog.provide('goog.ui.Container.EventType');
29 goog.provide('goog.ui.Container.Orientation');
30
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');
43
44
45
46 /**
47  * Base class for containers.  Extends {@link goog.ui.Component} by adding
48  * the following:
49  *  <ul>
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.
55  *  </ul>
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
61  *     interaction.
62  * @extends {goog.ui.Component}
63  * @constructor
64  */
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();
69 };
70 goog.inherits(goog.ui.Container, goog.ui.Component);
71 goog.tagUnsealableClass(goog.ui.Container);
72
73
74 /**
75  * Container-specific events.
76  * @enum {string}
77  */
78 goog.ui.Container.EventType = {
79   /**
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.
86    */
87   AFTER_SHOW: 'aftershow',
88
89   /**
90    * Dispatched after a goog.ui.Container becomes invisible. Non-cancellable.
91    */
92   AFTER_HIDE: 'afterhide'
93 };
94
95
96 /**
97  * Container orientation constants.
98  * @enum {string}
99  */
100 goog.ui.Container.Orientation = {
101   HORIZONTAL: 'horizontal',
102   VERTICAL: 'vertical'
103 };
104
105
106 /**
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}
110  * @private
111  */
112 goog.ui.Container.prototype.keyEventTarget_ = null;
113
114
115 /**
116  * Keyboard event handler.
117  * @type {goog.events.KeyHandler?}
118  * @private
119  */
120 goog.ui.Container.prototype.keyHandler_ = null;
121
122
123 /**
124  * Renderer for the container.  Defaults to {@link goog.ui.ContainerRenderer}.
125  * @type {goog.ui.ContainerRenderer?}
126  * @private
127  */
128 goog.ui.Container.prototype.renderer_ = null;
129
130
131 /**
132  * Container orientation; determines layout and default keyboard navigation.
133  * @type {?goog.ui.Container.Orientation}
134  * @private
135  */
136 goog.ui.Container.prototype.orientation_ = null;
137
138
139 /**
140  * Whether the container is set to be visible.  Defaults to true.
141  * @type {boolean}
142  * @private
143  */
144 goog.ui.Container.prototype.visible_ = true;
145
146
147 /**
148  * Whether the container is enabled and reacting to keyboard and mouse events.
149  * Defaults to true.
150  * @type {boolean}
151  * @private
152  */
153 goog.ui.Container.prototype.enabled_ = true;
154
155
156 /**
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.
159  * @type {boolean}
160  * @private
161  */
162 goog.ui.Container.prototype.focusable_ = true;
163
164
165 /**
166  * The 0-based index of the currently highlighted control in the container
167  * (-1 if none).
168  * @type {number}
169  * @private
170  */
171 goog.ui.Container.prototype.highlightedIndex_ = -1;
172
173
174 /**
175  * The currently open (expanded) control in the container (null if none).
176  * @type {goog.ui.Control?}
177  * @private
178  */
179 goog.ui.Container.prototype.openItem_ = null;
180
181
182 /**
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.
186  * @type {boolean}
187  * @private
188  */
189 goog.ui.Container.prototype.mouseButtonPressed_ = false;
190
191
192 /**
193  * Whether focus of child components should be allowed.  Only effective if
194  * focusable_ is set to false.
195  * @type {boolean}
196  * @private
197  */
198 goog.ui.Container.prototype.allowFocusableChildren_ = false;
199
200
201 /**
202  * Whether highlighting a child component should also open it.
203  * @type {boolean}
204  * @private
205  */
206 goog.ui.Container.prototype.openFollowsHighlight_ = true;
207
208
209 /**
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
213  * node in O(1) time.
214  * @type {Object}
215  * @private
216  */
217 goog.ui.Container.prototype.childElementIdMap_ = null;
218
219
220 // Event handler and renderer management.
221
222
223 /**
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
227  *     events.
228  */
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);
232 };
233
234
235 /**
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.
239  */
240 goog.ui.Container.prototype.setKeyEventTarget = function(element) {
241   if (this.focusable_) {
242     var oldTarget = this.getKeyEventTarget();
243     var inDocument = this.isInDocument();
244
245     this.keyEventTarget_ = element;
246     var newTarget = this.getKeyEventTarget();
247
248     if (inDocument) {
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;
254
255       // Listen for events on the new key target.
256       this.getKeyHandler().attach(newTarget);
257       this.enableFocusHandling_(true);
258     }
259   } else {
260     throw Error('Can\'t set key event target for container ' +
261         'that doesn\'t support keyboard focus!');
262   }
263 };
264
265
266 /**
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
270  * renderer.
271  * @return {!goog.events.KeyHandler} Keyboard event handler for this container.
272  */
273 goog.ui.Container.prototype.getKeyHandler = function() {
274   return this.keyHandler_ ||
275       (this.keyHandler_ = new goog.events.KeyHandler(this.getKeyEventTarget()));
276 };
277
278
279 /**
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.
283  */
284 goog.ui.Container.prototype.getRenderer = function() {
285   return this.renderer_;
286 };
287
288
289 /**
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.
293  */
294 goog.ui.Container.prototype.setRenderer = function(renderer) {
295   if (this.getElement()) {
296     // Too late.
297     throw Error(goog.ui.Component.Error.ALREADY_RENDERED);
298   }
299
300   this.renderer_ = renderer;
301 };
302
303
304 // Standard goog.ui.Component implementation.
305
306
307 /**
308  * Creates the container's DOM.
309  * @override
310  */
311 goog.ui.Container.prototype.createDom = function() {
312   // Delegate to renderer.
313   this.setElementInternal(this.renderer_.createDom(this));
314 };
315
316
317 /**
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).
322  * @override
323  */
324 goog.ui.Container.prototype.getContentElement = function() {
325   // Delegate to renderer.
326   return this.renderer_.getContentElement(this.getElement());
327 };
328
329
330 /**
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.
335  * @override
336  */
337 goog.ui.Container.prototype.canDecorate = function(element) {
338   // Delegate to renderer.
339   return this.renderer_.canDecorate(element);
340 };
341
342
343 /**
344  * Decorates the given element with this container. Overrides {@link
345  * goog.ui.Component#decorateInternal}.  Considered protected.
346  * @param {Element} element Element to decorate.
347  * @override
348  */
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;
355   }
356 };
357
358
359 /**
360  * Configures the container after its DOM has been rendered, and sets up event
361  * handling.  Overrides {@link goog.ui.Component#enterDocument}.
362  * @override
363  */
364 goog.ui.Container.prototype.enterDocument = function() {
365   goog.ui.Container.superClass_.enterDocument.call(this);
366
367   this.forEachChild(function(child) {
368     if (child.isInDocument()) {
369       this.registerChildId_(child);
370     }
371   }, this);
372
373   var elem = this.getElement();
374
375   // Call the renderer's initializeDom method to initialize the container's DOM.
376   this.renderer_.initializeDom(this);
377
378   // Initialize visibility (opt_force = true, so we don't dispatch events).
379   this.setVisible(this.visible_, true);
380
381   // Handle events dispatched by child controls.
382   this.getHandler().
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).
391
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).
396
397       // Handle mouse events on behalf of controls in the container.
398       listen(elem, [
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);
405
406   // If the container is focusable, set up keyboard event handling.
407   if (this.isFocusable()) {
408     this.enableFocusHandling_(true);
409   }
410 };
411
412
413 /**
414  * Sets up listening for events applicable to focusable containers.
415  * @param {boolean} enable Whether to enable or disable focus handling.
416  * @private
417  */
418 goog.ui.Container.prototype.enableFocusHandling_ = function(enable) {
419   var handler = this.getHandler();
420   var keyTarget = this.getKeyEventTarget();
421   if (enable) {
422     handler.
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);
427   } else {
428     handler.
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);
433   }
434 };
435
436
437 /**
438  * Cleans up the container before its DOM is removed from the document, and
439  * removes event handlers.  Overrides {@link goog.ui.Component#exitDocument}.
440  * @override
441  */
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);
446
447   if (this.openItem_) {
448     this.openItem_.setOpen(false);
449   }
450
451   this.mouseButtonPressed_ = false;
452
453   goog.ui.Container.superClass_.exitDocument.call(this);
454 };
455
456
457 /** @override */
458 goog.ui.Container.prototype.disposeInternal = function() {
459   goog.ui.Container.superClass_.disposeInternal.call(this);
460
461   if (this.keyHandler_) {
462     this.keyHandler_.dispose();
463     this.keyHandler_ = null;
464   }
465
466   this.keyEventTarget_ = null;
467   this.childElementIdMap_ = null;
468   this.openItem_ = null;
469   this.renderer_ = null;
470 };
471
472
473 // Default event handlers.
474
475
476 /**
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
480  *    the event.
481  */
482 goog.ui.Container.prototype.handleEnterItem = function(e) {
483   // Allow the Control to highlight itself.
484   return true;
485 };
486
487
488 /**
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.
492  */
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();
497     if (item) {
498       // Un-highlight previously highlighted item.
499       item.setHighlighted(false);
500     }
501
502     this.highlightedIndex_ = index;
503     item = this.getHighlighted();
504
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);
511     }
512
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)) {
517         item.setOpen(true);
518       } else {
519         this.openItem_.setOpen(false);
520       }
521     }
522   }
523
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);
531   }
532 };
533
534
535 /**
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.
539  */
540 goog.ui.Container.prototype.handleUnHighlightItem = function(e) {
541   if (e.target == this.getHighlighted()) {
542     this.highlightedIndex_ = -1;
543   }
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);
550 };
551
552
553 /**
554  * Handles OPEN events dispatched by items in the container when they are
555  * opened.
556  * @param {goog.events.Event} e Open event to handle.
557  */
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);
563     }
564     this.openItem_ = item;
565   }
566 };
567
568
569 /**
570  * Handles CLOSE events dispatched by items in the container when they are
571  * closed.
572  * @param {goog.events.Event} e Close event to handle.
573  */
574 goog.ui.Container.prototype.handleCloseItem = function(e) {
575   if (e.target == this.openItem_) {
576     this.openItem_ = null;
577   }
578 };
579
580
581 /**
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.
586  */
587 goog.ui.Container.prototype.handleMouseDown = function(e) {
588   if (this.enabled_) {
589     this.setMouseButtonPressed(true);
590   }
591
592   var keyTarget = this.getKeyEventTarget();
593   if (keyTarget && goog.dom.isFocusableTabIndex(keyTarget)) {
594     // The container is configured to receive keyboard focus.
595     keyTarget.focus();
596   } else {
597     // The control isn't configured to receive keyboard focus; prevent it
598     // from stealing focus or destroying the selection.
599     e.preventDefault();
600   }
601 };
602
603
604 /**
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.
608  */
609 goog.ui.Container.prototype.handleDocumentMouseUp = function(e) {
610   this.setMouseButtonPressed(false);
611 };
612
613
614 /**
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.
619  */
620 goog.ui.Container.prototype.handleChildMouseEvents = function(e) {
621   var control = this.getOwnerControl(/** @type {Node} */ (e.target));
622   if (control) {
623     // Child control identified; forward the event.
624     switch (e.type) {
625       case goog.events.EventType.MOUSEDOWN:
626         control.handleMouseDown(e);
627         break;
628       case goog.events.EventType.MOUSEUP:
629         control.handleMouseUp(e);
630         break;
631       case goog.events.EventType.MOUSEOVER:
632         control.handleMouseOver(e);
633         break;
634       case goog.events.EventType.MOUSEOUT:
635         control.handleMouseOut(e);
636         break;
637       case goog.events.EventType.CONTEXTMENU:
638         control.handleContextMenu(e);
639         break;
640     }
641   }
642 };
643
644
645 /**
646  * Returns the child control that owns the given DOM node, or null if no such
647  * control is found.
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).
651  * @protected
652  */
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) {
662       var id = node.id;
663       if (id in this.childElementIdMap_) {
664         return this.childElementIdMap_[id];
665       }
666       node = node.parentNode;
667     }
668   }
669   return null;
670 };
671
672
673 /**
674  * Handles focus events raised when the container's key event target receives
675  * keyboard focus.
676  * @param {goog.events.BrowserEvent} e Focus event to handle.
677  */
678 goog.ui.Container.prototype.handleFocus = function(e) {
679   // No-op in the base class.
680 };
681
682
683 /**
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.
687  */
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);
694   }
695 };
696
697
698 /**
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.
704  */
705 goog.ui.Container.prototype.handleKeyEvent = function(e) {
706   if (this.isEnabled() && this.isVisible() &&
707       (this.getChildCount() != 0 || this.keyEventTarget_) &&
708       this.handleKeyEventInternal(e)) {
709     e.preventDefault();
710     e.stopPropagation();
711     return true;
712   }
713   return false;
714 };
715
716
717 /**
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
724  *     its children).
725  */
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)) {
731     return true;
732   }
733
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)) {
738     return true;
739   }
740
741   // Do not handle the key event if any modifier key is pressed.
742   if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) {
743     return false;
744   }
745
746   // Either nothing is highlighted, or the highlighted control didn't handle
747   // the key event, so attempt to handle it here.
748   switch (e.keyCode) {
749     case goog.events.KeyCodes.ESC:
750       if (this.isFocusable()) {
751         this.getKeyEventTarget().blur();
752       } else {
753         return false;
754       }
755       break;
756
757     case goog.events.KeyCodes.HOME:
758       this.highlightFirst();
759       break;
760
761     case goog.events.KeyCodes.END:
762       this.highlightLast();
763       break;
764
765     case goog.events.KeyCodes.UP:
766       if (this.orientation_ == goog.ui.Container.Orientation.VERTICAL) {
767         this.highlightPrevious();
768       } else {
769         return false;
770       }
771       break;
772
773     case goog.events.KeyCodes.LEFT:
774       if (this.orientation_ == goog.ui.Container.Orientation.HORIZONTAL) {
775         if (this.isRightToLeft()) {
776           this.highlightNext();
777         } else {
778           this.highlightPrevious();
779         }
780       } else {
781         return false;
782       }
783       break;
784
785     case goog.events.KeyCodes.DOWN:
786       if (this.orientation_ == goog.ui.Container.Orientation.VERTICAL) {
787         this.highlightNext();
788       } else {
789         return false;
790       }
791       break;
792
793     case goog.events.KeyCodes.RIGHT:
794       if (this.orientation_ == goog.ui.Container.Orientation.HORIZONTAL) {
795         if (this.isRightToLeft()) {
796           this.highlightPrevious();
797         } else {
798           this.highlightNext();
799         }
800       } else {
801         return false;
802       }
803       break;
804
805     default:
806       return false;
807   }
808
809   return true;
810 };
811
812
813 // Child component management.
814
815
816 /**
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
820  *     to be created yet.
821  * @private
822  */
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();
826
827   // If the control's root element doesn't have a DOM ID assign one.
828   var id = childElem.id || (childElem.id = child.getId());
829
830   // Lazily create the child element ID map on first use.
831   if (!this.childElementIdMap_) {
832     this.childElementIdMap_ = {};
833   }
834   this.childElementIdMap_[id] = child;
835 };
836
837
838 /**
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).
844  * @override
845  */
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);
850 };
851
852
853 /**
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.
858  * @override
859  */
860 goog.ui.Container.prototype.getChild;
861
862
863 /**
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.
868  * @override
869  */
870 goog.ui.Container.prototype.getChildAt;
871
872
873 /**
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).
882  * @override
883  */
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);
891   }
892
893   // Disable mouse event handling by child controls.
894   control.setHandleMouseEvents(false);
895
896   // Let the superclass implementation do the work.
897   goog.ui.Container.superClass_.addChildAt.call(this, control, index,
898       opt_render);
899
900   if (control.isInDocument() && this.isInDocument()) {
901     this.registerChildId_(control);
902   }
903
904   // Update the highlight index, if needed.
905   if (index <= this.highlightedIndex_) {
906     this.highlightedIndex_++;
907   }
908 };
909
910
911 /**
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
919  *     false).
920  * @return {goog.ui.Control} The removed control, if any.
921  * @override
922  */
923 goog.ui.Container.prototype.removeChild = function(control, opt_unrender) {
924   control = goog.isString(control) ? this.getChild(control) : control;
925
926   if (control) {
927     var index = this.indexOfChild(control);
928     if (index != -1) {
929       if (index == this.highlightedIndex_) {
930         control.setHighlighted(false);
931         this.highlightedIndex_ = -1;
932       } else if (index < this.highlightedIndex_) {
933         this.highlightedIndex_--;
934       }
935     }
936
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);
941     }
942   }
943
944   control = /** @type {goog.ui.Control} */ (
945       goog.ui.Container.superClass_.removeChild.call(this, control,
946           opt_unrender));
947
948   // Re-enable mouse event handling (in case the control is reused elsewhere).
949   control.setHandleMouseEvents(true);
950
951   return control;
952 };
953
954
955 // Container state management.
956
957
958 /**
959  * Returns the container's orientation.
960  * @return {?goog.ui.Container.Orientation} Container orientation.
961  */
962 goog.ui.Container.prototype.getOrientation = function() {
963   return this.orientation_;
964 };
965
966
967 /**
968  * Sets the container's orientation.
969  * @param {goog.ui.Container.Orientation} orientation Container orientation.
970  */
971 // TODO(attila): Do we need to support containers with dynamic orientation?
972 goog.ui.Container.prototype.setOrientation = function(orientation) {
973   if (this.getElement()) {
974     // Too late.
975     throw Error(goog.ui.Component.Error.ALREADY_RENDERED);
976   }
977
978   this.orientation_ = orientation;
979 };
980
981
982 /**
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.
989  */
990 goog.ui.Container.prototype.isVisible = function() {
991   return this.visible_;
992 };
993
994
995 /**
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.
1003  */
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;
1008
1009     var elem = this.getElement();
1010     if (elem) {
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_);
1016       }
1017       if (!opt_force) {
1018         this.dispatchEvent(this.visible_ ?
1019             goog.ui.Container.EventType.AFTER_SHOW :
1020             goog.ui.Container.EventType.AFTER_HIDE);
1021       }
1022     }
1023
1024     return true;
1025   }
1026
1027   return false;
1028 };
1029
1030
1031 /**
1032  * Returns true if the container is enabled, false otherwise.
1033  * @return {boolean} Whether the container is enabled.
1034  */
1035 goog.ui.Container.prototype.isEnabled = function() {
1036   return this.enabled_;
1037 };
1038
1039
1040 /**
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.
1046  */
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)) {
1051     if (enable) {
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;
1059         } else {
1060           child.setEnabled(true);
1061         }
1062       });
1063     } else {
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);
1070         } else {
1071           child.wasDisabled = true;
1072         }
1073       });
1074       this.enabled_ = false;
1075       this.setMouseButtonPressed(false);
1076     }
1077
1078     if (this.isFocusable()) {
1079       // Enable keyboard access only for enabled & visible components.
1080       this.renderer_.enableTabIndex(this.getKeyEventTarget(),
1081           enable && this.visible_);
1082     }
1083   }
1084 };
1085
1086
1087 /**
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.
1092  */
1093 goog.ui.Container.prototype.isFocusable = function() {
1094   return this.focusable_;
1095 };
1096
1097
1098 /**
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.
1103  */
1104 goog.ui.Container.prototype.setFocusable = function(focusable) {
1105   if (focusable != this.focusable_ && this.isInDocument()) {
1106     this.enableFocusHandling_(focusable);
1107   }
1108   this.focusable_ = focusable;
1109   if (this.enabled_ && this.visible_) {
1110     this.renderer_.enableTabIndex(this.getKeyEventTarget(), focusable);
1111   }
1112 };
1113
1114
1115 /**
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.
1119  */
1120 goog.ui.Container.prototype.isFocusableChildrenAllowed = function() {
1121   return this.allowFocusableChildren_;
1122 };
1123
1124
1125 /**
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.
1129  */
1130 goog.ui.Container.prototype.setFocusableChildrenAllowed = function(focusable) {
1131   this.allowFocusableChildren_ = focusable;
1132 };
1133
1134
1135 /**
1136  * @return {boolean} Whether highlighting a child component should also open it.
1137  */
1138 goog.ui.Container.prototype.isOpenFollowsHighlight = function() {
1139   return this.openFollowsHighlight_;
1140 };
1141
1142
1143 /**
1144  * Sets whether highlighting a child component should also open it.
1145  * @param {boolean} follow Whether highlighting a child component also opens it.
1146  */
1147 goog.ui.Container.prototype.setOpenFollowsHighlight = function(follow) {
1148   this.openFollowsHighlight_ = follow;
1149 };
1150
1151
1152 // Highlight management.
1153
1154
1155 /**
1156  * Returns the index of the currently highlighted item (-1 if none).
1157  * @return {number} Index of the currently highlighted item.
1158  */
1159 goog.ui.Container.prototype.getHighlightedIndex = function() {
1160   return this.highlightedIndex_;
1161 };
1162
1163
1164 /**
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
1168  *     highlight).
1169  */
1170 goog.ui.Container.prototype.setHighlightedIndex = function(index) {
1171   var child = this.getChildAt(index);
1172   if (child) {
1173     child.setHighlighted(true);
1174   } else if (this.highlightedIndex_ > -1) {
1175     this.getHighlighted().setHighlighted(false);
1176   }
1177 };
1178
1179
1180 /**
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.
1184  */
1185 goog.ui.Container.prototype.setHighlighted = function(item) {
1186   this.setHighlightedIndex(this.indexOfChild(item));
1187 };
1188
1189
1190 /**
1191  * Returns the currently highlighted item (if any).
1192  * @return {goog.ui.Control?} Highlighted item (null if none).
1193  */
1194 goog.ui.Container.prototype.getHighlighted = function() {
1195   return this.getChildAt(this.highlightedIndex_);
1196 };
1197
1198
1199 /**
1200  * Highlights the first highlightable item in the container
1201  */
1202 goog.ui.Container.prototype.highlightFirst = function() {
1203   this.highlightHelper(function(index, max) {
1204     return (index + 1) % max;
1205   }, this.getChildCount() - 1);
1206 };
1207
1208
1209 /**
1210  * Highlights the last highlightable item in the container.
1211  */
1212 goog.ui.Container.prototype.highlightLast = function() {
1213   this.highlightHelper(function(index, max) {
1214     index--;
1215     return index < 0 ? max - 1 : index;
1216   }, 0);
1217 };
1218
1219
1220 /**
1221  * Highlights the next highlightable item (or the first if nothing is currently
1222  * highlighted).
1223  */
1224 goog.ui.Container.prototype.highlightNext = function() {
1225   this.highlightHelper(function(index, max) {
1226     return (index + 1) % max;
1227   }, this.highlightedIndex_);
1228 };
1229
1230
1231 /**
1232  * Highlights the previous highlightable item (or the last if nothing is
1233  * currently highlighted).
1234  */
1235 goog.ui.Container.prototype.highlightPrevious = function() {
1236   this.highlightHelper(function(index, max) {
1237     index--;
1238     return index < 0 ? max - 1 : index;
1239   }, this.highlightedIndex_);
1240 };
1241
1242
1243 /**
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.
1250  * @protected
1251  */
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();
1258
1259   curIndex = fn.call(this, curIndex, numItems);
1260   var visited = 0;
1261   while (visited <= numItems) {
1262     var control = this.getChildAt(curIndex);
1263     if (control && this.canHighlightItem(control)) {
1264       this.setHighlightedIndexFromKeyEvent(curIndex);
1265       return true;
1266     }
1267     visited++;
1268     curIndex = fn.call(this, curIndex, numItems);
1269   }
1270   return false;
1271 };
1272
1273
1274 /**
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.
1278  * @protected
1279  */
1280 goog.ui.Container.prototype.canHighlightItem = function(item) {
1281   return item.isVisible() && item.isEnabled() &&
1282       item.isSupportedState(goog.ui.Component.State.HOVER);
1283 };
1284
1285
1286 /**
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.
1292  * @protected
1293  */
1294 goog.ui.Container.prototype.setHighlightedIndexFromKeyEvent = function(index) {
1295   this.setHighlightedIndex(index);
1296 };
1297
1298
1299 /**
1300  * Returns the currently open (expanded) control in the container (null if
1301  * none).
1302  * @return {goog.ui.Control?} The currently open control.
1303  */
1304 goog.ui.Container.prototype.getOpenItem = function() {
1305   return this.openItem_;
1306 };
1307
1308
1309 /**
1310  * Returns true if the mouse button is pressed, false otherwise.
1311  * @return {boolean} Whether the mouse button is pressed.
1312  */
1313 goog.ui.Container.prototype.isMouseButtonPressed = function() {
1314   return this.mouseButtonPressed_;
1315 };
1316
1317
1318 /**
1319  * Sets or clears the "mouse button pressed" flag.
1320  * @param {boolean} pressed Whether the mouse button is presed.
1321  */
1322 goog.ui.Container.prototype.setMouseButtonPressed = function(pressed) {
1323   this.mouseButtonPressed_ = pressed;
1324 };