summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/highlighters/box-model.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/server/actors/highlighters/box-model.js892
1 files changed, 892 insertions, 0 deletions
diff --git a/devtools/server/actors/highlighters/box-model.js b/devtools/server/actors/highlighters/box-model.js
new file mode 100644
index 0000000000..9368f2f292
--- /dev/null
+++ b/devtools/server/actors/highlighters/box-model.js
@@ -0,0 +1,892 @@
+/* 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:
+ * <div class="highlighter-container" aria-hidden="true">
+ * <div class="box-model-root">
+ * <svg class="box-model-elements" hidden="true">
+ * <g class="box-model-regions">
+ * <path class="box-model-margin" points="..." />
+ * <path class="box-model-border" points="..." />
+ * <path class="box-model-padding" points="..." />
+ * <path class="box-model-content" points="..." />
+ * </g>
+ * <line class="box-model-guide-top" x1="..." y1="..." x2="..." y2="..." />
+ * <line class="box-model-guide-right" x1="..." y1="..." x2="..." y2="..." />
+ * <line class="box-model-guide-bottom" x1="..." y1="..." x2="..." y2="..." />
+ * <line class="box-model-guide-left" x1="..." y1="..." x2="..." y2="..." />
+ * </svg>
+ * <div class="box-model-infobar-container">
+ * <div class="box-model-infobar-arrow highlighter-infobar-arrow-top" />
+ * <div class="box-model-infobar">
+ * <div class="box-model-infobar-text" align="center">
+ * <span class="box-model-infobar-tagname">Node name</span>
+ * <span class="box-model-infobar-id">Node id</span>
+ * <span class="box-model-infobar-classes">.someClass</span>
+ * <span class="box-model-infobar-pseudo-classes">:hover</span>
+ * <span class="box-model-infobar-grid-type">Grid Type</span>
+ * <span class="box-model-infobar-flex-type">Flex Type</span>
+ * </div>
+ * </div>
+ * <div class="box-model-infobar-arrow box-model-infobar-arrow-bottom"/>
+ * </div>
+ * </div>
+ * </div>
+ */
+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 <script> tag for instance)
+ this._hide();
+ }
+
+ setIgnoreLayoutChanges(
+ false,
+ this.highlighterEnv.window.document.documentElement
+ );
+
+ return shown;
+ }
+
+ _scrollUpdate() {
+ this._moveInfobar();
+ }
+
+ /**
+ * Hide the highlighter, the outline and the infobar.
+ */
+ _hide() {
+ setIgnoreLayoutChanges(true);
+
+ this._untrackMutations();
+ this._hideBoxModel();
+ this._hideInfobar();
+
+ setIgnoreLayoutChanges(
+ false,
+ this.highlighterEnv.window.document.documentElement
+ );
+ }
+
+ /**
+ * Hide the infobar
+ */
+ _hideInfobar() {
+ this.getElement("infobar-container").setAttribute("hidden", "true");
+ }
+
+ /**
+ * Show the infobar
+ */
+ _showInfobar() {
+ this.getElement("infobar-container").removeAttribute("hidden");
+ this._updateInfobar();
+ }
+
+ /**
+ * Hide the box model
+ */
+ _hideBoxModel() {
+ this.getElement("elements").setAttribute("hidden", "true");
+ }
+
+ /**
+ * Show the box model
+ */
+ _showBoxModel() {
+ this.getElement("elements").removeAttribute("hidden");
+ }
+
+ /**
+ * Calculate an outer quad based on the quads returned by getAdjustedQuads.
+ * The BoxModelHighlighter may highlight more than one boxes, so in this case
+ * create a new quad that "contains" all of these quads.
+ * This is useful to position the guides and infobar.
+ * This may happen if the BoxModelHighlighter is used to highlight an inline
+ * element that spans line breaks.
+ * @param {String} region The box-model region to get the outer quad for.
+ * @return {Object} A quad-like object {p1,p2,p3,p4,bounds}
+ */
+ _getOuterQuad(region) {
+ const quads = this.currentQuads[region];
+ if (!quads || !quads.length) {
+ return null;
+ }
+
+ const quad = {
+ p1: { x: Infinity, y: Infinity },
+ p2: { x: -Infinity, y: Infinity },
+ p3: { x: -Infinity, y: -Infinity },
+ p4: { x: Infinity, y: -Infinity },
+ bounds: {
+ bottom: -Infinity,
+ height: 0,
+ left: Infinity,
+ right: -Infinity,
+ top: Infinity,
+ width: 0,
+ x: 0,
+ y: 0,
+ },
+ };
+
+ for (const q of quads) {
+ quad.p1.x = Math.min(quad.p1.x, q.p1.x);
+ quad.p1.y = Math.min(quad.p1.y, q.p1.y);
+ quad.p2.x = Math.max(quad.p2.x, q.p2.x);
+ quad.p2.y = Math.min(quad.p2.y, q.p2.y);
+ quad.p3.x = Math.max(quad.p3.x, q.p3.x);
+ quad.p3.y = Math.max(quad.p3.y, q.p3.y);
+ quad.p4.x = Math.min(quad.p4.x, q.p4.x);
+ quad.p4.y = Math.max(quad.p4.y, q.p4.y);
+
+ quad.bounds.bottom = Math.max(quad.bounds.bottom, q.bounds.bottom);
+ quad.bounds.top = Math.min(quad.bounds.top, q.bounds.top);
+ quad.bounds.left = Math.min(quad.bounds.left, q.bounds.left);
+ quad.bounds.right = Math.max(quad.bounds.right, q.bounds.right);
+ }
+ quad.bounds.x = quad.bounds.left;
+ quad.bounds.y = quad.bounds.top;
+ quad.bounds.width = quad.bounds.right - quad.bounds.left;
+ quad.bounds.height = quad.bounds.bottom - quad.bounds.top;
+
+ return quad;
+ }
+
+ /**
+ * Update the box model as per the current node.
+ *
+ * @return {boolean}
+ * True if the current node has a box model to be highlighted
+ */
+ _updateBoxModel() {
+ const options = this.options;
+ options.region = options.region || "content";
+
+ if (!this._nodeNeedsHighlighting()) {
+ this._hideBoxModel();
+ return false;
+ }
+
+ for (let i = 0; i < BOX_MODEL_REGIONS.length; i++) {
+ const boxType = BOX_MODEL_REGIONS[i];
+ const nextBoxType = BOX_MODEL_REGIONS[i + 1];
+ const box = this.getElement(boxType);
+
+ // Highlight all quads for this region by setting the "d" attribute of the
+ // corresponding <path>.
+ const path = [];
+ for (let j = 0; j < this.currentQuads[boxType].length; j++) {
+ const boxQuad = this.currentQuads[boxType][j];
+ const nextBoxQuad = this.currentQuads[nextBoxType]
+ ? this.currentQuads[nextBoxType][j]
+ : null;
+ path.push(this._getBoxPathCoordinates(boxQuad, nextBoxQuad));
+ }
+
+ box.setAttribute("d", path.join(" "));
+ box.removeAttribute("faded");
+
+ // If showOnly is defined, either hide the other regions, or fade them out
+ // if onlyRegionArea is set too.
+ if (options.showOnly && options.showOnly !== boxType) {
+ if (options.onlyRegionArea) {
+ box.setAttribute("faded", "true");
+ } else {
+ box.removeAttribute("d");
+ }
+ }
+
+ if (boxType === options.region && !options.hideGuides) {
+ this._showGuides(boxType);
+ } else if (options.hideGuides) {
+ this._hideGuides();
+ }
+ }
+
+ // Un-zoom the root wrapper if the page was zoomed.
+ const rootId = this.ID_CLASS_PREFIX + "elements";
+ this.markup.scaleRootElement(this.currentNode, rootId);
+
+ return true;
+ }
+
+ _getBoxPathCoordinates(boxQuad, nextBoxQuad) {
+ const { p1, p2, p3, p4 } = boxQuad;
+
+ let path;
+ if (!nextBoxQuad || !this.options.onlyRegionArea) {
+ // If this is the content box (inner-most box) or if we're not being asked
+ // to highlight only region areas, then draw a simple rectangle.
+ path =
+ "M" +
+ p1.x +
+ "," +
+ p1.y +
+ " " +
+ "L" +
+ p2.x +
+ "," +
+ p2.y +
+ " " +
+ "L" +
+ p3.x +
+ "," +
+ p3.y +
+ " " +
+ "L" +
+ p4.x +
+ "," +
+ p4.y +
+ " " +
+ "L" +
+ p1.x +
+ "," +
+ p1.y;
+ } else {
+ // Otherwise, just draw the region itself, not a filled rectangle.
+ const { p1: np1, p2: np2, p3: np3, p4: np4 } = nextBoxQuad;
+ path =
+ "M" +
+ p1.x +
+ "," +
+ p1.y +
+ " " +
+ "L" +
+ p2.x +
+ "," +
+ p2.y +
+ " " +
+ "L" +
+ p3.x +
+ "," +
+ p3.y +
+ " " +
+ "L" +
+ p4.x +
+ "," +
+ p4.y +
+ " " +
+ "L" +
+ p1.x +
+ "," +
+ p1.y +
+ " " +
+ "L" +
+ np1.x +
+ "," +
+ np1.y +
+ " " +
+ "L" +
+ np4.x +
+ "," +
+ np4.y +
+ " " +
+ "L" +
+ np3.x +
+ "," +
+ np3.y +
+ " " +
+ "L" +
+ np2.x +
+ "," +
+ np2.y +
+ " " +
+ "L" +
+ np1.x +
+ "," +
+ np1.y;
+ }
+
+ return path;
+ }
+
+ /**
+ * Can the current node be highlighted? Does it have quads.
+ * @return {Boolean}
+ */
+ _nodeNeedsHighlighting() {
+ return (
+ this.currentQuads.margin.length ||
+ this.currentQuads.border.length ||
+ this.currentQuads.padding.length ||
+ this.currentQuads.content.length
+ );
+ }
+
+ _getOuterBounds() {
+ for (const region of ["margin", "border", "padding", "content"]) {
+ const quad = this._getOuterQuad(region);
+
+ if (!quad) {
+ // Invisible element such as a script tag.
+ break;
+ }
+
+ const { bottom, height, left, right, top, width, x, y } = quad.bounds;
+
+ if (width > 0 || height > 0) {
+ return { bottom, height, left, right, top, width, x, y };
+ }
+ }
+
+ return {
+ bottom: 0,
+ height: 0,
+ left: 0,
+ right: 0,
+ top: 0,
+ width: 0,
+ x: 0,
+ y: 0,
+ };
+ }
+
+ /**
+ * We only want to show guides for horizontal and vertical edges as this helps
+ * to line them up. This method finds these edges and displays a guide there.
+ * @param {String} region The region around which the guides should be shown.
+ */
+ _showGuides(region) {
+ const quad = this._getOuterQuad(region);
+
+ if (!quad) {
+ // Invisible element such as a script tag.
+ return;
+ }
+
+ const { p1, p2, p3, p4 } = quad;
+
+ const allX = [p1.x, p2.x, p3.x, p4.x].sort((a, b) => a - b);
+ const allY = [p1.y, p2.y, p3.y, p4.y].sort((a, b) => a - b);
+ const toShowX = [];
+ const toShowY = [];
+
+ for (const arr of [allX, allY]) {
+ for (let i = 0; i < arr.length; i++) {
+ const val = arr[i];
+
+ if (i !== arr.lastIndexOf(val)) {
+ if (arr === allX) {
+ toShowX.push(val);
+ } else {
+ toShowY.push(val);
+ }
+ arr.splice(arr.lastIndexOf(val), 1);
+ }
+ }
+ }
+
+ // Move guide into place or hide it if no valid co-ordinate was found.
+ this._updateGuide("top", Math.round(toShowY[0]));
+ this._updateGuide("right", Math.round(toShowX[1]) - 1);
+ this._updateGuide("bottom", Math.round(toShowY[1]) - 1);
+ this._updateGuide("left", Math.round(toShowX[0]));
+ }
+
+ _hideGuides() {
+ for (const side of BOX_MODEL_SIDES) {
+ this.getElement("guide-" + side).setAttribute("hidden", "true");
+ }
+ }
+
+ /**
+ * Move a guide to the appropriate position and display it. If no point is
+ * passed then the guide is hidden.
+ *
+ * @param {String} side
+ * The guide to update
+ * @param {Integer} point
+ * x or y co-ordinate. If this is undefined we hide the guide.
+ */
+ _updateGuide(side, point) {
+ const guide = this.getElement("guide-" + side);
+
+ if (!point || point <= 0) {
+ guide.setAttribute("hidden", "true");
+ return false;
+ }
+
+ if (side === "top" || side === "bottom") {
+ guide.setAttribute("x1", "0");
+ guide.setAttribute("y1", point + "");
+ guide.setAttribute("x2", "100%");
+ guide.setAttribute("y2", point + "");
+ } else {
+ guide.setAttribute("x1", point + "");
+ guide.setAttribute("y1", "0");
+ guide.setAttribute("x2", point + "");
+ guide.setAttribute("y2", "100%");
+ }
+
+ guide.removeAttribute("hidden");
+
+ return true;
+ }
+
+ /**
+ * Update node information (displayName#id.class)
+ */
+ _updateInfobar() {
+ if (!this.currentNode) {
+ return;
+ }
+
+ const { bindingElement: node, pseudo } = getBindingElementAndPseudo(
+ this.currentNode
+ );
+
+ // Update the tag, id, classes, pseudo-classes and dimensions
+ const displayName = getNodeDisplayName(node);
+
+ const id = node.id ? "#" + node.id : "";
+
+ const classList = (node.classList || []).length
+ ? "." + [...node.classList].join(".")
+ : "";
+
+ let pseudos = this._getPseudoClasses(node).join("");
+ if (pseudo) {
+ pseudos += pseudo;
+ }
+
+ // We want to display the original `width` and `height`, instead of the ones affected
+ // by any zoom. Since the infobar can be displayed also for text nodes, we can't
+ // access the computed style for that, and this is why we recalculate them here.
+ const zoom = getCurrentZoom(this.win);
+ const quad = this._getOuterQuad("border");
+
+ if (!quad) {
+ return;
+ }
+
+ const { width, height } = quad.bounds;
+ const dim =
+ parseFloat((width / zoom).toPrecision(6)) +
+ " \u00D7 " +
+ parseFloat((height / zoom).toPrecision(6));
+
+ const { grid: gridType, flex: flexType } = getNodeGridFlexType(node);
+ const gridLayoutTextType = this._getLayoutTextType("gridtype", gridType);
+ const flexLayoutTextType = this._getLayoutTextType("flextype", flexType);
+
+ this.getElement("infobar-tagname").setTextContent(displayName);
+ this.getElement("infobar-id").setTextContent(id);
+ this.getElement("infobar-classes").setTextContent(classList);
+ this.getElement("infobar-pseudo-classes").setTextContent(pseudos);
+ this.getElement("infobar-dimensions").setTextContent(dim);
+ this.getElement("infobar-grid-type").setTextContent(gridLayoutTextType);
+ this.getElement("infobar-flex-type").setTextContent(flexLayoutTextType);
+
+ this._moveInfobar();
+ }
+
+ _getLayoutTextType(layoutTypeKey, { isContainer, isItem }) {
+ if (!isContainer && !isItem) {
+ return "";
+ }
+ if (isContainer && !isItem) {
+ return HighlightersBundle.formatValueSync(`${layoutTypeKey}-container`);
+ }
+ if (!isContainer && isItem) {
+ return HighlightersBundle.formatValueSync(`${layoutTypeKey}-item`);
+ }
+ return HighlightersBundle.formatValueSync(`${layoutTypeKey}-dual`);
+ }
+
+ _getPseudoClasses(node) {
+ if (node.nodeType !== nodeConstants.ELEMENT_NODE) {
+ // hasPseudoClassLock can only be used on Elements.
+ return [];
+ }
+
+ return PSEUDO_CLASSES.filter(pseudo => hasPseudoClassLock(node, pseudo));
+ }
+
+ /**
+ * Move the Infobar to the right place in the highlighter.
+ */
+ _moveInfobar() {
+ const bounds = this._getOuterBounds();
+ const container = this.getElement("infobar-container");
+
+ moveInfobar(container, bounds, this.win);
+ }
+
+ onPageHide({ target }) {
+ // If a pagehide event is triggered for current window's highlighter, hide the
+ // highlighter.
+ if (target.defaultView === this.win) {
+ this.hide();
+ }
+ }
+
+ onWillNavigate({ isTopLevel }) {
+ if (isTopLevel) {
+ this.hide();
+ }
+ }
+}
+
+exports.BoxModelHighlighter = BoxModelHighlighter;