chromium/third_party/google-closure-library/closure/goog/ui/tooltip.js

/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview Tooltip widget implementation.
 *
 * @see ../demos/tooltip.html
 */

goog.provide('goog.ui.Tooltip');
goog.provide('goog.ui.Tooltip.CursorTooltipPosition');
goog.provide('goog.ui.Tooltip.ElementTooltipPosition');
goog.provide('goog.ui.Tooltip.State');

goog.require('goog.Timer');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dispose');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.dom.safe');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.events.FocusHandler');
goog.require('goog.math.Box');
goog.require('goog.math.Coordinate');
goog.require('goog.positioning');
goog.require('goog.positioning.AnchoredPosition');
goog.require('goog.positioning.Corner');
goog.require('goog.positioning.Overflow');
goog.require('goog.positioning.OverflowStatus');
goog.require('goog.positioning.ViewportPosition');
goog.require('goog.structs.Set');
goog.require('goog.style');
goog.require('goog.ui.Popup');
goog.require('goog.ui.PopupBase');
goog.requireType('goog.events.BrowserEvent');
goog.requireType('goog.html.SafeHtml');
goog.requireType('goog.positioning.AbstractPosition');



/**
 * Tooltip widget. Can be attached to one or more elements and is shown, with a
 * slight delay, when the cursor is over the element or the element gains
 * focus.
 *
 * @param {Element|string=} opt_el Element to display tooltip for, either
 *     element reference or string id.
 * @param {?string=} opt_str Text message to display in tooltip.
 * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
 * @constructor
 * @extends {goog.ui.Popup}
 */
goog.ui.Tooltip = function(opt_el, opt_str, opt_domHelper) {
  'use strict';
  /**
   * Dom Helper
   * @type {goog.dom.DomHelper}
   * @private
   */
  this.dom_ = opt_domHelper ||
      (opt_el ? goog.dom.getDomHelper(goog.dom.getElement(opt_el)) :
                goog.dom.getDomHelper());

  goog.ui.Popup.call(this, this.dom_.createDom(goog.dom.TagName.DIV, {
    'style': 'position:absolute;display:none;'
  }));

  /**
   * Cursor position relative to the page.
   * @type {!goog.math.Coordinate}
   * @protected
   */
  this.cursorPosition = new goog.math.Coordinate(1, 1);

  /**
   * Elements this widget is attached to.
   * @type {goog.structs.Set}
   * @private
   */
  this.elements_ = new goog.structs.Set();

  /**
   * Keyboard focus event handler for elements inside the tooltip.
   * @private {?goog.events.FocusHandler}
   */
  this.tooltipFocusHandler_ = null;

  // Attach to element, if specified
  if (opt_el) {
    this.attach(opt_el);
  }

  // Set message, if specified.
  if (opt_str != null) {
    this.setText(opt_str);
  }
};
goog.inherits(goog.ui.Tooltip, goog.ui.Popup);


/**
 * List of active (open) tooltip widgets. Used to prevent multiple tooltips
 * from appearing at once.
 *
 * @type {!Array<goog.ui.Tooltip>}
 * @private
 */
goog.ui.Tooltip.activeInstances_ = [];


/**
 * Active element reference. Used by the delayed show functionality to keep
 * track of the element the mouse is over or the element with focus.
 * @type {?Element}
 * @private
 */
goog.ui.Tooltip.prototype.activeEl_ = null;


/**
 * CSS class name for tooltip.
 *
 * @type {string}
 */
goog.ui.Tooltip.prototype.className = goog.getCssName('goog-tooltip');


/**
 * Delay in milliseconds since the last mouseover or mousemove before the
 * tooltip is displayed for an element.
 *
 * @type {number}
 * @private
 */
goog.ui.Tooltip.prototype.showDelayMs_ = 500;


/**
 * Timer for when to show.
 *
 * @type {number|undefined}
 * @protected
 */
goog.ui.Tooltip.prototype.showTimer;


/**
 * Delay in milliseconds before tooltips are hidden.
 *
 * @type {number}
 * @private
 */
goog.ui.Tooltip.prototype.hideDelayMs_ = 0;


