| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562 | // Copyright 2005 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 Bubble component - handles display, hiding, etc. of the * actual bubble UI. * * This is used exclusively by code within the editor package, and should not * be used directly. * * @author robbyw@google.com (Robby Walker) */goog.provide('goog.ui.editor.Bubble');goog.require('goog.asserts');goog.require('goog.dom');goog.require('goog.dom.TagName');goog.require('goog.dom.ViewportSizeMonitor');goog.require('goog.dom.classlist');goog.require('goog.editor.style');goog.require('goog.events.EventHandler');goog.require('goog.events.EventTarget');goog.require('goog.events.EventType');goog.require('goog.functions');goog.require('goog.log');goog.require('goog.math.Box');goog.require('goog.object');goog.require('goog.positioning');goog.require('goog.positioning.Corner');goog.require('goog.positioning.Overflow');goog.require('goog.positioning.OverflowStatus');goog.require('goog.string');goog.require('goog.style');goog.require('goog.ui.Component');goog.require('goog.ui.PopupBase');goog.require('goog.userAgent');/** * Property bubble UI element. * @param {Element} parent The parent element for this bubble. * @param {number} zIndex The z index to draw the bubble at. * @constructor * @extends {goog.events.EventTarget} */goog.ui.editor.Bubble = function(parent, zIndex) {  goog.ui.editor.Bubble.base(this, 'constructor');  /**   * Dom helper for the document the bubble should be shown in.   * @type {!goog.dom.DomHelper}   * @private   */  this.dom_ = goog.dom.getDomHelper(parent);  /**   * Event handler for this bubble.   * @type {goog.events.EventHandler<!goog.ui.editor.Bubble>}   * @private   */  this.eventHandler_ = new goog.events.EventHandler(this);  /**   * Object that monitors the application window for size changes.   * @type {goog.dom.ViewportSizeMonitor}   * @private   */  this.viewPortSizeMonitor_ =      new goog.dom.ViewportSizeMonitor(this.dom_.getWindow());  /**   * Maps panel ids to panels.   * @type {Object<goog.ui.editor.Bubble.Panel_>}   * @private   */  this.panels_ = {};  /**   * Container element for the entire bubble.  This may contain elements related   * to look and feel or styling of the bubble.   * @type {Element}   * @private   */  this.bubbleContainer_ = this.dom_.createDom(      goog.dom.TagName.DIV,      {'className': goog.ui.editor.Bubble.BUBBLE_CLASSNAME});  goog.style.setElementShown(this.bubbleContainer_, false);  goog.dom.appendChild(parent, this.bubbleContainer_);  goog.style.setStyle(this.bubbleContainer_, 'zIndex', zIndex);  /**   * Container element for the bubble panels - this should be some inner element   * within (or equal to) bubbleContainer.   * @type {Element}   * @private   */  this.bubbleContents_ = this.createBubbleDom(this.dom_, this.bubbleContainer_);  /**   * Element showing the close box.   * @type {!Element}   * @private   */  this.closeBox_ = this.dom_.createDom(goog.dom.TagName.DIV, {    'className': goog.getCssName('tr_bubble_closebox'),    'innerHTML': ' '  });  this.bubbleContents_.appendChild(this.closeBox_);  // We make bubbles unselectable so that clicking on them does not steal focus  // or move the cursor away from the element the bubble is attached to.  goog.editor.style.makeUnselectable(this.bubbleContainer_, this.eventHandler_);  /**   * Popup that controls showing and hiding the bubble at the appropriate   * position.   * @type {goog.ui.PopupBase}   * @private   */  this.popup_ = new goog.ui.PopupBase(this.bubbleContainer_);};goog.inherits(goog.ui.editor.Bubble, goog.events.EventTarget);/** * The css class name of the bubble container element. * @type {string} */goog.ui.editor.Bubble.BUBBLE_CLASSNAME = goog.getCssName('tr_bubble');/** * Creates and adds DOM for the bubble UI to the given container.  This default * implementation just returns the container itself. * @param {!goog.dom.DomHelper} dom DOM helper to use. * @param {!Element} container Element to add the new elements to. * @return {!Element} The element where bubble content should be added. * @protected */goog.ui.editor.Bubble.prototype.createBubbleDom = function(dom, container) {  return container;};/** * A logger for goog.ui.editor.Bubble. * @type {goog.log.Logger} * @protected */goog.ui.editor.Bubble.prototype.logger =    goog.log.getLogger('goog.ui.editor.Bubble');/** @override */goog.ui.editor.Bubble.prototype.disposeInternal = function() {  goog.ui.editor.Bubble.base(this, 'disposeInternal');  goog.dom.removeNode(this.bubbleContainer_);  this.bubbleContainer_ = null;  this.eventHandler_.dispose();  this.eventHandler_ = null;  this.viewPortSizeMonitor_.dispose();  this.viewPortSizeMonitor_ = null;};/** * @return {Element} The element that where the bubble's contents go. */goog.ui.editor.Bubble.prototype.getContentElement = function() {  return this.bubbleContents_;};/** * @return {Element} The element that contains the bubble. * @protected */goog.ui.editor.Bubble.prototype.getContainerElement = function() {  return this.bubbleContainer_;};/** * @return {goog.events.EventHandler<T>} The event handler. * @protected * @this {T} * @template T */goog.ui.editor.Bubble.prototype.getEventHandler = function() {  return this.eventHandler_;};/** * Handles user resizing of window. * @private */goog.ui.editor.Bubble.prototype.handleWindowResize_ = function() {  if (this.isVisible()) {    this.reposition();  }};/** * Sets whether the bubble dismisses itself when the user clicks outside of it. * @param {boolean} autoHide Whether to autohide on an external click. */goog.ui.editor.Bubble.prototype.setAutoHide = function(autoHide) {  this.popup_.setAutoHide(autoHide);};/** * Returns whether there is already a panel of the given type. * @param {string} type Type of panel to check. * @return {boolean} Whether there is already a panel of the given type. */goog.ui.editor.Bubble.prototype.hasPanelOfType = function(type) {  return goog.object.some(      this.panels_, function(panel) { return panel.type == type; });};/** * Adds a panel to the bubble. * @param {string} type The type of bubble panel this is.  Should usually be *     the same as the tagName of the targetElement.  This ensures multiple *     bubble panels don't appear for the same element. * @param {string} title The title of the panel. * @param {Element} targetElement The target element of the bubble. * @param {function(Element): void} contentFn Function that when called with *     a container element, will add relevant panel content to it. * @param {boolean=} opt_preferTopPosition Whether to prefer placing the bubble *     above the element instead of below it.  Defaults to preferring below. *     If any panel prefers the top position, the top position is used. * @return {string} The id of the panel. */goog.ui.editor.Bubble.prototype.addPanel = function(    type, title, targetElement, contentFn, opt_preferTopPosition) {  var id = goog.string.createUniqueString();  var panel = new goog.ui.editor.Bubble.Panel_(      this.dom_, id, type, title, targetElement, !opt_preferTopPosition);  this.panels_[id] = panel;  // Insert the panel in string order of type.  Technically we could use binary  // search here but n is really small (probably 0 - 2) so it's not worth it.  // The last child of bubbleContents_ is the close box so we take care not  // to treat it as a panel element, and we also ensure it stays as the last  // element.  The intention here is not to create any artificial order, but  // just to ensure that it is always consistent.  var nextElement;  for (var i = 0, len = this.bubbleContents_.childNodes.length - 1; i < len;       i++) {    var otherChild = this.bubbleContents_.childNodes[i];    var otherPanel = this.panels_[otherChild.id];    if (otherPanel.type > type) {      nextElement = otherChild;      break;    }  }  goog.dom.insertSiblingBefore(      panel.element, nextElement || this.bubbleContents_.lastChild);  contentFn(panel.getContentElement());  goog.editor.style.makeUnselectable(panel.element, this.eventHandler_);  var numPanels = goog.object.getCount(this.panels_);  if (numPanels == 1) {    this.openBubble_();  } else if (numPanels == 2) {    goog.dom.classlist.add(        goog.asserts.assert(this.bubbleContainer_),        goog.getCssName('tr_multi_bubble'));  }  this.reposition();  return id;};/** * Removes the panel with the given id. * @param {string} id The id of the panel. */goog.ui.editor.Bubble.prototype.removePanel = function(id) {  var panel = this.panels_[id];  goog.dom.removeNode(panel.element);  delete this.panels_[id];  var numPanels = goog.object.getCount(this.panels_);  if (numPanels <= 1) {    goog.dom.classlist.remove(        goog.asserts.assert(this.bubbleContainer_),        goog.getCssName('tr_multi_bubble'));  }  if (numPanels == 0) {    this.closeBubble_();  } else {    this.reposition();  }};/** * Opens the bubble. * @private */goog.ui.editor.Bubble.prototype.openBubble_ = function() {  this.eventHandler_      .listen(this.closeBox_, goog.events.EventType.CLICK, this.closeBubble_)      .listen(          this.viewPortSizeMonitor_, goog.events.EventType.RESIZE,          this.handleWindowResize_)      .listen(          this.popup_, goog.ui.PopupBase.EventType.HIDE, this.handlePopupHide);  this.popup_.setVisible(true);  this.reposition();};/** * Closes the bubble. * @private */goog.ui.editor.Bubble.prototype.closeBubble_ = function() {  this.popup_.setVisible(false);};/** * Handles the popup's hide event by removing all panels and dispatching a * HIDE event. * @protected */goog.ui.editor.Bubble.prototype.handlePopupHide = function() {  // Remove the panel elements.  for (var panelId in this.panels_) {    goog.dom.removeNode(this.panels_[panelId].element);  }  // Update the state to reflect no panels.  this.panels_ = {};  goog.dom.classlist.remove(      goog.asserts.assert(this.bubbleContainer_),      goog.getCssName('tr_multi_bubble'));  this.eventHandler_.removeAll();  this.dispatchEvent(goog.ui.Component.EventType.HIDE);};/** * Returns the visibility of the bubble. * @return {boolean} True if visible false if not. */goog.ui.editor.Bubble.prototype.isVisible = function() {  return this.popup_.isVisible();};/** * The vertical clearance in pixels between the bottom of the targetElement * and the edge of the bubble. * @type {number} * @private */goog.ui.editor.Bubble.VERTICAL_CLEARANCE_ = goog.userAgent.IE ? 4 : 2;/** * Bubble's margin box to be passed to goog.positioning. * @type {goog.math.Box} * @private */goog.ui.editor.Bubble.MARGIN_BOX_ = new goog.math.Box(    goog.ui.editor.Bubble.VERTICAL_CLEARANCE_, 0,    goog.ui.editor.Bubble.VERTICAL_CLEARANCE_, 0);/** * Returns the margin box. * @return {goog.math.Box} * @protected */goog.ui.editor.Bubble.prototype.getMarginBox = function() {  return goog.ui.editor.Bubble.MARGIN_BOX_;};/** * Positions and displays this bubble below its targetElement. Assumes that * the bubbleContainer is already contained in the document object it applies * to. */goog.ui.editor.Bubble.prototype.reposition = function() {  var targetElement = null;  var preferBottomPosition = true;  for (var panelId in this.panels_) {    var panel = this.panels_[panelId];    // We don't care which targetElement we get, so we just take the last one.    targetElement = panel.targetElement;    preferBottomPosition = preferBottomPosition && panel.preferBottomPosition;  }  var status = goog.positioning.OverflowStatus.FAILED;  // Fix for bug when bubbleContainer and targetElement have  // opposite directionality, the bubble should anchor to the END of  // the targetElement instead of START.  var reverseLayout =      (goog.style.isRightToLeft(this.bubbleContainer_) !=       goog.style.isRightToLeft(targetElement));  // Try to put the bubble at the bottom of the target unless the plugin has  // requested otherwise.  if (preferBottomPosition) {    status = this.positionAtAnchor_(        reverseLayout ? goog.positioning.Corner.BOTTOM_END :                        goog.positioning.Corner.BOTTOM_START,        goog.positioning.Corner.TOP_START,        goog.positioning.Overflow.ADJUST_X | goog.positioning.Overflow.FAIL_Y);  }  if (status & goog.positioning.OverflowStatus.FAILED) {    // Try to put it at the top of the target if there is not enough    // space at the bottom.    status = this.positionAtAnchor_(        reverseLayout ? goog.positioning.Corner.TOP_END :                        goog.positioning.Corner.TOP_START,        goog.positioning.Corner.BOTTOM_START,        goog.positioning.Overflow.ADJUST_X | goog.positioning.Overflow.FAIL_Y);  }  if (status & goog.positioning.OverflowStatus.FAILED) {    // Put it at the bottom again with adjustment if there is no    // enough space at the top.    status = this.positionAtAnchor_(        reverseLayout ? goog.positioning.Corner.BOTTOM_END :                        goog.positioning.Corner.BOTTOM_START,        goog.positioning.Corner.TOP_START, goog.positioning.Overflow.ADJUST_X |            goog.positioning.Overflow.ADJUST_Y);    if (status & goog.positioning.OverflowStatus.FAILED) {      goog.log.warning(          this.logger,          'reposition(): positionAtAnchor() failed with ' + status);    }  }};/** * A helper for reposition() - positions the bubble in regards to the position * of the elements the bubble is attached to. * @param {goog.positioning.Corner} targetCorner The corner of *     the target element. * @param {goog.positioning.Corner} bubbleCorner The corner of the bubble. * @param {number} overflow Overflow handling mode bitmap, *     {@see goog.positioning.Overflow}. * @return {number} Status bitmap, {@see goog.positioning.OverflowStatus}. * @private */goog.ui.editor.Bubble.prototype.positionAtAnchor_ = function(    targetCorner, bubbleCorner, overflow) {  var targetElement = null;  for (var panelId in this.panels_) {    // For now, we use the outermost element.  This assumes the multiple    // elements this panel is showing for contain each other - in the event    // that is not generally the case this may need to be updated to pick    // the lowest or highest element depending on targetCorner.    var candidate = this.panels_[panelId].targetElement;    if (!targetElement || goog.dom.contains(candidate, targetElement)) {      targetElement = this.panels_[panelId].targetElement;    }  }  return goog.positioning.positionAtAnchor(      targetElement, targetCorner, this.bubbleContainer_, bubbleCorner, null,      this.getMarginBox(), overflow, null, this.getViewportBox());};/** * Returns the viewport box to use when positioning the bubble. * @return {goog.math.Box} * @protected */goog.ui.editor.Bubble.prototype.getViewportBox = goog.functions.NULL;/** * Private class used to describe a bubble panel. * @param {goog.dom.DomHelper} dom DOM helper used to create the panel. * @param {string} id ID of the panel. * @param {string} type Type of the panel. * @param {string} title Title of the panel. * @param {Element} targetElement Element the panel is showing for. * @param {boolean} preferBottomPosition Whether this panel prefers to show *     below the target element. * @constructor * @private */goog.ui.editor.Bubble.Panel_ = function(    dom, id, type, title, targetElement, preferBottomPosition) {  /**   * The type of bubble panel.   * @type {string}   */  this.type = type;  /**   * The target element of this bubble panel.   * @type {Element}   */  this.targetElement = targetElement;  /**   * Whether the panel prefers to be placed below the target element.   * @type {boolean}   */  this.preferBottomPosition = preferBottomPosition;  /**   * The element containing this panel.   */  this.element = dom.createDom(      goog.dom.TagName.DIV,      {className: goog.getCssName('tr_bubble_panel'), id: id},      dom.createDom(          goog.dom.TagName.DIV,          {className: goog.getCssName('tr_bubble_panel_title')},          title ? title + ':' : ''),  // TODO(robbyw): Does this work in bidi?      dom.createDom(          goog.dom.TagName.DIV,          {className: goog.getCssName('tr_bubble_panel_content')}));};/** * @return {Element} The element in the panel where content should go. */goog.ui.editor.Bubble.Panel_.prototype.getContentElement = function() {  return /** @type {Element} */ (this.element.lastChild);};
 |