/* 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, getBindingElementAndPseudo, hasPseudoClassLock, isNodeValid, moveInfobar, } = require("resource://devtools/server/actors/highlighters/utils/markup.js"); const { PSEUDO_CLASSES, } = require("resource://devtools/shared/css/constants.js"); const { getCurrentZoom, setIgnoreLayoutChanges, } = require("resource://devtools/shared/layout/utils.js"); const { getNodeDisplayName, getNodeGridFlexType, } = require("resource://devtools/server/actors/inspector/utils.js"); const nodeConstants = require("resource://devtools/shared/dom-node-constants.js"); loader.lazyGetter(this, "HighlightersBundle", () => { return new Localization(["devtools/shared/highlighters.ftl"], true); }); // Note that the order of items in this array is important because it is used // for drawing the BoxModelHighlighter's path elements correctly. const BOX_MODEL_REGIONS = ["margin", "border", "padding", "content"]; const BOX_MODEL_SIDES = ["top", "right", "bottom", "left"]; // Width of boxmodelhighlighter guides const GUIDE_STROKE_WIDTH = 1; /** * The BoxModelHighlighter draws the box model regions on top of a node. * If the node is a block box, then each region will be displayed as 1 polygon. * If the node is an inline box though, each region may be represented by 1 or * more polygons, depending on how many line boxes the inline element has. * * Usage example: * * let h = new BoxModelHighlighter(env); * h.show(node, options); * h.hide(); * h.destroy(); * * @param {String} options.region * Specifies the region that the guides should outline: * "content" (default), "padding", "border" or "margin". * @param {Boolean} options.hideGuides * Defaults to false * @param {Boolean} options.hideInfoBar * Defaults to false * @param {String} options.showOnly * If set, only this region will be highlighted. Use with onlyRegionArea * to only highlight the area of the region: * "content", "padding", "border" or "margin" * @param {Boolean} options.onlyRegionArea * This can be set to true to make each region's box only highlight the * area of the corresponding region rather than the area of nested * regions too. This is useful when used with showOnly. * * Structure: *
*/ class BoxModelHighlighter extends AutoRefreshHighlighter { constructor(highlighterEnv) { super(highlighterEnv); this.ID_CLASS_PREFIX = "box-model-"; this.markup = new CanvasFrameAnonymousContentHelper( this.highlighterEnv, this._buildMarkup.bind(this) ); this.isReady = this.markup.initialize(); this.onPageHide = this.onPageHide.bind(this); this.onWillNavigate = this.onWillNavigate.bind(this); this.highlighterEnv.on("will-navigate", this.onWillNavigate); const { pageListenerTarget } = highlighterEnv; pageListenerTarget.addEventListener("pagehide", this.onPageHide); } /** * Static getter that indicates that BoxModelHighlighter supports * highlighting in XUL windows. */ static get XULSupported() { return true; } get supportsSimpleHighlighters() { return true; } _buildMarkup() { const highlighterContainer = this.markup.anonymousContentDocument.createElement("div"); highlighterContainer.className = "highlighter-container box-model"; this.highlighterContainer = highlighterContainer; // We need a better solution for how to handle the highlighter from the // accessibility standpoint. For now, in order to avoid displaying it in the // accessibility tree lets hide it altogether. See bug 1598667 for more // context. highlighterContainer.setAttribute("aria-hidden", "true"); // Build the root wrapper, used to adapt to the page zoom. const rootWrapper = this.markup.createNode({ parent: highlighterContainer, attributes: { id: "root", class: "root" + (this.highlighterEnv.useSimpleHighlightersForReducedMotion ? " use-simple-highlighters" : ""), role: "presentation", }, prefix: this.ID_CLASS_PREFIX, }); // Building the SVG element with its polygons and lines const svg = this.markup.createSVGNode({ nodeType: "svg", parent: rootWrapper, attributes: { id: "elements", width: "100%", height: "100%", hidden: "true", role: "presentation", }, prefix: this.ID_CLASS_PREFIX, }); const regions = this.markup.createSVGNode({ nodeType: "g", parent: svg, attributes: { class: "regions", role: "presentation", }, prefix: this.ID_CLASS_PREFIX, }); for (const region of BOX_MODEL_REGIONS) { this.markup.createSVGNode({ nodeType: "path", parent: regions, attributes: { class: region, id: region, role: "presentation", }, prefix: this.ID_CLASS_PREFIX, }); } for (const side of BOX_MODEL_SIDES) { this.markup.createSVGNode({ nodeType: "line", parent: svg, attributes: { class: "guide-" + side, id: "guide-" + side, "stroke-width": GUIDE_STROKE_WIDTH, role: "presentation", }, prefix: this.ID_CLASS_PREFIX, }); } // Building the nodeinfo bar markup const infobarContainer = this.markup.createNode({ parent: rootWrapper, attributes: { class: "infobar-container", id: "infobar-container", position: "top", hidden: "true", }, prefix: this.ID_CLASS_PREFIX, }); const infobar = this.markup.createNode({ parent: infobarContainer, attributes: { class: "infobar", }, prefix: this.ID_CLASS_PREFIX, }); const texthbox = this.markup.createNode({ parent: infobar, attributes: { class: "infobar-text", }, prefix: this.ID_CLASS_PREFIX, }); this.markup.createNode({ nodeType: "span", parent: texthbox, attributes: { class: "infobar-tagname", id: "infobar-tagname", }, prefix: this.ID_CLASS_PREFIX, }); this.markup.createNode({ nodeType: "span", parent: texthbox, attributes: { class: "infobar-id", id: "infobar-id", }, prefix: this.ID_CLASS_PREFIX, }); this.markup.createNode({ nodeType: "span", parent: texthbox, attributes: { class: "infobar-classes", id: "infobar-classes", }, prefix: this.ID_CLASS_PREFIX, }); this.markup.createNode({ nodeType: "span", parent: texthbox, attributes: { class: "infobar-pseudo-classes", id: "infobar-pseudo-classes", }, prefix: this.ID_CLASS_PREFIX, }); this.markup.createNode({ nodeType: "span", parent: texthbox, attributes: { class: "infobar-dimensions", id: "infobar-dimensions", }, prefix: this.ID_CLASS_PREFIX, }); this.markup.createNode({ nodeType: "span", parent: texthbox, attributes: { class: "infobar-grid-type", id: "infobar-grid-type", }, prefix: this.ID_CLASS_PREFIX, }); this.markup.createNode({ nodeType: "span", parent: texthbox, attributes: { class: "infobar-flex-type", id: "infobar-flex-type", }, prefix: this.ID_CLASS_PREFIX, }); return highlighterContainer; } /** * Destroy the nodes. Remove listeners. */ destroy() { this.highlighterEnv.off("will-navigate", this.onWillNavigate); const { pageListenerTarget } = this.highlighterEnv; if (pageListenerTarget) { pageListenerTarget.removeEventListener("pagehide", this.onPageHide); } this.markup.destroy(); AutoRefreshHighlighter.prototype.destroy.call(this); } getElement(id) { return this.markup.getElement(this.ID_CLASS_PREFIX + id); } /** * Override the AutoRefreshHighlighter's _isNodeValid method to also return true for * text nodes since these can also be highlighted. * @param {DOMNode} node * @return {Boolean} */ _isNodeValid(node) { return ( node && (isNodeValid(node) || isNodeValid(node, nodeConstants.TEXT_NODE)) ); } /** * Show the highlighter on a given node */ _show() { if (!BOX_MODEL_REGIONS.includes(this.options.region)) { this.options.region = "content"; } const shown = this._update(); this._trackMutations(); return shown; } /** * Track the current node markup mutations so that the node info bar can be * updated to reflects the node's attributes */ _trackMutations() { if (isNodeValid(this.currentNode)) { const win = this.currentNode.ownerGlobal; this.currentNodeObserver = new win.MutationObserver(this.update); this.currentNodeObserver.observe(this.currentNode, { attributes: true }); } } _untrackMutations() { if (isNodeValid(this.currentNode) && this.currentNodeObserver) { this.currentNodeObserver.disconnect(); this.currentNodeObserver = null; } } /** * 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() { const node = this.currentNode; let shown = false; setIgnoreLayoutChanges(true); if (this._updateBoxModel()) { // Show the infobar only if configured to do so and the node is an element or a text // node. if ( !this.options.hideInfoBar && (node.nodeType === node.ELEMENT_NODE || node.nodeType === node.TEXT_NODE) ) { this._showInfobar(); } else { this._hideInfobar(); } this._updateSimpleHighlighters(); this._showBoxModel(); shown = true; } else { // Nothing to highlight (0px rectangle like a