/**
 * Timer for when to hide.
 *
 * @type {number|undefined}
 * @protected
 */
goog.ui.Tooltip.prototype.hideTimer;


/**
 * Element that triggered the tooltip.  Note that if a second element triggers
 * this tooltip, anchor becomes that second element, even if its show is
 * cancelled and the original tooltip survives.
 *
 * @type {Element|undefined}
 * @protected
 */
goog.ui.Tooltip.prototype.anchor;


/**
 * Possible states for the tooltip to be in.
 * @enum {number}
 */
goog.ui.Tooltip.State = {
  INACTIVE: 0,
  WAITING_TO_SHOW: 1,
  SHOWING: 2,
  WAITING_TO_HIDE: 3,
  UPDATING: 4  // waiting to show new hovercard while old one still showing.
};


/**
 * Popup activation types. Used to select a positioning strategy.
 * @enum {number}
 */
goog.ui.Tooltip.Activation = {
  CURSOR: 0,
  FOCUS: 1
};


/**
 * Whether the anchor has seen the cursor move or has received focus since the
 * tooltip was last shown. Used to ignore mouse over events triggered by view
 * changes and UI updates.
 * @type {boolean|undefined}
 * @private
 */
goog.ui.Tooltip.prototype.seenInteraction_;


/**
 * Whether the cursor must have moved before the tooltip will be shown.
 * @type {boolean|undefined}
 * @private
 */
goog.ui.Tooltip.prototype.requireInteraction_;


/**
 * If this tooltip's element contains another tooltip that becomes active, this
 * property identifies that tooltip so that we can check if this tooltip should
 * not be hidden because the nested tooltip is active.
 * @type {goog.ui.Tooltip}
 * @private
 */
goog.ui.Tooltip.prototype.childTooltip_;


/**
 * If this tooltip is inside another tooltip's element, then it may have
 * prevented that tooltip from hiding.  When this tooltip hides, we'll need
 * to check if the parent should be hidden as well.
 * @type {goog.ui.Tooltip}
 * @private
 */
goog.ui.Tooltip.prototype.parentTooltip_;


/**
 * Returns the dom helper that is being used on this component.
 * @return {goog.dom.DomHelper} The dom helper used on this component.
 */
goog.ui.Tooltip.prototype.getDomHelper = function() {
  'use strict';
  return this.dom_;
};


/**
 * @return {goog.ui.Tooltip} Active tooltip in a child element, or null if none.
 * @protected
 */
goog.ui.Tooltip.prototype.getChildTooltip = function() {
  'use strict';
  return this.childTooltip_;
};


/**
 * Attach to element. Tooltip will be displayed when the cursor is over the
 * element or when the element has been active for a few milliseconds.
 *
 * @param {Element|string} el Element to display tooltip for, either element
 *                            reference or string id.
 */
goog.ui.Tooltip.prototype.attach = function(el) {
  'use strict';
  el = goog.dom.getElement(el);

  this.elements_.add(el);
  goog.events.listen(
      el, goog.events.EventType.MOUSEOVER, this.handleMouseOver, false, this);
  goog.events.listen(
      el, goog.events.EventType.MOUSEOUT, this.handleMouseOutAndBlur, false,
      this);
  goog.events.listen(
      el, goog.events.EventType.MOUSEMOVE, this.handleMouseMove, false, this);
  goog.events.listen(
      el, goog.events.EventType.FOCUS, this.handleFocus, false, this);
  goog.events.listen(
      el, goog.events.EventType.BLUR, this.handleMouseOutAndBlur, false, this);
};


/**
 * Detach from element(s).
 *
 * @param {Element|string=} opt_el Element to detach from, either element
 *                                reference or string id. If no element is
 *                                specified all are detached.
 */
goog.ui.Tooltip.prototype.detach = function(opt_el) {
  'use strict';
  if (opt_el) {
    var el = goog.dom.getElement(opt_el);
    this.detachElement_(el);
    this.elements_.remove(el);
  } else {
    var a = this.elements_.getValues();
    for (var el, i = 0; el = a[i]; i++) {
      this.detachElement_(el);
    }
    this.elements_.clear();
  }
};


