1/* 2 * Copyright (C) 2013 Apple 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 6 * are met: 7 * 1. Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * 2. Redistributions in binary form must reproduce the above copyright 10 * notice, this list of conditions and the following disclaimer in the 11 * documentation and/or other materials provided with the distribution. 12 * 13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' 14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS 17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 23 * THE POSSIBILITY OF SUCH DAMAGE. 24 */ 25 26WebInspector.Popover = function(delegate) { 27 WebInspector.Object.call(this); 28 29 this.delegate = delegate; 30 this._edge = null; 31 this._frame = new WebInspector.Rect; 32 this._content = null; 33 this._targetFrame = new WebInspector.Rect; 34 this._anchorPoint = new WebInspector.Point; 35 this._preferredEdges = null; 36 37 this._contentNeedsUpdate = false; 38 39 this._element = document.createElement("div"); 40 this._element.className = WebInspector.Popover.StyleClassName; 41 this._canvasId = "popover-" + (WebInspector.Popover.canvasId++); 42 this._element.style.backgroundImage = "-webkit-canvas(" + this._canvasId + ")"; 43 this._element.addEventListener("transitionend", this, true); 44 45 this._container = this._element.appendChild(document.createElement("div")); 46 this._container.className = "container"; 47}; 48 49WebInspector.Popover.StyleClassName = "popover"; 50WebInspector.Popover.FadeOutClassName = "fade-out"; 51WebInspector.Popover.canvasId = 0; 52WebInspector.Popover.CornerRadius = 5; 53WebInspector.Popover.MinWidth = 40; 54WebInspector.Popover.MinHeight = 40; 55WebInspector.Popover.ShadowPadding = 5; 56WebInspector.Popover.ContentPadding = 5; 57WebInspector.Popover.AnchorSize = new WebInspector.Size(22, 11); 58WebInspector.Popover.ShadowEdgeInsets = new WebInspector.EdgeInsets(WebInspector.Popover.ShadowPadding); 59 60WebInspector.Popover.prototype = { 61 constructor: WebInspector.Popover, 62 63 // Public 64 65 get element() 66 { 67 return this._element; 68 }, 69 70 get frame() 71 { 72 return this._frame; 73 }, 74 75 get visible() 76 { 77 return this._element.parentNode === document.body && !this._element.classList.contains(WebInspector.Popover.FadeOutClassName); 78 }, 79 80 set frame(frame) 81 { 82 this._element.style.left = frame.minX() + "px"; 83 this._element.style.top = frame.minY() + "px"; 84 this._element.style.width = frame.size.width + "px"; 85 this._element.style.height = frame.size.height + "px"; 86 this._element.style.backgroundSize = frame.size.width + "px " + frame.size.height + "px"; 87 this._frame = frame; 88 }, 89 90 set content(content) 91 { 92 if (content === this._content) 93 return; 94 95 this._content = content; 96 97 this._contentNeedsUpdate = true; 98 99 if (this.visible) 100 this._update(true); 101 }, 102 103 update: function() 104 { 105 if (!this.visible) 106 return; 107 108 var previouslyFocusedElement = document.activeElement; 109 110 this._contentNeedsUpdate = true; 111 this._update(true); 112 113 if (previouslyFocusedElement) 114 previouslyFocusedElement.focus(); 115 }, 116 117 present: function(targetFrame, preferredEdges) 118 { 119 this._targetFrame = targetFrame; 120 this._preferredEdges = preferredEdges; 121 122 if (!this._content) 123 return; 124 125 window.addEventListener("mousedown", this, true); 126 window.addEventListener("scroll", this, true); 127 128 this._update(); 129 }, 130 131 dismiss: function() 132 { 133 if (this._element.parentNode !== document.body) 134 return; 135 136 window.removeEventListener("mousedown", this, true); 137 window.removeEventListener("scroll", this, true); 138 139 this._element.classList.add(WebInspector.Popover.FadeOutClassName); 140 141 if (this.delegate && typeof this.delegate.willDismissPopover === "function") 142 this.delegate.willDismissPopover(this); 143 }, 144 145 handleEvent: function(event) 146 { 147 switch (event.type) { 148 case "mousedown": 149 case "scroll": 150 if (!this._element.contains(event.target)) 151 this.dismiss(); 152 break; 153 case "transitionend": 154 if (event.target === this._element) { 155 document.body.removeChild(this._element); 156 this._element.classList.remove(WebInspector.Popover.FadeOutClassName); 157 this._container.textContent = ""; 158 if (this.delegate && typeof this.delegate.didDismissPopover === "function") 159 this.delegate.didDismissPopover(this); 160 break; 161 } 162 } 163 }, 164 165 // Private 166 167 _update: function(shouldAnimate) 168 { 169 if (shouldAnimate) 170 var previousEdge = this._edge; 171 172 var targetFrame = this._targetFrame; 173 var preferredEdges = this._preferredEdges; 174 175 // Ensure our element is on display so that its metrics can be resolved 176 // or interrupt any pending transition to remove it from display. 177 if (this._element.parentNode !== document.body) 178 document.body.appendChild(this._element); 179 else 180 this._element.classList.remove(WebInspector.Popover.FadeOutClassName); 181 182 if (this._contentNeedsUpdate) { 183 // Reset CSS properties on element so that the element may be sized to fit its content. 184 this._element.style.removeProperty("left"); 185 this._element.style.removeProperty("top"); 186 this._element.style.removeProperty("width"); 187 this._element.style.removeProperty("height"); 188 if (this._edge !== null) 189 this._element.classList.remove(this._cssClassNameForEdge()); 190 191 // Add the content in place of the wrapper to get the raw metrics. 192 this._element.replaceChild(this._content, this._container); 193 194 // Get the ideal size for the popover to fit its content. 195 var popoverBounds = this._element.getBoundingClientRect(); 196 this._preferredSize = new WebInspector.Size(Math.ceil(popoverBounds.width), Math.ceil(popoverBounds.height)); 197 } 198 199 const titleBarOffset = WebInspector.Platform.name === "mac" && !WebInspector.Platform.isLegacyMacOS ? 22 : 0; 200 var containerFrame = new WebInspector.Rect(0, titleBarOffset, window.innerWidth, window.innerHeight - titleBarOffset); 201 // The frame of the window with a little inset to make sure we have room for shadows. 202 containerFrame = containerFrame.inset(WebInspector.Popover.ShadowEdgeInsets); 203 204 // Work out the metrics for all edges. 205 var metrics = new Array(preferredEdges.length); 206 for (var edgeName in WebInspector.RectEdge) { 207 var edge = WebInspector.RectEdge[edgeName]; 208 var item = { 209 edge: edge, 210 metrics: this._bestMetricsForEdge(this._preferredSize, targetFrame, containerFrame, edge) 211 }; 212 var preferredIndex = preferredEdges.indexOf(edge); 213 if (preferredIndex !== -1) 214 metrics[preferredIndex] = item; 215 else 216 metrics.push(item); 217 } 218 219 function area(size) 220 { 221 return size.width * size.height; 222 } 223 224 // Find if any of those fit better than the frame for the preferred edge. 225 var bestEdge = metrics[0].edge; 226 var bestMetrics = metrics[0].metrics; 227 for (var i = 1; i < metrics.length; i++) { 228 var itemMetrics = metrics[i].metrics; 229 if (area(itemMetrics.contentSize) > area(bestMetrics.contentSize)) { 230 bestEdge = metrics[i].edge; 231 bestMetrics = itemMetrics; 232 } 233 } 234 235 var anchorPoint; 236 var bestFrame = bestMetrics.frame.round(); 237 238 this._edge = bestEdge; 239 240 if (bestFrame === WebInspector.Rect.ZERO_RECT) { 241 // The target for the popover is offscreen. 242 this.dismiss(); 243 } else { 244 switch (bestEdge) { 245 case WebInspector.RectEdge.MIN_X: // Displayed on the left of the target, arrow points right. 246 anchorPoint = new WebInspector.Point(bestFrame.size.width - WebInspector.Popover.ShadowPadding, targetFrame.midY() - bestFrame.minY()); 247 break; 248 case WebInspector.RectEdge.MAX_X: // Displayed on the right of the target, arrow points left. 249 anchorPoint = new WebInspector.Point(WebInspector.Popover.ShadowPadding, targetFrame.midY() - bestFrame.minY()); 250 break; 251 case WebInspector.RectEdge.MIN_Y: // Displayed above the target, arrow points down. 252 anchorPoint = new WebInspector.Point(targetFrame.midX() - bestFrame.minX(), bestFrame.size.height - WebInspector.Popover.ShadowPadding); 253 break; 254 case WebInspector.RectEdge.MAX_Y: // Displayed below the target, arrow points up. 255 anchorPoint = new WebInspector.Point(targetFrame.midX() - bestFrame.minX(), WebInspector.Popover.ShadowPadding); 256 break; 257 } 258 259 this._element.classList.add(this._cssClassNameForEdge()); 260 261 if (shouldAnimate && this._edge === previousEdge) 262 this._animateFrame(bestFrame); 263 else { 264 this.frame = bestFrame; 265 this._setAnchorPoint(anchorPoint); 266 this._drawBackground(); 267 } 268 269 // Make sure content is centered in case either of the dimension is smaller than the minimal bounds. 270 if (this._preferredSize.width < WebInspector.Popover.MinWidth || this._preferredSize.height < WebInspector.Popover.MinHeight) 271 this._container.classList.add("center"); 272 else 273 this._container.classList.remove("center"); 274 } 275 276 // Wrap the content in the container so that it's located correctly. 277 if (this._contentNeedsUpdate) { 278 this._container.textContent = ""; 279 this._element.replaceChild(this._container, this._content); 280 this._container.appendChild(this._content); 281 } 282 283 this._contentNeedsUpdate = false; 284 }, 285 286 _cssClassNameForEdge: function() 287 { 288 switch (this._edge) { 289 case WebInspector.RectEdge.MIN_X: // Displayed on the left of the target, arrow points right. 290 return "arrow-right"; 291 case WebInspector.RectEdge.MAX_X: // Displayed on the right of the target, arrow points left. 292 return "arrow-left"; 293 case WebInspector.RectEdge.MIN_Y: // Displayed above the target, arrow points down. 294 return "arrow-down"; 295 case WebInspector.RectEdge.MAX_Y: // Displayed below the target, arrow points up. 296 return "arrow-up"; 297 } 298 console.error("Unknown edge."); 299 return "arrow-up"; 300 }, 301 302 _setAnchorPoint: function(anchorPoint) { 303 anchorPoint.x = Math.floor(anchorPoint.x); 304 anchorPoint.y = Math.floor(anchorPoint.y); 305 this._anchorPoint = anchorPoint; 306 }, 307 308 _animateFrame: function(toFrame) 309 { 310 var startTime = Date.now(); 311 var duration = 350; 312 var epsilon = 1 / (200 * duration); 313 var spline = new WebInspector.UnitBezier(0.25, 0.1, 0.25, 1); 314 315 var fromFrame = this._frame.copy(); 316 317 var absoluteAnchorPoint = new WebInspector.Point( 318 fromFrame.minX() + this._anchorPoint.x, 319 fromFrame.minY() + this._anchorPoint.y 320 ); 321 322 function animatedValue(from, to, progress) 323 { 324 return from + (to - from) * progress; 325 } 326 327 function drawBackground() 328 { 329 var progress = spline.solve(Math.min((Date.now() - startTime) / duration, 1), epsilon); 330 331 this.frame = new WebInspector.Rect( 332 animatedValue(fromFrame.minX(), toFrame.minX(), progress), 333 animatedValue(fromFrame.minY(), toFrame.minY(), progress), 334 animatedValue(fromFrame.size.width, toFrame.size.width, progress), 335 animatedValue(fromFrame.size.height, toFrame.size.height, progress) 336 ).round(); 337 338 this._setAnchorPoint(new WebInspector.Point( 339 absoluteAnchorPoint.x - this._frame.minX(), 340 absoluteAnchorPoint.y - this._frame.minY() 341 )); 342 343 this._drawBackground(); 344 345 if (progress < 1) 346 window.requestAnimationFrame(drawBackground.bind(this)); 347 } 348 349 drawBackground.call(this); 350 }, 351 352 _drawBackground: function() 353 { 354 var scaleFactor = window.devicePixelRatio; 355 356 var width = this._frame.size.width; 357 var height = this._frame.size.height; 358 var scaledWidth = width * scaleFactor; 359 var scaledHeight = height * scaleFactor; 360 361 // Create a scratch canvas so we can draw the popover that will later be drawn into 362 // the final context with a shadow. 363 var scratchCanvas = document.createElement("canvas"); 364 scratchCanvas.width = scaledWidth; 365 scratchCanvas.height = scaledHeight; 366 367 var ctx = scratchCanvas.getContext("2d"); 368 ctx.scale(scaleFactor, scaleFactor); 369 370 // Bounds of the path don't take into account the arrow, but really only the tight bounding box 371 // of the content contained within the frame. 372 var bounds; 373 var arrowHeight = WebInspector.Popover.AnchorSize.height; 374 switch (this._edge) { 375 case WebInspector.RectEdge.MIN_X: // Displayed on the left of the target, arrow points right. 376 bounds = new WebInspector.Rect(0, 0, width - arrowHeight, height); 377 break; 378 case WebInspector.RectEdge.MAX_X: // Displayed on the right of the target, arrow points left. 379 bounds = new WebInspector.Rect(arrowHeight, 0, width - arrowHeight, height); 380 break; 381 case WebInspector.RectEdge.MIN_Y: // Displayed above the target, arrow points down. 382 bounds = new WebInspector.Rect(0, 0, width, height - arrowHeight); 383 break; 384 case WebInspector.RectEdge.MAX_Y: // Displayed below the target, arrow points up. 385 bounds = new WebInspector.Rect(0, arrowHeight, width, height - arrowHeight); 386 break; 387 } 388 389 bounds = bounds.inset(WebInspector.Popover.ShadowEdgeInsets); 390 391 // Clip the frame. 392 ctx.fillStyle = "black"; 393 this._drawFrame(ctx, bounds, this._edge, this._anchorPoint); 394 ctx.clip(); 395 396 // Gradient fill, top-to-bottom. 397 var fillGradient = ctx.createLinearGradient(0, 0, 0, height); 398 fillGradient.addColorStop(0, "rgba(255, 255, 255, 0.95)"); 399 fillGradient.addColorStop(1, "rgba(235, 235, 235, 0.95)"); 400 ctx.fillStyle = fillGradient; 401 ctx.fillRect(0, 0, width, height); 402 403 // Stroke. 404 ctx.strokeStyle = "rgba(0, 0, 0, 0.25)"; 405 ctx.lineWidth = 2; 406 this._drawFrame(ctx, bounds, this._edge, this._anchorPoint); 407 ctx.stroke(); 408 409 // Draw the popover into the final context with a drop shadow. 410 var finalContext = document.getCSSCanvasContext("2d", this._canvasId, scaledWidth, scaledHeight); 411 412 finalContext.clearRect(0, 0, scaledWidth, scaledHeight); 413 414 finalContext.shadowOffsetX = 1; 415 finalContext.shadowOffsetY = 1; 416 finalContext.shadowBlur = 5; 417 finalContext.shadowColor = "rgba(0, 0, 0, 0.5)"; 418 419 finalContext.drawImage(scratchCanvas, 0, 0, scaledWidth, scaledHeight); 420 }, 421 422 _bestMetricsForEdge: function(preferredSize, targetFrame, containerFrame, edge) 423 { 424 var x, y; 425 var width = preferredSize.width + (WebInspector.Popover.ShadowPadding * 2) + (WebInspector.Popover.ContentPadding * 2); 426 var height = preferredSize.height + (WebInspector.Popover.ShadowPadding * 2) + (WebInspector.Popover.ContentPadding * 2); 427 var arrowLength = WebInspector.Popover.AnchorSize.height; 428 429 switch (edge) { 430 case WebInspector.RectEdge.MIN_X: // Displayed on the left of the target, arrow points right. 431 width += arrowLength; 432 x = targetFrame.origin.x - width + WebInspector.Popover.ShadowPadding; 433 y = targetFrame.origin.y - (height - targetFrame.size.height) / 2; 434 break; 435 case WebInspector.RectEdge.MAX_X: // Displayed on the right of the target, arrow points left. 436 width += arrowLength; 437 x = targetFrame.origin.x + targetFrame.size.width - WebInspector.Popover.ShadowPadding; 438 y = targetFrame.origin.y - (height - targetFrame.size.height) / 2; 439 break; 440 case WebInspector.RectEdge.MIN_Y: // Displayed above the target, arrow points down. 441 height += arrowLength; 442 x = targetFrame.origin.x - (width - targetFrame.size.width) / 2; 443 y = targetFrame.origin.y - height + WebInspector.Popover.ShadowPadding; 444 break; 445 case WebInspector.RectEdge.MAX_Y: // Displayed below the target, arrow points up. 446 height += arrowLength; 447 x = targetFrame.origin.x - (width - targetFrame.size.width) / 2; 448 y = targetFrame.origin.y + targetFrame.size.height - WebInspector.Popover.ShadowPadding; 449 break; 450 } 451 452 if (edge === WebInspector.RectEdge.MIN_X || edge === WebInspector.RectEdge.MAX_X) { 453 if (y < containerFrame.minY()) 454 y = containerFrame.minY(); 455 if (y + height > containerFrame.maxY()) 456 y = containerFrame.maxY() - height; 457 } else { 458 if (x < containerFrame.minX()) 459 x = containerFrame.minX(); 460 if (x + width > containerFrame.maxX()) 461 x = containerFrame.maxX() - width; 462 } 463 464 var preferredFrame = new WebInspector.Rect(x, y, width, height); 465 var bestFrame = preferredFrame.intersectionWithRect(containerFrame); 466 467 width = bestFrame.size.width - (WebInspector.Popover.ShadowPadding * 2); 468 height = bestFrame.size.height - (WebInspector.Popover.ShadowPadding * 2); 469 470 switch (edge) { 471 case WebInspector.RectEdge.MIN_X: // Displayed on the left of the target, arrow points right. 472 case WebInspector.RectEdge.MAX_X: // Displayed on the right of the target, arrow points left. 473 width -= arrowLength; 474 break; 475 case WebInspector.RectEdge.MIN_Y: // Displayed above the target, arrow points down. 476 case WebInspector.RectEdge.MAX_Y: // Displayed below the target, arrow points up. 477 height -= arrowLength; 478 break; 479 } 480 481 return { 482 frame: bestFrame, 483 contentSize: new WebInspector.Size(width, height) 484 }; 485 }, 486 487 _drawFrame: function(ctx, bounds, anchorEdge) 488 { 489 var r = WebInspector.Popover.CornerRadius; 490 var arrowHalfLength = WebInspector.Popover.AnchorSize.width / 2; 491 var anchorPoint = this._anchorPoint; 492 493 ctx.beginPath(); 494 switch (anchorEdge) { 495 case WebInspector.RectEdge.MIN_X: // Displayed on the left of the target, arrow points right. 496 ctx.moveTo(bounds.maxX(), bounds.minY() + r); 497 ctx.lineTo(bounds.maxX(), anchorPoint.y - arrowHalfLength); 498 ctx.lineTo(anchorPoint.x, anchorPoint.y); 499 ctx.lineTo(bounds.maxX(), anchorPoint.y + arrowHalfLength); 500 ctx.arcTo(bounds.maxX(), bounds.maxY(), bounds.minX(), bounds.maxY(), r); 501 ctx.arcTo(bounds.minX(), bounds.maxY(), bounds.minX(), bounds.minY(), r); 502 ctx.arcTo(bounds.minX(), bounds.minY(), bounds.maxX(), bounds.minY(), r); 503 ctx.arcTo(bounds.maxX(), bounds.minY(), bounds.maxX(), bounds.maxY(), r); 504 break; 505 case WebInspector.RectEdge.MAX_X: // Displayed on the right of the target, arrow points left. 506 ctx.moveTo(bounds.minX(), bounds.maxY() - r); 507 ctx.lineTo(bounds.minX(), anchorPoint.y + arrowHalfLength); 508 ctx.lineTo(anchorPoint.x, anchorPoint.y); 509 ctx.lineTo(bounds.minX(), anchorPoint.y - arrowHalfLength); 510 ctx.arcTo(bounds.minX(), bounds.minY(), bounds.maxX(), bounds.minY(), r); 511 ctx.arcTo(bounds.maxX(), bounds.minY(), bounds.maxX(), bounds.maxY(), r); 512 ctx.arcTo(bounds.maxX(), bounds.maxY(), bounds.minX(), bounds.maxY(), r); 513 ctx.arcTo(bounds.minX(), bounds.maxY(), bounds.minX(), bounds.minY(), r); 514 break; 515 case WebInspector.RectEdge.MIN_Y: // Displayed above the target, arrow points down. 516 ctx.moveTo(bounds.maxX() - r, bounds.maxY()); 517 ctx.lineTo(anchorPoint.x + arrowHalfLength, bounds.maxY()); 518 ctx.lineTo(anchorPoint.x, anchorPoint.y); 519 ctx.lineTo(anchorPoint.x - arrowHalfLength, bounds.maxY()); 520 ctx.arcTo(bounds.minX(), bounds.maxY(), bounds.minX(), bounds.minY(), r); 521 ctx.arcTo(bounds.minX(), bounds.minY(), bounds.maxX(), bounds.minY(), r); 522 ctx.arcTo(bounds.maxX(), bounds.minY(), bounds.maxX(), bounds.maxY(), r); 523 ctx.arcTo(bounds.maxX(), bounds.maxY(), bounds.minX(), bounds.maxY(), r); 524 break; 525 case WebInspector.RectEdge.MAX_Y: // Displayed below the target, arrow points up. 526 ctx.moveTo(bounds.minX() + r, bounds.minY()); 527 ctx.lineTo(anchorPoint.x - arrowHalfLength, bounds.minY()); 528 ctx.lineTo(anchorPoint.x, anchorPoint.y); 529 ctx.lineTo(anchorPoint.x + arrowHalfLength, bounds.minY()); 530 ctx.arcTo(bounds.maxX(), bounds.minY(), bounds.maxX(), bounds.maxY(), r); 531 ctx.arcTo(bounds.maxX(), bounds.maxY(), bounds.minX(), bounds.maxY(), r); 532 ctx.arcTo(bounds.minX(), bounds.maxY(), bounds.minX(), bounds.minY(), r); 533 ctx.arcTo(bounds.minX(), bounds.minY(), bounds.maxX(), bounds.minY(), r); 534 break; 535 } 536 ctx.closePath(); 537 } 538 539}; 540 541WebInspector.Popover.prototype.__proto__ = WebInspector.Object.prototype; 542