summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/layout.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/layout.js')
-rw-r--r--devtools/server/actors/layout.js524
1 files changed, 524 insertions, 0 deletions
diff --git a/devtools/server/actors/layout.js b/devtools/server/actors/layout.js
new file mode 100644
index 0000000000..c4b5c8877e
--- /dev/null
+++ b/devtools/server/actors/layout.js
@@ -0,0 +1,524 @@
+/* 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,
+ ActorClassWithSpec,
+} = 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.
+ */
+
+const FlexboxActor = ActorClassWithSpec(flexboxSpec, {
+ /**
+ * @param {LayoutActor} layoutActor
+ * The LayoutActor instance.
+ * @param {DOMNode} containerEl
+ * The flex container element.
+ */
+ initialize(layoutActor, containerEl) {
+ Actor.prototype.initialize.call(this, layoutActor.conn);
+
+ this.containerEl = containerEl;
+ this.walker = layoutActor.walker;
+ },
+
+ destroy() {
+ Actor.prototype.destroy.call(this);
+
+ 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.
+ */
+const FlexItemActor = ActorClassWithSpec(flexItemSpec, {
+ /**
+ * @param {FlexboxActor} flexboxActor
+ * The FlexboxActor instance.
+ * @param {DOMNode} element
+ * The flex item element.
+ * @param {Object} flexItemSizing
+ * The flex item sizing data.
+ */
+ initialize(flexboxActor, element, flexItemSizing) {
+ Actor.prototype.initialize.call(this, flexboxActor.conn);
+
+ this.containerEl = flexboxActor.containerEl;
+ this.element = element;
+ this.flexItemSizing = flexItemSizing;
+ this.walker = flexboxActor.walker;
+ },
+
+ destroy() {
+ Actor.prototype.destroy.call(this);
+
+ 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.
+ */
+const GridActor = ActorClassWithSpec(gridSpec, {
+ /**
+ * @param {LayoutActor} layoutActor
+ * The LayoutActor instance.
+ * @param {DOMNode} containerEl
+ * The grid container element.
+ */
+ initialize(layoutActor, containerEl) {
+ Actor.prototype.initialize.call(this, layoutActor.conn);
+
+ this.containerEl = containerEl;
+ this.walker = layoutActor.walker;
+ },
+
+ destroy() {
+ Actor.prototype.destroy.call(this);
+
+ 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.
+ */
+const LayoutActor = ActorClassWithSpec(layoutSpec, {
+ initialize(conn, targetActor, walker) {
+ Actor.prototype.initialize.call(this, conn);
+
+ this.targetActor = targetActor;
+ this.walker = walker;
+ },
+
+ destroy() {
+ Actor.prototype.destroy.call(this);
+
+ 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;