/**
 * Detach from element.
 *
 * @param {Element} el Element to detach from.
 * @private
 */
goog.ui.Tooltip.prototype.detachElement_ = function(el) {
  'use strict';
  goog.events.unlisten(
      el, goog.events.EventType.MOUSEOVER, this.handleMouseOver, false, this);
  goog.events.unlisten(
      el, goog.events.EventType.MOUSEOUT, this.handleMouseOutAndBlur, false,
      this);
  goog.events.unlisten(
      el, goog.events.EventType.MOUSEMOVE, this.handleMouseMove, false, this);
  goog.events.unlisten(
      el, goog.events.EventType.FOCUS, this.handleFocus, false, this);
  goog.events.unlisten(
      el, goog.events.EventType.BLUR, this.handleMouseOutAndBlur, false, this);
};


/**
 * Sets delay in milliseconds before tooltip is displayed for an element.
 *
 * @param {number} delay The delay in milliseconds.
 */
goog.ui.Tooltip.prototype.setShowDelayMs = function(delay) {
  'use strict';
  this.showDelayMs_ = delay;
};


/**
 * @return {number} The delay in milliseconds before tooltip is displayed for an
 *     element.
 */
goog.ui.Tooltip.prototype.getShowDelayMs = function() {
  'use strict';
  return this.showDelayMs_;
};


/**
 * Sets delay in milliseconds before tooltip is hidden once the cursor leavs
 * the element.
 *
 * @param {number} delay The delay in milliseconds.
 */
goog.ui.Tooltip.prototype.setHideDelayMs = function(delay) {
  'use strict';
  this.hideDelayMs_ = delay;
};


/**
 * @return {number} The delay in milliseconds before tooltip is hidden once the
 *     cursor leaves the element.
 */
goog.ui.Tooltip.prototype.getHideDelayMs = function() {
  'use strict';
  return this.hideDelayMs_;
};


/**
 * Sets tooltip message as plain text.
 *
 * @param {string} str Text message to display in tooltip.
 */
goog.ui.Tooltip.prototype.setText = function(str) {
  'use strict';
  goog.dom.setTextContent(this.getElement(), str);
};


/**
 * Sets tooltip message as HTML markup.
 * @param {!goog.html.SafeHtml} html HTML message to display in tooltip.
 */
goog.ui.Tooltip.prototype.setSafeHtml = function(html) {
  'use strict';
  var element = this.getElement();
  if (element) {
    goog.dom.safe.setInnerHtml(element, html);
  }
};


/**
 * Sets tooltip element.
 *
 * @param {Element} el HTML element to use as the tooltip.
 * @override
 */
goog.ui.Tooltip.prototype.setElement = function(el) {
  'use strict';
  var oldElement = this.getElement();
  if (oldElement) {
    goog.dom.removeNode(oldElement);
  }
  goog.ui.Tooltip.superClass_.setElement.call(this, el);
  if (el) {
    var body = this.dom_.getDocument().body;
    body.insertBefore(el, body.lastChild);
    this.registerContentFocusEvents_();
  } else {
    goog.dispose(this.tooltipFocusHandler_);
    this.tooltipFocusHandler_ = null;
  }
};


/**
 * Handler for keyboard focus events of elements inside the tooltip's content
 * element. This should only be invoked if this.getElement() != null.
 * @private
 */
goog.ui.Tooltip.prototype.registerContentFocusEvents_ = function() {
  'use strict';
  goog.dispose(this.tooltipFocusHandler_);
  this.tooltipFocusHandler_ =
      new goog.events.FocusHandler(goog.asserts.assert(this.getElement()));
  this.registerDisposable(this.tooltipFocusHandler_);

  goog.events.listen(
      this.tooltipFocusHandler_, goog.events.FocusHandler.EventType.FOCUSIN,
      this.clearHideTimer, undefined /* opt_capt */, this);
  goog.events.listen(
      this.tooltipFocusHandler_, goog.events.FocusHandler.EventType.FOCUSOUT,
      this.startHideTimer, undefined /* opt_capt */, this);
};


/**
 * @return {string} The tooltip message as plain text.
 */
