/* 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 { Actor } = require("resource://devtools/shared/protocol.js"); const { flexboxSpec, flexItemSpec, gridSpec, layoutSpec, } = require("resource://devtools/shared/specs/layout.js"); const { getStringifiableFragments, } = require("resource://devtools/server/actors/utils/css-grid-utils.js"); loader.lazyRequireGetter( this, "CssLogic", "resource://devtools/server/actors/inspector/css-logic.js", true ); loader.lazyRequireGetter( this, "findGridParentContainerForNode", "resource://devtools/server/actors/inspector/utils.js", true ); loader.lazyRequireGetter( this, "getMatchingCSSRules", "resource://devtools/shared/inspector/css-logic.js", true ); loader.lazyRequireGetter( this, "nodeConstants", "resource://devtools/shared/dom-node-constants.js" ); /** * Set of actors the expose the CSS layout information to the devtools protocol clients. * * The |Layout| actor is the main entry point. It is used to get various CSS * layout-related information from the document. * * The |Flexbox| actor provides the container node information to inspect the flexbox * container. It is also used to return an array of |FlexItem| actors which provide the * flex item information. * * The |Grid| actor provides the grid fragment information to inspect the grid container. */ class FlexboxActor extends Actor { /** * @param {LayoutActor} layoutActor * The LayoutActor instance. * @param {DOMNode} containerEl * The flex container element. */ constructor(layoutActor, containerEl) { super(layoutActor.conn, flexboxSpec); this.containerEl = containerEl; this.walker = layoutActor.walker; } destroy() { super.destroy(); this.containerEl = null; this.walker = null; } form() { const styles = CssLogic.getComputedStyle(this.containerEl); const form = { actor: this.actorID, // The computed style properties of the flex container. properties: { "align-content": styles.alignContent, "align-items": styles.alignItems, "flex-direction": styles.flexDirection, "flex-wrap": styles.flexWrap, "justify-content": styles.justifyContent, }, }; // If the WalkerActor already knows the container element, then also return its // ActorID so we avoid the client from doing another round trip to get it in many // cases. if (this.walker.hasNode(this.containerEl)) { form.containerNodeActorID = this.walker.getNode(this.containerEl).actorID; } return form; } /** * Returns an array of FlexItemActor objects for all the flex item elements contained * in the flex container element. * * @return {Array} * An array of FlexItemActor objects. */ getFlexItems() { if (isNodeDead(this.containerEl)) { return []; } const flex = this.containerEl.getAsFlexContainer(); if (!flex) { return []; } const flexItemActors = []; const { crossAxisDirection, mainAxisDirection } = flex; for (const line of flex.getLines()) { for (const item of line.getItems()) { flexItemActors.push( new FlexItemActor(this, item.node, { crossAxisDirection, mainAxisDirection, crossMaxSize: item.crossMaxSize, crossMinSize: item.crossMinSize, mainBaseSize: item.mainBaseSize, mainDeltaSize: item.mainDeltaSize, mainMaxSize: item.mainMaxSize, mainMinSize: item.mainMinSize, lineGrowthState: line.growthState, clampState: item.clampState, }) ); } } return flexItemActors; } } /** * The FlexItemActor provides information about a flex items' data. */ class FlexItemActor extends Actor { /** * @param {FlexboxActor} flexboxActor * The FlexboxActor instance. * @param {DOMNode} element * The flex item element. * @param {object} flexItemSizing * The flex item sizing data. */ constructor(flexboxActor, element, flexItemSizing) { super(flexboxActor.conn, flexItemSpec); this.containerEl = flexboxActor.containerEl; this.element = element; this.flexItemSizing = flexItemSizing; this.walker = flexboxActor.walker; } destroy() { super.destroy(); this.containerEl = null; this.element = null; this.flexItemSizing = null; this.walker = null; } form() { const { mainAxisDirection } = this.flexItemSizing; const dimension = mainAxisDirection.startsWith("horizontal") ? "width" : "height"; // Find the authored sizing properties for this item. const properties = { "flex-basis": "", "flex-grow": "", "flex-shrink": "", // collect the shorthand as well to compute the longhands in an error case flex: "", [`min-${dimension}`]: "", [`max-${dimension}`]: "", [dimension]: "", }; const isElementNode = this.element.nodeType === this.element.ELEMENT_NODE; if (isElementNode) { const cssRules = getMatchingCSSRules(this.element); for (const name in properties) { const values = []; for (const rule of cssRules) { // For each rule, get the value and priority if available const value = rule.style.getPropertyValue(name); if (value !== "" && value !== "auto") { values.push({ value, priority: rule.style.getPropertyPriority(name), }); } } // Then go through the element style because it's usually more important, but // might not be if there is a prior !important property if ( this.element.style && this.element.style[name] && this.element.style[name] !== "auto" ) { values.push({ value: this.element.style.getPropertyValue(name), priority: this.element.style.getPropertyPriority(name), }); } // Now that we have a list of all the property's rule values, go through all the // values and show the property value with the highest priority. Therefore, show // the last !important value. Otherwise, show the last value stored. let rulePropertyValue = ""; if (values.length) { const lastValueIndex = values.length - 1; rulePropertyValue = values[lastValueIndex].value; for (const { priority, value } of values) { if (priority === "important") { rulePropertyValue = `${value} !important`; } } } properties[name] = rulePropertyValue; } } // Also find some computed sizing properties that will be useful for this item. const { flexGrow, flexShrink } = isElementNode ? CssLogic.getComputedStyle(this.element) : { flexGrow: null, flexShrink: null }; const computedStyle = { flexGrow, flexShrink }; // if a CSS variable was used in the flex shorthand the longhands will be empty. Try a simple recover and use the 3 values from the shorthand if ( properties["flex-grow"] === "" && properties["flex-shrink"] === "" && properties["flex-basis"] === "" && properties.flex !== "" ) { const parts = this.#parseFlexShorthand(properties.flex); if (parts.length === 3) { properties["flex-grow"] = parts[0]; properties["flex-shrink"] = parts[1]; properties["flex-basis"] = parts[2]; } } delete properties.flex; const form = { actor: this.actorID, // The flex item sizing data. flexItemSizing: this.flexItemSizing, // The authored style properties of the flex item. properties, // The computed style properties of the flex item. computedStyle, }; // If the WalkerActor already knows the flex item element, then also return its // ActorID so we avoid the client from doing another round trip to get it in many // cases. if (this.walker.hasNode(this.element)) { form.nodeActorID = this.walker.getNode(this.element).actorID; } return form; } /** * Parse a `flex` shorthand value into its longhand (flex-grow, flex-shrink, flex-basis) values * * @param {string} flexShorthandValue * @returns {string[]} */ #parseFlexShorthand(flexShorthandValue) { const lexer = new InspectorCSSParser(flexShorthandValue); /** * @param {InspectorCSSToken | null} current * @returns {string} */ function consume(current) { if (!current) { return ""; } // If it's a function, collect everything until the closing parenthesis if (current.tokenType === "Function") { const contents = []; let next; while ( (next = lexer.nextToken()) && next.tokenType !== "CloseParenthesis" ) { contents.push(consume(next)); } return `${current.value}(${contents.join("")})`; } return current.text; } const result = []; let token; while ((token = lexer.nextToken())) { const value = consume(token).trim(); if (value) { result.push(value); } } return result; } } /** * The GridActor provides information about a given grid's fragment data. */ class GridActor extends Actor { /** * @param {LayoutActor} layoutActor * The LayoutActor instance. * @param {DOMNode} containerEl * The grid container element. */ constructor(layoutActor, containerEl) { super(layoutActor.conn, gridSpec); this.containerEl = containerEl; this.walker = layoutActor.walker; } destroy() { super.destroy(); this.containerEl = null; this.gridFragments = null; this.walker = null; } form() { // Seralize the grid fragment data into JSON so protocol.js knows how to write // and read the data. const gridFragments = this.containerEl.getGridFragments(); this.gridFragments = getStringifiableFragments(gridFragments); // Record writing mode and text direction for use by the grid outline. const { direction, writingMode } = CssLogic.getComputedStyle( this.containerEl ); const form = { actor: this.actorID, direction, gridFragments: this.gridFragments, writingMode, }; // If the WalkerActor already knows the container element, then also return its // ActorID so we avoid the client from doing another round trip to get it in many // cases. if (this.walker.hasNode(this.containerEl)) { form.containerNodeActorID = this.walker.getNode(this.containerEl).actorID; } const gridContainerType = InspectorUtils.getGridContainerType( this.containerEl ); form.isSubgrid = gridContainerType & (InspectorUtils.GRID_SUBGRID_COL | InspectorUtils.GRID_SUBGRID_ROW); return form; } } /** * The CSS layout actor provides layout information for the given document. */ class LayoutActor extends Actor { constructor(conn, targetActor, walker) { super(conn, layoutSpec); this.targetActor = targetActor; this.walker = walker; } destroy() { super.destroy(); this.targetActor = null; this.walker = null; } /** * Helper function for getAsFlexItem, getCurrentGrid and getCurrentFlexbox. Returns the * grid or flex container (whichever is requested) found by iterating on the given * selected node. The current node can be a grid/flex container or grid/flex item. * If it is a grid/flex item, returns the parent grid/flex container. Otherwise, returns * null if the current or parent node is not a grid/flex container. * * @param {Node|NodeActor} node * The node to start iterating at. * @param {string} type * Can be "grid" or "flex", the display type we are searching for. * @param {boolean} onlyLookAtContainer * If true, only look at given node's container and iterate from there. * @return {GridActor|FlexboxActor|null} * The GridActor or FlexboxActor of the grid/flex container of the given node. * Otherwise, returns null. */ getCurrentDisplay(node, type, onlyLookAtContainer) { if (isNodeDead(node)) { return null; } // Given node can either be a Node or a NodeActor. if (node.rawNode) { node = node.rawNode; } const flexType = type === "flex"; const gridType = type === "grid"; const displayType = this.walker.getNode(node).displayType; // If the node is an element, check first if it is itself a flex or a grid. if (node.nodeType === node.ELEMENT_NODE) { if (!displayType) { return null; } if (flexType && displayType.includes("flex")) { if (!onlyLookAtContainer) { return new FlexboxActor(this, node); } const container = node.parentFlexElement; if (container) { return new FlexboxActor(this, container); } return null; } else if (gridType && displayType.includes("grid")) { return new GridActor(this, node); } } // Otherwise, check if this is a flex/grid item or the parent node is a flex/grid // container. // Note that text nodes that are children of flex/grid containers are wrapped in // anonymous containers, so even if their displayType getter returns null we still // want to walk up the chain to find their container. const parentFlexElement = node.parentFlexElement; if (parentFlexElement && flexType) { return new FlexboxActor(this, parentFlexElement); } const container = findGridParentContainerForNode(node); if (container && gridType) { return new GridActor(this, container); } return null; } /** * Returns the grid container for a given selected node. * The node itself can be a container, but if not, walk up the DOM to find its * container. * Returns null if no container can be found. * * @param {Node|NodeActor} node * The node to start iterating at. * @return {GridActor|null} * The GridActor of the grid container of the given node. Otherwise, returns * null. */ getCurrentGrid(node) { return this.getCurrentDisplay(node, "grid"); } /** * Returns the flex container for a given selected node. * The node itself can be a container, but if not, walk up the DOM to find its * container. * Returns null if no container can be found. * * @param {Node|NodeActor} node * The node to start iterating at. * @param {boolean | null} onlyLookAtParents * If true, skip the passed node and only start looking at its parent and up. * @return {FlexboxActor|null} * The FlexboxActor of the flex container of the given node. Otherwise, returns * null. */ getCurrentFlexbox(node, onlyLookAtParents) { return this.getCurrentDisplay(node, "flex", onlyLookAtParents); } /** * Returns an array of GridActor objects for all the grid elements contained in the * given root node. * * @param {Node|NodeActor} node * The root node for grid elements * @return {Array} An array of GridActor objects. */ getGrids(node) { if (isNodeDead(node)) { return []; } // Root node can either be a Node or a NodeActor. if (node.rawNode) { node = node.rawNode; } // Root node can be a #document object, which does not support getElementsWithGrid. if (node.nodeType === nodeConstants.DOCUMENT_NODE) { node = node.documentElement; } if (!node) { return []; } const gridElements = node.getElementsWithGrid(); let gridActors = gridElements.map(n => new GridActor(this, n)); if (this.targetActor.ignoreSubFrames) { return gridActors; } const frames = node.querySelectorAll("iframe, frame"); for (const frame of frames) { gridActors = gridActors.concat(this.getGrids(frame.contentDocument)); } return gridActors; } } function isNodeDead(node) { return !node || (node.rawNode && Cu.isDeadWrapper(node.rawNode)); } exports.FlexboxActor = FlexboxActor; exports.FlexItemActor = FlexItemActor; exports.GridActor = GridActor; exports.LayoutActor = LayoutActor;