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