text: // 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 Definition of the Bubble class.
   *
   *
   * @see ../demos/bubble.html
   *
   * TODO: support decoration and addChild
   */
  
  goog.provide('goog.ui.Bubble');
  
  goog.require('goog.Timer');
  goog.require('goog.dom.safe');
  goog.require('goog.events');
  goog.require('goog.events.EventType');
  goog.require('goog.html.SafeHtml');
  goog.require('goog.math.Box');
  goog.require('goog.positioning');
  goog.require('goog.positioning.AbsolutePosition');
  goog.require('goog.positioning.AnchoredPosition');
  goog.require('goog.positioning.Corner');
  goog.require('goog.positioning.CornerBit');
  goog.require('goog.string.Const');
  goog.require('goog.style');
  goog.require('goog.ui.Component');
  goog.require('goog.ui.Popup');
  
  
  goog.scope(function() {
  var SafeHtml = goog.html.SafeHtml;
  
  
  
  /**
   * The Bubble provides a general purpose bubble implementation that can be
   * anchored to a particular element and displayed for a period of time.
   *
   * @param {string|!goog.html.SafeHtml|?Element} message Message or an element
   *     to display inside the bubble. Strings are treated as plain-text and will
   *     be HTML escaped.
   * @param {Object=} opt_config The configuration
   *     for the bubble. If not specified, the default configuration will be
   *     used. {@see goog.ui.Bubble.defaultConfig}.
   * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
   * @constructor
   * @extends {goog.ui.Component}
   */
  goog.ui.Bubble = function(message, opt_config, opt_domHelper) {
    goog.ui.Component.call(this, opt_domHelper);
  
    if (typeof message === 'string') {
      message = goog.html.SafeHtml.htmlEscape(message);
    }
  
    /**
     * The HTML string or element to display inside the bubble.
     *
     * @type {!goog.html.SafeHtml|Element}
     * @private
     */
    this.message_ = message;
  
    /**
     * The Popup element used to position and display the bubble.
     *
     * @type {goog.ui.Popup}
     * @private
     */
    this.popup_ = new goog.ui.Popup();
  
    /**
     * Configuration map that contains bubble's UI elements.
     *
     * @type {Object}
     * @private
     */
    this.config_ = opt_config || goog.ui.Bubble.defaultConfig;
  
    /**
     * Id of the close button for this bubble.
     *
     * @type {string}
     * @private
     */
    this.closeButtonId_ = this.makeId('cb');
  
    /**
     * Id of the div for the embedded element.
     *
     * @type {string}
     * @private
     */
    this.messageId_ = this.makeId('mi');
  
  };
  goog.inherits(goog.ui.Bubble, goog.ui.Component);
  goog.tagUnsealableClass(goog.ui.Bubble);
  
  
  /**
   * In milliseconds, timeout after which the button auto-hides. Null means
   * infinite.
   * @type {?number}
   * @private
   */
  goog.ui.Bubble.prototype.timeout_ = null;
  
  
  /**
   * Key returned by the bubble timer.
   * @type {?number}
   * @private
   */
  goog.ui.Bubble.prototype.timerId_ = 0;
  
  
  /**
   * Key returned by the listen function for the close button.
   * @type {?goog.events.Key}
   * @private
   */
  goog.ui.Bubble.prototype.listener_ = null;
  
  
  
  /** @override */
  goog.ui.Bubble.prototype.createDom = function() {
    goog.ui.Bubble.superClass_.createDom.call(this);
  
    var element = this.getElement();
    element.style.position = 'absolute';
    element.style.visibility = 'hidden';
  
    this.popup_.setElement(element);
  };
  
  
  /**
   * Attaches the bubble to an anchor element. Computes the positioning and
   * orientation of the bubble.
   *
   * @param {Element} anchorElement The element to which we are attaching.
   */
  goog.ui.Bubble.prototype.attach = function(anchorElement) {
    this.setAnchoredPosition_(
        anchorElement, this.computePinnedCorner_(anchorElement));
  };
  
  
  /**
   * Sets the corner of the bubble to used in the positioning algorithm.
   *
   * @param {goog.positioning.Corner} corner The bubble corner used for
   *     positioning constants.
   */
  goog.ui.Bubble.prototype.setPinnedCorner = function(corner) {
    this.popup_.setPinnedCorner(corner);
  };
  
  
  /**
   * Sets the position of the bubble. Pass null for corner in AnchoredPosition
   * for corner to be computed automatically.
   *
   * @param {goog.positioning.AbstractPosition} position The position of the
   *     bubble.
   */
  goog.ui.Bubble.prototype.setPosition = function(position) {
    if (position instanceof goog.positioning.AbsolutePosition) {
      this.popup_.setPosition(position);
    } else if (position instanceof goog.positioning.AnchoredPosition) {
      this.setAnchoredPosition_(position.element, position.corner);
    } else {
      throw new Error('Bubble only supports absolute and anchored positions!');
    }
  };
  
  
  /**
   * Sets the timeout after which bubble hides itself.
   *
   * @param {number} timeout Timeout of the bubble.
   */
  goog.ui.Bubble.prototype.setTimeout = function(timeout) {
    this.timeout_ = timeout;
  };
  
  
  /**
   * Sets whether the bubble should be automatically hidden whenever user clicks
   * outside the bubble element.
   *
   * @param {boolean} autoHide Whether to hide if user clicks outside the bubble.
   */
  goog.ui.Bubble.prototype.setAutoHide = function(autoHide) {
    this.popup_.setAutoHide(autoHide);
  };
  
  
  /**
   * Sets whether the bubble should be visible.
   *
   * @param {boolean} visible Desired visibility state.
   */
  goog.ui.Bubble.prototype.setVisible = function(visible) {
    if (visible && !this.popup_.isVisible()) {
      this.configureElement_();
    }
    this.popup_.setVisible(visible);
    if (!this.popup_.isVisible()) {
      this.unconfigureElement_();
    }
  };
  
  
  /**
   * @return {boolean} Whether the bubble is visible.
   */
  goog.ui.Bubble.prototype.isVisible = function() {
    return this.popup_.isVisible();
  };
  
  
  /** @override */
  goog.ui.Bubble.prototype.disposeInternal = function() {
    this.unconfigureElement_();
    this.popup_.dispose();
    this.popup_ = null;
    goog.ui.Bubble.superClass_.disposeInternal.call(this);
  };
  
  
  /**
   * Creates element's contents and configures all timers. This is called on
   * setVisible(true).
   * @private
   */
  goog.ui.Bubble.prototype.configureElement_ = function() {
    if (!this.isInDocument()) {
      throw new Error('You must render the bubble before showing it!');
    }
  
    var element = this.getElement();
    var corner = this.popup_.getPinnedCorner();
    goog.dom.safe.setInnerHtml(
        /** @type {!Element} */ (element), this.computeHtmlForCorner_(corner));
  
    if (!(this.message_ instanceof SafeHtml)) {
      var messageDiv = this.getDomHelper().getElement(this.messageId_);
      this.getDomHelper().appendChild(messageDiv, this.message_);
    }
    var closeButton = this.getDomHelper().getElement(this.closeButtonId_);
    this.listener_ = goog.events.listen(
        closeButton, goog.events.EventType.CLICK, this.hideBubble_, false, this);
  
    if (this.timeout_) {
      this.timerId_ = goog.Timer.callOnce(this.hideBubble_, this.timeout_, this);
    }
  };
  
  
  /**
   * Gets rid of the element's contents and all associated timers and listeners.
   * This is called on dispose as well as on setVisible(false).
   * @private
   */
  goog.ui.Bubble.prototype.unconfigureElement_ = function() {
    if (this.listener_) {
      goog.events.unlistenByKey(this.listener_);
      this.listener_ = null;
    }
    if (this.timerId_) {
      goog.Timer.clear(this.timerId_);
      this.timerId_ = null;
    }
  
    var element = this.getElement();
    if (element) {
      this.getDomHelper().removeChildren(element);
      goog.dom.safe.setInnerHtml(element, goog.html.SafeHtml.EMPTY);
    }
  };
  
  
  /**
   * Computes bubble position based on anchored element.
   *
   * @param {Element} anchorElement The element to which we are attaching.
   * @param {goog.positioning.Corner} corner The bubble corner used for
   *     positioning.
   * @private
   */
  goog.ui.Bubble.prototype.setAnchoredPosition_ = function(
      anchorElement, corner) {
    this.popup_.setPinnedCorner(corner);
    var margin = this.createMarginForCorner_(corner);
    this.popup_.setMargin(margin);
    var anchorCorner = goog.positioning.flipCorner(corner);
    this.popup_.setPosition(
        new goog.positioning.AnchoredPosition(anchorElement, anchorCorner));
  };
  
  
  /**
   * Hides the bubble. This is called asynchronously by timer of event processor
   * for the mouse click on the close button.
   * @private
   */
  goog.ui.Bubble.prototype.hideBubble_ = function() {
    this.setVisible(false);
  };
  
  
  /**
   * Returns an AnchoredPosition that will position the bubble optimally
   * given the position of the anchor element and the size of the viewport.
   *
   * @param {Element} anchorElement The element to which the bubble is attached.
   * @return {!goog.positioning.AnchoredPosition} The AnchoredPosition
   *     to give to {@link #setPosition}.
   */
  goog.ui.Bubble.prototype.getComputedAnchoredPosition = function(anchorElement) {
    return new goog.positioning.AnchoredPosition(
        anchorElement, this.computePinnedCorner_(anchorElement));
  };
  
  
  /**
   * Computes the pinned corner for the bubble.
   * @param {Element} anchorElement The element to which the button is attached.
   * @return {goog.positioning.Corner} The pinned corner.
   * @private
   * @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
   */
  goog.ui.Bubble.prototype.computePinnedCorner_ = function(anchorElement) {
    var doc = this.getDomHelper().getOwnerDocument(anchorElement);
    var viewportElement = goog.style.getClientViewportElement(doc);
    var viewportWidth = viewportElement.offsetWidth;
    var viewportHeight = viewportElement.offsetHeight;
    var anchorElementOffset = goog.style.getPageOffset(anchorElement);
    var anchorElementSize = goog.style.getSize(anchorElement);
    var anchorType = 0;
    // right margin or left?
    if (viewportWidth - anchorElementOffset.x - anchorElementSize.width >
        anchorElementOffset.x) {
      anchorType += 1;
    }
    // attaches to the top or to the bottom?
    if (viewportHeight - anchorElementOffset.y - anchorElementSize.height >
        anchorElementOffset.y) {
      anchorType += 2;
    }
    return goog.ui.Bubble.corners_[anchorType];
  };
  
  
  /**
   * Computes the right offset for a given bubble corner
   * and creates a margin element for it. This is done to have the
   * button anchor element on its frame rather than on the corner.
   * @param {goog.positioning.Corner} corner The corner.
   * @return {!goog.math.Box} the computed margin. Only left or right fields are
   *     non-zero, but they may be negative.
   * @private
   * @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
   */
  goog.ui.Bubble.prototype.createMarginForCorner_ = function(corner) {
    var margin = new goog.math.Box(0, 0, 0, 0);
    if (corner & goog.positioning.CornerBit.RIGHT) {
      margin.right -= this.config_.marginShift;
    } else {
      margin.left -= this.config_.marginShift;
    }
    return margin;
  };
  
  
  /**
   * Computes the HTML string for a given bubble orientation.
   * @param {goog.positioning.Corner} corner The corner.
   * @return {!goog.html.SafeHtml} The HTML string to place inside the
   *     bubble's popup.
   * @private
   * @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
   */
  goog.ui.Bubble.prototype.computeHtmlForCorner_ = function(corner) {
    var bubbleTopClass;
    var bubbleBottomClass;
    switch (corner) {
      case goog.positioning.Corner.TOP_LEFT:
        bubbleTopClass = this.config_.cssBubbleTopLeftAnchor;
        bubbleBottomClass = this.config_.cssBubbleBottomNoAnchor;
        break;
      case goog.positioning.Corner.TOP_RIGHT:
        bubbleTopClass = this.config_.cssBubbleTopRightAnchor;
        bubbleBottomClass = this.config_.cssBubbleBottomNoAnchor;
        break;
      case goog.positioning.Corner.BOTTOM_LEFT:
        bubbleTopClass = this.config_.cssBubbleTopNoAnchor;
        bubbleBottomClass = this.config_.cssBubbleBottomLeftAnchor;
        break;
      case goog.positioning.Corner.BOTTOM_RIGHT:
        bubbleTopClass = this.config_.cssBubbleTopNoAnchor;
        bubbleBottomClass = this.config_.cssBubbleBottomRightAnchor;
        break;
      default:
        throw new Error('This corner type is not supported by bubble!');
    }
    var message = null;
    if (this.message_ instanceof SafeHtml) {
      message = this.message_;
    } else {
      message = SafeHtml.create('div', {'id': this.messageId_});
    }
  
    var tableRows = goog.html.SafeHtml.concat(
        SafeHtml.create(
            'tr', {},
            SafeHtml.create('td', {'colspan': 4, 'class': bubbleTopClass})),
        SafeHtml.create(
            'tr', {},
            SafeHtml.concat(
                SafeHtml.create('td', {'class': this.config_.cssBubbleLeft}),
                SafeHtml.create(
                    'td', {
                      'class': this.config_.cssBubbleFont,
                      'style':
                          goog.string.Const.from('padding:0 4px;background:white')
                    },
                    message),
                SafeHtml.create('td', {
                  'id': this.closeButtonId_,
                  'class': this.config_.cssCloseButton
                }),
                SafeHtml.create('td', {'class': this.config_.cssBubbleRight}))),
        SafeHtml.create(
            'tr', {},
            SafeHtml.create('td', {'colspan': 4, 'class': bubbleBottomClass})));
  
    return SafeHtml.create(
        'table', {
          'border': 0,
          'cellspacing': 0,
          'cellpadding': 0,
          'width': this.config_.bubbleWidth,
          'style': goog.string.Const.from('z-index:1')
        },
        tableRows);
  };
  
  
  /**
   * A default configuration for the bubble.
   *
   * @type {Object}
   */
  goog.ui.Bubble.defaultConfig = {
    bubbleWidth: 147,
    marginShift: 60,
    cssBubbleFont: goog.getCssName('goog-bubble-font'),
    cssCloseButton: goog.getCssName('goog-bubble-close-button'),
    cssBubbleTopRightAnchor: goog.getCssName('goog-bubble-top-right-anchor'),
    cssBubbleTopLeftAnchor: goog.getCssName('goog-bubble-top-left-anchor'),
    cssBubbleTopNoAnchor: goog.getCssName('goog-bubble-top-no-anchor'),
    cssBubbleBottomRightAnchor:
        goog.getCssName('goog-bubble-bottom-right-anchor'),
    cssBubbleBottomLeftAnchor: goog.getCssName('goog-bubble-bottom-left-anchor'),
    cssBubbleBottomNoAnchor: goog.getCssName('goog-bubble-bottom-no-anchor'),
    cssBubbleLeft: goog.getCssName('goog-bubble-left'),
    cssBubbleRight: goog.getCssName('goog-bubble-right')
  };
  
  
  /**
   * An auxiliary array optimizing the corner computation.
   *
   * @type {Array<goog.positioning.Corner>}
   * @private
   */
  goog.ui.Bubble.corners_ = [
    goog.positioning.Corner.BOTTOM_RIGHT, goog.positioning.Corner.BOTTOM_LEFT,
    goog.positioning.Corner.TOP_RIGHT, goog.positioning.Corner.TOP_LEFT
  ];
  });  // goog.scope
  