Bug 1913305 - Add test. r=mtigley
[gecko.git] / devtools / server / actors / layout.js
blobd046a6ca17d0ef28f850eed77f81dce1e840e2a2
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 const { Actor } = require("resource://devtools/shared/protocol.js");
8 const {
9   flexboxSpec,
10   flexItemSpec,
11   gridSpec,
12   layoutSpec,
13 } = require("resource://devtools/shared/specs/layout.js");
15 const {
16   getStringifiableFragments,
17 } = require("resource://devtools/server/actors/utils/css-grid-utils.js");
19 loader.lazyRequireGetter(
20   this,
21   "CssLogic",
22   "resource://devtools/server/actors/inspector/css-logic.js",
23   true
25 loader.lazyRequireGetter(
26   this,
27   "findGridParentContainerForNode",
28   "resource://devtools/server/actors/inspector/utils.js",
29   true
31 loader.lazyRequireGetter(
32   this,
33   "getCSSStyleRules",
34   "resource://devtools/shared/inspector/css-logic.js",
35   true
37 loader.lazyRequireGetter(
38   this,
39   "isCssPropertyKnown",
40   "resource://devtools/server/actors/css-properties.js",
41   true
43 loader.lazyRequireGetter(
44   this,
45   "parseDeclarations",
46   "resource://devtools/shared/css/parsing-utils.js",
47   true
49 loader.lazyRequireGetter(
50   this,
51   "nodeConstants",
52   "resource://devtools/shared/dom-node-constants.js"
55 /**
56  * Set of actors the expose the CSS layout information to the devtools protocol clients.
57  *
58  * The |Layout| actor is the main entry point. It is used to get various CSS
59  * layout-related information from the document.
60  *
61  * The |Flexbox| actor provides the container node information to inspect the flexbox
62  * container. It is also used to return an array of |FlexItem| actors which provide the
63  * flex item information.
64  *
65  * The |Grid| actor provides the grid fragment information to inspect the grid container.
66  */
68 class FlexboxActor extends Actor {
69   /**
70    * @param  {LayoutActor} layoutActor
71    *         The LayoutActor instance.
72    * @param  {DOMNode} containerEl
73    *         The flex container element.
74    */
75   constructor(layoutActor, containerEl) {
76     super(layoutActor.conn, flexboxSpec);
78     this.containerEl = containerEl;
79     this.walker = layoutActor.walker;
80   }
82   destroy() {
83     super.destroy();
85     this.containerEl = null;
86     this.walker = null;
87   }
89   form() {
90     const styles = CssLogic.getComputedStyle(this.containerEl);
92     const form = {
93       actor: this.actorID,
94       // The computed style properties of the flex container.
95       properties: {
96         "align-content": styles.alignContent,
97         "align-items": styles.alignItems,
98         "flex-direction": styles.flexDirection,
99         "flex-wrap": styles.flexWrap,
100         "justify-content": styles.justifyContent,
101       },
102     };
104     // If the WalkerActor already knows the container element, then also return its
105     // ActorID so we avoid the client from doing another round trip to get it in many
106     // cases.
107     if (this.walker.hasNode(this.containerEl)) {
108       form.containerNodeActorID = this.walker.getNode(this.containerEl).actorID;
109     }
111     return form;
112   }
114   /**
115    * Returns an array of FlexItemActor objects for all the flex item elements contained
116    * in the flex container element.
117    *
118    * @return {Array}
119    *         An array of FlexItemActor objects.
120    */
121   getFlexItems() {
122     if (isNodeDead(this.containerEl)) {
123       return [];
124     }
126     const flex = this.containerEl.getAsFlexContainer();
127     if (!flex) {
128       return [];
129     }
131     const flexItemActors = [];
132     const { crossAxisDirection, mainAxisDirection } = flex;
134     for (const line of flex.getLines()) {
135       for (const item of line.getItems()) {
136         flexItemActors.push(
137           new FlexItemActor(this, item.node, {
138             crossAxisDirection,
139             mainAxisDirection,
140             crossMaxSize: item.crossMaxSize,
141             crossMinSize: item.crossMinSize,
142             mainBaseSize: item.mainBaseSize,
143             mainDeltaSize: item.mainDeltaSize,
144             mainMaxSize: item.mainMaxSize,
145             mainMinSize: item.mainMinSize,
146             lineGrowthState: line.growthState,
147             clampState: item.clampState,
148           })
149         );
150       }
151     }
153     return flexItemActors;
154   }
158  * The FlexItemActor provides information about a flex items' data.
159  */
160 class FlexItemActor extends Actor {
161   /**
162    * @param  {FlexboxActor} flexboxActor
163    *         The FlexboxActor instance.
164    * @param  {DOMNode} element
165    *         The flex item element.
166    * @param  {Object} flexItemSizing
167    *         The flex item sizing data.
168    */
169   constructor(flexboxActor, element, flexItemSizing) {
170     super(flexboxActor.conn, flexItemSpec);
172     this.containerEl = flexboxActor.containerEl;
173     this.element = element;
174     this.flexItemSizing = flexItemSizing;
175     this.walker = flexboxActor.walker;
176   }
178   destroy() {
179     super.destroy();
181     this.containerEl = null;
182     this.element = null;
183     this.flexItemSizing = null;
184     this.walker = null;
185   }
187   form() {
188     const { mainAxisDirection } = this.flexItemSizing;
189     const dimension = mainAxisDirection.startsWith("horizontal")
190       ? "width"
191       : "height";
193     // Find the authored sizing properties for this item.
194     const properties = {
195       "flex-basis": "",
196       "flex-grow": "",
197       "flex-shrink": "",
198       [`min-${dimension}`]: "",
199       [`max-${dimension}`]: "",
200       [dimension]: "",
201     };
203     const isElementNode = this.element.nodeType === this.element.ELEMENT_NODE;
205     if (isElementNode) {
206       for (const name in properties) {
207         const values = [];
208         const cssRules = getCSSStyleRules(this.element);
210         for (const rule of cssRules) {
211           // For each rule, go through *all* properties, because there may be several of
212           // them in the same rule and some with !important flags (which would be more
213           // important even if placed before another property with the same name)
214           const declarations = parseDeclarations(
215             isCssPropertyKnown,
216             rule.style.cssText
217           );
219           for (const declaration of declarations) {
220             if (declaration.name === name && declaration.value !== "auto") {
221               values.push({
222                 value: declaration.value,
223                 priority: declaration.priority,
224               });
225             }
226           }
227         }
229         // Then go through the element style because it's usually more important, but
230         // might not be if there is a prior !important property
231         if (
232           this.element.style &&
233           this.element.style[name] &&
234           this.element.style[name] !== "auto"
235         ) {
236           values.push({
237             value: this.element.style.getPropertyValue(name),
238             priority: this.element.style.getPropertyPriority(name),
239           });
240         }
242         // Now that we have a list of all the property's rule values, go through all the
243         // values and show the property value with the highest priority. Therefore, show
244         // the last !important value. Otherwise, show the last value stored.
245         let rulePropertyValue = "";
247         if (values.length) {
248           const lastValueIndex = values.length - 1;
249           rulePropertyValue = values[lastValueIndex].value;
251           for (const { priority, value } of values) {
252             if (priority === "important") {
253               rulePropertyValue = `${value} !important`;
254             }
255           }
256         }
258         properties[name] = rulePropertyValue;
259       }
260     }
262     // Also find some computed sizing properties that will be useful for this item.
263     const { flexGrow, flexShrink } = isElementNode
264       ? CssLogic.getComputedStyle(this.element)
265       : { flexGrow: null, flexShrink: null };
266     const computedStyle = { flexGrow, flexShrink };
268     const form = {
269       actor: this.actorID,
270       // The flex item sizing data.
271       flexItemSizing: this.flexItemSizing,
272       // The authored style properties of the flex item.
273       properties,
274       // The computed style properties of the flex item.
275       computedStyle,
276     };
278     // If the WalkerActor already knows the flex item element, then also return its
279     // ActorID so we avoid the client from doing another round trip to get it in many
280     // cases.
281     if (this.walker.hasNode(this.element)) {
282       form.nodeActorID = this.walker.getNode(this.element).actorID;
283     }
285     return form;
286   }
290  * The GridActor provides information about a given grid's fragment data.
291  */
292 class GridActor extends Actor {
293   /**
294    * @param  {LayoutActor} layoutActor
295    *         The LayoutActor instance.
296    * @param  {DOMNode} containerEl
297    *         The grid container element.
298    */
299   constructor(layoutActor, containerEl) {
300     super(layoutActor.conn, gridSpec);
302     this.containerEl = containerEl;
303     this.walker = layoutActor.walker;
304   }
306   destroy() {
307     super.destroy();
309     this.containerEl = null;
310     this.gridFragments = null;
311     this.walker = null;
312   }
314   form() {
315     // Seralize the grid fragment data into JSON so protocol.js knows how to write
316     // and read the data.
317     const gridFragments = this.containerEl.getGridFragments();
318     this.gridFragments = getStringifiableFragments(gridFragments);
320     // Record writing mode and text direction for use by the grid outline.
321     const { direction, gridTemplateColumns, gridTemplateRows, writingMode } =
322       CssLogic.getComputedStyle(this.containerEl);
324     const form = {
325       actor: this.actorID,
326       direction,
327       gridFragments: this.gridFragments,
328       writingMode,
329     };
331     // If the WalkerActor already knows the container element, then also return its
332     // ActorID so we avoid the client from doing another round trip to get it in many
333     // cases.
334     if (this.walker.hasNode(this.containerEl)) {
335       form.containerNodeActorID = this.walker.getNode(this.containerEl).actorID;
336     }
338     form.isSubgrid =
339       gridTemplateRows.startsWith("subgrid") ||
340       gridTemplateColumns.startsWith("subgrid");
342     return form;
343   }
347  * The CSS layout actor provides layout information for the given document.
348  */
349 class LayoutActor extends Actor {
350   constructor(conn, targetActor, walker) {
351     super(conn, layoutSpec);
353     this.targetActor = targetActor;
354     this.walker = walker;
355   }
357   destroy() {
358     super.destroy();
360     this.targetActor = null;
361     this.walker = null;
362   }
364   /**
365    * Helper function for getAsFlexItem, getCurrentGrid and getCurrentFlexbox. Returns the
366    * grid or flex container (whichever is requested) found by iterating on the given
367    * selected node. The current node can be a grid/flex container or grid/flex item.
368    * If it is a grid/flex item, returns the parent grid/flex container. Otherwise, returns
369    * null if the current or parent node is not a grid/flex container.
370    *
371    * @param  {Node|NodeActor} node
372    *         The node to start iterating at.
373    * @param  {String} type
374    *         Can be "grid" or "flex", the display type we are searching for.
375    * @param  {Boolean} onlyLookAtContainer
376    *         If true, only look at given node's container and iterate from there.
377    * @return {GridActor|FlexboxActor|null}
378    *         The GridActor or FlexboxActor of the grid/flex container of the given node.
379    *         Otherwise, returns null.
380    */
381   getCurrentDisplay(node, type, onlyLookAtContainer) {
382     if (isNodeDead(node)) {
383       return null;
384     }
386     // Given node can either be a Node or a NodeActor.
387     if (node.rawNode) {
388       node = node.rawNode;
389     }
391     const flexType = type === "flex";
392     const gridType = type === "grid";
393     const displayType = this.walker.getNode(node).displayType;
395     // If the node is an element, check first if it is itself a flex or a grid.
396     if (node.nodeType === node.ELEMENT_NODE) {
397       if (!displayType) {
398         return null;
399       }
401       if (flexType && displayType.includes("flex")) {
402         if (!onlyLookAtContainer) {
403           return new FlexboxActor(this, node);
404         }
406         const container = node.parentFlexElement;
407         if (container) {
408           return new FlexboxActor(this, container);
409         }
411         return null;
412       } else if (gridType && displayType.includes("grid")) {
413         return new GridActor(this, node);
414       }
415     }
417     // Otherwise, check if this is a flex/grid item or the parent node is a flex/grid
418     // container.
419     // Note that text nodes that are children of flex/grid containers are wrapped in
420     // anonymous containers, so even if their displayType getter returns null we still
421     // want to walk up the chain to find their container.
422     const parentFlexElement = node.parentFlexElement;
423     if (parentFlexElement && flexType) {
424       return new FlexboxActor(this, parentFlexElement);
425     }
426     const container = findGridParentContainerForNode(node);
427     if (container && gridType) {
428       return new GridActor(this, container);
429     }
431     return null;
432   }
434   /**
435    * Returns the grid container for a given selected node.
436    * The node itself can be a container, but if not, walk up the DOM to find its
437    * container.
438    * Returns null if no container can be found.
439    *
440    * @param  {Node|NodeActor} node
441    *         The node to start iterating at.
442    * @return {GridActor|null}
443    *         The GridActor of the grid container of the given node. Otherwise, returns
444    *         null.
445    */
446   getCurrentGrid(node) {
447     return this.getCurrentDisplay(node, "grid");
448   }
450   /**
451    * Returns the flex container for a given selected node.
452    * The node itself can be a container, but if not, walk up the DOM to find its
453    * container.
454    * Returns null if no container can be found.
455    *
456    * @param  {Node|NodeActor} node
457    *         The node to start iterating at.
458    * @param  {Boolean|null} onlyLookAtParents
459    *         If true, skip the passed node and only start looking at its parent and up.
460    * @return {FlexboxActor|null}
461    *         The FlexboxActor of the flex container of the given node. Otherwise, returns
462    *         null.
463    */
464   getCurrentFlexbox(node, onlyLookAtParents) {
465     return this.getCurrentDisplay(node, "flex", onlyLookAtParents);
466   }
468   /**
469    * Returns an array of GridActor objects for all the grid elements contained in the
470    * given root node.
471    *
472    * @param  {Node|NodeActor} node
473    *         The root node for grid elements
474    * @return {Array} An array of GridActor objects.
475    */
476   getGrids(node) {
477     if (isNodeDead(node)) {
478       return [];
479     }
481     // Root node can either be a Node or a NodeActor.
482     if (node.rawNode) {
483       node = node.rawNode;
484     }
486     // Root node can be a #document object, which does not support getElementsWithGrid.
487     if (node.nodeType === nodeConstants.DOCUMENT_NODE) {
488       node = node.documentElement;
489     }
491     if (!node) {
492       return [];
493     }
495     const gridElements = node.getElementsWithGrid();
496     let gridActors = gridElements.map(n => new GridActor(this, n));
498     if (this.targetActor.ignoreSubFrames) {
499       return gridActors;
500     }
502     const frames = node.querySelectorAll("iframe, frame");
503     for (const frame of frames) {
504       gridActors = gridActors.concat(this.getGrids(frame.contentDocument));
505     }
507     return gridActors;
508   }
511 function isNodeDead(node) {
512   return !node || (node.rawNode && Cu.isDeadWrapper(node.rawNode));
515 exports.FlexboxActor = FlexboxActor;
516 exports.FlexItemActor = FlexItemActor;
517 exports.GridActor = GridActor;
518 exports.LayoutActor = LayoutActor;