diff options
Diffstat (limited to 'devtools/server/actors/highlighters/css-transform.js')
-rw-r--r-- | devtools/server/actors/highlighters/css-transform.js | 243 |
1 files changed, 243 insertions, 0 deletions
diff --git a/devtools/server/actors/highlighters/css-transform.js b/devtools/server/actors/highlighters/css-transform.js new file mode 100644 index 000000000..83a2c7e59 --- /dev/null +++ b/devtools/server/actors/highlighters/css-transform.js @@ -0,0 +1,243 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { extend } = require("sdk/core/heritage"); +const { AutoRefreshHighlighter } = require("./auto-refresh"); +const { + CanvasFrameAnonymousContentHelper, getComputedStyle, + createSVGNode, createNode } = require("./utils/markup"); +const { setIgnoreLayoutChanges, + getNodeBounds } = require("devtools/shared/layout/utils"); + +// The minimum distance a line should be before it has an arrow marker-end +const ARROW_LINE_MIN_DISTANCE = 10; + +var MARKER_COUNTER = 1; + +/** + * The CssTransformHighlighter is the class that draws an outline around a + * transformed element and an outline around where it would be if untransformed + * as well as arrows connecting the 2 outlines' corners. + */ +function CssTransformHighlighter(highlighterEnv) { + AutoRefreshHighlighter.call(this, highlighterEnv); + + this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv, + this._buildMarkup.bind(this)); +} + +CssTransformHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, { + typeName: "CssTransformHighlighter", + + ID_CLASS_PREFIX: "css-transform-", + + _buildMarkup: function () { + let container = createNode(this.win, { + attributes: { + "class": "highlighter-container" + } + }); + + // The root wrapper is used to unzoom the highlighter when needed. + let rootWrapper = createNode(this.win, { + parent: container, + attributes: { + "id": "root", + "class": "root" + }, + prefix: this.ID_CLASS_PREFIX + }); + + let svg = createSVGNode(this.win, { + nodeType: "svg", + parent: rootWrapper, + attributes: { + "id": "elements", + "hidden": "true", + "width": "100%", + "height": "100%" + }, + prefix: this.ID_CLASS_PREFIX + }); + + // Add a marker tag to the svg root for the arrow tip + this.markerId = "arrow-marker-" + MARKER_COUNTER; + MARKER_COUNTER++; + let marker = createSVGNode(this.win, { + nodeType: "marker", + parent: svg, + attributes: { + "id": this.markerId, + "markerWidth": "10", + "markerHeight": "5", + "orient": "auto", + "markerUnits": "strokeWidth", + "refX": "10", + "refY": "5", + "viewBox": "0 0 10 10" + }, + prefix: this.ID_CLASS_PREFIX + }); + createSVGNode(this.win, { + nodeType: "path", + parent: marker, + attributes: { + "d": "M 0 0 L 10 5 L 0 10 z", + "fill": "#08C" + } + }); + + let shapesGroup = createSVGNode(this.win, { + nodeType: "g", + parent: svg + }); + + // Create the 2 polygons (transformed and untransformed) + createSVGNode(this.win, { + nodeType: "polygon", + parent: shapesGroup, + attributes: { + "id": "untransformed", + "class": "untransformed" + }, + prefix: this.ID_CLASS_PREFIX + }); + createSVGNode(this.win, { + nodeType: "polygon", + parent: shapesGroup, + attributes: { + "id": "transformed", + "class": "transformed" + }, + prefix: this.ID_CLASS_PREFIX + }); + + // Create the arrows + for (let nb of ["1", "2", "3", "4"]) { + createSVGNode(this.win, { + nodeType: "line", + parent: shapesGroup, + attributes: { + "id": "line" + nb, + "class": "line", + "marker-end": "url(#" + this.markerId + ")" + }, + prefix: this.ID_CLASS_PREFIX + }); + } + + return container; + }, + + /** + * Destroy the nodes. Remove listeners. + */ + destroy: function () { + AutoRefreshHighlighter.prototype.destroy.call(this); + this.markup.destroy(); + }, + + getElement: function (id) { + return this.markup.getElement(this.ID_CLASS_PREFIX + id); + }, + + /** + * Show the highlighter on a given node + */ + _show: function () { + if (!this._isTransformed(this.currentNode)) { + this.hide(); + return false; + } + + return this._update(); + }, + + /** + * Checks if the supplied node is transformed and not inline + */ + _isTransformed: function (node) { + let style = getComputedStyle(node); + return style && (style.transform !== "none" && style.display !== "inline"); + }, + + _setPolygonPoints: function (quad, id) { + let points = []; + for (let point of ["p1", "p2", "p3", "p4"]) { + points.push(quad[point].x + "," + quad[point].y); + } + this.getElement(id).setAttribute("points", points.join(" ")); + }, + + _setLinePoints: function (p1, p2, id) { + let line = this.getElement(id); + line.setAttribute("x1", p1.x); + line.setAttribute("y1", p1.y); + line.setAttribute("x2", p2.x); + line.setAttribute("y2", p2.y); + + let dist = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); + if (dist < ARROW_LINE_MIN_DISTANCE) { + line.removeAttribute("marker-end"); + } else { + line.setAttribute("marker-end", "url(#" + this.markerId + ")"); + } + }, + + /** + * Update the highlighter on the current highlighted node (the one that was + * passed as an argument to show(node)). + * Should be called whenever node size or attributes change + */ + _update: function () { + setIgnoreLayoutChanges(true); + + // Getting the points for the transformed shape + let quads = this.currentQuads.border; + if (!quads.length || + quads[0].bounds.width <= 0 || quads[0].bounds.height <= 0) { + this._hideShapes(); + return false; + } + + let [quad] = quads; + + // Getting the points for the untransformed shape + let untransformedQuad = getNodeBounds(this.win, this.currentNode); + + this._setPolygonPoints(quad, "transformed"); + this._setPolygonPoints(untransformedQuad, "untransformed"); + for (let nb of ["1", "2", "3", "4"]) { + this._setLinePoints(untransformedQuad["p" + nb], quad["p" + nb], "line" + nb); + } + + // Adapt to the current zoom + this.markup.scaleRootElement(this.currentNode, this.ID_CLASS_PREFIX + "root"); + + this._showShapes(); + + setIgnoreLayoutChanges(false, this.highlighterEnv.window.document.documentElement); + return true; + }, + + /** + * Hide the highlighter, the outline and the infobar. + */ + _hide: function () { + setIgnoreLayoutChanges(true); + this._hideShapes(); + setIgnoreLayoutChanges(false, this.highlighterEnv.window.document.documentElement); + }, + + _hideShapes: function () { + this.getElement("elements").setAttribute("hidden", "true"); + }, + + _showShapes: function () { + this.getElement("elements").removeAttribute("hidden"); + } +}); +exports.CssTransformHighlighter = CssTransformHighlighter; |