| Index: chrome/browser/resources/shared/js/cr/ui/bubble.js
|
| diff --git a/chrome/browser/resources/shared/js/cr/ui/bubble.js b/chrome/browser/resources/shared/js/cr/ui/bubble.js
|
| index 4876a7d41fd7b7fceaa121e72b59b4ecabc59932..c11d2d7940f2659fd24b6fa3cb94a43bdd5647f6 100644
|
| --- a/chrome/browser/resources/shared/js/cr/ui/bubble.js
|
| +++ b/chrome/browser/resources/shared/js/cr/ui/bubble.js
|
| @@ -31,6 +31,28 @@ cr.define('cr.ui', function() {
|
| };
|
|
|
| /**
|
| + * The bubble alignment specifies the position of the bubble in relation to
|
| + * the anchor node.
|
| + * @enum
|
| + */
|
| + var BubbleAlignment = {
|
| + // The bubble is positioned just above or below the anchor node (as
|
| + // specified by the arrow location) so that the arrow points at the midpoint
|
| + // of the anchor.
|
| + ARROW_TO_MID_ANCHOR: 'arrow-to-mid-anchor',
|
| + // The bubble is positioned just above or below the anchor node (as
|
| + // specified by the arrow location) so that its reference edge lines up with
|
| + // the edge of the anchor.
|
| + BUBBLE_EDGE_TO_ANCHOR_EDGE: 'bubble-edge-anchor-edge',
|
| + // The bubble is positioned so that it is entirely within view and does not
|
| + // obstruct the anchor element, if possible. The specified arrow location is
|
| + // taken into account as the preferred alignment but may be overruled if
|
| + // there is insufficient space (see BubbleBase.reposition for the exact
|
| + // placement algorithm).
|
| + ENTIRELY_VISIBLE: 'entirely-visible'
|
| + };
|
| +
|
| + /**
|
| * Abstract base class that provides common functionality for implementing
|
| * free-floating informational bubbles with a triangular arrow pointing at an
|
| * anchor node.
|
| @@ -45,7 +67,7 @@ cr.define('cr.ui', function() {
|
| BubbleBase.ARROW_OFFSET = 30;
|
|
|
| BubbleBase.prototype = {
|
| - // Set up the prototype chain
|
| + // Set up the prototype chain.
|
| __proto__: HTMLDivElement.prototype,
|
|
|
| /**
|
| @@ -58,6 +80,7 @@ cr.define('cr.ui', function() {
|
| '<div class="bubble-shadow"></div>' +
|
| '<div class="bubble-arrow"></div>';
|
| this.hidden = true;
|
| + this.bubbleAlignment = BubbleAlignment.ENTIRELY_VISIBLE;
|
| },
|
|
|
| /**
|
| @@ -104,14 +127,105 @@ cr.define('cr.ui', function() {
|
| },
|
|
|
| /**
|
| + * Set the bubble alignment. Only available when the bubble is not being
|
| + * shown.
|
| + * @param {cr.ui.BubbleAlignment} alignment The new bubble alignment.
|
| + */
|
| + set bubbleAlignment(alignment) {
|
| + if (!this.hidden)
|
| + return;
|
| +
|
| + this.bubbleAlignment_ = alignment;
|
| + },
|
| +
|
| + /**
|
| + * Update the position of the bubble. Whenever the layout may have changed,
|
| + * the bubble should either be repositioned by calling this function or
|
| + * hidden so that it does not point to a nonsensical location on the page.
|
| + */
|
| + reposition: function() {
|
| + var documentWidth = document.documentElement.clientWidth;
|
| + var documentHeight = document.documentElement.clientHeight;
|
| + var anchor = this.anchorNode_.getBoundingClientRect();
|
| + var anchorMid = (anchor.left + anchor.right) / 2;
|
| + var bubble = this.getBoundingClientRect();
|
| + var arrow = this.querySelector('.bubble-arrow').getBoundingClientRect();
|
| +
|
| + if (this.bubbleAlignment_ == BubbleAlignment.ENTIRELY_VISIBLE) {
|
| + // Work out horizontal placement. The bubble is initially positioned so
|
| + // that the arrow tip points toward the midpoint of the anchor and is
|
| + // BubbleBase.ARROW_OFFSET pixels from the reference edge and (as
|
| + // specified by the arrow location). If the bubble is not entirely
|
| + // within view, it is then shifted, preserving the arrow tip position.
|
| + var left = this.arrowAtRight_ ?
|
| + anchorMid + BubbleBase.ARROW_OFFSET - bubble.width :
|
| + anchorMid - BubbleBase.ARROW_OFFSET;
|
| + if (document.documentElement.dir == 'rtl')
|
| + left = Math.min(Math.max(left, 0), documentWidth - bubble.width);
|
| + else
|
| + left = Math.max(Math.min(left, documentWidth - bubble.width), 0);
|
| + var arrowTip = Math.min(Math.max(this.arrowAtRight_ ?
|
| + left + bubble.width - anchorMid : anchorMid - left,
|
| + arrow.width / 2), bubble.width - arrow.width / 2);
|
| +
|
| + // Work out the vertical placement, attempting to fit the bubble
|
| + // entirely into view. The following placements are considered in
|
| + // decreasing order of preference:
|
| + // * Outside the anchor, arrow tip touching the anchor (arrow at
|
| + // top/bottom as specified by the arrow location).
|
| + // * Outside the anchor, arrow tip touching the anchor (arrow at
|
| + // bottom/top, opposite the specified arrow location).
|
| + // * Outside the anchor, arrow tip overlapping the anchor (arrow at
|
| + // top/bottom as specified by the arrow location).
|
| + // * Outside the anchor, arrow tip overlapping the anchor (arrow at
|
| + // bottom/top, opposite the specified arrow location).
|
| + // * Overlapping the anchor.
|
| + var offsetTop = Math.min(documentHeight - anchor.bottom - bubble.height,
|
| + arrow.height / 2);
|
| + var offsetBottom = Math.min(anchor.top - bubble.height,
|
| + arrow.height / 2);
|
| + if (offsetTop < 0 && offsetBottom < 0) {
|
| + var top = 0;
|
| + this.updateArrowPosition_(false, false, arrowTip);
|
| + } else if (offsetTop > offsetBottom ||
|
| + offsetTop == offsetBottom && this.arrowAtTop_) {
|
| + var top = anchor.bottom + offsetTop;
|
| + this.updateArrowPosition_(true, true, arrowTip);
|
| + } else {
|
| + var top = anchor.top - bubble.height - offsetBottom;
|
| + this.updateArrowPosition_(true, false, arrowTip);
|
| + }
|
| + } else {
|
| + if (this.bubbleAlignment_ ==
|
| + BubbleAlignment.BUBBLE_EDGE_TO_ANCHOR_EDGE) {
|
| + var left = this.arrowAtRight_ ? anchor.right - bubble.width :
|
| + anchor.left;
|
| + } else {
|
| + var left = this.arrowAtRight_ ?
|
| + anchorMid - this.clientWidth + BubbleBase.ARROW_OFFSET :
|
| + anchorMid - BubbleBase.ARROW_OFFSET;
|
| + }
|
| + var top = this.arrowAtTop_ ? anchor.bottom + arrow.height / 2 :
|
| + anchor.top - this.clientHeight - arrow.height / 2;
|
| + this.updateArrowPosition_(true, this.arrowAtTop_,
|
| + BubbleBase.ARROW_OFFSET);
|
| + }
|
| +
|
| + this.style.left = left + 'px';
|
| + this.style.top = top + 'px';
|
| + },
|
| +
|
| + /**
|
| * Show the bubble.
|
| */
|
| show: function() {
|
| if (!this.hidden)
|
| return;
|
|
|
| - document.body.appendChild(this);
|
| + this.attachToDOM_();
|
| this.hidden = false;
|
| + this.reposition();
|
| + this.anchorNode_.showingBubble = true;
|
|
|
| var doc = this.ownerDocument;
|
| this.eventTracker_ = new EventTracker;
|
| @@ -126,8 +240,9 @@ cr.define('cr.ui', function() {
|
| if (this.hidden)
|
| return;
|
|
|
| - this.hidden = true;
|
| this.eventTracker_.removeAll();
|
| + this.anchorNode_.showingBubble = false;
|
| + this.hidden = true;
|
| this.parentNode.removeChild(this);
|
| },
|
|
|
| @@ -145,47 +260,40 @@ cr.define('cr.ui', function() {
|
| },
|
|
|
| /**
|
| + * Attach the bubble to the document's DOM.
|
| + * @private
|
| + */
|
| + attachToDOM_: function() {
|
| + document.body.appendChild(this);
|
| + },
|
| +
|
| + /**
|
| * Update the arrow so that it appears at the correct position.
|
| * @param {Boolean} visible Whether the arrow should be visible.
|
| - * @param {number} The horizontal distance between the tip of the arrow and
|
| - * the reference edge of the bubble (as specified by the arrow location).
|
| + * @param {Boolean} atTop Whether the arrow should be at the top of the
|
| + * bubble.
|
| + * @param {number} tipOffset The horizontal distance between the tip of the
|
| + * arrow and the reference edge of the bubble (as specified by the arrow
|
| + * location).
|
| * @private
|
| */
|
| - updateArrowPosition_: function(visible, tipOffset) {
|
| + updateArrowPosition_: function(visible, atTop, tipOffset) {
|
| var bubbleArrow = this.querySelector('.bubble-arrow');
|
| -
|
| - if (visible) {
|
| - bubbleArrow.style.display = 'block';
|
| - } else {
|
| - bubbleArrow.style.display = 'none';
|
| + bubbleArrow.hidden = !visible;
|
| + if (!visible)
|
| return;
|
| - }
|
|
|
| var edgeOffset = (-bubbleArrow.clientHeight / 2) + 'px';
|
| - bubbleArrow.style.top = this.arrowAtTop_ ? edgeOffset : 'auto';
|
| - bubbleArrow.style.bottom = this.arrowAtTop_ ? 'auto' : edgeOffset;
|
| + bubbleArrow.style.top = atTop ? edgeOffset : 'auto';
|
| + bubbleArrow.style.bottom = atTop ? 'auto' : edgeOffset;
|
|
|
| - edgeOffset = (tipOffset - bubbleArrow.clientHeight / 2) + 'px';
|
| + edgeOffset = (tipOffset - bubbleArrow.offsetWidth / 2) + 'px';
|
| bubbleArrow.style.left = this.arrowAtRight_ ? 'auto' : edgeOffset;
|
| bubbleArrow.style.right = this.arrowAtRight_ ? edgeOffset : 'auto';
|
| },
|
| };
|
|
|
| /**
|
| - * The bubble alignment specifies the horizontal position of the bubble in
|
| - * relation to the anchor node.
|
| - * @enum
|
| - */
|
| - var BubbleAlignment = {
|
| - // The bubble is positioned so that the tip of the arrow points to the
|
| - // middle of the anchor node.
|
| - ARROW_TO_MID_ANCHOR: 'arrow-to-mid-anchor',
|
| - // The bubble is positioned so that the edge nearest to the arrow is lined
|
| - // up with the edge of the anchor node.
|
| - BUBBLE_EDGE_TO_ANCHOR_EDGE: 'bubble-edge-anchor-edge'
|
| - };
|
| -
|
| - /**
|
| * A bubble that remains open until the user explicitly dismisses it or clicks
|
| * outside the bubble after it has been shown for at least the specified
|
| * amount of time (making it less likely that the user will unintentionally
|
| @@ -226,18 +334,6 @@ cr.define('cr.ui', function() {
|
| },
|
|
|
| /**
|
| - * Set the bubble alignment. Only available when the bubble is not being
|
| - * shown.
|
| - * @param {cr.ui.BubbleAlignment} alignment The new bubble alignment.
|
| - */
|
| - set bubbleAlignment(alignment) {
|
| - if (!this.hidden)
|
| - return;
|
| -
|
| - this.bubbleAlignment_ = alignment;
|
| - },
|
| -
|
| - /**
|
| * Set the delay before the user is allowed to click outside the bubble to
|
| * dismiss it. Using a delay makes it less likely that the user will
|
| * unintentionally dismiss the bubble.
|
| @@ -256,36 +352,6 @@ cr.define('cr.ui', function() {
|
| },
|
|
|
| /**
|
| - * Update the position of the bubble. This is automatically called when the
|
| - * window is resized, but should also be called any time the layout may have
|
| - * changed.
|
| - */
|
| - reposition: function() {
|
| - var clientRect = this.anchorNode_.getBoundingClientRect();
|
| - var bubbleArrow = this.querySelector('.bubble-arrow');
|
| - var arrowOffsetY = bubbleArrow.offsetHeight / 2;
|
| -
|
| - var left;
|
| - if (this.bubbleAlignment_ ==
|
| - BubbleAlignment.BUBBLE_EDGE_TO_ANCHOR_EDGE) {
|
| - left = this.arrowAtRight_ ? clientRect.right - this.clientWidth :
|
| - clientRect.left;
|
| - } else {
|
| - var anchorMid = (clientRect.left + clientRect.right) / 2;
|
| - left = this.arrowAtRight_ ?
|
| - anchorMid - this.clientWidth + BubbleBase.ARROW_OFFSET :
|
| - anchorMid - BubbleBase.ARROW_OFFSET;
|
| - }
|
| - var top = this.arrowAtTop_ ? clientRect.bottom + arrowOffsetY :
|
| - clientRect.top - this.clientHeight - arrowOffsetY;
|
| -
|
| - this.style.left = left + 'px';
|
| - this.style.top = top + 'px';
|
| -
|
| - this.updateArrowPosition_(true, BubbleBase.ARROW_OFFSET);
|
| - },
|
| -
|
| - /**
|
| * Show the bubble.
|
| */
|
| show: function() {
|
| @@ -294,7 +360,6 @@ cr.define('cr.ui', function() {
|
|
|
| BubbleBase.prototype.show.call(this);
|
|
|
| - this.reposition();
|
| this.showTime_ = Date.now();
|
| this.eventTracker_.add(window, 'resize', this.reposition.bind(this));
|
| },
|
| @@ -322,8 +387,8 @@ cr.define('cr.ui', function() {
|
|
|
| return {
|
| ArrowLocation: ArrowLocation,
|
| - BubbleBase: BubbleBase,
|
| BubbleAlignment: BubbleAlignment,
|
| + BubbleBase: BubbleBase,
|
| Bubble: Bubble
|
| };
|
| });
|
|
|