// 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;
};
