node-tabbing-order.js (9883B)
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 loader.lazyRequireGetter( 8 this, 9 ["setIgnoreLayoutChanges", "getCurrentZoom"], 10 "resource://devtools/shared/layout/utils.js", 11 true 12 ); 13 loader.lazyRequireGetter( 14 this, 15 "AutoRefreshHighlighter", 16 "resource://devtools/server/actors/highlighters/auto-refresh.js", 17 true 18 ); 19 loader.lazyRequireGetter( 20 this, 21 ["CanvasFrameAnonymousContentHelper"], 22 "resource://devtools/server/actors/highlighters/utils/markup.js", 23 true 24 ); 25 26 /** 27 * The NodeTabbingOrderHighlighter draws an outline around a node (based on its 28 * border bounds). 29 * 30 * Usage example: 31 * 32 * const h = new NodeTabbingOrderHighlighter(env); 33 * await h.isReady(); 34 * h.show(node, options); 35 * h.hide(); 36 * h.destroy(); 37 * 38 * @param {number} options.index 39 * Tabbing index value to be displayed in the highlighter info bar. 40 */ 41 class NodeTabbingOrderHighlighter extends AutoRefreshHighlighter { 42 constructor(highlighterEnv) { 43 super(highlighterEnv); 44 45 this._doNotStartRefreshLoop = true; 46 this.markup = new CanvasFrameAnonymousContentHelper( 47 this.highlighterEnv, 48 this._buildMarkup.bind(this), 49 { 50 contentRootHostClassName: "devtools-highlighter-tabbing-order", 51 } 52 ); 53 this.isReady = this.markup.initialize(); 54 } 55 56 _buildMarkup() { 57 this.rootEl = this.markup.createNode({ 58 attributes: { 59 id: "tabbing-order-root", 60 class: "tabbing-order-root highlighter-container tabbing-order", 61 "aria-hidden": "true", 62 }, 63 }); 64 65 const container = this.markup.createNode({ 66 parent: this.rootEl, 67 attributes: { 68 id: "tabbing-order-container", 69 width: "100%", 70 height: "100%", 71 hidden: "true", 72 }, 73 }); 74 75 // Building the SVG element 76 this.markup.createNode({ 77 parent: container, 78 attributes: { 79 class: "tabbing-order-bounds", 80 id: "tabbing-order-bounds", 81 }, 82 }); 83 84 // Building the nodeinfo bar markup 85 86 const infobarContainer = this.markup.createNode({ 87 parent: this.rootEl, 88 attributes: { 89 class: "tabbing-order-infobar-container", 90 id: "tabbing-order-infobar-container", 91 position: "top", 92 hidden: "true", 93 }, 94 }); 95 96 const infobar = this.markup.createNode({ 97 parent: infobarContainer, 98 attributes: { 99 class: "tabbing-order-infobar", 100 }, 101 }); 102 103 this.markup.createNode({ 104 parent: infobar, 105 attributes: { 106 class: "tabbing-order-infobar-text", 107 id: "tabbing-order-infobar-text", 108 }, 109 }); 110 111 return this.rootEl; 112 } 113 114 /** 115 * Destroy the nodes. Remove listeners. 116 */ 117 destroy() { 118 this.markup.destroy(); 119 this.rootEl = null; 120 121 AutoRefreshHighlighter.prototype.destroy.call(this); 122 } 123 124 getElement(id) { 125 return this.markup.getElement(id); 126 } 127 128 /** 129 * Update focused styling for a node tabbing index highlight. 130 * 131 * @param {boolean} focused 132 * Indicates if the highlighted node needs to be focused. 133 */ 134 updateFocus(focused) { 135 const root = this.getElement("tabbing-order-root"); 136 root.classList?.toggle("focused", focused); 137 } 138 139 /** 140 * Show the highlighter on a given node 141 */ 142 _show() { 143 return this._update(); 144 } 145 146 /** 147 * Update the highlighter on the current highlighted node (the one that was 148 * passed as an argument to show(node)). 149 * Should be called whenever node size or attributes change 150 */ 151 _update() { 152 let shown = false; 153 setIgnoreLayoutChanges(true); 154 155 if (this._updateTabbingOrder()) { 156 this._showInfobar(); 157 this._showTabbingOrder(); 158 shown = true; 159 setIgnoreLayoutChanges( 160 false, 161 this.highlighterEnv.window.document.documentElement 162 ); 163 } else { 164 // Nothing to highlight (0px rectangle like a <script> tag for instance) 165 this._hide(); 166 } 167 168 return shown; 169 } 170 171 /** 172 * Hide the highlighter, the outline and the infobar. 173 */ 174 _hide() { 175 setIgnoreLayoutChanges(true); 176 177 this._hideTabbingOrder(); 178 this._hideInfobar(); 179 180 setIgnoreLayoutChanges( 181 false, 182 this.highlighterEnv.window.document.documentElement 183 ); 184 } 185 186 /** 187 * Hide the infobar 188 */ 189 _hideInfobar() { 190 this.getElement("tabbing-order-infobar-container").setAttribute( 191 "hidden", 192 "true" 193 ); 194 } 195 196 /** 197 * Show the infobar 198 */ 199 _showInfobar() { 200 if (!this.currentNode) { 201 return; 202 } 203 204 this.getElement("tabbing-order-infobar-container").removeAttribute( 205 "hidden" 206 ); 207 this.getElement("tabbing-order-infobar-text").setTextContent( 208 this.options.index 209 ); 210 const bounds = this._getBounds(); 211 const container = this.getElement("tabbing-order-infobar-container"); 212 213 moveInfobar(container, bounds, this.win); 214 } 215 216 /** 217 * Hide the tabbing order highlighter 218 */ 219 _hideTabbingOrder() { 220 this.getElement("tabbing-order-container").setAttribute("hidden", "true"); 221 } 222 223 /** 224 * Show the tabbing order highlighter 225 */ 226 _showTabbingOrder() { 227 this.getElement("tabbing-order-container").removeAttribute("hidden"); 228 } 229 230 /** 231 * Calculate border bounds based on the quads returned by getAdjustedQuads. 232 * 233 * @return {object} A bounds object {bottom,height,left,right,top,width,x,y} 234 */ 235 _getBorderBounds() { 236 const quads = this.currentQuads.border; 237 if (!quads || !quads.length) { 238 return null; 239 } 240 241 const bounds = { 242 bottom: -Infinity, 243 height: 0, 244 left: Infinity, 245 right: -Infinity, 246 top: Infinity, 247 width: 0, 248 x: 0, 249 y: 0, 250 }; 251 252 for (const q of quads) { 253 bounds.bottom = Math.max(bounds.bottom, q.bounds.bottom); 254 bounds.top = Math.min(bounds.top, q.bounds.top); 255 bounds.left = Math.min(bounds.left, q.bounds.left); 256 bounds.right = Math.max(bounds.right, q.bounds.right); 257 } 258 bounds.x = bounds.left; 259 bounds.y = bounds.top; 260 bounds.width = bounds.right - bounds.left; 261 bounds.height = bounds.bottom - bounds.top; 262 263 return bounds; 264 } 265 266 /** 267 * Update the tabbing order index as per the current node. 268 * 269 * @return {boolean} 270 * True if the current node has a tabbing order index to be 271 * highlighted 272 */ 273 _updateTabbingOrder() { 274 if (!this._nodeNeedsHighlighting()) { 275 this._hideTabbingOrder(); 276 return false; 277 } 278 279 const boundsEl = this.getElement("tabbing-order-bounds"); 280 const { left, top, width, height } = this._getBounds(); 281 boundsEl.setAttribute( 282 "style", 283 `top: ${top}px; left: ${left}px; width: ${width}px; height: ${height}px;` 284 ); 285 286 // Un-zoom the root wrapper if the page was zoomed. 287 this.markup.scaleRootElement(this.currentNode, "tabbing-order-container"); 288 289 return true; 290 } 291 292 /** 293 * Can the current node be highlighted? Does it have quads. 294 * 295 * @return {boolean} 296 */ 297 _nodeNeedsHighlighting() { 298 return ( 299 this.currentQuads.margin.length || 300 this.currentQuads.border.length || 301 this.currentQuads.padding.length || 302 this.currentQuads.content.length 303 ); 304 } 305 306 _getBounds() { 307 const borderBounds = this._getBorderBounds(); 308 let bounds = { 309 bottom: 0, 310 height: 0, 311 left: 0, 312 right: 0, 313 top: 0, 314 width: 0, 315 x: 0, 316 y: 0, 317 }; 318 319 if (!borderBounds) { 320 // Invisible element such as a script tag. 321 return bounds; 322 } 323 324 const { bottom, height, left, right, top, width, x, y } = borderBounds; 325 if (width > 0 || height > 0) { 326 bounds = { bottom, height, left, right, top, width, x, y }; 327 } 328 329 return bounds; 330 } 331 } 332 333 /** 334 * Move the infobar to the right place in the highlighter. The infobar is used 335 * to display element's tabbing order index. 336 * 337 * @param {DOMNode} container 338 * The container element which will be used to position the infobar. 339 * @param {object} bounds 340 * The content bounds of the container element. 341 * @param {Window} win 342 * The window object. 343 */ 344 function moveInfobar(container, bounds, win) { 345 const zoom = getCurrentZoom(win); 346 const { computedStyle } = container; 347 const margin = 2; 348 const arrowSize = 349 parseFloat( 350 computedStyle.getPropertyValue("--highlighter-bubble-arrow-size") 351 ) - 2; 352 const containerHeight = parseFloat(computedStyle.getPropertyValue("height")); 353 const containerWidth = parseFloat(computedStyle.getPropertyValue("width")); 354 355 const topBoundary = margin; 356 const bottomBoundary = 357 win.document.scrollingElement.scrollHeight - containerHeight - margin - 1; 358 const leftBoundary = containerWidth / 2 + margin; 359 360 let top = bounds.y - containerHeight - arrowSize; 361 let left = bounds.x + bounds.width / 2; 362 const bottom = bounds.bottom + arrowSize; 363 let positionAttribute = "top"; 364 365 const canBePlacedOnTop = top >= topBoundary; 366 const canBePlacedOnBottom = bottomBoundary - bottom > 0; 367 368 if (!canBePlacedOnTop && canBePlacedOnBottom) { 369 top = bottom; 370 positionAttribute = "bottom"; 371 } 372 373 let hideArrow = false; 374 if (top < topBoundary) { 375 hideArrow = true; 376 top = topBoundary; 377 } else if (top > bottomBoundary) { 378 hideArrow = true; 379 top = bottomBoundary; 380 } 381 382 if (left < leftBoundary) { 383 hideArrow = true; 384 left = leftBoundary; 385 } 386 387 if (hideArrow) { 388 container.setAttribute("hide-arrow", "true"); 389 } else { 390 container.removeAttribute("hide-arrow"); 391 } 392 393 container.setAttribute( 394 "style", 395 ` 396 position: absolute; 397 transform-origin: 0 0; 398 transform: scale(${1 / zoom}) translate(calc(${left}px - 50%), ${top}px)` 399 ); 400 401 container.setAttribute("position", positionAttribute); 402 } 403 404 exports.NodeTabbingOrderHighlighter = NodeTabbingOrderHighlighter;