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 |