OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2011 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 {cr.ui.div} |
| 19 */ |
| 20 var ExpandableBubble = cr.ui.define('div'); |
| 21 |
| 22 ExpandableBubble.prototype = { |
| 23 __proto__: HTMLDivElement.prototype, |
| 24 |
| 25 /** @inheritDoc */ |
| 26 decorate: function() { |
| 27 this.className = 'expandable-bubble'; |
| 28 this.innerHTML = |
| 29 '<div class="expandable-bubble-contents">' + |
| 30 '<div class="expandable-bubble-title"></div>' + |
| 31 '<div class="expandable-bubble-main" hidden></div>' + |
| 32 '</div>' + |
| 33 '<div class="expandable-bubble-close" hidden></div>' + |
| 34 '<div class="expandable-bubble-shadow"></div>' + |
| 35 '<div class="expandable-bubble-arrow"></div>'; |
| 36 |
| 37 this.hidden = true; |
| 38 }, |
| 39 |
| 40 /** |
| 41 * Sets the title of the bubble. The title is always visible when the |
| 42 * bubble is visible. |
| 43 * @type {Node} An HTML element to set as the title. |
| 44 */ |
| 45 set contentTitle(node) { |
| 46 var bubbleTitle = this.querySelector('.expandable-bubble-title'); |
| 47 bubbleTitle.textContent = ''; |
| 48 bubbleTitle.appendChild(node); |
| 49 }, |
| 50 |
| 51 /** |
| 52 * Sets the content node of the bubble. The content node is only visible |
| 53 * when the bubble is expanded. |
| 54 * @param {Node} An HTML element. |
| 55 */ |
| 56 set content(node) { |
| 57 var bubbleMain = this.querySelector('.expandable-bubble-main'); |
| 58 bubbleMain.textContent = ''; |
| 59 bubbleMain.appendChild(node); |
| 60 }, |
| 61 |
| 62 /** |
| 63 * Sets the anchor node, i.e. the node that this bubble points at and |
| 64 * partially overlaps. |
| 65 * @param {HTMLElement} node The new anchor node. |
| 66 */ |
| 67 set anchorNode(node) { |
| 68 this.anchorNode_ = node; |
| 69 |
| 70 if (!this.hidden) |
| 71 this.resizeAndReposition_(); |
| 72 }, |
| 73 |
| 74 /** |
| 75 * Updates the position of the bubble. |
| 76 * @private |
| 77 */ |
| 78 reposition_: function() { |
| 79 var clientRect = this.anchorNode_.getBoundingClientRect(); |
| 80 this.style.left = this.style.right = clientRect.left + 'px'; |
| 81 |
| 82 var top = clientRect.top - 1; |
| 83 this.style.top = this.expanded ? |
| 84 (top - this.offsetHeight + this.unexpandedHeight) + 'px' : |
| 85 top + 'px'; |
| 86 }, |
| 87 |
| 88 /** |
| 89 * Resizes the bubble and then repositions it. |
| 90 * @private |
| 91 */ |
| 92 resizeAndReposition_: function() { |
| 93 var clientRect = this.anchorNode_.getBoundingClientRect(); |
| 94 var width = clientRect.width; |
| 95 if (this.expanded) { |
| 96 var expandedWidth = 250; |
| 97 this.style.marginLeft = (width - expandedWidth) + 'px'; |
| 98 width = expandedWidth; |
| 99 } else { |
| 100 this.style.marginLeft = '0'; |
| 101 } |
| 102 |
| 103 // Width is dynamic (when not expanded) based on the width of the anchor |
| 104 // node, and the title and shadow need to follow suit. |
| 105 this.style.width = width + 'px'; |
| 106 if (width > 0) { |
| 107 var bubbleTitle = this.querySelector('.expandable-bubble-title'); |
| 108 bubbleTitle.style.width = width ? width - 2 + 'px' : 0 + 'px'; |
| 109 var bubbleContent = this.querySelector('.expandable-bubble-main'); |
| 110 bubbleContent.style.width = width ? width - 12 + 'px' : 0 + 'px'; |
| 111 var bubbleShadow = this.querySelector('.expandable-bubble-shadow'); |
| 112 bubbleShadow.style.width = width ? width + 2 + 'px' : 0 + 'px'; |
| 113 } |
| 114 |
| 115 // Also reposition the bubble -- dimensions have potentially changed. |
| 116 this.reposition_(); |
| 117 }, |
| 118 |
| 119 /* |
| 120 * Expand the bubble (bringing the full content into view). |
| 121 * @private |
| 122 */ |
| 123 expandBubble_: function() { |
| 124 this.querySelector('.expandable-bubble-main').hidden = false; |
| 125 this.querySelector('.expandable-bubble-close').hidden = false; |
| 126 this.expanded = true; |
| 127 this.resizeAndReposition_(); |
| 128 }, |
| 129 |
| 130 /** |
| 131 * Collapse the bubble, hiding the main content and the close button. |
| 132 * This is automatically called when the window is resized. |
| 133 * @private |
| 134 */ |
| 135 collapseBubble_: function() { |
| 136 this.querySelector('.expandable-bubble-main').hidden = true; |
| 137 this.querySelector('.expandable-bubble-close').hidden = true; |
| 138 this.expanded = false; |
| 139 this.resizeAndReposition_(); |
| 140 }, |
| 141 |
| 142 /** |
| 143 * The onclick handler for the notification (expands the bubble). |
| 144 * @param {Event} e The event. |
| 145 * @private |
| 146 */ |
| 147 onNotificationClick_ : function(e) { |
| 148 if (!this.contains(e.target)) |
| 149 return; |
| 150 |
| 151 if (!this.expanded) { |
| 152 // Save the height of the unexpanded bubble, so we can make sure to |
| 153 // position it correctly (arrow points in the same location) after |
| 154 // we expand it. |
| 155 this.unexpandedHeight = this.offsetHeight; |
| 156 } |
| 157 |
| 158 this.expandBubble_(); |
| 159 }, |
| 160 |
| 161 /** |
| 162 * Shows the bubble. The bubble will start collapsed and expand when |
| 163 * clicked. |
| 164 */ |
| 165 show: function() { |
| 166 if (!this.hidden) |
| 167 return; |
| 168 |
| 169 document.body.appendChild(this); |
| 170 this.hidden = false; |
| 171 this.resizeAndReposition_(); |
| 172 |
| 173 this.eventTracker_ = new EventTracker; |
| 174 this.eventTracker_.add(window, |
| 175 'load', this.resizeAndReposition_.bind(this)); |
| 176 this.eventTracker_.add(window, |
| 177 'resize', this.resizeAndReposition_.bind(this)); |
| 178 this.eventTracker_.add(this, 'click', this.onNotificationClick_); |
| 179 |
| 180 var doc = this.ownerDocument; |
| 181 this.eventTracker_.add(doc, 'keydown', this, true); |
| 182 this.eventTracker_.add(doc, 'mousedown', this, true); |
| 183 }, |
| 184 |
| 185 /** |
| 186 * Hides the bubble from view. |
| 187 */ |
| 188 hide: function() { |
| 189 this.hidden = true; |
| 190 this.eventTracker_.removeAll(); |
| 191 this.parentNode.removeChild(this); |
| 192 }, |
| 193 |
| 194 /** |
| 195 * Handles keydown and mousedown events, dismissing the bubble if |
| 196 * necessary. |
| 197 * @param {Event} e The event. |
| 198 * @private |
| 199 */ |
| 200 handleEvent: function(e) { |
| 201 var handled = false; |
| 202 switch (e.type) { |
| 203 case 'keydown': |
| 204 if (e.keyCode == 27) { // Esc. |
| 205 if (this.expanded) { |
| 206 this.collapseBubble_(); |
| 207 handled = true; |
| 208 } |
| 209 } |
| 210 break; |
| 211 |
| 212 case 'mousedown': |
| 213 if (e.target == this.querySelector('.expandable-bubble-close')) { |
| 214 this.hide(); |
| 215 handled = true; |
| 216 } else if (!this.contains(e.target)) { |
| 217 if (this.expanded) { |
| 218 this.collapseBubble_(); |
| 219 handled = true; |
| 220 } |
| 221 } |
| 222 break; |
| 223 } |
| 224 |
| 225 if (handled) { |
| 226 // The bubble emulates a focus grab when expanded, so when we've |
| 227 // collapsed/hide the bubble we consider the event handles and don't |
| 228 // need to propagate it further. |
| 229 e.stopPropagation(); |
| 230 e.preventDefault(); |
| 231 } |
| 232 }, |
| 233 }; |
| 234 |
| 235 /** |
| 236 * Whether the bubble is expanded or not. |
| 237 * @type {boolean} |
| 238 */ |
| 239 cr.defineProperty(ExpandableBubble, 'expanded', cr.PropertyKind.BOOL_ATTR); |
| 240 |
| 241 return { |
| 242 ExpandableBubble: ExpandableBubble |
| 243 }; |
| 244 }); |
OLD | NEW |