goog.ui.Tooltip.prototype.getText = function() {
  'use strict';
  return goog.dom.getTextContent(this.getElement());
};


/**
 * @return {string} The tooltip message as HTML as plain string.
 */
goog.ui.Tooltip.prototype.getHtml = function() {
  'use strict';
  return this.getElement().innerHTML;
};


/**
 * @return {goog.ui.Tooltip.State} Current state of tooltip.
 */
goog.ui.Tooltip.prototype.getState = function() {
  'use strict';
  return this.showTimer ?
                         (this.isVisible() ? goog.ui.Tooltip.State.UPDATING :
                                             goog.ui.Tooltip.State.WAITING_TO_SHOW) :
      this.hideTimer   ? goog.ui.Tooltip.State.WAITING_TO_HIDE :
      this.isVisible() ? goog.ui.Tooltip.State.SHOWING :
                         goog.ui.Tooltip.State.INACTIVE;
};


/**
 * Sets whether tooltip requires the mouse to have moved or the anchor receive
 * focus before the tooltip will be shown.
 * @param {boolean} requireInteraction Whether tooltip should require some user
 *     interaction before showing tooltip.
 */
goog.ui.Tooltip.prototype.setRequireInteraction = function(requireInteraction) {
  'use strict';
  this.requireInteraction_ = requireInteraction;
};


/**
 * Returns true if the coord is in the tooltip.
 * @param {goog.math.Coordinate} coord Coordinate being tested.
 * @return {boolean} Whether the coord is in the tooltip.
 */
goog.ui.Tooltip.prototype.isCoordinateInTooltip = function(coord) {
  'use strict';
  // Check if coord is inside the tooltip
  if (!this.isVisible()) {
    return false;
  }

  var offset = goog.style.getPageOffset(this.getElement());
  var size = goog.style.getSize(this.getElement());
  return offset.x <= coord.x && coord.x <= offset.x + size.width &&
      offset.y <= coord.y && coord.y <= offset.y + size.height;
};


/**
 * Called before the popup is shown.
 *
 * @return {boolean} Whether tooltip should be shown.
 * @protected
 * @override
 */
goog.ui.Tooltip.prototype.onBeforeShow = function() {
  'use strict';
  if (!goog.ui.PopupBase.prototype.onBeforeShow.call(this)) {
    return false;
  }

  // Hide all open tooltips except if this tooltip is triggered by an element
  // inside another tooltip.
  if (this.anchor) {
    for (var tt, i = 0; tt = goog.ui.Tooltip.activeInstances_[i]; i++) {
      if (!goog.dom.contains(tt.getElement(), this.anchor)) {
        tt.setVisible(false);
      }
    }
  }
  goog.array.insert(goog.ui.Tooltip.activeInstances_, this);

  var element = this.getElement();
  element.className = this.className;
  this.clearHideTimer();

  // Register event handlers for tooltip. Used to prevent the tooltip from
  // closing if the cursor is over the tooltip rather then the element that
  // triggered it.
  goog.events.listen(
      element, goog.events.EventType.MOUSEOVER, this.handleTooltipMouseOver,
      false, this);
  goog.events.listen(
      element, goog.events.EventType.MOUSEOUT, this.handleTooltipMouseOut,
      false, this);

  this.clearShowTimer();
  return true;
};


/** @override */
goog.ui.Tooltip.prototype.onHide = function() {
  'use strict';
  goog.array.remove(goog.ui.Tooltip.activeInstances_, this);

  // Hide all open tooltips triggered by an element inside this tooltip.
  var element = this.getElement();
  for (var tt, i = 0; tt = goog.ui.Tooltip.activeInstances_[i]; i++) {
    if (tt.anchor && goog.dom.contains(element, tt.anchor)) {
      tt.setVisible(false);
    }
  }

  // If this tooltip is inside another tooltip, start hide timer for that
  // tooltip in case this tooltip was the only reason it was still showing.
  if (this.parentTooltip_) {
    this.parentTooltip_.startHideTimer();
  }

  goog.events.unlisten(
      element, goog.events.EventType.MOUSEOVER, this.handleTooltipMouseOver,
      false, this);
  goog.events.unlisten(
      element, goog.events.EventType.MOUSEOUT, this.handleTooltipMouseOut,
      false, this);

  this.anchor = undefined;
  // If we are still waiting to show a different hovercard, don't abort it
  // because you think you haven't seen a mouse move:
  if (this.getState() == goog.ui.Tooltip.State.INACTIVE) {
    this.seenInteraction_ = false;
  }

  goog.ui.PopupBase.prototype.onHide.call(this);
};


