/* 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, "getCSSStyleRules", "resource://devtools/shared/inspector/css-logic.js", true ); loader.lazyRequireGetter( this, "isCssPropertyKnown", "resource://devtools/server/actors/css-properties.js", true ); loader.lazyRequireGetter( this, "parseDeclarations", "resource://devtools/shared/css/parsing-utils.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": "", [`min-${dimension}`]: "", [`max-${dimension}`]: "", [dimension]: "", }; const isElementNode = this.element.nodeType === this.element.ELEMENT_NODE; if (isElementNode) { for (const name in properties) { const values = []; const cssRules = getCSSStyleRules(this.element); for (const rule of cssRules) { // For each rule, go through *all* properties, because there may be several of // them in the same rule and some with !important flags (which would be more // important even if placed before another property with the same name) const declarations = parseDeclarations( isCssPropertyKnown, rule.style.cssText ); for (const declaration of declarations) { if (declaration.name === name && declaration.value !== "auto") { values.push({ value: declaration.value, priority: declaration.priority, }); } } } // 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 }; 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; } } /** * 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, gridTemplateColumns, gridTemplateRows, 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; } form.isSubgrid = gridTemplateRows.startsWith("subgrid") || gridTemplateColumns.startsWith("subgrid"); 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;