/* * Copyright (C) 2013 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ WebInspector.Popover = function(delegate) { WebInspector.Object.call(this); this.delegate = delegate; this._edge = null; this._frame = new WebInspector.Rect; this._content = null; this._targetFrame = new WebInspector.Rect; this._preferredEdges = null; this._contentNeedsUpdate = false; this._element = document.createElement("div"); this._element.className = WebInspector.Popover.StyleClassName; this._canvasId = "popover-" + (WebInspector.Popover.canvasId++); this._element.style.backgroundImage = "-webkit-canvas(" + this._canvasId + ")"; this._element.addEventListener("transitionend", this, true); this._container = this._element.appendChild(document.createElement("div")); this._container.className = "container"; }; WebInspector.Popover.StyleClassName = "popover"; WebInspector.Popover.FadeOutClassName = "fade-out"; WebInspector.Popover.canvasId = 0; WebInspector.Popover.CornerRadius = 5; WebInspector.Popover.MinWidth = 40; WebInspector.Popover.MinHeight = 40; WebInspector.Popover.ShadowPadding = 5; WebInspector.Popover.ContentPadding = 5; WebInspector.Popover.AnchorSize = new WebInspector.Size(22, 11); WebInspector.Popover.ShadowEdgeInsets = new WebInspector.EdgeInsets(WebInspector.Popover.ShadowPadding); WebInspector.Popover.prototype = { constructor: WebInspector.Popover, // Public get element() { return this._element; }, get frame() { return this._frame; }, get visible() { return this._element.parentNode === document.body && !this._element.classList.contains(WebInspector.Popover.FadeOutClassName); }, set frame(frame) { this._element.style.left = frame.origin.x + "px"; this._element.style.top = frame.origin.y + "px"; this._element.style.width = frame.size.width + "px"; this._element.style.height = frame.size.height + "px"; this._element.style.backgroundSize = frame.size.width + "px " + frame.size.height + "px"; this._frame = frame; }, set content(content) { if (content === this._content) return; this._content = content; this._contentNeedsUpdate = true; if (this.visible) this._update(); }, update: function() { if (!this.visible) return; var previouslyFocusedElement = document.activeElement; this._contentNeedsUpdate = true; this._update(); if (previouslyFocusedElement) previouslyFocusedElement.focus(); }, /** * @param {WebInspector.Rect} targetFrame * @param {WebInspector.RectEdge}[] preferredEdges */ present: function(targetFrame, preferredEdges) { this._targetFrame = targetFrame; this._preferredEdges = preferredEdges; if (!this._content) return; window.addEventListener("mousedown", this, true); window.addEventListener("scroll", this, true); this._update(); }, dismiss: function() { if (this._element.parentNode !== document.body) return; window.removeEventListener("mousedown", this, true); window.removeEventListener("scroll", this, true); this._element.classList.add(WebInspector.Popover.FadeOutClassName); if (this.delegate && typeof this.delegate.willDismissPopover === "function") this.delegate.willDismissPopover(this); }, handleEvent: function(event) { switch (event.type) { case "mousedown": case "scroll": if (!this._element.contains(event.target)) this.dismiss(); break; case "transitionend": document.body.removeChild(this._element); this._element.classList.remove(WebInspector.Popover.FadeOutClassName); this._container.textContent = ""; if (this.delegate && typeof this.delegate.didDismissPopover === "function") this.delegate.didDismissPopover(this); break; } }, // Private _update: function() { var targetFrame = this._targetFrame; var preferredEdges = this._preferredEdges; // Ensure our element is on display so that its metrics can be resolved // or interrupt any pending transition to remove it from display. if (this._element.parentNode !== document.body) document.body.appendChild(this._element); else this._element.classList.remove(WebInspector.Popover.FadeOutClassName); if (this._contentNeedsUpdate) { // Reset CSS properties on element so that the element may be sized to fit its content. this._element.style.removeProperty("left"); this._element.style.removeProperty("top"); this._element.style.removeProperty("width"); this._element.style.removeProperty("height"); if (this._edge !== null) this._element.classList.remove(this._cssClassNameForEdge()); // Add the content in place of the wrapper to get the raw metrics. this._element.replaceChild(this._content, this._container); // Get the ideal size for the popover to fit its content. var popoverBounds = this._element.getBoundingClientRect(); this._preferredSize = new WebInspector.Size(Math.ceil(popoverBounds.width), Math.ceil(popoverBounds.height)); } // The frame of the window with a little inset to make sure we have room for shadows. var containerFrame = new WebInspector.Rect(0, 0, window.innerWidth, window.innerHeight); containerFrame = containerFrame.inset(WebInspector.Popover.ShadowEdgeInsets); // Work out the metrics for all edges. var metrics = new Array(preferredEdges.length); for (var edgeName in WebInspector.RectEdge) { var edge = WebInspector.RectEdge[edgeName]; var item = { edge: edge, metrics: this._bestMetricsForEdge(this._preferredSize, targetFrame, containerFrame, edge) }; var preferredIndex = preferredEdges.indexOf(edge); if (preferredIndex !== -1) metrics[preferredIndex] = item; else metrics.push(item); } function area(size) { return size.width * size.height; } // Find if any of those fit better than the frame for the preferred edge. var bestEdge = metrics[0].edge; var bestMetrics = metrics[0].metrics; for (var i = 1; i < metrics.length; i++) { var itemMetrics = metrics[i].metrics; if (area(itemMetrics.contentSize) > area(bestMetrics.contentSize)) { bestEdge = metrics[i].edge; bestMetrics = itemMetrics; } } var anchorPoint; var bestFrame = bestMetrics.frame; this.frame = bestFrame; this._edge = bestEdge; if (this.frame === WebInspector.Rect.ZERO_RECT) { // The target for the popover is offscreen. this.dismiss(); } else { switch (bestEdge) { case WebInspector.RectEdge.MIN_X: // Displayed on the left of the target, arrow points right. anchorPoint = new WebInspector.Point(bestFrame.size.width - WebInspector.Popover.ShadowPadding, targetFrame.midY() - bestFrame.minY()); break; case WebInspector.RectEdge.MAX_X: // Displayed on the right of the target, arrow points left. anchorPoint = new WebInspector.Point(WebInspector.Popover.ShadowPadding, targetFrame.midY() - bestFrame.minY()); break; case WebInspector.RectEdge.MIN_Y: // Displayed above the target, arrow points down. anchorPoint = new WebInspector.Point(targetFrame.midX() - bestFrame.minX(), bestFrame.size.height - WebInspector.Popover.ShadowPadding); break; case WebInspector.RectEdge.MAX_Y: // Displayed below the target, arrow points up. anchorPoint = new WebInspector.Point(targetFrame.midX() - bestFrame.minX(), WebInspector.Popover.ShadowPadding); break; } this._element.classList.add(this._cssClassNameForEdge()); this._drawBackground(bestEdge, anchorPoint); // Make sure content is centered in case either of the dimension is smaller than the minimal bounds. if (this._preferredSize.width < WebInspector.Popover.MinWidth || this._preferredSize.height < WebInspector.Popover.MinHeight) this._container.classList.add("center"); else this._container.classList.remove("center"); } // Wrap the content in the container so that it's located correctly. if (this._contentNeedsUpdate) { this._container.textContent = ""; this._element.replaceChild(this._container, this._content); this._container.appendChild(this._content); } this._contentNeedsUpdate = false; }, _cssClassNameForEdge: function() { switch (this._edge) { case WebInspector.RectEdge.MIN_X: // Displayed on the left of the target, arrow points right. return "arrow-right"; case WebInspector.RectEdge.MAX_X: // Displayed on the right of the target, arrow points left. return "arrow-left"; case WebInspector.RectEdge.MIN_Y: // Displayed above the target, arrow points down. return "arrow-down"; case WebInspector.RectEdge.MAX_Y: // Displayed below the target, arrow points up. return "arrow-up"; } console.error("Unknown edge."); return "arrow-up"; }, _drawBackground: function(edge, anchorPoint) { var scaleFactor = window.devicePixelRatio; var width = this._frame.size.width; var height = this._frame.size.height; var scaledWidth = width * scaleFactor; var scaledHeight = height * scaleFactor; // Create a scratch canvas so we can draw the popover that will later be drawn into // the final context with a shadow. var scratchCanvas = document.createElement("canvas"); scratchCanvas.width = scaledWidth; scratchCanvas.height = scaledHeight; var ctx = scratchCanvas.getContext("2d"); ctx.scale(scaleFactor, scaleFactor); // Bounds of the path don't take into account the arrow, but really only the tight bounding box // of the content contained within the frame. var bounds; var arrowHeight = WebInspector.Popover.AnchorSize.height; switch (edge) { case WebInspector.RectEdge.MIN_X: // Displayed on the left of the target, arrow points right. bounds = new WebInspector.Rect(0, 0, width - arrowHeight, height); break; case WebInspector.RectEdge.MAX_X: // Displayed on the right of the target, arrow points left. bounds = new WebInspector.Rect(arrowHeight, 0, width - arrowHeight, height); break; case WebInspector.RectEdge.MIN_Y: // Displayed above the target, arrow points down. bounds = new WebInspector.Rect(0, 0, width, height - arrowHeight); break; case WebInspector.RectEdge.MAX_Y: // Displayed below the target, arrow points up. bounds = new WebInspector.Rect(0, arrowHeight, width, height - arrowHeight); break; } bounds = bounds.inset(WebInspector.Popover.ShadowEdgeInsets); // Clip the frame. ctx.fillStyle = "black"; this._drawFrame(ctx, bounds, edge, anchorPoint); ctx.clip(); // Gradient fill, top-to-bottom. var fillGradient = ctx.createLinearGradient(0, 0, 0, height); fillGradient.addColorStop(0, "rgba(255, 255, 255, 0.95)"); fillGradient.addColorStop(1, "rgba(235, 235, 235, 0.95)"); ctx.fillStyle = fillGradient; ctx.fillRect(0, 0, width, height); // Stroke. ctx.strokeStyle = "rgba(0, 0, 0, 0.25)"; ctx.lineWidth = 2; this._drawFrame(ctx, bounds, edge, anchorPoint); ctx.stroke(); // Draw the popover into the final context with a drop shadow. var finalContext = document.getCSSCanvasContext("2d", this._canvasId, scaledWidth, scaledHeight); finalContext.clearRect(0, 0, scaledWidth, scaledHeight); finalContext.shadowOffsetX = 1; finalContext.shadowOffsetY = 1; finalContext.shadowBlur = 5; finalContext.shadowColor = "rgba(0, 0, 0, 0.5)"; finalContext.drawImage(scratchCanvas, 0, 0, scaledWidth, scaledHeight); }, _bestMetricsForEdge: function(preferredSize, targetFrame, containerFrame, edge) { var x, y; var width = preferredSize.width + (WebInspector.Popover.ShadowPadding * 2) + (WebInspector.Popover.ContentPadding * 2); var height = preferredSize.height + (WebInspector.Popover.ShadowPadding * 2) + (WebInspector.Popover.ContentPadding * 2); var arrowLength = WebInspector.Popover.AnchorSize.height; switch (edge) { case WebInspector.RectEdge.MIN_X: // Displayed on the left of the target, arrow points right. width += arrowLength; x = targetFrame.origin.x - width + WebInspector.Popover.ShadowPadding; y = targetFrame.origin.y - (height - targetFrame.size.height) / 2; break; case WebInspector.RectEdge.MAX_X: // Displayed on the right of the target, arrow points left. width += arrowLength; x = targetFrame.origin.x + targetFrame.size.width - WebInspector.Popover.ShadowPadding; y = targetFrame.origin.y - (height - targetFrame.size.height) / 2; break; case WebInspector.RectEdge.MIN_Y: // Displayed above the target, arrow points down. height += arrowLength; x = targetFrame.origin.x - (width - targetFrame.size.width) / 2; y = targetFrame.origin.y - height + WebInspector.Popover.ShadowPadding; break; case WebInspector.RectEdge.MAX_Y: // Displayed below the target, arrow points up. height += arrowLength; x = targetFrame.origin.x - (width - targetFrame.size.width) / 2; y = targetFrame.origin.y + targetFrame.size.height - WebInspector.Popover.ShadowPadding; break; } if (edge === WebInspector.RectEdge.MIN_X || edge === WebInspector.RectEdge.MAX_X) { if (y < containerFrame.minY()) y = containerFrame.minY(); if (y + height > containerFrame.maxY()) y = containerFrame.maxY() - height; } else { if (x < containerFrame.minX()) x = containerFrame.minX(); if (x + width > containerFrame.maxX()) x = containerFrame.maxX() - width; } var preferredFrame = new WebInspector.Rect(x, y, width, height); var bestFrame = preferredFrame.intersectionWithRect(containerFrame); width = bestFrame.size.width - (WebInspector.Popover.ShadowPadding * 2); height = bestFrame.size.height - (WebInspector.Popover.ShadowPadding * 2); switch (edge) { case WebInspector.RectEdge.MIN_X: // Displayed on the left of the target, arrow points right. case WebInspector.RectEdge.MAX_X: // Displayed on the right of the target, arrow points left. width -= arrowLength; break; case WebInspector.RectEdge.MIN_Y: // Displayed above the target, arrow points down. case WebInspector.RectEdge.MAX_Y: // Displayed below the target, arrow points up. height -= arrowLength; break; } return { frame: bestFrame, contentSize: new WebInspector.Size(width, height) }; }, _drawFrame: function(ctx, bounds, anchorEdge, anchorPoint) { var r = WebInspector.Popover.CornerRadius; var arrowHalfLength = WebInspector.Popover.AnchorSize.width / 2; ctx.beginPath(); switch (anchorEdge) { case WebInspector.RectEdge.MIN_X: // Displayed on the left of the target, arrow points right. ctx.moveTo(bounds.maxX(), bounds.minY() + r); ctx.lineTo(bounds.maxX(), anchorPoint.y - arrowHalfLength); ctx.lineTo(anchorPoint.x, anchorPoint.y); ctx.lineTo(bounds.maxX(), anchorPoint.y + arrowHalfLength); ctx.arcTo(bounds.maxX(), bounds.maxY(), bounds.minX(), bounds.maxY(), r); ctx.arcTo(bounds.minX(), bounds.maxY(), bounds.minX(), bounds.minY(), r); ctx.arcTo(bounds.minX(), bounds.minY(), bounds.maxX(), bounds.minY(), r); ctx.arcTo(bounds.maxX(), bounds.minY(), bounds.maxX(), bounds.maxY(), r); break; case WebInspector.RectEdge.MAX_X: // Displayed on the right of the target, arrow points left. ctx.moveTo(bounds.minX(), bounds.maxY() - r); ctx.lineTo(bounds.minX(), anchorPoint.y + arrowHalfLength); ctx.lineTo(anchorPoint.x, anchorPoint.y); ctx.lineTo(bounds.minX(), anchorPoint.y - arrowHalfLength); ctx.arcTo(bounds.minX(), bounds.minY(), bounds.maxX(), bounds.minY(), r); ctx.arcTo(bounds.maxX(), bounds.minY(), bounds.maxX(), bounds.maxY(), r); ctx.arcTo(bounds.maxX(), bounds.maxY(), bounds.minX(), bounds.maxY(), r); ctx.arcTo(bounds.minX(), bounds.maxY(), bounds.minX(), bounds.minY(), r); break; case WebInspector.RectEdge.MIN_Y: // Displayed above the target, arrow points down. ctx.moveTo(bounds.maxX() - r, bounds.maxY()); ctx.lineTo(anchorPoint.x + arrowHalfLength, bounds.maxY()); ctx.lineTo(anchorPoint.x, anchorPoint.y); ctx.lineTo(anchorPoint.x - arrowHalfLength, bounds.maxY()); ctx.arcTo(bounds.minX(), bounds.maxY(), bounds.minX(), bounds.minY(), r); ctx.arcTo(bounds.minX(), bounds.minY(), bounds.maxX(), bounds.minY(), r); ctx.arcTo(bounds.maxX(), bounds.minY(), bounds.maxX(), bounds.maxY(), r); ctx.arcTo(bounds.maxX(), bounds.maxY(), bounds.minX(), bounds.maxY(), r); break; case WebInspector.RectEdge.MAX_Y: // Displayed below the target, arrow points up. ctx.moveTo(bounds.minX() + r, bounds.minY()); ctx.lineTo(anchorPoint.x - arrowHalfLength, bounds.minY()); ctx.lineTo(anchorPoint.x, anchorPoint.y); ctx.lineTo(anchorPoint.x + arrowHalfLength, bounds.minY()); ctx.arcTo(bounds.maxX(), bounds.minY(), bounds.maxX(), bounds.maxY(), r); ctx.arcTo(bounds.maxX(), bounds.maxY(), bounds.minX(), bounds.maxY(), r); ctx.arcTo(bounds.minX(), bounds.maxY(), bounds.minX(), bounds.minY(), r); ctx.arcTo(bounds.minX(), bounds.minY(), bounds.maxX(), bounds.minY(), r); break; } ctx.closePath(); } }; WebInspector.Popover.prototype.__proto__ = WebInspector.Object.prototype;