/**
 * Called by timer from mouse over handler. Shows tooltip if cursor is still
 * over the same element.
 *
 * @param {Element} el Element to show tooltip for.
 * @param {goog.positioning.AbstractPosition=} opt_pos Position to display popup
 *     at.
 */
goog.ui.Tooltip.prototype.maybeShow = function(el, opt_pos) {
  'use strict';
  // Assert that the mouse is still over the same element, and that we have not
  // detached from the anchor in the meantime.
  if (this.anchor == el && this.elements_.contains(this.anchor)) {
    if (this.seenInteraction_ || !this.requireInteraction_) {
      // If it is currently showing, then hide it, and abort if it doesn't hide.
      this.setVisible(false);
      if (!this.isVisible()) {
        this.positionAndShow_(el, opt_pos);
      }
    } else {
      this.anchor = undefined;
    }
  }
  this.showTimer = undefined;
};


/**
 * @return {goog.structs.Set} Elements this widget is attached to.
 * @protected
 */
goog.ui.Tooltip.prototype.getElements = function() {
  'use strict';
  return this.elements_;
};


/**
 * @return {Element} Active element reference.
 */
goog.ui.Tooltip.prototype.getActiveElement = function() {
  'use strict';
  return this.activeEl_;
};


/**
 * @param {Element} activeEl Active element reference.
 * @protected
 */
goog.ui.Tooltip.prototype.setActiveElement = function(activeEl) {
  'use strict';
  this.activeEl_ = activeEl;
};


/**
 * Shows tooltip for a specific element.
 *
 * @param {Element} el Element to show tooltip for.
 * @param {goog.positioning.AbstractPosition=} opt_pos Position to display popup
 *     at.
 */
goog.ui.Tooltip.prototype.showForElement = function(el, opt_pos) {
  'use strict';
  this.attach(el);
  this.activeEl_ = el;

  this.positionAndShow_(el, opt_pos);
};


/**
 * Sets tooltip position and shows it.
 *
 * @param {Element} el Element to show tooltip for.
 * @param {goog.positioning.AbstractPosition=} opt_pos Position to display popup
 *     at.
 * @private
 */
goog.ui.Tooltip.prototype.positionAndShow_ = function(el, opt_pos) {
  'use strict';
  this.anchor = el;
  this.setPosition(
      opt_pos ||
      this.getPositioningStrategy(goog.ui.Tooltip.Activation.CURSOR));
  this.setVisible(true);
};


/**
 * Called by timer from mouse out handler. Hides tooltip if cursor is still
 * outside element and tooltip, or if a child of tooltip has the focus.
 * @param {?Element|undefined} el Tooltip's anchor when hide timer was started.
 */
goog.ui.Tooltip.prototype.maybeHide = function(el) {
  'use strict';
  this.hideTimer = undefined;
  if (el == this.anchor) {
    var dom = this.getDomHelper();
    var focusedEl = dom.getActiveElement();
    // If the tooltip content is focused, then don't hide the tooltip.
    var tooltipContentFocused = focusedEl && this.getElement() &&
        dom.contains(this.getElement(), focusedEl);
    if ((this.activeEl_ == null ||
         (this.activeEl_ != this.getElement() &&
          !this.elements_.contains(this.activeEl_))) &&
        !tooltipContentFocused && !this.hasActiveChild()) {
      this.setVisible(false);
    }
  }
};


/**
 * @return {boolean} Whether tooltip element contains an active child tooltip,
 *     and should thus not be hidden.  When the child tooltip is hidden, it
 *     will check if the parent should be hidden, too.
 * @protected
 */
