// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // require: event_tracker.js cr.define('cr.ui', function() { /** * The arrow location specifies how the arrow and bubble are positioned in * relation to the anchor node. * @enum */ var ArrowLocation = { // The arrow is positioned at the top and the start of the bubble. In left // to right mode this is the top left. The entire bubble is positioned below // the anchor node. TOP_START: 'top-start', // The arrow is positioned at the top and the end of the bubble. In left to // right mode this is the top right. The entire bubble is positioned below // the anchor node. TOP_END: 'top-end', // The arrow is positioned at the bottom and the start of the bubble. In // left to right mode this is the bottom left. The entire bubble is // positioned above the anchor node. BOTTOM_START: 'bottom-start', // The arrow is positioned at the bottom and the end of the bubble. In // left to right mode this is the bottom right. The entire bubble is // positioned above the anchor node. BOTTOM_END: 'bottom-end' }; /** * 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' }; /** * The horizontal distance between the tip of the arrow and the start or the * end of the bubble (as specified by the arrow location). * @const */ var ARROW_OFFSET_X = 30; /** * The vertical distance between the tip of the arrow and the bottom or top of * the bubble (as specified by the arrow location). Note, if you change this * then you should also change the "top" and "bottom" values for .bubble-arrow * in bubble.css. * @const */ var ARROW_OFFSET_Y = 8; /** * Bubble is a free-floating informational bubble with a triangular arrow * that points at a place of interest on the page. */ var Bubble = cr.ui.define('div'); Bubble.prototype = { __proto__: HTMLDivElement.prototype, decorate: function() { this.className = 'bubble'; this.innerHTML = '
' + '' + '' + ''; this.hidden = true; this.handleCloseEvent = this.hide; this.deactivateToDismissDelay_ = 0; this.bubbleAlignment = BubbleAlignment.ARROW_TO_MID_ANCHOR; }, /** * Sets the child node of the bubble. * @param {node} An HTML element */ set content(node) { var bubbleContent = this.querySelector('.bubble-contents'); bubbleContent.innerHTML = ''; bubbleContent.appendChild(node); }, /** * Handles close event which is triggered when the close button * is clicked. By default is set to this.hide. * @param {function} A function with no parameters */ set handleCloseEvent(func) { this.handleCloseEvent_ = func; }, /** * Sets the anchor node, i.e. the node that this bubble points at. * @param {HTMLElement} node The new anchor node. */ set anchorNode(node) { this.anchorNode_ = node; if (!this.hidden) this.reposition(); }, /** * Sets the arrow location. * @param {cr.ui.ArrowLocation} arrowLocation The new arrow location. */ setArrowLocation: function(arrowLocation) { this.isRight_ = arrowLocation == ArrowLocation.TOP_END || arrowLocation == ArrowLocation.BOTTOM_END; if (document.documentElement.dir == 'rtl') this.isRight_ = !this.isRight_; this.isTop_ = arrowLocation == ArrowLocation.TOP_START || arrowLocation == ArrowLocation.TOP_END; var bubbleArrow = this.querySelector('.bubble-arrow'); bubbleArrow.setAttribute('is-right', this.isRight_); bubbleArrow.setAttribute('is-top', this.isTop_); if (!this.hidden) this.reposition(); }, /** * Sets the bubble alignment. * @param {cr.ui.BubbleAlignment} alignment The new bubble alignment. */ set bubbleAlignment(alignment) { this.bubbleAlignment_ = alignment; }, /** * Sets 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. * @param {int} delay The delay in miliseconds. */ set deactivateToDismissDelay(delay) { this.deactivateToDismissDelay_ = delay; }, /** * Hides or shows the close button. * @param {Boolean} isVisible True if the close button should be visible. */ setCloseButtonVisible: function(isVisible) { this.querySelector('.bubble-close').hidden = !isVisible; }, /** * Updates 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 left; if (this.bubbleAlignment_ == BubbleAlignment.BUBBLE_EDGE_TO_ANCHOR_EDGE) { left = this.isRight_ ? clientRect.right - this.clientWidth : clientRect.left; } else { var anchorMid = (clientRect.left + clientRect.right) / 2; left = this.isRight_ ? anchorMid - this.clientWidth + ARROW_OFFSET_X : anchorMid - ARROW_OFFSET_X; } var top = this.isTop_ ? clientRect.bottom + ARROW_OFFSET_Y : clientRect.top - this.clientHeight - ARROW_OFFSET_Y; this.style.left = left + 'px'; this.style.top = top + 'px'; }, /** * Starts showing the bubble. The bubble will show until the user clicks * away or presses Escape. */ show: function() { if (!this.hidden) return; document.body.appendChild(this); this.hidden = false; this.reposition(); this.showTime_ = Date.now(); this.eventTracker_ = new EventTracker; this.eventTracker_.add(window, 'resize', this.reposition.bind(this)); var doc = this.ownerDocument; this.eventTracker_.add(doc, 'keydown', this, true); this.eventTracker_.add(doc, 'mousedown', this, true); }, /** * Hides the bubble from view. */ hide: function() { this.hidden = true; this.eventTracker_.removeAll(); this.parentNode.removeChild(this); }, /** * Handles keydown and mousedown events, dismissing the bubble if * necessary. * @param {Event} e The event. */ handleEvent: function(e) { switch (e.type) { case 'keydown': { if (e.keyCode == 27) // Esc this.hide(); break; } case 'mousedown': { if (e.target == this.querySelector('.bubble-close')) { this.handleCloseEvent_(); } else if (!this.contains(e.target)) { if (Date.now() - this.showTime_ < this.deactivateToDismissDelay_) return; this.hide(); } else { return; } break; } } }, }; return { ArrowLocation: ArrowLocation, Bubble: Bubble, BubbleAlignment: BubbleAlignment }; });