1 /* 2 * Copyright (C) 2009 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31 /** 32 * @constructor 33 * @extends {WebInspector.View} 34 * @param {!WebInspector.PopoverHelper=} popoverHelper 35 */ 36 WebInspector.Popover = function(popoverHelper) 37 { 38 WebInspector.View.call(this); 39 this.markAsRoot(); 40 this.element.className = "popover custom-popup-vertical-scroll custom-popup-horizontal-scroll"; // Override 41 42 this._popupArrowElement = document.createElement("div"); 43 this._popupArrowElement.className = "arrow"; 44 this.element.appendChild(this._popupArrowElement); 45 46 this._contentDiv = document.createElement("div"); 47 this._contentDiv.className = "content"; 48 this.element.appendChild(this._contentDiv); 49 50 this._popoverHelper = popoverHelper; 51 } 52 53 WebInspector.Popover.prototype = { 54 /** 55 * @param {!Element} element 56 * @param {!Element|!AnchorBox} anchor 57 * @param {?number=} preferredWidth 58 * @param {?number=} preferredHeight 59 * @param {?WebInspector.Popover.Orientation=} arrowDirection 60 */ 61 show: function(element, anchor, preferredWidth, preferredHeight, arrowDirection) 62 { 63 this._innerShow(null, element, anchor, preferredWidth, preferredHeight, arrowDirection); 64 }, 65 66 /** 67 * @param {!WebInspector.View} view 68 * @param {!Element|!AnchorBox} anchor 69 * @param {?number=} preferredWidth 70 * @param {?number=} preferredHeight 71 */ 72 showView: function(view, anchor, preferredWidth, preferredHeight) 73 { 74 this._innerShow(view, view.element, anchor, preferredWidth, preferredHeight); 75 }, 76 77 /** 78 * @param {?WebInspector.View} view 79 * @param {!Element} contentElement 80 * @param {!Element|!AnchorBox} anchor 81 * @param {?number=} preferredWidth 82 * @param {?number=} preferredHeight 83 * @param {?WebInspector.Popover.Orientation=} arrowDirection 84 */ 85 _innerShow: function(view, contentElement, anchor, preferredWidth, preferredHeight, arrowDirection) 86 { 87 if (this._disposed) 88 return; 89 this.contentElement = contentElement; 90 91 // This should not happen, but we hide previous popup to be on the safe side. 92 if (WebInspector.Popover._popover) 93 WebInspector.Popover._popover.detach(); 94 WebInspector.Popover._popover = this; 95 96 // Temporarily attach in order to measure preferred dimensions. 97 var preferredSize = view ? view.measurePreferredSize() : this.contentElement.measurePreferredSize(); 98 preferredWidth = preferredWidth || preferredSize.width; 99 preferredHeight = preferredHeight || preferredSize.height; 100 101 WebInspector.View.prototype.show.call(this, document.body); 102 103 if (view) 104 view.show(this._contentDiv); 105 else 106 this._contentDiv.appendChild(this.contentElement); 107 108 this._positionElement(anchor, preferredWidth, preferredHeight, arrowDirection); 109 110 if (this._popoverHelper) { 111 this._contentDiv.addEventListener("mousemove", this._popoverHelper._killHidePopoverTimer.bind(this._popoverHelper), true); 112 this.element.addEventListener("mouseout", this._popoverHelper._popoverMouseOut.bind(this._popoverHelper), true); 113 } 114 }, 115 116 hide: function() 117 { 118 this.detach(); 119 delete WebInspector.Popover._popover; 120 }, 121 122 get disposed() 123 { 124 return this._disposed; 125 }, 126 127 dispose: function() 128 { 129 if (this.isShowing()) 130 this.hide(); 131 this._disposed = true; 132 }, 133 134 setCanShrink: function(canShrink) 135 { 136 this._hasFixedHeight = !canShrink; 137 this._contentDiv.classList.add("fixed-height"); 138 }, 139 140 /** 141 * @param {!Element|!AnchorBox} anchorElement 142 * @param {number} preferredWidth 143 * @param {number} preferredHeight 144 * @param {?WebInspector.Popover.Orientation=} arrowDirection 145 */ 146 _positionElement: function(anchorElement, preferredWidth, preferredHeight, arrowDirection) 147 { 148 const borderWidth = 25; 149 const scrollerWidth = this._hasFixedHeight ? 0 : 11; 150 const arrowHeight = 15; 151 const arrowOffset = 10; 152 const borderRadius = 10; 153 154 // Skinny tooltips are not pretty, their arrow location is not nice. 155 preferredWidth = Math.max(preferredWidth, 50); 156 // Position relative to main DevTools element. 157 const container = WebInspector.Dialog.modalHostView().element; 158 const totalWidth = container.offsetWidth; 159 const totalHeight = container.offsetHeight; 160 161 var anchorBox = anchorElement instanceof AnchorBox ? anchorElement : anchorElement.boxInWindow(window); 162 anchorBox = anchorBox.relativeToElement(container); 163 var newElementPosition = { x: 0, y: 0, width: preferredWidth + scrollerWidth, height: preferredHeight }; 164 165 var verticalAlignment; 166 var roomAbove = anchorBox.y; 167 var roomBelow = totalHeight - anchorBox.y - anchorBox.height; 168 169 if ((roomAbove > roomBelow) || (arrowDirection === WebInspector.Popover.Orientation.Bottom)) { 170 // Positioning above the anchor. 171 if ((anchorBox.y > newElementPosition.height + arrowHeight + borderRadius) || (arrowDirection === WebInspector.Popover.Orientation.Bottom)) 172 newElementPosition.y = anchorBox.y - newElementPosition.height - arrowHeight; 173 else { 174 newElementPosition.y = borderRadius; 175 newElementPosition.height = anchorBox.y - borderRadius * 2 - arrowHeight; 176 if (this._hasFixedHeight && newElementPosition.height < preferredHeight) { 177 newElementPosition.y = borderRadius; 178 newElementPosition.height = preferredHeight; 179 } 180 } 181 verticalAlignment = WebInspector.Popover.Orientation.Bottom; 182 } else { 183 // Positioning below the anchor. 184 newElementPosition.y = anchorBox.y + anchorBox.height + arrowHeight; 185 if ((newElementPosition.y + newElementPosition.height + borderRadius >= totalHeight) && (arrowDirection !== WebInspector.Popover.Orientation.Top)) { 186 newElementPosition.height = totalHeight - borderRadius - newElementPosition.y; 187 if (this._hasFixedHeight && newElementPosition.height < preferredHeight) { 188 newElementPosition.y = totalHeight - preferredHeight - borderRadius; 189 newElementPosition.height = preferredHeight; 190 } 191 } 192 // Align arrow. 193 verticalAlignment = WebInspector.Popover.Orientation.Top; 194 } 195 196 var horizontalAlignment; 197 if (anchorBox.x + newElementPosition.width < totalWidth) { 198 newElementPosition.x = Math.max(borderRadius, anchorBox.x - borderRadius - arrowOffset); 199 horizontalAlignment = "left"; 200 } else if (newElementPosition.width + borderRadius * 2 < totalWidth) { 201 newElementPosition.x = totalWidth - newElementPosition.width - borderRadius; 202 horizontalAlignment = "right"; 203 // Position arrow accurately. 204 var arrowRightPosition = Math.max(0, totalWidth - anchorBox.x - anchorBox.width - borderRadius - arrowOffset); 205 arrowRightPosition += anchorBox.width / 2; 206 arrowRightPosition = Math.min(arrowRightPosition, newElementPosition.width - borderRadius - arrowOffset); 207 this._popupArrowElement.style.right = arrowRightPosition + "px"; 208 } else { 209 newElementPosition.x = borderRadius; 210 newElementPosition.width = totalWidth - borderRadius * 2; 211 newElementPosition.height += scrollerWidth; 212 horizontalAlignment = "left"; 213 if (verticalAlignment === WebInspector.Popover.Orientation.Bottom) 214 newElementPosition.y -= scrollerWidth; 215 // Position arrow accurately. 216 this._popupArrowElement.style.left = Math.max(0, anchorBox.x - borderRadius * 2 - arrowOffset) + "px"; 217 this._popupArrowElement.style.left += anchorBox.width / 2; 218 } 219 220 this.element.className = "popover custom-popup-vertical-scroll custom-popup-horizontal-scroll " + verticalAlignment + "-" + horizontalAlignment + "-arrow"; 221 this.element.positionAt(newElementPosition.x - borderWidth, newElementPosition.y - borderWidth, container); 222 this.element.style.width = newElementPosition.width + borderWidth * 2 + "px"; 223 this.element.style.height = newElementPosition.height + borderWidth * 2 + "px"; 224 }, 225 226 __proto__: WebInspector.View.prototype 227 } 228 229 /** 230 * @constructor 231 * @param {!Element} panelElement 232 * @param {function(!Element, !Event):(!Element|!AnchorBox)|undefined} getAnchor 233 * @param {function(!Element, !WebInspector.Popover):undefined} showPopover 234 * @param {function()=} onHide 235 * @param {boolean=} disableOnClick 236 */ 237 WebInspector.PopoverHelper = function(panelElement, getAnchor, showPopover, onHide, disableOnClick) 238 { 239 this._panelElement = panelElement; 240 this._getAnchor = getAnchor; 241 this._showPopover = showPopover; 242 this._onHide = onHide; 243 this._disableOnClick = !!disableOnClick; 244 panelElement.addEventListener("mousedown", this._mouseDown.bind(this), false); 245 panelElement.addEventListener("mousemove", this._mouseMove.bind(this), false); 246 panelElement.addEventListener("mouseout", this._mouseOut.bind(this), false); 247 this.setTimeout(1000); 248 } 249 250 WebInspector.PopoverHelper.prototype = { 251 setTimeout: function(timeout) 252 { 253 this._timeout = timeout; 254 }, 255 256 /** 257 * @param {!MouseEvent} event 258 * @return {boolean} 259 */ 260 _eventInHoverElement: function(event) 261 { 262 if (!this._hoverElement) 263 return false; 264 var box = this._hoverElement instanceof AnchorBox ? this._hoverElement : this._hoverElement.boxInWindow(); 265 return (box.x <= event.clientX && event.clientX <= box.x + box.width && 266 box.y <= event.clientY && event.clientY <= box.y + box.height); 267 }, 268 269 _mouseDown: function(event) 270 { 271 if (this._disableOnClick || !this._eventInHoverElement(event)) 272 this.hidePopover(); 273 else { 274 this._killHidePopoverTimer(); 275 this._handleMouseAction(event, true); 276 } 277 }, 278 279 _mouseMove: function(event) 280 { 281 // Pretend that nothing has happened. 282 if (this._eventInHoverElement(event)) 283 return; 284 285 this._startHidePopoverTimer(); 286 this._handleMouseAction(event, false); 287 }, 288 289 _popoverMouseOut: function(event) 290 { 291 if (!this.isPopoverVisible()) 292 return; 293 if (event.relatedTarget && !event.relatedTarget.isSelfOrDescendant(this._popover._contentDiv)) 294 this._startHidePopoverTimer(); 295 }, 296 297 _mouseOut: function(event) 298 { 299 if (!this.isPopoverVisible()) 300 return; 301 if (!this._eventInHoverElement(event)) 302 this._startHidePopoverTimer(); 303 }, 304 305 _startHidePopoverTimer: function() 306 { 307 // User has 500ms (this._timeout / 2) to reach the popup. 308 if (!this._popover || this._hidePopoverTimer) 309 return; 310 311 /** 312 * @this {WebInspector.PopoverHelper} 313 */ 314 function doHide() 315 { 316 this._hidePopover(); 317 delete this._hidePopoverTimer; 318 } 319 this._hidePopoverTimer = setTimeout(doHide.bind(this), this._timeout / 2); 320 }, 321 322 _handleMouseAction: function(event, isMouseDown) 323 { 324 this._resetHoverTimer(); 325 if (event.which && this._disableOnClick) 326 return; 327 this._hoverElement = this._getAnchor(event.target, event); 328 if (!this._hoverElement) 329 return; 330 const toolTipDelay = isMouseDown ? 0 : (this._popup ? this._timeout * 0.6 : this._timeout); 331 this._hoverTimer = setTimeout(this._mouseHover.bind(this, this._hoverElement), toolTipDelay); 332 }, 333 334 _resetHoverTimer: function() 335 { 336 if (this._hoverTimer) { 337 clearTimeout(this._hoverTimer); 338 delete this._hoverTimer; 339 } 340 }, 341 342 /** 343 * @return {boolean} 344 */ 345 isPopoverVisible: function() 346 { 347 return !!this._popover; 348 }, 349 350 hidePopover: function() 351 { 352 this._resetHoverTimer(); 353 this._hidePopover(); 354 }, 355 356 _hidePopover: function() 357 { 358 if (!this._popover) 359 return; 360 361 if (this._onHide) 362 this._onHide(); 363 364 this._popover.dispose(); 365 delete this._popover; 366 this._hoverElement = null; 367 }, 368 369 _mouseHover: function(element) 370 { 371 delete this._hoverTimer; 372 373 this._hidePopover(); 374 this._popover = new WebInspector.Popover(this); 375 this._showPopover(element, this._popover); 376 }, 377 378 _killHidePopoverTimer: function() 379 { 380 if (this._hidePopoverTimer) { 381 clearTimeout(this._hidePopoverTimer); 382 delete this._hidePopoverTimer; 383 384 // We know that we reached the popup, but we might have moved over other elements. 385 // Discard pending command. 386 this._resetHoverTimer(); 387 } 388 } 389 } 390 391 /** @enum {string} */ 392 WebInspector.Popover.Orientation = { 393 Top: "top", 394 Bottom: "bottom" 395 } 396