goog.ui.Tooltip.prototype.hasActiveChild = function() {
  'use strict';
  return !!(this.childTooltip_ && this.childTooltip_.activeEl_);
};


/**
 * Saves the current mouse cursor position to `this.cursorPosition`.
 * @param {goog.events.BrowserEvent} event MOUSEOVER or MOUSEMOVE event.
 * @private
 */
goog.ui.Tooltip.prototype.saveCursorPosition_ = function(event) {
  'use strict';
  var scroll = this.dom_.getDocumentScroll();
  this.cursorPosition.x = event.clientX + scroll.x;
  this.cursorPosition.y = event.clientY + scroll.y;
};


/**
 * Handler for mouse over events.
 *
 * @param {goog.events.BrowserEvent} event Event object.
 * @protected
 */
goog.ui.Tooltip.prototype.handleMouseOver = function(event) {
  'use strict';
  var el = this.getAnchorFromElement(/** @type {Element} */ (event.target));
  this.activeEl_ = el;
  this.clearHideTimer();
  if (el != this.anchor) {
    this.anchor = el;
    this.startShowTimer(el);
    this.checkForParentTooltip_();
    this.saveCursorPosition_(event);
  }
};


/**
 * Find anchor containing the given element, if any.
 *
 * @param {Element} el Element that triggered event.
 * @return {Element} Element in elements_ array that contains given element,
 *     or null if not found.
 * @protected
 */
goog.ui.Tooltip.prototype.getAnchorFromElement = function(el) {
  'use strict';
  // FireFox has a bug where mouse events relating to <input> elements are
  // sometimes duplicated (often in FF2, rarely in FF3): once for the
  // <input> element and once for a magic hidden <div> element.  JavaScript
  // code does not have sufficient permissions to read properties on that
  // magic element and thus will throw an error in this call to
  // getAnchorFromElement_().  In that case we swallow the error.
  // See https://bugzilla.mozilla.org/show_bug.cgi?id=330961
  try {
    while (el && !this.elements_.contains(el)) {
      el = /** @type {Element} */ (el.parentNode);
    }
    return el;
  } catch (e) {
    return null;
  }
};


/**
 * Handler for mouse move events.
 *
 * @param {goog.events.BrowserEvent} event MOUSEMOVE event.
 * @protected
 */
goog.ui.Tooltip.prototype.handleMouseMove = function(event) {
  'use strict';
  this.saveCursorPosition_(event);
  this.seenInteraction_ = true;
};


/**
 * Handler for focus events.
 *
 * @param {goog.events.BrowserEvent} event Event object.
 * @protected
 */
goog.ui.Tooltip.prototype.handleFocus = function(event) {
  'use strict';
  var el = this.getAnchorFromElement(/** @type {Element} */ (event.target));
  this.activeEl_ = el;
  this.seenInteraction_ = true;

  if (this.anchor != el) {
    this.anchor = el;
    var pos = this.getPositioningStrategy(goog.ui.Tooltip.Activation.FOCUS);
    this.clearHideTimer();
    this.startShowTimer(el, pos);

    this.checkForParentTooltip_();
  }
};


/**
 * Return a Position instance for repositioning the tooltip. Override in
 * subclasses to customize the way repositioning is done.
 *
 * @param {goog.ui.Tooltip.Activation} activationType Information about what
 *    kind of event caused the popup to be shown.
 * @return {!goog.positioning.AbstractPosition} The position object used
 *    to position the tooltip.
 * @protected
 */
goog.ui.Tooltip.prototype.getPositioningStrategy = function(activationType) {
  'use strict';
  if (activationType == goog.ui.Tooltip.Activation.CURSOR) {
    var coord = this.cursorPosition.clone();
    return new goog.ui.Tooltip.CursorTooltipPosition(coord);
  }
  return new goog.ui.Tooltip.ElementTooltipPosition(this.activeEl_);
};


/**
 * Looks for an active tooltip whose element contains this tooltip's anchor.
 * This allows us to prevent hides until they are really necessary.
 *
 * @private
 */
