box-model.js (26080B)
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 getBindingElementAndPseudo, 13 hasPseudoClassLock, 14 isNodeValid, 15 moveInfobar, 16 } = require("resource://devtools/server/actors/highlighters/utils/markup.js"); 17 const { 18 PSEUDO_CLASSES, 19 } = require("resource://devtools/shared/css/constants.js"); 20 const { 21 getCurrentZoom, 22 setIgnoreLayoutChanges, 23 } = require("resource://devtools/shared/layout/utils.js"); 24 const { 25 getNodeDisplayName, 26 getNodeGridFlexType, 27 } = require("resource://devtools/server/actors/inspector/utils.js"); 28 const nodeConstants = require("resource://devtools/shared/dom-node-constants.js"); 29 loader.lazyGetter(this, "HighlightersBundle", () => { 30 return new Localization(["devtools/shared/highlighters.ftl"], true); 31 }); 32 loader.lazyRequireGetter(this, "flags", "resource://devtools/shared/flags.js"); 33 34 // Note that the order of items in this array is important because it is used 35 // for drawing the BoxModelHighlighter's path elements correctly. 36 const BOX_MODEL_REGIONS = ["margin", "border", "padding", "content"]; 37 const BOX_MODEL_SIDES = ["top", "right", "bottom", "left"]; 38 // Width of boxmodelhighlighter guides 39 const GUIDE_STROKE_WIDTH = 1; 40 41 /** 42 * The BoxModelHighlighter draws the box model regions on top of a node. 43 * If the node is a block box, then each region will be displayed as 1 polygon. 44 * If the node is an inline box though, each region may be represented by 1 or 45 * more polygons, depending on how many line boxes the inline element has. 46 * 47 * Usage example: 48 * 49 * let h = new BoxModelHighlighter(env); 50 * h.show(node, options); 51 * h.hide(); 52 * h.destroy(); 53 * 54 * @param {string} options.region 55 * Specifies the region that the guides should outline: 56 * "content" (default), "padding", "border" or "margin". 57 * @param {boolean} options.hideGuides 58 * Defaults to false 59 * @param {boolean} options.hideInfoBar 60 * Defaults to false 61 * @param {string} options.showOnly 62 * If set, only this region will be highlighted. Use with onlyRegionArea 63 * to only highlight the area of the region: 64 * "content", "padding", "border" or "margin" 65 * @param {boolean} options.onlyRegionArea 66 * This can be set to true to make each region's box only highlight the 67 * area of the corresponding region rather than the area of nested 68 * regions too. This is useful when used with showOnly. 69 * 70 * Structure: 71 * <div class="highlighter-container" aria-hidden="true"> 72 * <div class="box-model-root"> 73 * <svg class="box-model-elements" hidden="true"> 74 * <g class="box-model-regions"> 75 * <path class="box-model-margin" points="..." /> 76 * <path class="box-model-border" points="..." /> 77 * <path class="box-model-padding" points="..." /> 78 * <path class="box-model-content" points="..." /> 79 * </g> 80 * <line class="box-model-guide-top" x1="..." y1="..." x2="..." y2="..." /> 81 * <line class="box-model-guide-right" x1="..." y1="..." x2="..." y2="..." /> 82 * <line class="box-model-guide-bottom" x1="..." y1="..." x2="..." y2="..." /> 83 * <line class="box-model-guide-left" x1="..." y1="..." x2="..." y2="..." /> 84 * </svg> 85 * <div class="box-model-infobar-container"> 86 * <div class="box-model-infobar-arrow highlighter-infobar-arrow-top" /> 87 * <div class="box-model-infobar"> 88 * <div class="box-model-infobar-text" align="center"> 89 * <span class="box-model-infobar-tagname">Node name</span> 90 * <span class="box-model-infobar-id">Node id</span> 91 * <span class="box-model-infobar-classes">.someClass</span> 92 * <span class="box-model-infobar-pseudo-classes">:hover</span> 93 * <span class="box-model-infobar-grid-type">Grid Type</span> 94 * <span class="box-model-infobar-flex-type">Flex Type</span> 95 * </div> 96 * </div> 97 * <div class="box-model-infobar-arrow box-model-infobar-arrow-bottom"/> 98 * </div> 99 * </div> 100 * </div> 101 */ 102 class BoxModelHighlighter extends AutoRefreshHighlighter { 103 constructor(highlighterEnv) { 104 super(highlighterEnv); 105 106 this.markup = new CanvasFrameAnonymousContentHelper( 107 this.highlighterEnv, 108 this._buildMarkup.bind(this), 109 { 110 contentRootHostClassName: "devtools-highlighter-box-model", 111 } 112 ); 113 this.isReady = this.markup.initialize(); 114 115 this.onPageHide = this.onPageHide.bind(this); 116 this.onWillNavigate = this.onWillNavigate.bind(this); 117 118 this.highlighterEnv.on("will-navigate", this.onWillNavigate); 119 120 const { pageListenerTarget } = highlighterEnv; 121 pageListenerTarget.addEventListener("pagehide", this.onPageHide); 122 } 123 124 /** 125 * Static getter that indicates that BoxModelHighlighter supports 126 * highlighting in XUL windows. 127 */ 128 static get XULSupported() { 129 return true; 130 } 131 132 get supportsSimpleHighlighters() { 133 return true; 134 } 135 136 _buildMarkup() { 137 const highlighterContainer = 138 this.markup.anonymousContentDocument.createElement("div"); 139 highlighterContainer.className = "highlighter-container box-model"; 140 141 this.highlighterContainer = highlighterContainer; 142 // We need a better solution for how to handle the highlighter from the 143 // accessibility standpoint. For now, in order to avoid displaying it in the 144 // accessibility tree lets hide it altogether. See bug 1598667 for more 145 // context. 146 highlighterContainer.setAttribute("aria-hidden", "true"); 147 148 // Build the root wrapper, used to adapt to the page zoom. 149 this.rootEl = this.markup.createNode({ 150 parent: highlighterContainer, 151 attributes: { 152 id: "box-model-root", 153 class: 154 "box-model-root" + 155 (this.highlighterEnv.useSimpleHighlightersForReducedMotion 156 ? " use-simple-highlighters" 157 : ""), 158 role: "presentation", 159 }, 160 }); 161 162 // Building the SVG element with its polygons and lines 163 164 const svg = this.markup.createSVGNode({ 165 nodeType: "svg", 166 parent: this.rootEl, 167 attributes: { 168 id: "box-model-elements", 169 width: "100%", 170 height: "100%", 171 hidden: "true", 172 role: "presentation", 173 }, 174 }); 175 176 const regions = this.markup.createSVGNode({ 177 nodeType: "g", 178 parent: svg, 179 attributes: { 180 class: "box-model-regions", 181 role: "presentation", 182 }, 183 }); 184 185 for (const region of BOX_MODEL_REGIONS) { 186 this.markup.createSVGNode({ 187 nodeType: "path", 188 parent: regions, 189 attributes: { 190 class: "box-model-" + region, 191 id: "box-model-" + region, 192 role: "presentation", 193 }, 194 }); 195 } 196 197 for (const side of BOX_MODEL_SIDES) { 198 this.markup.createSVGNode({ 199 nodeType: "line", 200 parent: svg, 201 attributes: { 202 class: "box-model-guide-" + side, 203 id: "box-model-guide-" + side, 204 "stroke-width": GUIDE_STROKE_WIDTH, 205 role: "presentation", 206 }, 207 }); 208 } 209 210 // Building the nodeinfo bar markup 211 212 const infobarContainer = this.markup.createNode({ 213 parent: this.rootEl, 214 attributes: { 215 class: "box-model-infobar-container", 216 id: "box-model-infobar-container", 217 position: "top", 218 hidden: "true", 219 }, 220 }); 221 222 const infobar = this.markup.createNode({ 223 parent: infobarContainer, 224 attributes: { 225 class: "box-model-infobar", 226 }, 227 }); 228 229 const texthbox = this.markup.createNode({ 230 parent: infobar, 231 attributes: { 232 class: "box-model-infobar-text", 233 }, 234 }); 235 this.markup.createNode({ 236 nodeType: "span", 237 parent: texthbox, 238 attributes: { 239 class: "box-model-infobar-tagname", 240 id: "box-model-infobar-tagname", 241 }, 242 }); 243 this.markup.createNode({ 244 nodeType: "span", 245 parent: texthbox, 246 attributes: { 247 class: "box-model-infobar-id", 248 id: "box-model-infobar-id", 249 }, 250 }); 251 this.markup.createNode({ 252 nodeType: "span", 253 parent: texthbox, 254 attributes: { 255 class: "box-model-infobar-classes", 256 id: "box-model-infobar-classes", 257 }, 258 }); 259 this.markup.createNode({ 260 nodeType: "span", 261 parent: texthbox, 262 attributes: { 263 class: "box-model-infobar-pseudo-classes", 264 id: "box-model-infobar-pseudo-classes", 265 }, 266 }); 267 this.markup.createNode({ 268 nodeType: "span", 269 parent: texthbox, 270 attributes: { 271 class: "box-model-infobar-dimensions", 272 id: "box-model-infobar-dimensions", 273 }, 274 }); 275 276 this.markup.createNode({ 277 nodeType: "span", 278 parent: texthbox, 279 attributes: { 280 class: "box-model-infobar-grid-type", 281 id: "box-model-infobar-grid-type", 282 }, 283 }); 284 285 this.markup.createNode({ 286 nodeType: "span", 287 parent: texthbox, 288 attributes: { 289 class: "box-model-infobar-flex-type", 290 id: "box-model-infobar-flex-type", 291 }, 292 }); 293 294 return highlighterContainer; 295 } 296 297 /** 298 * Destroy the nodes. Remove listeners. 299 */ 300 destroy() { 301 this.highlighterEnv.off("will-navigate", this.onWillNavigate); 302 303 const { pageListenerTarget } = this.highlighterEnv; 304 if (pageListenerTarget) { 305 pageListenerTarget.removeEventListener("pagehide", this.onPageHide); 306 } 307 308 this.markup.destroy(); 309 this.rootEl = null; 310 311 AutoRefreshHighlighter.prototype.destroy.call(this); 312 } 313 314 getElement(id) { 315 return this.markup.getElement(id); 316 } 317 318 /** 319 * Override the AutoRefreshHighlighter's _isNodeValid method to also return true for 320 * text nodes since these can also be highlighted. 321 * 322 * @param {DOMNode} node 323 * @return {boolean} 324 */ 325 _isNodeValid(node) { 326 return ( 327 node && (isNodeValid(node) || isNodeValid(node, nodeConstants.TEXT_NODE)) 328 ); 329 } 330 331 /** 332 * Show the highlighter on a given node 333 */ 334 _show() { 335 if (!BOX_MODEL_REGIONS.includes(this.options.region)) { 336 this.options.region = "content"; 337 } 338 339 const shown = this._update(); 340 this._trackMutations(); 341 return shown; 342 } 343 344 /** 345 * Track the current node markup mutations so that the node info bar can be 346 * updated to reflects the node's attributes 347 */ 348 _trackMutations() { 349 if (isNodeValid(this.currentNode)) { 350 const win = this.currentNode.ownerGlobal; 351 this.currentNodeObserver = new win.MutationObserver(this.update); 352 this.currentNodeObserver.observe(this.currentNode, { attributes: true }); 353 } 354 } 355 356 _untrackMutations() { 357 if (isNodeValid(this.currentNode) && this.currentNodeObserver) { 358 this.currentNodeObserver.disconnect(); 359 this.currentNodeObserver = null; 360 } 361 } 362 363 /** 364 * Update the highlighter on the current highlighted node (the one that was 365 * passed as an argument to show(node)). 366 * Should be called whenever node size or attributes change 367 */ 368 _update() { 369 const node = this.currentNode; 370 let shown = false; 371 setIgnoreLayoutChanges(true); 372 373 if (this._updateBoxModel()) { 374 const isElementOrTextNode = 375 node.nodeType === node.ELEMENT_NODE || node.nodeType === node.TEXT_NODE; 376 // Show the infobar only if configured to do so and the node is an element or a text 377 // node. 378 if (isElementOrTextNode) { 379 if (!this.options.hideInfoBar) { 380 this._showInfobar(); 381 } else if (flags.testing) { 382 // Even if we don't want to show the info bar, update the infobar data (tag, id, 383 // classes, …) anyway so it's easier to debug/test 384 this._updateInfobarNodeData(); 385 } else { 386 this._hideInfobar(); 387 } 388 } else { 389 this._hideInfobar(); 390 } 391 392 this._updateSimpleHighlighters(); 393 this._showBoxModel(); 394 395 shown = true; 396 } else { 397 // Nothing to highlight (0px rectangle like a <script> tag for instance) 398 this._hide(); 399 } 400 401 setIgnoreLayoutChanges( 402 false, 403 this.highlighterEnv.window.document.documentElement 404 ); 405 406 return shown; 407 } 408 409 _scrollUpdate() { 410 this._moveInfobar(); 411 } 412 413 /** 414 * Hide the highlighter, the outline and the infobar. 415 */ 416 _hide() { 417 setIgnoreLayoutChanges(true); 418 419 this._untrackMutations(); 420 this._hideBoxModel(); 421 this._hideInfobar(); 422 423 setIgnoreLayoutChanges( 424 false, 425 this.highlighterEnv.window.document.documentElement 426 ); 427 } 428 429 /** 430 * Hide the infobar 431 */ 432 _hideInfobar() { 433 this.getElement("box-model-infobar-container").setAttribute( 434 "hidden", 435 "true" 436 ); 437 } 438 439 /** 440 * Show the infobar 441 */ 442 _showInfobar() { 443 this.getElement("box-model-infobar-container").removeAttribute("hidden"); 444 this._updateInfobar(); 445 } 446 447 /** 448 * Hide the box model 449 */ 450 _hideBoxModel() { 451 this.getElement("box-model-elements").setAttribute("hidden", "true"); 452 } 453 454 /** 455 * Show the box model 456 */ 457 _showBoxModel() { 458 this.getElement("box-model-elements").removeAttribute("hidden"); 459 } 460 461 /** 462 * Calculate an outer quad based on the quads returned by getAdjustedQuads. 463 * The BoxModelHighlighter may highlight more than one boxes, so in this case 464 * create a new quad that "contains" all of these quads. 465 * This is useful to position the guides and infobar. 466 * This may happen if the BoxModelHighlighter is used to highlight an inline 467 * element that spans line breaks. 468 * 469 * @param {string} region The box-model region to get the outer quad for. 470 * @return {object} A quad-like object {p1,p2,p3,p4,bounds} 471 */ 472 _getOuterQuad(region) { 473 const quads = this.currentQuads[region]; 474 if (!quads || !quads.length) { 475 return null; 476 } 477 478 const quad = { 479 p1: { x: Infinity, y: Infinity }, 480 p2: { x: -Infinity, y: Infinity }, 481 p3: { x: -Infinity, y: -Infinity }, 482 p4: { x: Infinity, y: -Infinity }, 483 bounds: { 484 bottom: -Infinity, 485 height: 0, 486 left: Infinity, 487 right: -Infinity, 488 top: Infinity, 489 width: 0, 490 x: 0, 491 y: 0, 492 }, 493 }; 494 495 for (const q of quads) { 496 quad.p1.x = Math.min(quad.p1.x, q.p1.x); 497 quad.p1.y = Math.min(quad.p1.y, q.p1.y); 498 quad.p2.x = Math.max(quad.p2.x, q.p2.x); 499 quad.p2.y = Math.min(quad.p2.y, q.p2.y); 500 quad.p3.x = Math.max(quad.p3.x, q.p3.x); 501 quad.p3.y = Math.max(quad.p3.y, q.p3.y); 502 quad.p4.x = Math.min(quad.p4.x, q.p4.x); 503 quad.p4.y = Math.max(quad.p4.y, q.p4.y); 504 505 quad.bounds.bottom = Math.max(quad.bounds.bottom, q.bounds.bottom); 506 quad.bounds.top = Math.min(quad.bounds.top, q.bounds.top); 507 quad.bounds.left = Math.min(quad.bounds.left, q.bounds.left); 508 quad.bounds.right = Math.max(quad.bounds.right, q.bounds.right); 509 } 510 quad.bounds.x = quad.bounds.left; 511 quad.bounds.y = quad.bounds.top; 512 quad.bounds.width = quad.bounds.right - quad.bounds.left; 513 quad.bounds.height = quad.bounds.bottom - quad.bounds.top; 514 515 return quad; 516 } 517 518 /** 519 * Update the box model as per the current node. 520 * 521 * @return {boolean} 522 * True if the current node has a box model to be highlighted 523 */ 524 _updateBoxModel() { 525 const options = this.options; 526 options.region = options.region || "content"; 527 528 if (!this._nodeNeedsHighlighting()) { 529 this._hideBoxModel(); 530 return false; 531 } 532 533 for (let i = 0; i < BOX_MODEL_REGIONS.length; i++) { 534 const boxType = BOX_MODEL_REGIONS[i]; 535 const nextBoxType = BOX_MODEL_REGIONS[i + 1]; 536 const box = this.getElement("box-model-" + boxType); 537 538 // Highlight all quads for this region by setting the "d" attribute of the 539 // corresponding <path>. 540 const path = []; 541 for (let j = 0; j < this.currentQuads[boxType].length; j++) { 542 const boxQuad = this.currentQuads[boxType][j]; 543 const nextBoxQuad = this.currentQuads[nextBoxType] 544 ? this.currentQuads[nextBoxType][j] 545 : null; 546 path.push(this._getBoxPathCoordinates(boxQuad, nextBoxQuad)); 547 } 548 549 box.setAttribute("d", path.join(" ")); 550 box.removeAttribute("faded"); 551 552 // If showOnly is defined, either hide the other regions, or fade them out 553 // if onlyRegionArea is set too. 554 if (options.showOnly && options.showOnly !== boxType) { 555 if (options.onlyRegionArea) { 556 box.setAttribute("faded", "true"); 557 } else { 558 box.removeAttribute("d"); 559 } 560 } 561 562 if (boxType === options.region && !options.hideGuides) { 563 this._showGuides(boxType); 564 } else if (options.hideGuides) { 565 this._hideGuides(); 566 } 567 } 568 569 // Un-zoom the root wrapper if the page was zoomed. 570 this.markup.scaleRootElement(this.currentNode, "box-model-elements"); 571 572 return true; 573 } 574 575 _getBoxPathCoordinates(boxQuad, nextBoxQuad) { 576 const { p1, p2, p3, p4 } = boxQuad; 577 578 let path; 579 if (!nextBoxQuad || !this.options.onlyRegionArea) { 580 // If this is the content box (inner-most box) or if we're not being asked 581 // to highlight only region areas, then draw a simple rectangle. 582 path = 583 "M" + 584 p1.x + 585 "," + 586 p1.y + 587 " " + 588 "L" + 589 p2.x + 590 "," + 591 p2.y + 592 " " + 593 "L" + 594 p3.x + 595 "," + 596 p3.y + 597 " " + 598 "L" + 599 p4.x + 600 "," + 601 p4.y + 602 " " + 603 "L" + 604 p1.x + 605 "," + 606 p1.y; 607 } else { 608 // Otherwise, just draw the region itself, not a filled rectangle. 609 const { p1: np1, p2: np2, p3: np3, p4: np4 } = nextBoxQuad; 610 path = 611 "M" + 612 p1.x + 613 "," + 614 p1.y + 615 " " + 616 "L" + 617 p2.x + 618 "," + 619 p2.y + 620 " " + 621 "L" + 622 p3.x + 623 "," + 624 p3.y + 625 " " + 626 "L" + 627 p4.x + 628 "," + 629 p4.y + 630 " " + 631 "L" + 632 p1.x + 633 "," + 634 p1.y + 635 " " + 636 "L" + 637 np1.x + 638 "," + 639 np1.y + 640 " " + 641 "L" + 642 np4.x + 643 "," + 644 np4.y + 645 " " + 646 "L" + 647 np3.x + 648 "," + 649 np3.y + 650 " " + 651 "L" + 652 np2.x + 653 "," + 654 np2.y + 655 " " + 656 "L" + 657 np1.x + 658 "," + 659 np1.y; 660 } 661 662 return path; 663 } 664 665 /** 666 * Can the current node be highlighted? Does it have quads. 667 * 668 * @return {boolean} 669 */ 670 _nodeNeedsHighlighting() { 671 return ( 672 this.currentQuads.margin.length || 673 this.currentQuads.border.length || 674 this.currentQuads.padding.length || 675 this.currentQuads.content.length 676 ); 677 } 678 679 _getOuterBounds() { 680 for (const region of ["margin", "border", "padding", "content"]) { 681 const quad = this._getOuterQuad(region); 682 683 if (!quad) { 684 // Invisible element such as a script tag. 685 break; 686 } 687 688 const { bottom, height, left, right, top, width, x, y } = quad.bounds; 689 690 if (width > 0 || height > 0) { 691 return { bottom, height, left, right, top, width, x, y }; 692 } 693 } 694 695 return { 696 bottom: 0, 697 height: 0, 698 left: 0, 699 right: 0, 700 top: 0, 701 width: 0, 702 x: 0, 703 y: 0, 704 }; 705 } 706 707 /** 708 * We only want to show guides for horizontal and vertical edges as this helps 709 * to line them up. This method finds these edges and displays a guide there. 710 * 711 * @param {string} region The region around which the guides should be shown. 712 */ 713 _showGuides(region) { 714 const quad = this._getOuterQuad(region); 715 716 if (!quad) { 717 // Invisible element such as a script tag. 718 return; 719 } 720 721 const { p1, p2, p3, p4 } = quad; 722 723 const allX = [p1.x, p2.x, p3.x, p4.x].sort((a, b) => a - b); 724 const allY = [p1.y, p2.y, p3.y, p4.y].sort((a, b) => a - b); 725 const toShowX = []; 726 const toShowY = []; 727 728 for (const arr of [allX, allY]) { 729 for (let i = 0; i < arr.length; i++) { 730 const val = arr[i]; 731 732 if (i !== arr.lastIndexOf(val)) { 733 if (arr === allX) { 734 toShowX.push(val); 735 } else { 736 toShowY.push(val); 737 } 738 arr.splice(arr.lastIndexOf(val), 1); 739 } 740 } 741 } 742 743 // Move guide into place or hide it if no valid co-ordinate was found. 744 this._updateGuide("top", Math.round(toShowY[0])); 745 this._updateGuide("right", Math.round(toShowX[1]) - 1); 746 this._updateGuide("bottom", Math.round(toShowY[1]) - 1); 747 this._updateGuide("left", Math.round(toShowX[0])); 748 } 749 750 _hideGuides() { 751 for (const side of BOX_MODEL_SIDES) { 752 this.getElement("box-model-guide-" + side).setAttribute("hidden", "true"); 753 } 754 } 755 756 /** 757 * Move a guide to the appropriate position and display it. If no point is 758 * passed then the guide is hidden. 759 * 760 * @param {string} side 761 * The guide to update 762 * @param {Integer} point 763 * x or y co-ordinate. If this is undefined we hide the guide. 764 */ 765 _updateGuide(side, point) { 766 const guide = this.getElement("box-model-guide-" + side); 767 768 if (!point || point <= 0) { 769 guide.setAttribute("hidden", "true"); 770 return false; 771 } 772 773 if (side === "top" || side === "bottom") { 774 guide.setAttribute("x1", "0"); 775 guide.setAttribute("y1", point + ""); 776 guide.setAttribute("x2", "100%"); 777 guide.setAttribute("y2", point + ""); 778 } else { 779 guide.setAttribute("x1", point + ""); 780 guide.setAttribute("y1", "0"); 781 guide.setAttribute("x2", point + ""); 782 guide.setAttribute("y2", "100%"); 783 } 784 785 guide.removeAttribute("hidden"); 786 787 return true; 788 } 789 790 /** 791 * Update node information (displayName#id.class) 792 */ 793 _updateInfobar() { 794 if (!this.currentNode) { 795 return; 796 } 797 798 const { bindingElement: node } = getBindingElementAndPseudo( 799 this.currentNode 800 ); 801 802 // We want to display the original `width` and `height`, instead of the ones affected 803 // by any zoom. Since the infobar can be displayed also for text nodes, we can't 804 // access the computed style for that, and this is why we recalculate them here. 805 const zoom = getCurrentZoom(this.win); 806 const quad = this._getOuterQuad("border"); 807 808 if (!quad) { 809 return; 810 } 811 812 const { width, height } = quad.bounds; 813 const dim = 814 parseFloat((width / zoom).toPrecision(6)) + 815 " \u00D7 " + 816 parseFloat((height / zoom).toPrecision(6)); 817 818 const { grid: gridType, flex: flexType } = getNodeGridFlexType(node); 819 const gridLayoutTextType = this._getLayoutTextType("gridtype", gridType); 820 const flexLayoutTextType = this._getLayoutTextType("flextype", flexType); 821 822 // Update the tag, id, classes, pseudo-classes 823 this._updateInfobarNodeData(); 824 this.getElement("box-model-infobar-dimensions").setTextContent(dim); 825 this.getElement("box-model-infobar-grid-type").setTextContent( 826 gridLayoutTextType 827 ); 828 this.getElement("box-model-infobar-flex-type").setTextContent( 829 flexLayoutTextType 830 ); 831 832 this._moveInfobar(); 833 } 834 835 _updateInfobarNodeData() { 836 if (!this.currentNode) { 837 return; 838 } 839 840 // The binding element of a pseudo element can also be a pseudo element (for example 841 // ::before::marker), so walk up through the tree until we get a non pseudo binding 842 // element. 843 let node = this.currentNode, 844 pseudo = ""; 845 while (true) { 846 const res = getBindingElementAndPseudo(node); 847 848 // Stop as soon as the binding element is the same as the passed node, meaning we 849 // found the ultimate originating element (https://drafts.csswg.org/selectors-4/#ultimate-originating-element). 850 if (res.bindingElement === node) { 851 break; 852 } 853 854 node = res.bindingElement; 855 pseudo = res.pseudo + pseudo; 856 } 857 858 // Update the tag, id, classes, pseudo-classes and dimensions 859 const displayName = getNodeDisplayName(node); 860 861 const id = node.id ? "#" + node.id : ""; 862 863 const classList = node.classList?.length 864 ? "." + [...node.classList].join(".") 865 : ""; 866 867 let pseudos = this._getPseudoClasses(node).join(""); 868 if (pseudo) { 869 pseudos += pseudo; 870 } 871 872 this.getElement("box-model-infobar-tagname").setTextContent(displayName); 873 this.getElement("box-model-infobar-id").setTextContent(id); 874 this.getElement("box-model-infobar-classes").setTextContent(classList); 875 this.getElement("box-model-infobar-pseudo-classes").setTextContent(pseudos); 876 } 877 878 _getLayoutTextType(layoutTypeKey, { isContainer, isItem }) { 879 if (!isContainer && !isItem) { 880 return ""; 881 } 882 if (isContainer && !isItem) { 883 return HighlightersBundle.formatValueSync(`${layoutTypeKey}-container`); 884 } 885 if (!isContainer && isItem) { 886 return HighlightersBundle.formatValueSync(`${layoutTypeKey}-item`); 887 } 888 return HighlightersBundle.formatValueSync(`${layoutTypeKey}-dual`); 889 } 890 891 _getPseudoClasses(node) { 892 if (node.nodeType !== nodeConstants.ELEMENT_NODE) { 893 // hasPseudoClassLock can only be used on Elements. 894 return []; 895 } 896 897 return PSEUDO_CLASSES.filter(pseudo => hasPseudoClassLock(node, pseudo)); 898 } 899 900 /** 901 * Move the Infobar to the right place in the highlighter. 902 */ 903 _moveInfobar() { 904 const bounds = this._getOuterBounds(); 905 const container = this.getElement("box-model-infobar-container"); 906 907 moveInfobar(container, bounds, this.win); 908 } 909 910 onPageHide({ target }) { 911 // If a pagehide event is triggered for current window's highlighter, hide the 912 // highlighter. 913 if (target.defaultView === this.win) { 914 this.hide(); 915 } 916 } 917 918 onWillNavigate({ isTopLevel }) { 919 if (isTopLevel) { 920 this.hide(); 921 } 922 } 923 } 924 925 exports.BoxModelHighlighter = BoxModelHighlighter;