Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(113)

Side by Side Diff: chrome/browser/resources/shared/js/cr/ui/bubble.js

Issue 10907148: Implement popup bubbles for the controlled setting indicator (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: All comments addressed. Created 8 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 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 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 // require: event_tracker.js 5 // require: event_tracker.js
6 6
7 cr.define('cr.ui', function() { 7 cr.define('cr.ui', function() {
8 8
9 /** 9 /**
10 * The arrow location specifies how the arrow and bubble are positioned in 10 * The arrow location specifies how the arrow and bubble are positioned in
(...skipping 13 matching lines...) Expand all
24 // left to right mode this is the bottom left. The entire bubble is 24 // left to right mode this is the bottom left. The entire bubble is
25 // positioned above the anchor node. 25 // positioned above the anchor node.
26 BOTTOM_START: 'bottom-start', 26 BOTTOM_START: 'bottom-start',
27 // The arrow is positioned at the bottom and the end of the bubble. In 27 // The arrow is positioned at the bottom and the end of the bubble. In
28 // left to right mode this is the bottom right. The entire bubble is 28 // left to right mode this is the bottom right. The entire bubble is
29 // positioned above the anchor node. 29 // positioned above the anchor node.
30 BOTTOM_END: 'bottom-end' 30 BOTTOM_END: 'bottom-end'
31 }; 31 };
32 32
33 /** 33 /**
34 * The bubble alignment specifies the position of the bubble in relation to
35 * the anchor node.
36 * @enum
37 */
38 var BubbleAlignment = {
39 // The bubble is positioned just above or below the anchor node (as
40 // specified by the arrow location) so that the arrow points at the midpoint
41 // of the anchor.
42 ARROW_TO_MID_ANCHOR: 'arrow-to-mid-anchor',
43 // The bubble is positioned just above or below the anchor node (as
44 // specified by the arrow location) so that its reference edge lines up with
45 // the edge of the anchor.
46 BUBBLE_EDGE_TO_ANCHOR_EDGE: 'bubble-edge-anchor-edge',
47 // The bubble is positioned so that it is entirely within view and does not
48 // obstruct the anchor element, if possible. The specified arrow location is
49 // taken into account as the preferred alignment but may be overruled if
50 // there is insufficient space (see BubbleBase.reposition for the exact
51 // placement algorithm).
52 ENTIRELY_VISIBLE: 'entirely-visible'
53 };
54
55 /**
34 * Abstract base class that provides common functionality for implementing 56 * Abstract base class that provides common functionality for implementing
35 * free-floating informational bubbles with a triangular arrow pointing at an 57 * free-floating informational bubbles with a triangular arrow pointing at an
36 * anchor node. 58 * anchor node.
37 */ 59 */
38 var BubbleBase = cr.ui.define('div'); 60 var BubbleBase = cr.ui.define('div');
39 61
40 /** 62 /**
41 * The horizontal distance between the tip of the arrow and the reference edge 63 * The horizontal distance between the tip of the arrow and the reference edge
42 * of the bubble (as specified by the arrow location). 64 * of the bubble (as specified by the arrow location).
43 * @const 65 * @const
44 */ 66 */
45 BubbleBase.ARROW_OFFSET = 30; 67 BubbleBase.ARROW_OFFSET = 30;
46 68
47 BubbleBase.prototype = { 69 BubbleBase.prototype = {
48 // Set up the prototype chain 70 // Set up the prototype chain.
49 __proto__: HTMLDivElement.prototype, 71 __proto__: HTMLDivElement.prototype,
50 72
51 /** 73 /**
52 * Initialization function for the cr.ui framework. 74 * Initialization function for the cr.ui framework.
53 */ 75 */
54 decorate: function() { 76 decorate: function() {
55 this.className = 'bubble'; 77 this.className = 'bubble';
56 this.innerHTML = 78 this.innerHTML =
57 '<div class="bubble-content"></div>' + 79 '<div class="bubble-content"></div>' +
58 '<div class="bubble-shadow"></div>' + 80 '<div class="bubble-shadow"></div>' +
59 '<div class="bubble-arrow"></div>'; 81 '<div class="bubble-arrow"></div>';
60 this.hidden = true; 82 this.hidden = true;
83 this.bubbleAlignment = BubbleAlignment.ENTIRELY_VISIBLE;
61 }, 84 },
62 85
63 /** 86 /**
64 * Set the anchor node, i.e. the node that this bubble points at. Only 87 * Set the anchor node, i.e. the node that this bubble points at. Only
65 * available when the bubble is not being shown. 88 * available when the bubble is not being shown.
66 * @param {HTMLElement} node The new anchor node. 89 * @param {HTMLElement} node The new anchor node.
67 */ 90 */
68 set anchorNode(node) { 91 set anchorNode(node) {
69 if (!this.hidden) 92 if (!this.hidden)
70 return; 93 return;
(...skipping 26 matching lines...) Expand all
97 120
98 this.arrowAtRight_ = location == ArrowLocation.TOP_END || 121 this.arrowAtRight_ = location == ArrowLocation.TOP_END ||
99 location == ArrowLocation.BOTTOM_END; 122 location == ArrowLocation.BOTTOM_END;
100 if (document.documentElement.dir == 'rtl') 123 if (document.documentElement.dir == 'rtl')
101 this.arrowAtRight_ = !this.arrowAtRight_; 124 this.arrowAtRight_ = !this.arrowAtRight_;
102 this.arrowAtTop_ = location == ArrowLocation.TOP_START || 125 this.arrowAtTop_ = location == ArrowLocation.TOP_START ||
103 location == ArrowLocation.TOP_END; 126 location == ArrowLocation.TOP_END;
104 }, 127 },
105 128
106 /** 129 /**
130 * Set the bubble alignment. Only available when the bubble is not being
131 * shown.
132 * @param {cr.ui.BubbleAlignment} alignment The new bubble alignment.
133 */
134 set bubbleAlignment(alignment) {
135 if (!this.hidden)
136 return;
137
138 this.bubbleAlignment_ = alignment;
139 },
140
141 /**
142 * Update the position of the bubble. Whenever the layout may have changed,
143 * the bubble should either be repositioned by calling this function or
144 * hidden so that it does not point to a nonsensical location on the page.
145 */
146 reposition: function() {
147 var documentWidth = document.documentElement.clientWidth;
148 var documentHeight = document.documentElement.clientHeight;
149 var anchor = this.anchorNode_.getBoundingClientRect();
150 var anchorMid = (anchor.left + anchor.right) / 2;
151 var bubble = this.getBoundingClientRect();
152 var arrow = this.querySelector('.bubble-arrow').getBoundingClientRect();
153
154 if (this.bubbleAlignment_ == BubbleAlignment.ENTIRELY_VISIBLE) {
155 // Work out horizontal placement. The bubble is initially positioned so
156 // that the arrow tip points toward the midpoint of the anchor and is
157 // BubbleBase.ARROW_OFFSET pixels from the reference edge and (as
158 // specified by the arrow location). If the bubble is not entirely
159 // within view, it is then shifted, preserving the arrow tip position.
160 var left = this.arrowAtRight_ ?
161 anchorMid + BubbleBase.ARROW_OFFSET - bubble.width :
162 anchorMid - BubbleBase.ARROW_OFFSET;
163 if (document.documentElement.dir == 'rtl')
164 left = Math.min(Math.max(left, 0), documentWidth - bubble.width);
165 else
166 left = Math.max(Math.min(left, documentWidth - bubble.width), 0);
167 var arrowTip = Math.min(Math.max(this.arrowAtRight_ ?
168 left + bubble.width - anchorMid : anchorMid - left,
169 arrow.width / 2), bubble.width - arrow.width / 2);
170
171 // Work out the vertical placement, attempting to fit the bubble
172 // entirely into view. The following placements are considered in
173 // decreasing order of preference:
174 // * Outside the anchor, arrow tip touching the anchor (arrow at
175 // top/bottom as specified by the arrow location).
176 // * Outside the anchor, arrow tip touching the anchor (arrow at
177 // bottom/top, opposite the specified arrow location).
178 // * Outside the anchor, arrow tip overlapping the anchor (arrow at
179 // top/bottom as specified by the arrow location).
180 // * Outside the anchor, arrow tip overlapping the anchor (arrow at
181 // bottom/top, opposite the specified arrow location).
182 // * Overlapping the anchor.
183 var offsetTop = Math.min(documentHeight - anchor.bottom - bubble.height,
184 arrow.height / 2);
185 var offsetBottom = Math.min(anchor.top - bubble.height,
186 arrow.height / 2);
187 if (offsetTop < 0 && offsetBottom < 0) {
188 var top = 0;
189 this.updateArrowPosition_(false, false, arrowTip);
190 } else if (offsetTop > offsetBottom ||
191 offsetTop == offsetBottom && this.arrowAtTop_) {
192 var top = anchor.bottom + offsetTop;
193 this.updateArrowPosition_(true, true, arrowTip);
194 } else {
195 var top = anchor.top - bubble.height - offsetBottom;
196 this.updateArrowPosition_(true, false, arrowTip);
197 }
198 } else {
199 if (this.bubbleAlignment_ ==
200 BubbleAlignment.BUBBLE_EDGE_TO_ANCHOR_EDGE) {
201 var left = this.arrowAtRight_ ? anchor.right - bubble.width :
202 anchor.left;
203 } else {
204 var left = this.arrowAtRight_ ?
205 anchorMid - this.clientWidth + BubbleBase.ARROW_OFFSET :
206 anchorMid - BubbleBase.ARROW_OFFSET;
207 }
208 var top = this.arrowAtTop_ ? anchor.bottom + arrow.height / 2 :
209 anchor.top - this.clientHeight - arrow.height / 2;
210 this.updateArrowPosition_(true, this.arrowAtTop_,
211 BubbleBase.ARROW_OFFSET);
212 }
213
214 this.style.left = left + 'px';
215 this.style.top = top + 'px';
216 },
217
218 /**
107 * Show the bubble. 219 * Show the bubble.
108 */ 220 */
109 show: function() { 221 show: function() {
110 if (!this.hidden) 222 if (!this.hidden)
111 return; 223 return;
112 224
113 document.body.appendChild(this); 225 this.attachToDOM_();
114 this.hidden = false; 226 this.hidden = false;
227 this.reposition();
228 this.anchorNode_.showingBubble = true;
115 229
116 var doc = this.ownerDocument; 230 var doc = this.ownerDocument;
117 this.eventTracker_ = new EventTracker; 231 this.eventTracker_ = new EventTracker;
118 this.eventTracker_.add(doc, 'keydown', this, true); 232 this.eventTracker_.add(doc, 'keydown', this, true);
119 this.eventTracker_.add(doc, 'mousedown', this, true); 233 this.eventTracker_.add(doc, 'mousedown', this, true);
120 }, 234 },
121 235
122 /** 236 /**
123 * Hide the bubble. 237 * Hide the bubble.
124 */ 238 */
125 hide: function() { 239 hide: function() {
126 if (this.hidden) 240 if (this.hidden)
127 return; 241 return;
128 242
243 this.eventTracker_.removeAll();
244 this.anchorNode_.showingBubble = false;
129 this.hidden = true; 245 this.hidden = true;
130 this.eventTracker_.removeAll();
131 this.parentNode.removeChild(this); 246 this.parentNode.removeChild(this);
132 }, 247 },
133 248
134 /** 249 /**
135 * Handle keyboard events, dismissing the bubble if necessary. 250 * Handle keyboard events, dismissing the bubble if necessary.
136 * @param {Event} event The event. 251 * @param {Event} event The event.
137 */ 252 */
138 handleEvent: function(event) { 253 handleEvent: function(event) {
139 // Close the bubble when the user presses <Esc>. 254 // Close the bubble when the user presses <Esc>.
140 if (event.type == 'keydown' && event.keyCode == 27) { 255 if (event.type == 'keydown' && event.keyCode == 27) {
141 this.hide(); 256 this.hide();
142 event.preventDefault(); 257 event.preventDefault();
143 event.stopPropagation(); 258 event.stopPropagation();
144 } 259 }
145 }, 260 },
146 261
147 /** 262 /**
263 * Attach the bubble to the document's DOM.
264 * @private
265 */
266 attachToDOM_: function() {
267 document.body.appendChild(this);
268 },
269
270 /**
148 * Update the arrow so that it appears at the correct position. 271 * Update the arrow so that it appears at the correct position.
149 * @param {Boolean} visible Whether the arrow should be visible. 272 * @param {Boolean} visible Whether the arrow should be visible.
150 * @param {number} The horizontal distance between the tip of the arrow and 273 * @param {Boolean} atTop Whether the arrow should be at the top of the
151 * the reference edge of the bubble (as specified by the arrow location). 274 * bubble.
275 * @param {number} tipOffset The horizontal distance between the tip of the
276 * arrow and the reference edge of the bubble (as specified by the arrow
277 * location).
152 * @private 278 * @private
153 */ 279 */
154 updateArrowPosition_: function(visible, tipOffset) { 280 updateArrowPosition_: function(visible, atTop, tipOffset) {
155 var bubbleArrow = this.querySelector('.bubble-arrow'); 281 var bubbleArrow = this.querySelector('.bubble-arrow');
156 282 bubbleArrow.hidden = !visible;
157 if (visible) { 283 if (!visible)
158 bubbleArrow.style.display = 'block';
159 } else {
160 bubbleArrow.style.display = 'none';
161 return; 284 return;
162 }
163 285
164 var edgeOffset = (-bubbleArrow.clientHeight / 2) + 'px'; 286 var edgeOffset = (-bubbleArrow.clientHeight / 2) + 'px';
165 bubbleArrow.style.top = this.arrowAtTop_ ? edgeOffset : 'auto'; 287 bubbleArrow.style.top = atTop ? edgeOffset : 'auto';
166 bubbleArrow.style.bottom = this.arrowAtTop_ ? 'auto' : edgeOffset; 288 bubbleArrow.style.bottom = atTop ? 'auto' : edgeOffset;
167 289
168 edgeOffset = (tipOffset - bubbleArrow.clientHeight / 2) + 'px'; 290 edgeOffset = (tipOffset - bubbleArrow.offsetWidth / 2) + 'px';
169 bubbleArrow.style.left = this.arrowAtRight_ ? 'auto' : edgeOffset; 291 bubbleArrow.style.left = this.arrowAtRight_ ? 'auto' : edgeOffset;
170 bubbleArrow.style.right = this.arrowAtRight_ ? edgeOffset : 'auto'; 292 bubbleArrow.style.right = this.arrowAtRight_ ? edgeOffset : 'auto';
171 }, 293 },
172 }; 294 };
173 295
174 /** 296 /**
175 * The bubble alignment specifies the horizontal position of the bubble in
176 * relation to the anchor node.
177 * @enum
178 */
179 var BubbleAlignment = {
180 // The bubble is positioned so that the tip of the arrow points to the
181 // middle of the anchor node.
182 ARROW_TO_MID_ANCHOR: 'arrow-to-mid-anchor',
183 // The bubble is positioned so that the edge nearest to the arrow is lined
184 // up with the edge of the anchor node.
185 BUBBLE_EDGE_TO_ANCHOR_EDGE: 'bubble-edge-anchor-edge'
186 };
187
188 /**
189 * A bubble that remains open until the user explicitly dismisses it or clicks 297 * A bubble that remains open until the user explicitly dismisses it or clicks
190 * outside the bubble after it has been shown for at least the specified 298 * outside the bubble after it has been shown for at least the specified
191 * amount of time (making it less likely that the user will unintentionally 299 * amount of time (making it less likely that the user will unintentionally
192 * dismiss the bubble). The bubble repositions itself on layout changes. 300 * dismiss the bubble). The bubble repositions itself on layout changes.
193 */ 301 */
194 var Bubble = cr.ui.define('div'); 302 var Bubble = cr.ui.define('div');
195 303
196 Bubble.prototype = { 304 Bubble.prototype = {
197 // Set up the prototype chain 305 // Set up the prototype chain
198 __proto__: BubbleBase.prototype, 306 __proto__: BubbleBase.prototype,
(...skipping 20 matching lines...) Expand all
219 * @param {function} handler The new handler, a function with no parameters. 327 * @param {function} handler The new handler, a function with no parameters.
220 */ 328 */
221 set handleCloseEvent(handler) { 329 set handleCloseEvent(handler) {
222 if (!this.hidden) 330 if (!this.hidden)
223 return; 331 return;
224 332
225 this.handleCloseEvent_ = handler; 333 this.handleCloseEvent_ = handler;
226 }, 334 },
227 335
228 /** 336 /**
229 * Set the bubble alignment. Only available when the bubble is not being
230 * shown.
231 * @param {cr.ui.BubbleAlignment} alignment The new bubble alignment.
232 */
233 set bubbleAlignment(alignment) {
234 if (!this.hidden)
235 return;
236
237 this.bubbleAlignment_ = alignment;
238 },
239
240 /**
241 * Set the delay before the user is allowed to click outside the bubble to 337 * Set the delay before the user is allowed to click outside the bubble to
242 * dismiss it. Using a delay makes it less likely that the user will 338 * dismiss it. Using a delay makes it less likely that the user will
243 * unintentionally dismiss the bubble. 339 * unintentionally dismiss the bubble.
244 * @param {number} delay The delay in milliseconds. 340 * @param {number} delay The delay in milliseconds.
245 */ 341 */
246 set deactivateToDismissDelay(delay) { 342 set deactivateToDismissDelay(delay) {
247 this.deactivateToDismissDelay_ = delay; 343 this.deactivateToDismissDelay_ = delay;
248 }, 344 },
249 345
250 /** 346 /**
251 * Hide or show the close button. 347 * Hide or show the close button.
252 * @param {Boolean} isVisible True if the close button should be visible. 348 * @param {Boolean} isVisible True if the close button should be visible.
253 */ 349 */
254 set closeButtonVisible(isVisible) { 350 set closeButtonVisible(isVisible) {
255 this.querySelector('.bubble-close').hidden = !isVisible; 351 this.querySelector('.bubble-close').hidden = !isVisible;
256 }, 352 },
257 353
258 /** 354 /**
259 * Update the position of the bubble. This is automatically called when the
260 * window is resized, but should also be called any time the layout may have
261 * changed.
262 */
263 reposition: function() {
264 var clientRect = this.anchorNode_.getBoundingClientRect();
265 var bubbleArrow = this.querySelector('.bubble-arrow');
266 var arrowOffsetY = bubbleArrow.offsetHeight / 2;
267
268 var left;
269 if (this.bubbleAlignment_ ==
270 BubbleAlignment.BUBBLE_EDGE_TO_ANCHOR_EDGE) {
271 left = this.arrowAtRight_ ? clientRect.right - this.clientWidth :
272 clientRect.left;
273 } else {
274 var anchorMid = (clientRect.left + clientRect.right) / 2;
275 left = this.arrowAtRight_ ?
276 anchorMid - this.clientWidth + BubbleBase.ARROW_OFFSET :
277 anchorMid - BubbleBase.ARROW_OFFSET;
278 }
279 var top = this.arrowAtTop_ ? clientRect.bottom + arrowOffsetY :
280 clientRect.top - this.clientHeight - arrowOffsetY;
281
282 this.style.left = left + 'px';
283 this.style.top = top + 'px';
284
285 this.updateArrowPosition_(true, BubbleBase.ARROW_OFFSET);
286 },
287
288 /**
289 * Show the bubble. 355 * Show the bubble.
290 */ 356 */
291 show: function() { 357 show: function() {
292 if (!this.hidden) 358 if (!this.hidden)
293 return; 359 return;
294 360
295 BubbleBase.prototype.show.call(this); 361 BubbleBase.prototype.show.call(this);
296 362
297 this.reposition();
298 this.showTime_ = Date.now(); 363 this.showTime_ = Date.now();
299 this.eventTracker_.add(window, 'resize', this.reposition.bind(this)); 364 this.eventTracker_.add(window, 'resize', this.reposition.bind(this));
300 }, 365 },
301 366
302 /** 367 /**
303 * Handle keyboard and mouse events, dismissing the bubble if necessary. 368 * Handle keyboard and mouse events, dismissing the bubble if necessary.
304 * @param {Event} event The event. 369 * @param {Event} event The event.
305 */ 370 */
306 handleEvent: function(event) { 371 handleEvent: function(event) {
307 BubbleBase.prototype.handleEvent.call(this, event); 372 BubbleBase.prototype.handleEvent.call(this, event);
308 373
309 if (event.type == 'mousedown') { 374 if (event.type == 'mousedown') {
310 // Dismiss the bubble when the user clicks on the close button. 375 // Dismiss the bubble when the user clicks on the close button.
311 if (event.target == this.querySelector('.bubble-close')) { 376 if (event.target == this.querySelector('.bubble-close')) {
312 this.handleCloseEvent_(); 377 this.handleCloseEvent_();
313 // Dismiss the bubble when the user clicks outside it after the 378 // Dismiss the bubble when the user clicks outside it after the
314 // specified delay has passed. 379 // specified delay has passed.
315 } else if (!this.contains(event.target) && 380 } else if (!this.contains(event.target) &&
316 Date.now() - this.showTime_ >= this.deactivateToDismissDelay_) { 381 Date.now() - this.showTime_ >= this.deactivateToDismissDelay_) {
317 this.hide(); 382 this.hide();
318 } 383 }
319 } 384 }
320 }, 385 },
321 }; 386 };
322 387
323 return { 388 return {
324 ArrowLocation: ArrowLocation, 389 ArrowLocation: ArrowLocation,
390 BubbleAlignment: BubbleAlignment,
325 BubbleBase: BubbleBase, 391 BubbleBase: BubbleBase,
326 BubbleAlignment: BubbleAlignment,
327 Bubble: Bubble 392 Bubble: Bubble
328 }; 393 };
329 }); 394 });
OLDNEW
« no previous file with comments | « chrome/browser/resources/shared/css/bubble.css ('k') | chrome/browser/resources/shared/js/cr/ui/focus_manager.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698