goog.ui.Tooltip.prototype.checkForParentTooltip_ = function() {
  'use strict';
  if (this.anchor) {
    for (var tt, i = 0; tt = goog.ui.Tooltip.activeInstances_[i]; i++) {
      if (goog.dom.contains(tt.getElement(), this.anchor)) {
        tt.childTooltip_ = this;
        this.parentTooltip_ = tt;
      }
    }
  }
};


/**
 * Handler for mouse out and blur events.
 *
 * @param {goog.events.BrowserEvent} event Event object.
 * @protected
 */
goog.ui.Tooltip.prototype.handleMouseOutAndBlur = function(event) {
  'use strict';
  var el = this.getAnchorFromElement(/** @type {Element} */ (event.target));
  var elTo = this.getAnchorFromElement(
      /** @type {Element} */ (event.relatedTarget));
  if (el == elTo) {
    // We haven't really left the anchor, just moved from one child to
    // another.
    return;
  }

  if (el == this.activeEl_) {
    this.activeEl_ = null;
  }

  this.clearShowTimer();
  this.seenInteraction_ = false;
  if (this.isVisible() &&
      (!event.relatedTarget ||
       !goog.dom.contains(this.getElement(), event.relatedTarget))) {
    this.startHideTimer();
  } else {
    this.anchor = undefined;
  }
};


/**
 * Handler for mouse over events for the tooltip element.
 *
 * @param {goog.events.BrowserEvent} event Event object.
 * @protected
 */
goog.ui.Tooltip.prototype.handleTooltipMouseOver = function(event) {
  'use strict';
  var element = this.getElement();
  if (this.activeEl_ != element) {
    this.clearHideTimer();
    this.activeEl_ = element;
  }
};


/**
 * Handler for mouse out events for the tooltip element.
 *
 * @param {goog.events.BrowserEvent} event Event object.
 * @protected
 */
goog.ui.Tooltip.prototype.handleTooltipMouseOut = function(event) {
  'use strict';
  var element = this.getElement();
  if (this.activeEl_ == element &&
      (!event.relatedTarget ||
       !goog.dom.contains(element, event.relatedTarget))) {
    this.activeEl_ = null;
    this.startHideTimer();
  }
};


/**
 * Helper method, starts timer that calls maybeShow. Parameters are passed to
 * the maybeShow method.
 *
 * @param {Element} el Element to show tooltip for.
 * @param {goog.positioning.AbstractPosition=} opt_pos Position to display popup
 *     at.
 * @protected
 */
goog.ui.Tooltip.prototype.startShowTimer = function(el, opt_pos) {
  'use strict';
  if (!this.showTimer) {
    this.showTimer = goog.Timer.callOnce(
        goog.bind(this.maybeShow, this, el, opt_pos), this.showDelayMs_);
  }
};


/**
 * Helper method called to clear the show timer.
 *
 * @protected
 */
goog.ui.Tooltip.prototype.clearShowTimer = function() {
  'use strict';
  if (this.showTimer) {
    goog.Timer.clear(this.showTimer);
    this.showTimer = undefined;
  }
};


/**
 * Helper method called to start the close timer.
 * @protected
 */
goog.ui.Tooltip.prototype.startHideTimer = function() {
  'use strict';
  if (this.getState() == goog.ui.Tooltip.State.SHOWING) {
    this.hideTimer = goog.Timer.callOnce(
        goog.bind(this.maybeHide, this, this.anchor), this.getHideDelayMs());
  }
};


/**
 * Helper method called to clear the close timer.
 * @protected
 */
goog.ui.Tooltip.prototype.clearHideTimer = function() {
  'use strict';
  if (this.hideTimer) {
    goog.Timer.clear(this.hideTimer);
    this.hideTimer = undefined;
  }
};


/** @override */
goog.ui.Tooltip.prototype.disposeInternal = function() {
  'use strict';
  this.setVisible(false);
  this.clearShowTimer();
  this.detach();
  if (this.getElement()) {
    goog.dom.removeNode(this.getElement());
  }
  this.activeEl_ = null;
  delete this.dom_;
  goog.ui.Tooltip.superClass_.disposeInternal.call(this);
};



