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.
6 * @fileoverview Bubble implementation.
9 // TODO(xiyuan): Move this into shared.
10 cr.define('cr.ui', function() {
12 * Creates a bubble div.
14 * @extends {HTMLDivElement}
16 var Bubble = cr.ui.define('div');
30 * Bubble attachment side.
34 RIGHT: 'bubble-right',
37 BOTTOM: 'bubble-bottom'
41 __proto__: HTMLDivElement.prototype,
43 // Anchor element for this bubble.
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,
50 // With help of these elements we create closed artificial tab-cycle through
52 firstBubbleElement_: undefined,
53 lastBubbleElement_: undefined,
55 // Whether to hide bubble when key is pressed.
56 hideOnKeyPress_: true,
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);
74 * Element that should be focused on hide.
77 set elementToFocusOnHide(value) {
78 this.elementToFocusOnHide_ = value;
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.
87 set lastBubbleElement(value) {
88 this.lastBubbleElement_ = value;
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.
97 set firstBubbleElement(value) {
98 this.firstBubbleElement_ = value;
102 * Whether to hide bubble when key is pressed.
105 set hideOnKeyPress(value) {
106 this.hideOnKeyPress_ = value;
110 * Whether to hide bubble when clicked inside bubble element.
114 set hideOnSelfClick(value) {
116 this.removeEventListener('click', this.selfClickHandler_);
118 this.addEventListener('click', this.selfClickHandler_);
122 * Handler for click event which prevents bubble auto hide.
125 handleSelfClick_: function(e) {
126 // Allow clicking on [x] button.
127 if (e.target && e.target.classList.contains('close-button'))
133 * Sets the attachment of the bubble.
134 * @param {!Attachment} attachment Bubble attachment.
136 setAttachment_: function(attachment) {
137 for (var k in Bubble.Attachment) {
138 var v = Bubble.Attachment[k];
139 this.classList.toggle(v, v == attachment);
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.
152 showContentAt_: function(pos, attachment, opt_content) {
153 this.style.top = this.style.left = this.style.right = this.style.bottom =
156 if (typeof pos[k] == 'number')
157 this.style[k] = pos[k] + 'px';
159 if (opt_content !== undefined) {
161 this.appendChild(opt_content);
163 this.setAttachment_(attachment);
165 this.classList.remove('faded');
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.
176 showForElement: function(el, attachment, opt_offset, opt_padding) {
177 this.showContentForElement(
178 el, attachment, undefined, opt_offset, opt_padding);
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.
195 showContentForElement: function(el, attachment, opt_content,
196 opt_offset, opt_padding) {
197 /** @const */ var ARROW_OFFSET = 25;
198 /** @const */ var DEFAULT_PADDING = 18;
200 if (opt_padding == undefined)
201 opt_padding = DEFAULT_PADDING;
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];
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;
216 case Bubble.Attachment.RIGHT:
217 pos.top = origin.top + offset[1] - ARROW_OFFSET;
218 pos.right = origin.right + el.offsetWidth + opt_padding;
220 case Bubble.Attachment.BOTTOM:
221 pos.right = origin.right + offset[0] - ARROW_OFFSET;
222 pos.top = origin.top + el.offsetHeight + opt_padding;
224 case Bubble.Attachment.LEFT:
225 pos.top = origin.top + offset[1] - ARROW_OFFSET;
226 pos.left = origin.left + el.offsetWidth + opt_padding;
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;
235 case Bubble.Attachment.RIGHT:
236 pos.top = origin.top + offset[1] - ARROW_OFFSET;
237 pos.left = origin.left + el.offsetWidth + opt_padding;
239 case Bubble.Attachment.BOTTOM:
240 pos.left = origin.left + offset[0] - ARROW_OFFSET;
241 pos.top = origin.top + el.offsetHeight + opt_padding;
243 case Bubble.Attachment.LEFT:
244 pos.top = origin.top + offset[1] - ARROW_OFFSET;
245 pos.right = origin.right + el.offsetWidth + opt_padding;
251 this.showContentAt_(pos, attachment, opt_content);
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.
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);
278 if (!this.classList.contains('faded'))
279 this.classList.add('faded');
283 * Hides the bubble anchored to the given element (if any).
284 * @param {!Object} el Anchor element.
286 hideForElement: function(el) {
287 if (!this.hidden && this.anchor_ == el)
292 * Handler for faded transition end.
295 handleTransitionEnd_: function(e) {
296 if (this.classList.contains('faded')) {
298 if (this.elementToFocusOnHide_)
299 this.elementToFocusOnHide_.focus();
304 * Handler of document click event.
307 handleDocClick_: function(e) {
308 // Ignore clicks on anchor element.
309 if (e.target == this.anchor_)
317 * Handle of document keydown event.
320 handleDocKeyDown_: function(e) {
324 if (this.hideOnKeyPress_) {
328 // Artificial tab-cycle.
329 if (e.keyCode == KeyCodes.TAB && e.shiftKey == true &&
330 e.target == this.firstBubbleElement_) {
331 this.lastBubbleElement_.focus();
334 if (e.keyCode == KeyCodes.TAB && e.shiftKey == false &&
335 e.target == this.lastBubbleElement_) {
336 this.firstBubbleElement_.focus();
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')))
347 * Handler of window blur event.
350 handleWindowBlur_: function(e) {