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.
5 // require: event_tracker.js
7 cr.define('cr.ui', function() {
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.
18 * @extends {HTMLDivElement}
19 * @implements {EventListener}
21 var ExpandableBubble = cr.ui.define('div');
23 ExpandableBubble.prototype = {
24 __proto__: HTMLDivElement.prototype,
27 decorate: function() {
28 this.className = 'expandable-bubble';
30 '<div class="expandable-bubble-contents">' +
31 '<div class="expandable-bubble-title"></div>' +
32 '<div class="expandable-bubble-main" hidden></div>' +
34 '<div class="expandable-bubble-close" hidden></div>';
37 this.bubbleSuppressed = false;
38 this.handleCloseEvent = this.hide;
42 * Sets the title of the bubble. The title is always visible when the
44 * @param {Node} node An HTML element to set as the title.
46 set contentTitle(node) {
47 var bubbleTitle = this.querySelector('.expandable-bubble-title');
48 bubbleTitle.textContent = '';
49 bubbleTitle.appendChild(node);
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.
58 var bubbleMain = this.querySelector('.expandable-bubble-main');
59 bubbleMain.textContent = '';
60 bubbleMain.appendChild(node);
64 * Sets the anchor node, i.e. the node that this bubble points at and
66 * @param {HTMLElement} node The new anchor node.
68 set anchorNode(node) {
69 this.anchorNode_ = node;
72 this.resizeAndReposition();
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.
80 set handleCloseEvent(func) {
81 this.handleCloseEvent_ = func;
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).
94 set suppressed(suppress) {
96 // If the bubble is already hidden, then we don't need to suppress it.
101 } else if (this.bubbleSuppressed) {
104 this.bubbleSuppressed = suppress;
105 this.resizeAndReposition();
109 * Updates the position of the bubble.
112 reposition_: function() {
113 var clientRect = this.anchorNode_.getBoundingClientRect();
115 // Center bubble in collapsed mode (if it doesn't take up all the room we
119 offset = (clientRect.width - parseInt(this.style.width, 10)) / 2;
120 this.style.left = this.style.right = clientRect.left + offset + 'px';
122 var top = Math.max(0, clientRect.top - 4);
123 this.style.top = this.expanded ?
124 (top - this.offsetHeight + this.unexpandedHeight) + 'px' :
129 * Resizes the bubble and then repositions it.
132 resizeAndReposition: function() {
133 var clientRect = this.anchorNode_.getBoundingClientRect();
134 var width = clientRect.width;
136 var bubbleTitle = this.querySelector('.expandable-bubble-title');
137 var closeElement = this.querySelector('.expandable-bubble-close');
138 var closeWidth = this.expanded ? closeElement.clientWidth : 0;
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 = '';
146 // We always show the full title but never show less width than 250
149 Math.max(250, bubbleTitle.scrollWidth + closeWidth + margin);
150 this.style.marginLeft = (width - expandedWidth) + 'px';
151 width = expandedWidth;
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;
157 this.style.marginLeft = '0';
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';
165 // Also reposition the bubble -- dimensions have potentially changed.
170 * Expand the bubble (bringing the full content into view).
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();
181 * Collapse the bubble, hiding the main content and the close button.
182 * This is automatically called when the window is resized.
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();
193 * The onclick handler for the notification (expands the bubble).
194 * @param {Event} e The event.
196 * @suppress {checkTypes}
197 * TODO(vitalyp): remove suppression when the extern
198 * Node.prototype.contains() will be fixed.
200 onNotificationClick_: function(e) {
201 if (!this.contains(/** @type {!Node} */(e.target)))
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
208 this.unexpandedHeight = this.offsetHeight;
211 this.expandBubble_();
215 * Shows the bubble. The bubble will start collapsed and expand when
222 document.body.appendChild(this);
224 this.resizeAndReposition();
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_);
233 var doc = this.ownerDocument;
234 this.eventTracker_.add(assert(doc), 'keydown', this, true);
235 this.eventTracker_.add(assert(doc), 'mousedown', this, true);
239 * Hides the bubble from view.
243 this.bubbleSuppressed = false;
244 this.eventTracker_.removeAll();
245 this.parentNode.removeChild(this);
249 * Handles keydown and mousedown events, dismissing the bubble if
251 * @param {Event} e The event.
253 * @suppress {checkTypes}
254 * TODO(vitalyp): remove suppression when the extern
255 * Node.prototype.contains() will be fixed.
257 handleEvent: function(e) {
261 if (e.keyCode == 27) { // Esc.
263 this.collapseBubble_();
270 if (e.target == this.querySelector('.expandable-bubble-close')) {
271 this.handleCloseEvent_();
273 } else if (!this.contains(/** @type {!Node} */(e.target))) {
275 this.collapseBubble_();
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.
293 * Whether the bubble is expanded or not.
295 cr.defineProperty(ExpandableBubble, 'expanded', cr.PropertyKind.BOOL_ATTR);
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.
303 cr.defineProperty(ExpandableBubble, 'masked', cr.PropertyKind.BOOL_ATTR);
306 ExpandableBubble: ExpandableBubble