1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5// require: event_tracker.js 6 7cr.define('cr.ui', function() { 8 9 /** 10 * The arrow location specifies how the arrow and bubble are positioned in 11 * relation to the anchor node. 12 * @enum 13 */ 14 var ArrowLocation = { 15 // The arrow is positioned at the top and the start of the bubble. In left 16 // to right mode this is the top left. The entire bubble is positioned below 17 // the anchor node. 18 TOP_START: 'top-start', 19 // The arrow is positioned at the top and the end of the bubble. In left to 20 // right mode this is the top right. The entire bubble is positioned below 21 // the anchor node. 22 TOP_END: 'top-end', 23 // The arrow is positioned at the bottom and the start of the bubble. In 24 // left to right mode this is the bottom left. The entire bubble is 25 // positioned above the anchor node. 26 BOTTOM_START: 'bottom-start', 27 // The arrow is positioned at the bottom and the end of the bubble. In 28 // left to right mode this is the bottom right. The entire bubble is 29 // positioned above the anchor node. 30 BOTTOM_END: 'bottom-end' 31 }; 32 33 /** 34 * The bubble alignment specifies the horizontal position of the bubble in 35 * relation to the anchor node. 36 * @enum 37 */ 38 var BubbleAlignment = { 39 // The bubble is positioned so that the tip of the arrow points to the 40 // middle of the anchor node. 41 ARROW_TO_MID_ANCHOR: 'arrow-to-mid-anchor', 42 // The bubble is positioned so that the edge nearest to the arrow is lined 43 // up with the edge of the anchor node. 44 BUBBLE_EDGE_TO_ANCHOR_EDGE: 'bubble-edge-anchor-edge' 45 }; 46 47 /** 48 * The horizontal distance between the tip of the arrow and the start or the 49 * end of the bubble (as specified by the arrow location). 50 * @const 51 */ 52 var ARROW_OFFSET_X = 30; 53 54 /** 55 * The vertical distance between the tip of the arrow and the bottom or top of 56 * the bubble (as specified by the arrow location). Note, if you change this 57 * then you should also change the "top" and "bottom" values for .bubble-arrow 58 * in bubble.css. 59 * @const 60 */ 61 var ARROW_OFFSET_Y = 8; 62 63 /** 64 * Bubble is a free-floating informational bubble with a triangular arrow 65 * that points at a place of interest on the page. 66 */ 67 var Bubble = cr.ui.define('div'); 68 69 Bubble.prototype = { 70 __proto__: HTMLDivElement.prototype, 71 72 decorate: function() { 73 this.className = 'bubble'; 74 this.innerHTML = 75 '<div class="bubble-contents"></div>' + 76 '<div class="bubble-close"></div>' + 77 '<div class="bubble-shadow"></div>' + 78 '<div class="bubble-arrow"></div>'; 79 80 this.hidden = true; 81 this.handleCloseEvent = this.hide; 82 this.deactivateToDismissDelay_ = 0; 83 this.bubbleAlignment = BubbleAlignment.ARROW_TO_MID_ANCHOR; 84 }, 85 86 /** 87 * Sets the child node of the bubble. 88 * @param {node} An HTML element 89 */ 90 set content(node) { 91 var bubbleContent = this.querySelector('.bubble-contents'); 92 bubbleContent.innerHTML = ''; 93 bubbleContent.appendChild(node); 94 }, 95 96 /** 97 * Handles close event which is triggered when the close button 98 * is clicked. By default is set to this.hide. 99 * @param {function} A function with no parameters 100 */ 101 set handleCloseEvent(func) { 102 this.handleCloseEvent_ = func; 103 }, 104 105 /** 106 * Sets the anchor node, i.e. the node that this bubble points at. 107 * @param {HTMLElement} node The new anchor node. 108 */ 109 set anchorNode(node) { 110 this.anchorNode_ = node; 111 112 if (!this.hidden) 113 this.reposition(); 114 }, 115 116 /** 117 * Sets the arrow location. 118 * @param {cr.ui.ArrowLocation} arrowLocation The new arrow location. 119 */ 120 setArrowLocation: function(arrowLocation) { 121 this.isRight_ = arrowLocation == ArrowLocation.TOP_END || 122 arrowLocation == ArrowLocation.BOTTOM_END; 123 if (document.documentElement.dir == 'rtl') 124 this.isRight_ = !this.isRight_; 125 this.isTop_ = arrowLocation == ArrowLocation.TOP_START || 126 arrowLocation == ArrowLocation.TOP_END; 127 128 var bubbleArrow = this.querySelector('.bubble-arrow'); 129 bubbleArrow.setAttribute('is-right', this.isRight_); 130 bubbleArrow.setAttribute('is-top', this.isTop_); 131 132 if (!this.hidden) 133 this.reposition(); 134 }, 135 136 /** 137 * Sets the bubble alignment. 138 * @param {cr.ui.BubbleAlignment} alignment The new bubble alignment. 139 */ 140 set bubbleAlignment(alignment) { 141 this.bubbleAlignment_ = alignment; 142 }, 143 144 /** 145 * Sets the delay before the user is allowed to click outside the bubble 146 * to dismiss it. Using a delay makes it less likely that the user will 147 * unintentionally dismiss the bubble. 148 * @param {int} delay The delay in miliseconds. 149 */ 150 set deactivateToDismissDelay(delay) { 151 this.deactivateToDismissDelay_ = delay; 152 }, 153 154 /** 155 * Hides or shows the close button. 156 * @param {Boolean} isVisible True if the close button should be visible. 157 */ 158 setCloseButtonVisible: function(isVisible) { 159 this.querySelector('.bubble-close').hidden = !isVisible; 160 }, 161 162 /** 163 * Updates the position of the bubble. This is automatically called when 164 * the window is resized, but should also be called any time the layout 165 * may have changed. 166 */ 167 reposition: function() { 168 var clientRect = this.anchorNode_.getBoundingClientRect(); 169 170 var left; 171 if (this.bubbleAlignment_ == 172 BubbleAlignment.BUBBLE_EDGE_TO_ANCHOR_EDGE) { 173 left = this.isRight_ ? clientRect.right - this.clientWidth : 174 clientRect.left; 175 } else { 176 var anchorMid = (clientRect.left + clientRect.right) / 2; 177 left = this.isRight_ ? anchorMid - this.clientWidth + ARROW_OFFSET_X : 178 anchorMid - ARROW_OFFSET_X; 179 } 180 var top = this.isTop_ ? clientRect.bottom + ARROW_OFFSET_Y : 181 clientRect.top - this.clientHeight - ARROW_OFFSET_Y; 182 183 this.style.left = left + 'px'; 184 this.style.top = top + 'px'; 185 }, 186 187 /** 188 * Starts showing the bubble. The bubble will show until the user clicks 189 * away or presses Escape. 190 */ 191 show: function() { 192 if (!this.hidden) 193 return; 194 195 document.body.appendChild(this); 196 this.hidden = false; 197 this.reposition(); 198 this.showTime_ = Date.now(); 199 200 this.eventTracker_ = new EventTracker; 201 this.eventTracker_.add(window, 'resize', this.reposition.bind(this)); 202 203 var doc = this.ownerDocument; 204 this.eventTracker_.add(doc, 'keydown', this, true); 205 this.eventTracker_.add(doc, 'mousedown', this, true); 206 }, 207 208 /** 209 * Hides the bubble from view. 210 */ 211 hide: function() { 212 this.hidden = true; 213 this.eventTracker_.removeAll(); 214 this.parentNode.removeChild(this); 215 }, 216 217 /** 218 * Handles keydown and mousedown events, dismissing the bubble if 219 * necessary. 220 * @param {Event} e The event. 221 */ 222 handleEvent: function(e) { 223 switch (e.type) { 224 case 'keydown': { 225 if (e.keyCode == 27) // Esc 226 this.hide(); 227 break; 228 } 229 case 'mousedown': { 230 if (e.target == this.querySelector('.bubble-close')) { 231 this.handleCloseEvent_(); 232 } else if (!this.contains(e.target)) { 233 if (Date.now() - this.showTime_ < this.deactivateToDismissDelay_) 234 return; 235 this.hide(); 236 } else { 237 return; 238 } 239 break; 240 } 241 } 242 }, 243 }; 244 245 return { 246 ArrowLocation: ArrowLocation, 247 Bubble: Bubble, 248 BubbleAlignment: BubbleAlignment 249 }; 250}); 251