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