css-transform.js (6838B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { 8 AutoRefreshHighlighter, 9 } = require("resource://devtools/server/actors/highlighters/auto-refresh.js"); 10 const { 11 CanvasFrameAnonymousContentHelper, 12 getComputedStyle, 13 } = require("resource://devtools/server/actors/highlighters/utils/markup.js"); 14 const { 15 setIgnoreLayoutChanges, 16 getNodeBounds, 17 } = require("resource://devtools/shared/layout/utils.js"); 18 19 // The minimum distance a line should be before it has an arrow marker-end 20 const ARROW_LINE_MIN_DISTANCE = 10; 21 22 var MARKER_COUNTER = 1; 23 24 /** 25 * The CssTransformHighlighter is the class that draws an outline around a 26 * transformed element and an outline around where it would be if untransformed 27 * as well as arrows connecting the 2 outlines' corners. 28 */ 29 class CssTransformHighlighter extends AutoRefreshHighlighter { 30 constructor(highlighterEnv) { 31 super(highlighterEnv); 32 33 this.markup = new CanvasFrameAnonymousContentHelper( 34 this.highlighterEnv, 35 this._buildMarkup.bind(this), 36 { 37 contentRootHostClassName: "devtools-highlighter-css-transform", 38 } 39 ); 40 this.isReady = this.markup.initialize(); 41 } 42 43 _buildMarkup() { 44 const container = this.markup.createNode({ 45 attributes: { 46 class: "highlighter-container", 47 }, 48 }); 49 50 // The root wrapper is used to unzoom the highlighter when needed. 51 this.rootEl = this.markup.createNode({ 52 parent: container, 53 attributes: { 54 id: "css-transform-root", 55 class: "css-transform-root", 56 }, 57 }); 58 59 const svg = this.markup.createSVGNode({ 60 nodeType: "svg", 61 parent: this.rootEl, 62 attributes: { 63 id: "css-transform-elements", 64 hidden: "true", 65 width: "100%", 66 height: "100%", 67 }, 68 }); 69 70 // Add a marker tag to the svg root for the arrow tip 71 this.markerId = "css-transform-arrow-marker-" + MARKER_COUNTER; 72 MARKER_COUNTER++; 73 const marker = this.markup.createSVGNode({ 74 nodeType: "marker", 75 parent: svg, 76 attributes: { 77 id: this.markerId, 78 markerWidth: "10", 79 markerHeight: "5", 80 orient: "auto", 81 markerUnits: "strokeWidth", 82 refX: "10", 83 refY: "5", 84 viewBox: "0 0 10 10", 85 }, 86 }); 87 this.markup.createSVGNode({ 88 nodeType: "path", 89 parent: marker, 90 attributes: { 91 d: "M 0 0 L 10 5 L 0 10 z", 92 fill: "#08C", 93 }, 94 }); 95 96 const shapesGroup = this.markup.createSVGNode({ 97 nodeType: "g", 98 parent: svg, 99 }); 100 101 // Create the 2 polygons (transformed and untransformed) 102 this.markup.createSVGNode({ 103 nodeType: "polygon", 104 parent: shapesGroup, 105 attributes: { 106 id: "css-transform-untransformed", 107 class: "css-transform-untransformed", 108 }, 109 }); 110 this.markup.createSVGNode({ 111 nodeType: "polygon", 112 parent: shapesGroup, 113 attributes: { 114 id: "css-transform-transformed", 115 class: "css-transform-transformed", 116 }, 117 }); 118 119 // Create the arrows 120 for (const nb of ["1", "2", "3", "4"]) { 121 this.markup.createSVGNode({ 122 nodeType: "line", 123 parent: shapesGroup, 124 attributes: { 125 id: "css-transform-line" + nb, 126 class: "css-transform-line", 127 "marker-end": "url(#" + this.markerId + ")", 128 }, 129 }); 130 } 131 132 return container; 133 } 134 135 /** 136 * Destroy the nodes. Remove listeners. 137 */ 138 destroy() { 139 AutoRefreshHighlighter.prototype.destroy.call(this); 140 this.markup.destroy(); 141 this.rootEl = null; 142 } 143 144 getElement(id) { 145 return this.markup.getElement(id); 146 } 147 148 /** 149 * Show the highlighter on a given node 150 */ 151 _show() { 152 if (!this._isTransformed(this.currentNode)) { 153 this.hide(); 154 return false; 155 } 156 157 return this._update(); 158 } 159 160 /** 161 * Checks if the supplied node is transformed and not inline 162 */ 163 _isTransformed(node) { 164 const style = getComputedStyle(node); 165 return style && style.transform !== "none" && style.display !== "inline"; 166 } 167 168 _setPolygonPoints(quad, id) { 169 const points = []; 170 for (const point of ["p1", "p2", "p3", "p4"]) { 171 points.push(quad[point].x + "," + quad[point].y); 172 } 173 this.getElement(id).setAttribute("points", points.join(" ")); 174 } 175 176 _setLinePoints(p1, p2, id) { 177 const line = this.getElement(id); 178 line.setAttribute("x1", p1.x); 179 line.setAttribute("y1", p1.y); 180 line.setAttribute("x2", p2.x); 181 line.setAttribute("y2", p2.y); 182 183 const dist = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); 184 if (dist < ARROW_LINE_MIN_DISTANCE) { 185 line.removeAttribute("marker-end"); 186 } else { 187 line.setAttribute("marker-end", "url(#" + this.markerId + ")"); 188 } 189 } 190 191 /** 192 * Update the highlighter on the current highlighted node (the one that was 193 * passed as an argument to show(node)). 194 * Should be called whenever node size or attributes change 195 */ 196 _update() { 197 setIgnoreLayoutChanges(true); 198 199 // Getting the points for the transformed shape 200 const quads = this.currentQuads.border; 201 if ( 202 !quads.length || 203 quads[0].bounds.width <= 0 || 204 quads[0].bounds.height <= 0 205 ) { 206 this._hideShapes(); 207 return false; 208 } 209 210 const [quad] = quads; 211 212 // Getting the points for the untransformed shape 213 const untransformedQuad = getNodeBounds(this.win, this.currentNode); 214 215 this._setPolygonPoints(quad, "css-transform-transformed"); 216 this._setPolygonPoints(untransformedQuad, "css-transform-untransformed"); 217 this._setLinePoints(untransformedQuad.p1, quad.p1, "css-transform-line1"); 218 this._setLinePoints(untransformedQuad.p2, quad.p2, "css-transform-line2"); 219 this._setLinePoints(untransformedQuad.p3, quad.p3, "css-transform-line3"); 220 this._setLinePoints(untransformedQuad.p4, quad.p4, "css-transform-line4"); 221 222 // Adapt to the current zoom 223 this.markup.scaleRootElement(this.currentNode, "css-transform-root"); 224 225 this._showShapes(); 226 227 setIgnoreLayoutChanges( 228 false, 229 this.highlighterEnv.window.document.documentElement 230 ); 231 return true; 232 } 233 234 /** 235 * Hide the highlighter, the outline and the infobar. 236 */ 237 _hide() { 238 setIgnoreLayoutChanges(true); 239 this._hideShapes(); 240 setIgnoreLayoutChanges( 241 false, 242 this.highlighterEnv.window.document.documentElement 243 ); 244 } 245 246 _hideShapes() { 247 this.getElement("css-transform-elements").setAttribute("hidden", "true"); 248 } 249 250 _showShapes() { 251 this.getElement("css-transform-elements").removeAttribute("hidden"); 252 } 253 } 254 255 exports.CssTransformHighlighter = CssTransformHighlighter;