Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / ui / webui / resources / js / cr / ui / expandable_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 cr.define('cr.ui', function() {
8   'use strict';
9
10   /**
11    * ExpandableBubble is a free-floating compact informational bubble with an
12    * arrow that points at a place of interest on the page. When clicked, the
13    * bubble expands to show more of its content. Width of the bubble is the
14    * width of the node it is overlapping when unexpanded. Expanded, it is of a
15    * fixed width, but variable height. Currently the arrow is always positioned
16    * at the bottom right and points down.
17    * @constructor
18    * @extends {HTMLDivElement}
19    * @implements {EventListener}
20    */
21   var ExpandableBubble = cr.ui.define('div');
22
23   ExpandableBubble.prototype = {
24     __proto__: HTMLDivElement.prototype,
25
26     /** @override */
27     decorate: function() {
28       this.className = 'expandable-bubble';
29       this.innerHTML =
30           '<div class="expandable-bubble-contents">' +
31             '<div class="expandable-bubble-title"></div>' +
32             '<div class="expandable-bubble-main" hidden></div>' +
33           '</div>' +
34           '<div class="expandable-bubble-close" hidden></div>';
35
36       this.hidden = true;
37       this.bubbleSuppressed = false;
38       this.handleCloseEvent = this.hide;
39     },
40
41     /**
42      * Sets the title of the bubble. The title is always visible when the
43      * bubble is visible.
44      * @param {Node} node An HTML element to set as the title.
45      */
46     set contentTitle(node) {
47       var bubbleTitle = this.querySelector('.expandable-bubble-title');
48       bubbleTitle.textContent = '';
49       bubbleTitle.appendChild(node);
50     },
51
52     /**
53      * Sets the content node of the bubble. The content node is only visible
54      * when the bubble is expanded.
55      * @param {Node} node An HTML element.
56      */
57     set content(node) {
58       var bubbleMain = this.querySelector('.expandable-bubble-main');
59       bubbleMain.textContent = '';
60       bubbleMain.appendChild(node);
61     },
62
63     /**
64      * Sets the anchor node, i.e. the node that this bubble points at and
65      * partially overlaps.
66      * @param {HTMLElement} node The new anchor node.
67      */
68     set anchorNode(node) {
69       this.anchorNode_ = node;
70
71       if (!this.hidden)
72         this.resizeAndReposition();
73     },
74
75     /**
76      * Handles the close event which is triggered when the close button
77      * is clicked. By default is set to this.hide.
78      * @param {Function} func A function with no parameters.
79      */
80     set handleCloseEvent(func) {
81       this.handleCloseEvent_ = func;
82     },
83
84     /**
85      * Temporarily suppresses the bubble from view (and toggles it back).
86      * 'Suppressed' and 'hidden' are two bubble states that both indicate that
87      * the bubble should not be visible, but when you 'un-suppress' a bubble,
88      * only a suppressed bubble becomes visible. This can be handy, for example,
89      * if the user switches away from the app card (then we need to know which
90      * bubbles to show (only the suppressed ones, not the hidden ones). Hiding
91      * and un-hiding a bubble overrides the suppressed state (a bubble cannot
92      * be suppressed but not hidden).
93      */
94     set suppressed(suppress) {
95       if (suppress) {
96         // If the bubble is already hidden, then we don't need to suppress it.
97         if (this.hidden)
98           return;
99
100         this.hidden = true;
101       } else if (this.bubbleSuppressed) {
102         this.hidden = false;
103       }
104       this.bubbleSuppressed = suppress;
105       this.resizeAndReposition();
106     },
107
108     /**
109      * Updates the position of the bubble.
110      * @private
111      */
112     reposition_: function() {
113       var clientRect = this.anchorNode_.getBoundingClientRect();
114
115       // Center bubble in collapsed mode (if it doesn't take up all the room we
116       // have).
117       var offset = 0;
118       if (!this.expanded)
119         offset = (clientRect.width - parseInt(this.style.width, 10)) / 2;
120       this.style.left = this.style.right = clientRect.left + offset + 'px';
121
122       var top = Math.max(0, clientRect.top - 4);
123       this.style.top = this.expanded ?
124           (top - this.offsetHeight + this.unexpandedHeight) + 'px' :
125           top + 'px';
126     },
127
128     /**
129      * Resizes the bubble and then repositions it.
130      * @private
131      */
132     resizeAndReposition: function() {
133       var clientRect = this.anchorNode_.getBoundingClientRect();
134       var width = clientRect.width;
135
136       var bubbleTitle = this.querySelector('.expandable-bubble-title');
137       var closeElement = this.querySelector('.expandable-bubble-close');
138       var closeWidth = this.expanded ? closeElement.clientWidth : 0;
139       var margin = 15;
140
141       // Suppress the width style so we can get it to calculate its width.
142       // We'll set the right width again when we are done.
143       bubbleTitle.style.width = '';
144
145       if (this.expanded) {
146         // We always show the full title but never show less width than 250
147         // pixels.
148         var expandedWidth =
149             Math.max(250, bubbleTitle.scrollWidth + closeWidth + margin);
150         this.style.marginLeft = (width - expandedWidth) + 'px';
151         width = expandedWidth;
152       } else {
153         var newWidth = Math.min(bubbleTitle.scrollWidth + margin, width);
154         // If we've maxed out in width then apply the mask.
155         this.masked = newWidth == width;
156         width = newWidth;
157         this.style.marginLeft = '0';
158       }
159
160       // Width is determined by the width of the title (when not expanded) but
161       // capped to the width of the anchor node.
162       this.style.width = width + 'px';
163       bubbleTitle.style.width = Math.max(0, width - margin - closeWidth) + 'px';
164
165       // Also reposition the bubble -- dimensions have potentially changed.
166       this.reposition_();
167     },
168
169     /**
170      * Expand the bubble (bringing the full content into view).
171      * @private
172      */
173     expandBubble_: function() {
174       this.querySelector('.expandable-bubble-main').hidden = false;
175       this.querySelector('.expandable-bubble-close').hidden = false;
176       this.expanded = true;
177       this.resizeAndReposition();
178     },
179
180     /**
181      * Collapse the bubble, hiding the main content and the close button.
182      * This is automatically called when the window is resized.
183      * @private
184      */
185     collapseBubble_: function() {
186       this.querySelector('.expandable-bubble-main').hidden = true;
187       this.querySelector('.expandable-bubble-close').hidden = true;
188       this.expanded = false;
189       this.resizeAndReposition();
190     },
191
192     /**
193      * The onclick handler for the notification (expands the bubble).
194      * @param {Event} e The event.
195      * @private
196      * @suppress {checkTypes}
197      * TODO(vitalyp): remove suppression when the extern
198      * Node.prototype.contains() will be fixed.
199      */
200     onNotificationClick_: function(e) {
201       if (!this.contains(/** @type {!Node} */(e.target)))
202         return;
203
204       if (!this.expanded) {
205         // Save the height of the unexpanded bubble, so we can make sure to
206         // position it correctly (arrow points in the same location) after
207         // we expand it.
208         this.unexpandedHeight = this.offsetHeight;
209       }
210
211       this.expandBubble_();
212     },
213
214     /**
215      * Shows the bubble. The bubble will start collapsed and expand when
216      * clicked.
217      */
218     show: function() {
219       if (!this.hidden)
220         return;
221
222       document.body.appendChild(this);
223       this.hidden = false;
224       this.resizeAndReposition();
225
226       this.eventTracker_ = new EventTracker;
227       this.eventTracker_.add(window,
228                              'load', this.resizeAndReposition.bind(this));
229       this.eventTracker_.add(window,
230                              'resize', this.resizeAndReposition.bind(this));
231       this.eventTracker_.add(this, 'click', this.onNotificationClick_);
232
233       var doc = this.ownerDocument;
234       this.eventTracker_.add(assert(doc), 'keydown', this, true);
235       this.eventTracker_.add(assert(doc), 'mousedown', this, true);
236     },
237
238     /**
239      * Hides the bubble from view.
240      */
241     hide: function() {
242       this.hidden = true;
243       this.bubbleSuppressed = false;
244       this.eventTracker_.removeAll();
245       this.parentNode.removeChild(this);
246     },
247
248     /**
249      * Handles keydown and mousedown events, dismissing the bubble if
250      * necessary.
251      * @param {Event} e The event.
252      * @private
253      * @suppress {checkTypes}
254      * TODO(vitalyp): remove suppression when the extern
255      * Node.prototype.contains() will be fixed.
256      */
257     handleEvent: function(e) {
258       var handled = false;
259       switch (e.type) {
260         case 'keydown':
261           if (e.keyCode == 27) {  // Esc.
262             if (this.expanded) {
263               this.collapseBubble_();
264               handled = true;
265             }
266           }
267           break;
268
269         case 'mousedown':
270           if (e.target == this.querySelector('.expandable-bubble-close')) {
271             this.handleCloseEvent_();
272             handled = true;
273           } else if (!this.contains(/** @type {!Node} */(e.target))) {
274             if (this.expanded) {
275               this.collapseBubble_();
276               handled = true;
277             }
278           }
279           break;
280       }
281
282       if (handled) {
283         // The bubble emulates a focus grab when expanded, so when we've
284         // collapsed/hide the bubble we consider the event handles and don't
285         // need to propagate it further.
286         e.stopPropagation();
287         e.preventDefault();
288       }
289     },
290   };
291
292   /**
293    * Whether the bubble is expanded or not.
294    */
295   cr.defineProperty(ExpandableBubble, 'expanded', cr.PropertyKind.BOOL_ATTR);
296
297   /**
298    * Whether the title needs to be masked out towards the right, which indicates
299    * to the user that part of the text is clipped. This is only used when the
300    * bubble is collapsed and the title doesn't fit because it is maxed out in
301    * width within the anchored node.
302    */
303   cr.defineProperty(ExpandableBubble, 'masked', cr.PropertyKind.BOOL_ATTR);
304
305   return {
306     ExpandableBubble: ExpandableBubble
307   };
308 });