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