Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / ui / webui / resources / js / cr / ui / bubble.js
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 // require: event_tracker.js
6
7 // TODO(vitalyp): Inline the enums below into cr.ui definition function, remove
8 // cr.exportPath() call and remove exportPath from exports in cr.js when this
9 // issue will be fixed:
10 // https://github.com/google/closure-compiler/issues/544
11 cr.exportPath('cr.ui');
12
13 /**
14  * The arrow location specifies how the arrow and bubble are positioned in
15  * relation to the anchor node.
16  * @enum {string}
17  */
18 cr.ui.ArrowLocation = {
19   // The arrow is positioned at the top and the start of the bubble. In left
20   // to right mode this is the top left. The entire bubble is positioned below
21   // the anchor node.
22   TOP_START: 'top-start',
23   // The arrow is positioned at the top and the end of the bubble. In left to
24   // right mode this is the top right. The entire bubble is positioned below
25   // the anchor node.
26   TOP_END: 'top-end',
27   // The arrow is positioned at the bottom and the start of the bubble. In
28   // left to right mode this is the bottom left. The entire bubble is
29   // positioned above the anchor node.
30   BOTTOM_START: 'bottom-start',
31   // The arrow is positioned at the bottom and the end of the bubble. In
32   // left to right mode this is the bottom right. The entire bubble is
33   // positioned above the anchor node.
34   BOTTOM_END: 'bottom-end'
35 };
36
37 /**
38  * The bubble alignment specifies the position of the bubble in relation to
39  * the anchor node.
40  * @enum {string}
41  */
42 cr.ui.BubbleAlignment = {
43   // The bubble is positioned just above or below the anchor node (as
44   // specified by the arrow location) so that the arrow points at the midpoint
45   // of the anchor.
46   ARROW_TO_MID_ANCHOR: 'arrow-to-mid-anchor',
47   // The bubble is positioned just above or below the anchor node (as
48   // specified by the arrow location) so that its reference edge lines up with
49   // the edge of the anchor.
50   BUBBLE_EDGE_TO_ANCHOR_EDGE: 'bubble-edge-anchor-edge',
51   // The bubble is positioned so that it is entirely within view and does not
52   // obstruct the anchor element, if possible. The specified arrow location is
53   // taken into account as the preferred alignment but may be overruled if
54   // there is insufficient space (see BubbleBase.reposition for the exact
55   // placement algorithm).
56   ENTIRELY_VISIBLE: 'entirely-visible'
57 };
58
59 cr.define('cr.ui', function() {
60   /**
61    * Abstract base class that provides common functionality for implementing
62    * free-floating informational bubbles with a triangular arrow pointing at an
63    * anchor node.
64    * @constructor
65    * @extends {HTMLDivElement}
66    * @implements {EventListener}
67    */
68   var BubbleBase = cr.ui.define('div');
69
70   /**
71    * The horizontal distance between the tip of the arrow and the reference edge
72    * of the bubble (as specified by the arrow location). In pixels.
73    * @type {number}
74    * @const
75    */
76   BubbleBase.ARROW_OFFSET = 30;
77
78   /**
79    * Minimum horizontal spacing between edge of bubble and edge of viewport
80    * (when using the ENTIRELY_VISIBLE alignment). In pixels.
81    * @type {number}
82    * @const
83    */
84   BubbleBase.MIN_VIEWPORT_EDGE_MARGIN = 2;
85
86   BubbleBase.prototype = {
87     // Set up the prototype chain.
88     __proto__: HTMLDivElement.prototype,
89
90     /**
91      * @type {Node}
92      * @private
93      */
94     anchorNode_: null,
95
96     /**
97      * Initialization function for the cr.ui framework.
98      */
99     decorate: function() {
100       this.className = 'bubble';
101       this.innerHTML =
102           '<div class="bubble-content"></div>' +
103           '<div class="bubble-shadow"></div>' +
104           '<div class="bubble-arrow"></div>';
105       this.hidden = true;
106       this.bubbleAlignment = cr.ui.BubbleAlignment.ENTIRELY_VISIBLE;
107     },
108
109     /**
110      * Set the anchor node, i.e. the node that this bubble points at. Only
111      * available when the bubble is not being shown.
112      * @param {HTMLElement} node The new anchor node.
113      */
114     set anchorNode(node) {
115       if (!this.hidden)
116         return;
117
118       this.anchorNode_ = node;
119     },
120
121     /**
122      * Set the conent of the bubble. Only available when the bubble is not being
123      * shown.
124      * @param {HTMLElement} node The root node of the new content.
125      */
126     set content(node) {
127       if (!this.hidden)
128         return;
129
130       var bubbleContent = this.querySelector('.bubble-content');
131       bubbleContent.innerHTML = '';
132       bubbleContent.appendChild(node);
133     },
134
135     /**
136      * Set the arrow location. Only available when the bubble is not being
137      * shown.
138      * @param {cr.ui.ArrowLocation} location The new arrow location.
139      */
140     set arrowLocation(location) {
141       if (!this.hidden)
142         return;
143
144       this.arrowAtRight_ = location == cr.ui.ArrowLocation.TOP_END ||
145                            location == cr.ui.ArrowLocation.BOTTOM_END;
146       if (document.documentElement.dir == 'rtl')
147         this.arrowAtRight_ = !this.arrowAtRight_;
148       this.arrowAtTop_ = location == cr.ui.ArrowLocation.TOP_START ||
149                          location == cr.ui.ArrowLocation.TOP_END;
150     },
151
152     /**
153      * Set the bubble alignment. Only available when the bubble is not being
154      * shown.
155      * @param {cr.ui.BubbleAlignment} alignment The new bubble alignment.
156      */
157     set bubbleAlignment(alignment) {
158       if (!this.hidden)
159         return;
160
161       this.bubbleAlignment_ = alignment;
162     },
163
164     /**
165      * Update the position of the bubble. Whenever the layout may have changed,
166      * the bubble should either be repositioned by calling this function or
167      * hidden so that it does not point to a nonsensical location on the page.
168      */
169     reposition: function() {
170       var documentWidth = document.documentElement.clientWidth;
171       var documentHeight = document.documentElement.clientHeight;
172       var anchor = this.anchorNode_.getBoundingClientRect();
173       var anchorMid = (anchor.left + anchor.right) / 2;
174       var bubble = this.getBoundingClientRect();
175       var arrow = this.querySelector('.bubble-arrow').getBoundingClientRect();
176
177       if (this.bubbleAlignment_ == cr.ui.BubbleAlignment.ENTIRELY_VISIBLE) {
178         // Work out horizontal placement. The bubble is initially positioned so
179         // that the arrow tip points toward the midpoint of the anchor and is
180         // BubbleBase.ARROW_OFFSET pixels from the reference edge and (as
181         // specified by the arrow location). If the bubble is not entirely
182         // within view, it is then shifted, preserving the arrow tip position.
183         var left = this.arrowAtRight_ ?
184            anchorMid + BubbleBase.ARROW_OFFSET - bubble.width :
185            anchorMid - BubbleBase.ARROW_OFFSET;
186         var max_left_pos =
187             documentWidth - bubble.width - BubbleBase.MIN_VIEWPORT_EDGE_MARGIN;
188         var min_left_pos = BubbleBase.MIN_VIEWPORT_EDGE_MARGIN;
189         if (document.documentElement.dir == 'rtl')
190           left = Math.min(Math.max(left, min_left_pos), max_left_pos);
191         else
192           left = Math.max(Math.min(left, max_left_pos), min_left_pos);
193         var arrowTip = Math.min(
194             Math.max(arrow.width / 2,
195                      this.arrowAtRight_ ? left + bubble.width - anchorMid :
196                                           anchorMid - left),
197             bubble.width - arrow.width / 2);
198
199         // Work out the vertical placement, attempting to fit the bubble
200         // entirely into view. The following placements are considered in
201         // decreasing order of preference:
202         // * Outside the anchor, arrow tip touching the anchor (arrow at
203         //   top/bottom as specified by the arrow location).
204         // * Outside the anchor, arrow tip touching the anchor (arrow at
205         //   bottom/top, opposite the specified arrow location).
206         // * Outside the anchor, arrow tip overlapping the anchor (arrow at
207         //   top/bottom as specified by the arrow location).
208         // * Outside the anchor, arrow tip overlapping the anchor (arrow at
209         //   bottom/top, opposite the specified arrow location).
210         // * Overlapping the anchor.
211         var offsetTop = Math.min(documentHeight - anchor.bottom - bubble.height,
212                                  arrow.height / 2);
213         var offsetBottom = Math.min(anchor.top - bubble.height,
214                                     arrow.height / 2);
215         if (offsetTop < 0 && offsetBottom < 0) {
216           var top = 0;
217           this.updateArrowPosition_(false, false, arrowTip);
218         } else if (offsetTop > offsetBottom ||
219                    offsetTop == offsetBottom && this.arrowAtTop_) {
220           var top = anchor.bottom + offsetTop;
221           this.updateArrowPosition_(true, true, arrowTip);
222         } else {
223           var top = anchor.top - bubble.height - offsetBottom;
224           this.updateArrowPosition_(true, false, arrowTip);
225         }
226       } else {
227         if (this.bubbleAlignment_ ==
228             cr.ui.BubbleAlignment.BUBBLE_EDGE_TO_ANCHOR_EDGE) {
229           var left = this.arrowAtRight_ ? anchor.right - bubble.width :
230               anchor.left;
231         } else {
232           var left = this.arrowAtRight_ ?
233               anchorMid - this.clientWidth + BubbleBase.ARROW_OFFSET :
234               anchorMid - BubbleBase.ARROW_OFFSET;
235         }
236         var top = this.arrowAtTop_ ? anchor.bottom + arrow.height / 2 :
237             anchor.top - this.clientHeight - arrow.height / 2;
238         this.updateArrowPosition_(true, this.arrowAtTop_,
239                                   BubbleBase.ARROW_OFFSET);
240       }
241
242       this.style.left = left + 'px';
243       this.style.top = top + 'px';
244     },
245
246     /**
247      * Show the bubble.
248      */
249     show: function() {
250       if (!this.hidden)
251         return;
252
253       this.attachToDOM_();
254       this.hidden = false;
255       this.reposition();
256
257       var doc = assert(this.ownerDocument);
258       this.eventTracker_ = new EventTracker;
259       this.eventTracker_.add(doc, 'keydown', this, true);
260       this.eventTracker_.add(doc, 'mousedown', this, true);
261     },
262
263     /**
264      * Hide the bubble.
265      */
266     hide: function() {
267       if (this.hidden)
268         return;
269
270       this.eventTracker_.removeAll();
271       this.hidden = true;
272       this.parentNode.removeChild(this);
273     },
274
275     /**
276      * Handle keyboard events, dismissing the bubble if necessary.
277      * @param {Event} event The event.
278      */
279     handleEvent: function(event) {
280       // Close the bubble when the user presses <Esc>.
281       if (event.type == 'keydown' && event.keyCode == 27) {
282         this.hide();
283         event.preventDefault();
284         event.stopPropagation();
285       }
286     },
287
288     /**
289      * Attach the bubble to the document's DOM.
290      * @private
291      */
292     attachToDOM_: function() {
293       document.body.appendChild(this);
294     },
295
296     /**
297      * Update the arrow so that it appears at the correct position.
298      * @param {boolean} visible Whether the arrow should be visible.
299      * @param {boolean} atTop Whether the arrow should be at the top of the
300      * bubble.
301      * @param {number} tipOffset The horizontal distance between the tip of the
302      * arrow and the reference edge of the bubble (as specified by the arrow
303      * location).
304      * @private
305      */
306     updateArrowPosition_: function(visible, atTop, tipOffset) {
307       var bubbleArrow = this.querySelector('.bubble-arrow');
308       bubbleArrow.hidden = !visible;
309       if (!visible)
310         return;
311
312       var edgeOffset = (-bubbleArrow.clientHeight / 2) + 'px';
313       bubbleArrow.style.top = atTop ? edgeOffset : 'auto';
314       bubbleArrow.style.bottom = atTop ? 'auto' : edgeOffset;
315
316       edgeOffset = (tipOffset - bubbleArrow.offsetWidth / 2) + 'px';
317       bubbleArrow.style.left = this.arrowAtRight_ ? 'auto' : edgeOffset;
318       bubbleArrow.style.right = this.arrowAtRight_ ? edgeOffset : 'auto';
319     },
320   };
321
322   /**
323    * A bubble that remains open until the user explicitly dismisses it or clicks
324    * outside the bubble after it has been shown for at least the specified
325    * amount of time (making it less likely that the user will unintentionally
326    * dismiss the bubble). The bubble repositions itself on layout changes.
327    * @constructor
328    * @extends {cr.ui.BubbleBase}
329    */
330   var Bubble = cr.ui.define('div');
331
332   Bubble.prototype = {
333     // Set up the prototype chain.
334     __proto__: BubbleBase.prototype,
335
336     /**
337      * Initialization function for the cr.ui framework.
338      */
339     decorate: function() {
340       BubbleBase.prototype.decorate.call(this);
341
342       var close = document.createElement('div');
343       close.className = 'bubble-close';
344       this.insertBefore(close, this.querySelector('.bubble-content'));
345
346       this.handleCloseEvent = this.hide;
347       this.deactivateToDismissDelay_ = 0;
348       this.bubbleAlignment = cr.ui.BubbleAlignment.ARROW_TO_MID_ANCHOR;
349     },
350
351     /**
352      * Handler for close events triggered when the close button is clicked. By
353      * default, set to this.hide. Only available when the bubble is not being
354      * shown.
355      * @param {function(): *} handler The new handler, a function with no
356      *     parameters.
357      */
358     set handleCloseEvent(handler) {
359       if (!this.hidden)
360         return;
361
362       this.handleCloseEvent_ = handler;
363     },
364
365     /**
366      * Set the delay before the user is allowed to click outside the bubble to
367      * dismiss it. Using a delay makes it less likely that the user will
368      * unintentionally dismiss the bubble.
369      * @param {number} delay The delay in milliseconds.
370      */
371     set deactivateToDismissDelay(delay) {
372       this.deactivateToDismissDelay_ = delay;
373     },
374
375     /**
376      * Hide or show the close button.
377      * @param {boolean} isVisible True if the close button should be visible.
378      */
379     set closeButtonVisible(isVisible) {
380       this.querySelector('.bubble-close').hidden = !isVisible;
381     },
382
383     /**
384      * Show the bubble.
385      */
386     show: function() {
387       if (!this.hidden)
388         return;
389
390       BubbleBase.prototype.show.call(this);
391
392       this.showTime_ = Date.now();
393       this.eventTracker_.add(window, 'resize', this.reposition.bind(this));
394     },
395
396     /**
397      * Handle keyboard and mouse events, dismissing the bubble if necessary.
398      * @param {Event} event The event.
399      * @suppress {checkTypes}
400      * TODO(vitalyp): remove suppression when the extern
401      * Node.prototype.contains() will be fixed.
402      */
403     handleEvent: function(event) {
404       BubbleBase.prototype.handleEvent.call(this, event);
405
406       if (event.type == 'mousedown') {
407         // Dismiss the bubble when the user clicks on the close button.
408         if (event.target == this.querySelector('.bubble-close')) {
409           this.handleCloseEvent_();
410         // Dismiss the bubble when the user clicks outside it after the
411         // specified delay has passed.
412         } else if (!this.contains(event.target) &&
413             Date.now() - this.showTime_ >= this.deactivateToDismissDelay_) {
414           this.hide();
415         }
416       }
417     },
418   };
419
420   /**
421    * A bubble that closes automatically when the user clicks or moves the focus
422    * outside the bubble and its target element, scrolls the underlying document
423    * or resizes the window.
424    * @constructor
425    * @extends {cr.ui.BubbleBase}
426    */
427   var AutoCloseBubble = cr.ui.define('div');
428
429   AutoCloseBubble.prototype = {
430     // Set up the prototype chain.
431     __proto__: BubbleBase.prototype,
432
433     /**
434      * Initialization function for the cr.ui framework.
435      */
436     decorate: function() {
437       BubbleBase.prototype.decorate.call(this);
438       this.classList.add('auto-close-bubble');
439     },
440
441     /**
442      * Set the DOM sibling node, i.e. the node as whose sibling the bubble
443      * should join the DOM to ensure that focusable elements inside the bubble
444      * follow the target element in the document's tab order. Only available
445      * when the bubble is not being shown.
446      * @param {HTMLElement} node The new DOM sibling node.
447      */
448     set domSibling(node) {
449       if (!this.hidden)
450         return;
451
452       this.domSibling_ = node;
453     },
454
455     /**
456      * Show the bubble.
457      */
458     show: function() {
459       if (!this.hidden)
460         return;
461
462       BubbleBase.prototype.show.call(this);
463       this.domSibling_.showingBubble = true;
464
465       var doc = this.ownerDocument;
466       this.eventTracker_.add(doc, 'mousewheel', this, true);
467       this.eventTracker_.add(doc, 'scroll', this, true);
468       this.eventTracker_.add(doc, 'elementFocused', this, true);
469       this.eventTracker_.add(window, 'resize', this);
470     },
471
472     /**
473      * Hide the bubble.
474      */
475     hide: function() {
476       BubbleBase.prototype.hide.call(this);
477       this.domSibling_.showingBubble = false;
478     },
479
480     /**
481      * Handle events, closing the bubble when the user clicks or moves the focus
482      * outside the bubble and its target element, scrolls the underlying
483      * document or resizes the window.
484      * @param {Event} event The event.
485      * @suppress {checkTypes}
486      * TODO(vitalyp): remove suppression when the extern
487      * Node.prototype.contains() will be fixed.
488      */
489     handleEvent: function(event) {
490       BubbleBase.prototype.handleEvent.call(this, event);
491
492       switch (event.type) {
493         // Close the bubble when the user clicks outside it, except if it is a
494         // left-click on the bubble's target element (allowing the target to
495         // handle the event and close the bubble itself).
496         case 'mousedown':
497           var target = assertInstanceof(event.target, Node);
498           if (event.button == 0 && this.anchorNode_.contains(target))
499             break;
500         // Close the bubble when the underlying document is scrolled.
501         case 'mousewheel':
502         case 'scroll':
503           var target = assertInstanceof(event.target, Node);
504           if (this.contains(target))
505             break;
506         // Close the bubble when the window is resized.
507         case 'resize':
508           this.hide();
509           break;
510         // Close the bubble when the focus moves to an element that is not the
511         // bubble target and is not inside the bubble.
512         case 'elementFocused':
513           var target = assertInstanceof(event.target, Node);
514           if (!this.anchorNode_.contains(target) && !this.contains(target))
515             this.hide();
516           break;
517       }
518     },
519
520     /**
521      * Attach the bubble to the document's DOM, making it a sibling of the
522      * |domSibling_| so that focusable elements inside the bubble follow the
523      * target element in the document's tab order.
524      * @private
525      */
526     attachToDOM_: function() {
527       var parent = this.domSibling_.parentNode;
528       parent.insertBefore(this, this.domSibling_.nextSibling);
529     },
530   };
531
532
533   return {
534     BubbleBase: BubbleBase,
535     Bubble: Bubble,
536     AutoCloseBubble: AutoCloseBubble
537   };
538 });