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"; 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 contentElement.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.addStyleClass("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 const totalWidth = window.innerWidth; 157 const totalHeight = window.innerHeight; 158 159 var anchorBox = anchorElement instanceof AnchorBox ? anchorElement : anchorElement.boxInWindow(window); 160 var newElementPosition = { x: 0, y: 0, width: preferredWidth + scrollerWidth, height: preferredHeight }; 161 162 var verticalAlignment; 163 var roomAbove = anchorBox.y; 164 var roomBelow = totalHeight - anchorBox.y - anchorBox.height; 165 166 if ((roomAbove > roomBelow) || (arrowDirection === WebInspector.Popover.Orientation.Bottom)) { 167 // Positioning above the anchor. 168 if ((anchorBox.y > newElementPosition.height + arrowHeight + borderRadius) || (arrowDirection === WebInspector.Popover.Orientation.Bottom)) 169 newElementPosition.y = anchorBox.y - newElementPosition.height - arrowHeight; 170 else { 171 newElementPosition.y = borderRadius; 172 newElementPosition.height = anchorBox.y - borderRadius * 2 - arrowHeight; 173 if (this._hasFixedHeight && newElementPosition.height < preferredHeight) { 174 newElementPosition.y = borderRadius; 175 newElementPosition.height = preferredHeight; 176 } 177 } 178 verticalAlignment = WebInspector.Popover.Orientation.Bottom; 179 } else { 180 // Positioning below the anchor. 181 newElementPosition.y = anchorBox.y + anchorBox.height + arrowHeight; 182 if ((newElementPosition.y + newElementPosition.height + arrowHeight - borderWidth >= totalHeight) && (arrowDirection !== WebInspector.Popover.Orientation.Top)) { 183 newElementPosition.height = totalHeight - anchorBox.y - anchorBox.height - borderRadius * 2 - arrowHeight; 184 if (this._hasFixedHeight && newElementPosition.height < preferredHeight) { 185 newElementPosition.y = totalHeight - preferredHeight - borderRadius; 186 newElementPosition.height = preferredHeight; 187 } 188 } 189 // Align arrow. 190 verticalAlignment = WebInspector.Popover.Orientation.Top; 191 } 192 193 var horizontalAlignment; 194 if (anchorBox.x + newElementPosition.width < totalWidth) { 195 newElementPosition.x = Math.max(borderRadius, anchorBox.x - borderRadius - arrowOffset); 196 horizontalAlignment = "left"; 197 } else if (newElementPosition.width + borderRadius * 2 < totalWidth) { 198 newElementPosition.x = totalWidth - newElementPosition.width - borderRadius; 199 horizontalAlignment = "right"; 200 // Position arrow accurately. 201 var arrowRightPosition = Math.max(0, totalWidth - anchorBox.x - anchorBox.width - borderRadius - arrowOffset); 202 arrowRightPosition += anchorBox.width / 2; 203 arrowRightPosition = Math.min(arrowRightPosition, newElementPosition.width - borderRadius - arrowOffset); 204 this._popupArrowElement.style.right = arrowRightPosition + "px"; 205 } else { 206 newElementPosition.x = borderRadius; 207 newElementPosition.width = totalWidth - borderRadius * 2; 208 newElementPosition.height += scrollerWidth; 209 horizontalAlignment = "left"; 210 if (verticalAlignment === WebInspector.Popover.Orientation.Bottom) 211 newElementPosition.y -= scrollerWidth; 212 // Position arrow accurately. 213 this._popupArrowElement.style.left = Math.max(0, anchorBox.x - borderRadius * 2 - arrowOffset) + "px"; 214 this._popupArrowElement.style.left += anchorBox.width / 2; 215 } 216 217 this.element.className = "popover custom-popup-vertical-scroll custom-popup-horizontal-scroll " + verticalAlignment + "-" + horizontalAlignment + "-arrow"; 218 this.element.positionAt(newElementPosition.x - borderWidth, newElementPosition.y - borderWidth); 219 this.element.style.width = newElementPosition.width + borderWidth * 2 + "px"; 220 this.element.style.height = newElementPosition.height + borderWidth * 2 + "px"; 221 }, 222 223 __proto__: WebInspector.View.prototype 224 } 225 226 /** 227 * @constructor 228 * @param {Element} panelElement 229 * @param {function(Element, Event):(Element|AnchorBox)|undefined} getAnchor 230 * @param {function(Element, WebInspector.Popover):undefined} showPopover 231 * @param {function()=} onHide 232 * @param {boolean=} disableOnClick 233 */ 234 WebInspector.PopoverHelper = function(panelElement, getAnchor, showPopover, onHide, disableOnClick) 235 { 236 this._panelElement = panelElement; 237 this._getAnchor = getAnchor; 238 this._showPopover = showPopover; 239 this._onHide = onHide; 240 this._disableOnClick = !!disableOnClick; 241 panelElement.addEventListener("mousedown", this._mouseDown.bind(this), false); 242 panelElement.addEventListener("mousemove", this._mouseMove.bind(this), false); 243 panelElement.addEventListener("mouseout", this._mouseOut.bind(this), false); 244 this.setTimeout(1000); 245 } 246 247 WebInspector.PopoverHelper.prototype = { 248 setTimeout: function(timeout) 249 { 250 this._timeout = timeout; 251 }, 252 253 /** 254 * @param {MouseEvent} event 255 * @return {boolean} 256 */ 257 _eventInHoverElement: function(event) 258 { 259 if (!this._hoverElement) 260 return false; 261 var box = this._hoverElement instanceof AnchorBox ? this._hoverElement : this._hoverElement.boxInWindow(); 262 return (box.x <= event.clientX && event.clientX <= box.x + box.width && 263 box.y <= event.clientY && event.clientY <= box.y + box.height); 264 }, 265 266 _mouseDown: function(event) 267 { 268 if (this._disableOnClick || !this._eventInHoverElement(event)) 269 this.hidePopover(); 270 else { 271 this._killHidePopoverTimer(); 272 this._handleMouseAction(event, true); 273 } 274 }, 275 276 _mouseMove: function(event) 277 { 278 // Pretend that nothing has happened. 279 if (this._eventInHoverElement(event)) 280 return; 281 282 this._startHidePopoverTimer(); 283 this._handleMouseAction(event, false); 284 }, 285 286 _popoverMouseOut: function(event) 287 { 288 if (!this.isPopoverVisible()) 289 return; 290 if (event.relatedTarget && !event.relatedTarget.isSelfOrDescendant(this._popover._contentDiv)) 291 this._startHidePopoverTimer(); 292 }, 293 294 _mouseOut: function(event) 295 { 296 if (!this.isPopoverVisible()) 297 return; 298 if (!this._eventInHoverElement(event)) 299 this._startHidePopoverTimer(); 300 }, 301 302 _startHidePopoverTimer: function() 303 { 304 // User has 500ms (this._timeout / 2) to reach the popup. 305 if (!this._popover || this._hidePopoverTimer) 306 return; 307 308 function doHide() 309 { 310 this._hidePopover(); 311 delete this._hidePopoverTimer; 312 } 313 this._hidePopoverTimer = setTimeout(doHide.bind(this), this._timeout / 2); 314 }, 315 316 _handleMouseAction: function(event, isMouseDown) 317 { 318 this._resetHoverTimer(); 319 if (event.which && this._disableOnClick) 320 return; 321 this._hoverElement = this._getAnchor(event.target, event); 322 if (!this._hoverElement) 323 return; 324 const toolTipDelay = isMouseDown ? 0 : (this._popup ? this._timeout * 0.6 : this._timeout); 325 this._hoverTimer = setTimeout(this._mouseHover.bind(this, this._hoverElement), toolTipDelay); 326 }, 327 328 _resetHoverTimer: function() 329 { 330 if (this._hoverTimer) { 331 clearTimeout(this._hoverTimer); 332 delete this._hoverTimer; 333 } 334 }, 335 336 isPopoverVisible: function() 337 { 338 return !!this._popover; 339 }, 340 341 hidePopover: function() 342 { 343 this._resetHoverTimer(); 344 this._hidePopover(); 345 }, 346 347 _hidePopover: function() 348 { 349 if (!this._popover) 350 return; 351 352 if (this._onHide) 353 this._onHide(); 354 355 this._popover.dispose(); 356 delete this._popover; 357 this._hoverElement = null; 358 }, 359 360 _mouseHover: function(element) 361 { 362 delete this._hoverTimer; 363 364 this._hidePopover(); 365 this._popover = new WebInspector.Popover(this); 366 this._showPopover(element, this._popover); 367 }, 368 369 _killHidePopoverTimer: function() 370 { 371 if (this._hidePopoverTimer) { 372 clearTimeout(this._hidePopoverTimer); 373 delete this._hidePopoverTimer; 374 375 // We know that we reached the popup, but we might have moved over other elements. 376 // Discard pending command. 377 this._resetHoverTimer(); 378 } 379 } 380 } 381 382 /** @enum {string} */ 383 WebInspector.Popover.Orientation = { 384 Top: "top", 385 Bottom: "bottom" 386 } 387 388 /** 389 * @constructor 390 * @param {string} title 391 */ 392 WebInspector.PopoverContentHelper = function(title) 393 { 394 this._contentTable = document.createElement("table"); 395 var titleCell = this._createCell(WebInspector.UIString("%s - Details", title), "popover-details-title"); 396 titleCell.colSpan = 2; 397 var titleRow = document.createElement("tr"); 398 titleRow.appendChild(titleCell); 399 this._contentTable.appendChild(titleRow); 400 } 401 402 WebInspector.PopoverContentHelper.prototype = { 403 contentTable: function() 404 { 405 return this._contentTable; 406 }, 407 408 /** 409 * @param {string=} styleName 410 */ 411 _createCell: function(content, styleName) 412 { 413 var text = document.createElement("label"); 414 text.appendChild(document.createTextNode(content)); 415 var cell = document.createElement("td"); 416 cell.className = "popover-details"; 417 if (styleName) 418 cell.className += " " + styleName; 419 cell.textContent = content; 420 return cell; 421 }, 422 423 appendTextRow: function(title, content) 424 { 425 var row = document.createElement("tr"); 426 row.appendChild(this._createCell(title, "popover-details-row-title")); 427 row.appendChild(this._createCell(content, "popover-details-row-data")); 428 this._contentTable.appendChild(row); 429 }, 430 431 /** 432 * @param {string=} titleStyle 433 */ 434 appendElementRow: function(title, content, titleStyle) 435 { 436 var row = document.createElement("tr"); 437 var titleCell = this._createCell(title, "popover-details-row-title"); 438 if (titleStyle) 439 titleCell.addStyleClass(titleStyle); 440 row.appendChild(titleCell); 441 var cell = document.createElement("td"); 442 cell.className = "details"; 443 cell.appendChild(content); 444 row.appendChild(cell); 445 this._contentTable.appendChild(row); 446 }, 447 448 appendStackTrace: function(title, stackTrace, callFrameLinkifier) 449 { 450 this.appendTextRow("", ""); 451 var framesTable = document.createElement("table"); 452 for (var i = 0; i < stackTrace.length; ++i) { 453 var stackFrame = stackTrace[i]; 454 var row = document.createElement("tr"); 455 row.className = "details"; 456 row.appendChild(this._createCell(stackFrame.functionName ? stackFrame.functionName : WebInspector.UIString("(anonymous function)"), "function-name")); 457 row.appendChild(this._createCell(" @ ")); 458 var linkCell = document.createElement("td"); 459 var urlElement = callFrameLinkifier(stackFrame); 460 linkCell.appendChild(urlElement); 461 row.appendChild(linkCell); 462 framesTable.appendChild(row); 463 } 464 this.appendElementRow(title, framesTable, "popover-stacktrace-title"); 465 } 466 } 467