/**
 * Popup position implementation that positions the popup (the tooltip in this
 * case) based on the cursor position. It's positioned below the cursor to the
 * right if there's enough room to fit all of it inside the Viewport. Otherwise
 * it's displayed as far right as possible either above or below the element.
 *
 * Used to position tooltips triggered by the cursor.
 *
 * @param {number|!goog.math.Coordinate} arg1 Left position or coordinate.
 * @param {number=} opt_arg2 Top position.
 * @constructor
 * @extends {goog.positioning.ViewportPosition}
 * @final
 */
goog.ui.Tooltip.CursorTooltipPosition = function(arg1, opt_arg2) {
  'use strict';
  goog.positioning.ViewportPosition.call(this, arg1, opt_arg2);
};
goog.inherits(
    goog.ui.Tooltip.CursorTooltipPosition, goog.positioning.ViewportPosition);


/**
 * Repositions the popup based on cursor position.
 *
 * @param {Element} element The DOM element of the popup.
 * @param {goog.positioning.Corner} popupCorner The corner of the popup element
 *     that that should be positioned adjacent to the anchorElement.
 * @param {goog.math.Box=} opt_margin A margin specified in pixels.
 * @override
 */
goog.ui.Tooltip.CursorTooltipPosition.prototype.reposition = function(
    element, popupCorner, opt_margin) {
  'use strict';
  var viewportElt = goog.style.getClientViewportElement(element);
  var viewport = goog.style.getVisibleRectForElement(viewportElt);
  var margin = opt_margin ? new goog.math.Box(
                                opt_margin.top + 10, opt_margin.right,
                                opt_margin.bottom, opt_margin.left + 10) :
                            new goog.math.Box(10, 0, 0, 10);

  if (goog.positioning.positionAtCoordinate(
          this.coordinate, element, goog.positioning.Corner.TOP_START, margin,
          viewport,
          goog.positioning.Overflow.ADJUST_X |
              goog.positioning.Overflow.FAIL_Y) &
      goog.positioning.OverflowStatus.FAILED) {
    goog.positioning.positionAtCoordinate(
        this.coordinate, element, goog.positioning.Corner.TOP_START, margin,
        viewport,
        goog.positioning.Overflow.ADJUST_X |
            goog.positioning.Overflow.ADJUST_Y);
  }
};



/**
 * Popup position implementation that positions the popup (the tooltip in this
 * case) based on the element position. It's positioned below the element to the
 * right if there's enough room to fit all of it inside the Viewport. Otherwise
 * it's displayed as far right as possible either above or below the element.
 *
 * Used to position tooltips triggered by focus changes.
 *
 * @param {Element} element The element to anchor the popup at.
 * @constructor
 * @extends {goog.positioning.AnchoredPosition}
 */
goog.ui.Tooltip.ElementTooltipPosition = function(element) {
  'use strict';
  goog.positioning.AnchoredPosition.call(
      this, element, goog.positioning.Corner.BOTTOM_RIGHT);
};
goog.inherits(
    goog.ui.Tooltip.ElementTooltipPosition, goog.positioning.AnchoredPosition);


/**
 * Repositions the popup based on element position.
 *
 * @param {Element} element The DOM element of the popup.
 * @param {goog.positioning.Corner} popupCorner The corner of the popup element
 *     that should be positioned adjacent to the anchorElement.
 * @param {goog.math.Box=} opt_margin A margin specified in pixels.
 * @override
 */
goog.ui.Tooltip.ElementTooltipPosition.prototype.reposition = function(
    element, popupCorner, opt_margin) {
  'use strict';
  var offset = new goog.math.Coordinate(10, 0);

  if (goog.positioning.positionAtAnchor(
          this.element, this.corner, element, popupCorner, offset, opt_margin,
          goog.positioning.Overflow.ADJUST_X |
              goog.positioning.Overflow.FAIL_Y) &
      goog.positioning.OverflowStatus.FAILED) {
    goog.positioning.positionAtAnchor(
        this.element, goog.positioning.Corner.TOP_RIGHT, element,
        goog.positioning.Corner.BOTTOM_LEFT, offset, opt_margin,
        goog.positioning.Overflow.ADJUST_X |
            goog.positioning.Overflow.ADJUST_Y);
  }
};