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 /** 6 * @fileoverview Bubble implementation. 7 */ 8 9 // TODO(xiyuan): Move this into shared. 10 cr.define('cr.ui', function() { 11 /** 12 * Creates a bubble div. 13 * @constructor 14 * @extends {HTMLDivElement} 15 */ 16 var Bubble = cr.ui.define('div'); 17 18 /** 19 * Bubble attachment side. 20 * @enum {string} 21 */ 22 Bubble.Attachment = { 23 RIGHT: 'bubble-right', 24 LEFT: 'bubble-left', 25 TOP: 'bubble-top', 26 BOTTOM: 'bubble-bottom' 27 }; 28 29 Bubble.prototype = { 30 __proto__: HTMLDivElement.prototype, 31 32 // Anchor element for this bubble. 33 anchor_: undefined, 34 35 // If defined, sets focus to this element once bubble is closed. Focus is 36 // set to this element only if there's no any other focused element. 37 elementToFocusOnHide_: undefined, 38 39 // Whether to hide bubble when key is pressed. 40 hideOnKeyPress_: true, 41 42 /** @override */ 43 decorate: function() { 44 this.docKeyDownHandler_ = this.handleDocKeyDown_.bind(this); 45 this.selfClickHandler_ = this.handleSelfClick_.bind(this); 46 this.ownerDocument.addEventListener('click', 47 this.handleDocClick_.bind(this)); 48 this.ownerDocument.addEventListener('keydown', 49 this.docKeyDownHandler_); 50 window.addEventListener('blur', this.handleWindowBlur_.bind(this)); 51 this.addEventListener('webkitTransitionEnd', 52 this.handleTransitionEnd_.bind(this)); 53 }, 54 55 /** 56 * Element that should be focused on hide. 57 * @type {HTMLElement} 58 */ 59 set elementToFocusOnHide(value) { 60 this.elementToFocusOnHide_ = value; 61 }, 62 63 /** 64 * Whether to hide bubble when key is pressed. 65 * @type {boolean} 66 */ 67 set hideOnKeyPress(value) { 68 this.hideOnKeyPress_ = value; 69 }, 70 71 /** 72 * Whether to hide bubble when clicked inside bubble element. 73 * Default is true. 74 * @type {boolean} 75 */ 76 set hideOnSelfClick(value) { 77 if (value) 78 this.removeEventListener('click', this.selfClickHandler_); 79 else 80 this.addEventListener('click', this.selfClickHandler_); 81 }, 82 83 /** 84 * Handler for click event which prevents bubble auto hide. 85 * @private 86 */ 87 handleSelfClick_: function(e) { 88 // Allow clicking on [x] button. 89 if (e.target && e.target.classList.contains('close-button')) 90 return; 91 92 e.stopPropagation(); 93 }, 94 95 /** 96 * Sets the attachment of the bubble. 97 * @param {!Attachment} attachment Bubble attachment. 98 */ 99 setAttachment_: function(attachment) { 100 for (var k in Bubble.Attachment) { 101 var v = Bubble.Attachment[k]; 102 this.classList.toggle(v, v == attachment); 103 } 104 }, 105 106 /** 107 * Shows the bubble for given anchor element. 108 * @param {!Object} pos Bubble position (left, top, right, bottom in px). 109 * @param {!Attachment} attachment Bubble attachment (on which side of the 110 * specified position it should be displayed). 111 * @param {HTMLElement} opt_content Content to show in bubble. 112 * If not specified, bubble element content is shown. 113 * @private 114 */ 115 showContentAt_: function(pos, attachment, opt_content) { 116 this.style.top = this.style.left = this.style.right = this.style.bottom = 117 'auto'; 118 for (var k in pos) { 119 if (typeof pos[k] == 'number') 120 this.style[k] = pos[k] + 'px'; 121 } 122 if (opt_content !== undefined) { 123 this.innerHTML = ''; 124 this.appendChild(opt_content); 125 } 126 this.setAttachment_(attachment); 127 this.hidden = false; 128 this.classList.remove('faded'); 129 }, 130 131 /** 132 * Shows the bubble for given anchor element. Bubble content is not cleared. 133 * @param {!HTMLElement} el Anchor element of the bubble. 134 * @param {!Attachment} attachment Bubble attachment (on which side of the 135 * element it should be displayed). 136 * @param {number=} opt_offset Offset of the bubble. 137 * @param {number=} opt_padding Optional padding of the bubble. 138 */ 139 showForElement: function(el, attachment, opt_offset, opt_padding) { 140 this.showContentForElement( 141 el, attachment, undefined, opt_offset, opt_padding); 142 }, 143 144 /** 145 * Shows the bubble for given anchor element. 146 * @param {!HTMLElement} el Anchor element of the bubble. 147 * @param {!Attachment} attachment Bubble attachment (on which side of the 148 * element it should be displayed). 149 * @param {HTMLElement} opt_content Content to show in bubble. 150 * If not specified, bubble element content is shown. 151 * @param {number=} opt_offset Offset of the bubble attachment point from 152 * left (for vertical attachment) or top (for horizontal attachment) 153 * side of the element. If not specified, the bubble is positioned to 154 * be aligned with the left/top side of the element but not farther than 155 * half of its width/height. 156 * @param {number=} opt_padding Optional padding of the bubble. 157 */ 158 showContentForElement: function(el, attachment, opt_content, 159 opt_offset, opt_padding) { 160 /** @const */ var ARROW_OFFSET = 25; 161 /** @const */ var DEFAULT_PADDING = 18; 162 163 if (opt_padding == undefined) 164 opt_padding = DEFAULT_PADDING; 165 166 var origin = cr.ui.login.DisplayManager.getPosition(el); 167 var offset = opt_offset == undefined ? 168 [Math.min(ARROW_OFFSET, el.offsetWidth / 2), 169 Math.min(ARROW_OFFSET, el.offsetHeight / 2)] : 170 [opt_offset, opt_offset]; 171 172 var pos = {}; 173 if (isRTL()) { 174 switch (attachment) { 175 case Bubble.Attachment.TOP: 176 pos.right = origin.right + offset[0] - ARROW_OFFSET; 177 pos.bottom = origin.bottom + el.offsetHeight + opt_padding; 178 break; 179 case Bubble.Attachment.RIGHT: 180 pos.top = origin.top + offset[1] - ARROW_OFFSET; 181 pos.right = origin.right + el.offsetWidth + opt_padding; 182 break; 183 case Bubble.Attachment.BOTTOM: 184 pos.right = origin.right + offset[0] - ARROW_OFFSET; 185 pos.top = origin.top + el.offsetHeight + opt_padding; 186 break; 187 case Bubble.Attachment.LEFT: 188 pos.top = origin.top + offset[1] - ARROW_OFFSET; 189 pos.left = origin.left + el.offsetWidth + opt_padding; 190 break; 191 } 192 } else { 193 switch (attachment) { 194 case Bubble.Attachment.TOP: 195 pos.left = origin.left + offset[0] - ARROW_OFFSET; 196 pos.bottom = origin.bottom + el.offsetHeight + opt_padding; 197 break; 198 case Bubble.Attachment.RIGHT: 199 pos.top = origin.top + offset[1] - ARROW_OFFSET; 200 pos.left = origin.left + el.offsetWidth + opt_padding; 201 break; 202 case Bubble.Attachment.BOTTOM: 203 pos.left = origin.left + offset[0] - ARROW_OFFSET; 204 pos.top = origin.top + el.offsetHeight + opt_padding; 205 break; 206 case Bubble.Attachment.LEFT: 207 pos.top = origin.top + offset[1] - ARROW_OFFSET; 208 pos.right = origin.right + el.offsetWidth + opt_padding; 209 break; 210 } 211 } 212 213 this.anchor_ = el; 214 this.showContentAt_(pos, attachment, opt_content); 215 }, 216 217 /** 218 * Shows the bubble for given anchor element. 219 * @param {!HTMLElement} el Anchor element of the bubble. 220 * @param {string} text Text content to show in bubble. 221 * @param {!Attachment} attachment Bubble attachment (on which side of the 222 * element it should be displayed). 223 * @param {number=} opt_offset Offset of the bubble attachment point from 224 * left (for vertical attachment) or top (for horizontal attachment) 225 * side of the element. If not specified, the bubble is positioned to 226 * be aligned with the left/top side of the element but not farther than 227 * half of its weight/height. 228 * @param {number=} opt_padding Optional padding of the bubble. 229 */ 230 showTextForElement: function(el, text, attachment, 231 opt_offset, opt_padding) { 232 var span = this.ownerDocument.createElement('span'); 233 span.textContent = text; 234 this.showContentForElement(el, attachment, span, opt_offset, opt_padding); 235 }, 236 237 /** 238 * Hides the bubble. 239 */ 240 hide: function() { 241 if (!this.classList.contains('faded')) 242 this.classList.add('faded'); 243 }, 244 245 /** 246 * Hides the bubble anchored to the given element (if any). 247 * @param {!Object} el Anchor element. 248 */ 249 hideForElement: function(el) { 250 if (!this.hidden && this.anchor_ == el) 251 this.hide(); 252 }, 253 254 /** 255 * Handler for faded transition end. 256 * @private 257 */ 258 handleTransitionEnd_: function(e) { 259 if (this.classList.contains('faded')) { 260 this.hidden = true; 261 if (this.elementToFocusOnHide_ && 262 document.activeElement == document.body) { 263 // Restore focus to default element only if there's no other 264 // element that is focused. 265 this.elementToFocusOnHide_.focus(); 266 } 267 } 268 }, 269 270 /** 271 * Handler of document click event. 272 * @private 273 */ 274 handleDocClick_: function(e) { 275 // Ignore clicks on anchor element. 276 if (e.target == this.anchor_) 277 return; 278 279 if (!this.hidden) 280 this.hide(); 281 }, 282 283 /** 284 * Handle of document keydown event. 285 * @private 286 */ 287 handleDocKeyDown_: function(e) { 288 if (this.hideOnKeyPress_ && !this.hidden) { 289 this.hide(); 290 return; 291 } 292 293 if (e.keyCode == 27 && !this.hidden) { 294 if (this.elementToFocusOnHide_) 295 this.elementToFocusOnHide_.focus(); 296 this.hide(); 297 } 298 }, 299 300 /** 301 * Handler of window blur event. 302 * @private 303 */ 304 handleWindowBlur_: function(e) { 305 if (!this.hidden) 306 this.hide(); 307 } 308 }; 309 310 return { 311 Bubble: Bubble 312 }; 313 }); 314