summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/highlighters/css-transform.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/highlighters/css-transform.js')
-rw-r--r--devtools/server/actors/highlighters/css-transform.js265
1 files changed, 265 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 0000000000..c9f16b42e0
--- /dev/null
+++ b/devtools/server/actors/highlighters/css-transform.js
@@ -0,0 +1,265 @@
+/* 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 {
+ AutoRefreshHighlighter,
+} = require("resource://devtools/server/actors/highlighters/auto-refresh.js");
+const {
+ CanvasFrameAnonymousContentHelper,
+ getComputedStyle,
+} = require("resource://devtools/server/actors/highlighters/utils/markup.js");
+const {
+ setIgnoreLayoutChanges,
+ getNodeBounds,
+} = require("resource://devtools/shared/layout/utils.js");
+
+// 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.
+ */
+class CssTransformHighlighter extends AutoRefreshHighlighter {
+ constructor(highlighterEnv) {
+ super(highlighterEnv);
+
+ this.ID_CLASS_PREFIX = "css-transform-";
+
+ this.markup = new CanvasFrameAnonymousContentHelper(
+ this.highlighterEnv,
+ this._buildMarkup.bind(this)
+ );
+ this.isReady = this.markup.initialize();
+ }
+
+ _buildMarkup() {
+ const container = this.markup.createNode({
+ attributes: {
+ class: "highlighter-container",
+ },
+ });
+
+ // The root wrapper is used to unzoom the highlighter when needed.
+ const rootWrapper = this.markup.createNode({
+ parent: container,
+ attributes: {
+ id: "root",
+ class: "root",
+ },
+ prefix: this.ID_CLASS_PREFIX,
+ });
+
+ const svg = this.markup.createSVGNode({
+ 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++;
+ const marker = this.markup.createSVGNode({
+ 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,
+ });
+ this.markup.createSVGNode({
+ nodeType: "path",
+ parent: marker,
+ attributes: {
+ d: "M 0 0 L 10 5 L 0 10 z",
+ fill: "#08C",
+ },
+ });
+
+ const shapesGroup = this.markup.createSVGNode({
+ nodeType: "g",
+ parent: svg,
+ });
+
+ // Create the 2 polygons (transformed and untransformed)
+ this.markup.createSVGNode({
+ nodeType: "polygon",
+ parent: shapesGroup,
+ attributes: {
+ id: "untransformed",
+ class: "untransformed",
+ },
+ prefix: this.ID_CLASS_PREFIX,
+ });
+ this.markup.createSVGNode({
+ nodeType: "polygon",
+ parent: shapesGroup,
+ attributes: {
+ id: "transformed",
+ class: "transformed",
+ },
+ prefix: this.ID_CLASS_PREFIX,
+ });
+
+ // Create the arrows
+ for (const nb of ["1", "2", "3", "4"]) {
+ this.markup.createSVGNode({
+ 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() {
+ AutoRefreshHighlighter.prototype.destroy.call(this);
+ this.markup.destroy();
+ }
+
+ getElement(id) {
+ return this.markup.getElement(this.ID_CLASS_PREFIX + id);
+ }
+
+ /**
+ * Show the highlighter on a given node
+ */
+ _show() {
+ if (!this._isTransformed(this.currentNode)) {
+ this.hide();
+ return false;
+ }
+
+ return this._update();
+ }
+
+ /**
+ * Checks if the supplied node is transformed and not inline
+ */
+ _isTransformed(node) {
+ const style = getComputedStyle(node);
+ return style && style.transform !== "none" && style.display !== "inline";
+ }
+
+ _setPolygonPoints(quad, id) {
+ const points = [];
+ for (const point of ["p1", "p2", "p3", "p4"]) {
+ points.push(quad[point].x + "," + quad[point].y);
+ }
+ this.getElement(id).setAttribute("points", points.join(" "));
+ }
+
+ _setLinePoints(p1, p2, id) {
+ const line = this.getElement(id);
+ line.setAttribute("x1", p1.x);
+ line.setAttribute("y1", p1.y);
+ line.setAttribute("x2", p2.x);
+ line.setAttribute("y2", p2.y);
+
+ const 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() {
+ setIgnoreLayoutChanges(true);
+
+ // Getting the points for the transformed shape
+ const quads = this.currentQuads.border;
+ if (
+ !quads.length ||
+ quads[0].bounds.width <= 0 ||
+ quads[0].bounds.height <= 0
+ ) {
+ this._hideShapes();
+ return false;
+ }
+
+ const [quad] = quads;
+
+ // Getting the points for the untransformed shape
+ const untransformedQuad = getNodeBounds(this.win, this.currentNode);
+
+ this._setPolygonPoints(quad, "transformed");
+ this._setPolygonPoints(untransformedQuad, "untransformed");
+ for (const 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() {
+ setIgnoreLayoutChanges(true);
+ this._hideShapes();
+ setIgnoreLayoutChanges(
+ false,
+ this.highlighterEnv.window.document.documentElement
+ );
+ }
+
+ _hideShapes() {
+ this.getElement("elements").setAttribute("hidden", "true");
+ }
+
+ _showShapes() {
+ this.getElement("elements").removeAttribute("hidden");
+ }
+}
+
+exports.CssTransformHighlighter = CssTransformHighlighter;