| OLD | NEW |
| (Empty) |
| 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. | |
| 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 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 }); | |
| OLD | NEW |