summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/grids/grid-inspector.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/inspector/grids/grid-inspector.js776
1 files changed, 776 insertions, 0 deletions
diff --git a/devtools/client/inspector/grids/grid-inspector.js b/devtools/client/inspector/grids/grid-inspector.js
new file mode 100644
index 0000000000..c6aac90592
--- /dev/null
+++ b/devtools/client/inspector/grids/grid-inspector.js
@@ -0,0 +1,776 @@
+/* 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 flags = require("resource://devtools/shared/flags.js");
+const { throttle } = require("resource://devtools/shared/throttle.js");
+
+const gridsReducer = require("resource://devtools/client/inspector/grids/reducers/grids.js");
+const highlighterSettingsReducer = require("resource://devtools/client/inspector/grids/reducers/highlighter-settings.js");
+const {
+ updateGridColor,
+ updateGridHighlighted,
+ updateGrids,
+} = require("resource://devtools/client/inspector/grids/actions/grids.js");
+const {
+ updateShowGridAreas,
+ updateShowGridLineNumbers,
+ updateShowInfiniteLines,
+} = require("resource://devtools/client/inspector/grids/actions/highlighter-settings.js");
+
+loader.lazyRequireGetter(
+ this,
+ "compareFragmentsGeometry",
+ "resource://devtools/client/inspector/grids/utils/utils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "parseURL",
+ "resource://devtools/client/shared/source-utils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "asyncStorage",
+ "resource://devtools/shared/async-storage.js"
+);
+
+const CSS_GRID_COUNT_HISTOGRAM_ID = "DEVTOOLS_NUMBER_OF_CSS_GRIDS_IN_A_PAGE";
+
+const SHOW_GRID_AREAS = "devtools.gridinspector.showGridAreas";
+const SHOW_GRID_LINE_NUMBERS = "devtools.gridinspector.showGridLineNumbers";
+const SHOW_INFINITE_LINES_PREF = "devtools.gridinspector.showInfiniteLines";
+
+const TELEMETRY_GRID_AREAS_OVERLAY_CHECKED =
+ "devtools.grid.showGridAreasOverlay.checked";
+const TELEMETRY_GRID_LINE_NUMBERS_CHECKED =
+ "devtools.grid.showGridLineNumbers.checked";
+const TELEMETRY_INFINITE_LINES_CHECKED =
+ "devtools.grid.showInfiniteLines.checked";
+
+// Default grid colors.
+const GRID_COLORS = [
+ "#9400FF",
+ "#DF00A9",
+ "#0A84FF",
+ "#12BC00",
+ "#EA8000",
+ "#00B0BD",
+ "#D70022",
+ "#4B42FF",
+ "#B5007F",
+ "#058B00",
+ "#A47F00",
+ "#005A71",
+];
+
+class GridInspector {
+ constructor(inspector, window) {
+ this.document = window.document;
+ this.inspector = inspector;
+ this.store = inspector.store;
+ this.telemetry = inspector.telemetry;
+
+ // Maximum number of grid highlighters that can be displayed.
+ this.maxHighlighters = Services.prefs.getIntPref(
+ "devtools.gridinspector.maxHighlighters"
+ );
+
+ this.store.injectReducer("grids", gridsReducer);
+ this.store.injectReducer("highlighterSettings", highlighterSettingsReducer);
+
+ this.onHighlighterShown = this.onHighlighterShown.bind(this);
+ this.onHighlighterHidden = this.onHighlighterHidden.bind(this);
+ this.onNavigate = this.onNavigate.bind(this);
+ this.onReflow = throttle(this.onReflow, 500, this);
+ this.onSetGridOverlayColor = this.onSetGridOverlayColor.bind(this);
+ this.onSidebarSelect = this.onSidebarSelect.bind(this);
+ this.onToggleGridHighlighter = this.onToggleGridHighlighter.bind(this);
+ this.onToggleShowGridAreas = this.onToggleShowGridAreas.bind(this);
+ this.onToggleShowGridLineNumbers =
+ this.onToggleShowGridLineNumbers.bind(this);
+ this.onToggleShowInfiniteLines = this.onToggleShowInfiniteLines.bind(this);
+ this.updateGridPanel = this.updateGridPanel.bind(this);
+ this.listenForGridHighlighterEvents =
+ this.listenForGridHighlighterEvents.bind(this);
+
+ this.init();
+ }
+
+ get highlighters() {
+ if (!this._highlighters) {
+ this._highlighters = this.inspector.highlighters;
+ }
+
+ return this._highlighters;
+ }
+
+ /**
+ * Initializes the grid inspector by fetching the LayoutFront from the walker and
+ * loading the highlighter settings.
+ */
+ async init() {
+ if (!this.inspector) {
+ return;
+ }
+
+ if (flags.testing) {
+ // In tests, we start listening immediately to avoid having to simulate a mousemove.
+ this.listenForGridHighlighterEvents();
+ } else {
+ this.document.addEventListener(
+ "mousemove",
+ this.listenForGridHighlighterEvents,
+ {
+ once: true,
+ }
+ );
+ }
+
+ this.inspector.sidebar.on("select", this.onSidebarSelect);
+ this.inspector.on("new-root", this.onNavigate);
+
+ this.onSidebarSelect();
+ }
+
+ listenForGridHighlighterEvents() {
+ this.highlighters.on("grid-highlighter-hidden", this.onHighlighterHidden);
+ this.highlighters.on("grid-highlighter-shown", this.onHighlighterShown);
+ }
+
+ /**
+ * Get the LayoutActor fronts for all interesting targets where we have inspectors.
+ *
+ * @return {Array} The list of LayoutActor fronts
+ */
+ async getLayoutFronts() {
+ const inspectorFronts = await this.inspector.getAllInspectorFronts();
+ const layoutFronts = await Promise.all(
+ inspectorFronts.map(({ walker }) => walker.getLayoutInspector())
+ );
+ return layoutFronts.filter(front => !front.isDestroyed());
+ }
+
+ /**
+ * Destruction function called when the inspector is destroyed. Removes event listeners
+ * and cleans up references.
+ */
+ destroy() {
+ if (this._highlighters) {
+ this.highlighters.off(
+ "grid-highlighter-hidden",
+ this.onHighlighterHidden
+ );
+ this.highlighters.off("grid-highlighter-shown", this.onHighlighterShown);
+ }
+ this.document.removeEventListener(
+ "mousemove",
+ this.listenForGridHighlighterEvents
+ );
+
+ this.inspector.sidebar.off("select", this.onSidebarSelect);
+ this.inspector.off("new-root", this.onNavigate);
+
+ this.inspector.off("reflow-in-selected-target", this.onReflow);
+
+ this._highlighters = null;
+ this.document = null;
+ this.inspector = null;
+ this.store = null;
+ }
+
+ getComponentProps() {
+ return {
+ onSetGridOverlayColor: this.onSetGridOverlayColor,
+ onToggleGridHighlighter: this.onToggleGridHighlighter,
+ onToggleShowGridAreas: this.onToggleShowGridAreas,
+ onToggleShowGridLineNumbers: this.onToggleShowGridLineNumbers,
+ onToggleShowInfiniteLines: this.onToggleShowInfiniteLines,
+ };
+ }
+
+ /**
+ * Returns the initial color linked to a grid container. Will attempt to check the
+ * current grid highlighter state and the store.
+ *
+ * @param {NodeFront} nodeFront
+ * The NodeFront for which we need the color.
+ * @param {String} customColor
+ * The color fetched from the custom palette, if it exists.
+ * @param {String} fallbackColor
+ * The color to use if no color could be found for the node front.
+ * @return {String} color
+ * The color to use.
+ */
+ getInitialGridColor(nodeFront, customColor, fallbackColor) {
+ const highlighted = this.highlighters.gridHighlighters.has(nodeFront);
+
+ let color;
+ if (customColor) {
+ color = customColor;
+ } else if (
+ highlighted &&
+ this.highlighters.state.grids.has(nodeFront.actorID)
+ ) {
+ // If the node front is currently highlighted, use the color from the highlighter
+ // options.
+ color = this.highlighters.state.grids.get(nodeFront.actorID).options
+ .color;
+ } else {
+ // Otherwise use the color defined in the store for this node front.
+ color = this.getGridColorForNodeFront(nodeFront);
+ }
+
+ return color || fallbackColor;
+ }
+
+ /**
+ * Returns the color set for the grid highlighter associated with the provided
+ * nodeFront.
+ *
+ * @param {NodeFront} nodeFront
+ * The NodeFront for which we need the color.
+ */
+ getGridColorForNodeFront(nodeFront) {
+ const { grids } = this.store.getState();
+
+ for (const grid of grids) {
+ if (grid.nodeFront === nodeFront) {
+ return grid.color;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Given a list of new grid fronts, and if there are highlighted grids, check
+ * if their fragments have changed.
+ *
+ * @param {Array} newGridFronts
+ * A list of GridFront objects.
+ * @return {Boolean}
+ */
+ haveCurrentFragmentsChanged(newGridFronts) {
+ const gridHighlighters = this.highlighters.gridHighlighters;
+
+ if (!gridHighlighters.size) {
+ return false;
+ }
+
+ const gridFronts = newGridFronts.filter(g =>
+ gridHighlighters.has(g.containerNodeFront)
+ );
+ if (!gridFronts.length) {
+ return false;
+ }
+
+ const { grids } = this.store.getState();
+
+ for (const node of gridHighlighters.keys()) {
+ const oldFragments = grids.find(g => g.nodeFront === node).gridFragments;
+ const newFragments = newGridFronts.find(
+ g => g.containerNodeFront === node
+ ).gridFragments;
+
+ if (!compareFragmentsGeometry(oldFragments, newFragments)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the layout panel is visible, and false otherwise.
+ */
+ isPanelVisible() {
+ return (
+ this.inspector &&
+ this.inspector.toolbox &&
+ this.inspector.sidebar &&
+ this.inspector.toolbox.currentToolId === "inspector" &&
+ this.inspector.sidebar.getCurrentTabID() === "layoutview"
+ );
+ }
+
+ /**
+ * Updates the grid panel by dispatching the new grid data. This is called when the
+ * layout view becomes visible or the view needs to be updated with new grid data.
+ */
+ async updateGridPanel() {
+ // Stop refreshing if the inspector or store is already destroyed.
+ if (!this.inspector || !this.store) {
+ return;
+ }
+
+ try {
+ await this._updateGridPanel();
+ } catch (e) {
+ this._throwUnlessDestroyed(
+ e,
+ "Inspector destroyed while executing updateGridPanel"
+ );
+ }
+ }
+
+ async _updateGridPanel() {
+ const gridFronts = await this.getGrids();
+
+ if (!gridFronts.length) {
+ try {
+ this.store.dispatch(updateGrids([]));
+ this.inspector.emit("grid-panel-updated");
+ return;
+ } catch (e) {
+ // This call might fail if called asynchrously after the toolbox is finished
+ // closing.
+ return;
+ }
+ }
+
+ const currentUrl = this.inspector.currentTarget.url;
+
+ // Log how many CSS Grid elements DevTools sees.
+ if (currentUrl != this.inspector.previousURL) {
+ this.telemetry
+ .getHistogramById(CSS_GRID_COUNT_HISTOGRAM_ID)
+ .add(gridFronts.length);
+ this.inspector.previousURL = currentUrl;
+ }
+
+ // Get the hostname, if there is no hostname, fall back on protocol
+ // ex: `data:` uri, and `about:` pages
+ const hostname =
+ parseURL(currentUrl).hostname || parseURL(currentUrl).protocol;
+ const customColors =
+ (await asyncStorage.getItem("gridInspectorHostColors")) || {};
+
+ const grids = [];
+ for (let i = 0; i < gridFronts.length; i++) {
+ const grid = gridFronts[i];
+ let nodeFront = grid.containerNodeFront;
+
+ // If the GridFront didn't yet have access to the NodeFront for its container, then
+ // get it from the walker. This happens when the walker hasn't yet seen this
+ // particular DOM Node in the tree yet, or when we are connected to an older server.
+ if (!nodeFront) {
+ try {
+ nodeFront = await grid.walkerFront.getNodeFromActor(grid.actorID, [
+ "containerEl",
+ ]);
+ } catch (e) {
+ // This call might fail if called asynchrously after the toolbox is finished
+ // closing.
+ return;
+ }
+ }
+
+ const colorForHost = customColors[hostname]
+ ? customColors[hostname][i]
+ : null;
+ const fallbackColor = GRID_COLORS[i % GRID_COLORS.length];
+ const color = this.getInitialGridColor(
+ nodeFront,
+ colorForHost,
+ fallbackColor
+ );
+ const highlighted = this.highlighters.gridHighlighters.has(nodeFront);
+ const disabled =
+ !highlighted &&
+ this.maxHighlighters > 1 &&
+ this.highlighters.gridHighlighters.size === this.maxHighlighters;
+ const isSubgrid = grid.isSubgrid;
+ const gridData = {
+ id: i,
+ actorID: grid.actorID,
+ color,
+ disabled,
+ direction: grid.direction,
+ gridFragments: grid.gridFragments,
+ highlighted,
+ isSubgrid,
+ nodeFront,
+ parentNodeActorID: null,
+ subgrids: [],
+ writingMode: grid.writingMode,
+ };
+
+ if (isSubgrid) {
+ let parentGridNodeFront;
+
+ try {
+ parentGridNodeFront = await nodeFront.walkerFront.getParentGridNode(
+ nodeFront
+ );
+ } catch (e) {
+ // This call might fail if called asynchrously after the toolbox is finished
+ // closing.
+ return;
+ }
+
+ if (!parentGridNodeFront) {
+ return;
+ }
+
+ const parentIndex = grids.findIndex(
+ g => g.nodeFront.actorID === parentGridNodeFront.actorID
+ );
+ gridData.parentNodeActorID = parentGridNodeFront.actorID;
+ grids[parentIndex].subgrids.push(gridData.id);
+ }
+
+ grids.push(gridData);
+ }
+
+ // We need to make sure that nested subgrids are displayed above their parent grid
+ // containers, so update the z-index of each grid before rendering them.
+ for (const root of grids.filter(g => !g.parentNodeActorID)) {
+ this._updateZOrder(grids, root);
+ }
+
+ this.store.dispatch(updateGrids(grids));
+ this.inspector.emit("grid-panel-updated");
+ }
+
+ /**
+ * Get all GridFront instances from the server(s).
+ *
+ *
+ * @return {Array} The list of GridFronts
+ */
+ async getGrids() {
+ const promises = [];
+ try {
+ const layoutFronts = await this.getLayoutFronts();
+ for (const layoutFront of layoutFronts) {
+ promises.push(layoutFront.getAllGrids());
+ }
+ } catch (e) {
+ // This call might fail if called asynchrously after the toolbox is finished closing
+ }
+
+ const gridFronts = (await Promise.all(promises)).flat();
+ return gridFronts;
+ }
+
+ /**
+ * Handler for "grid-highlighter-shown" events emitted from the
+ * HighlightersOverlay. Passes nodefront and event name to handleHighlighterChange.
+ * Required since on and off events need the same reference object.
+ *
+ * @param {NodeFront} nodeFront
+ * The NodeFront of the grid container element for which the grid
+ * highlighter is shown for.
+ */
+ onHighlighterShown(nodeFront) {
+ this.onHighlighterChange(nodeFront, true);
+ }
+
+ /**
+ * Handler for "grid-highlighter-hidden" events emitted from the
+ * HighlightersOverlay. Passes nodefront and event name to handleHighlighterChange.
+ * Required since on and off events need the same reference object.
+ *
+ * @param {NodeFront} nodeFront
+ * The NodeFront of the grid container element for which the grid highlighter
+ * is hidden for.
+ */
+ onHighlighterHidden(nodeFront) {
+ this.onHighlighterChange(nodeFront, false);
+ }
+
+ /**
+ * Handler for "grid-highlighter-shown" and "grid-highlighter-hidden" events emitted
+ * from the HighlightersOverlay. Updates the NodeFront's grid highlighted state.
+ *
+ * @param {NodeFront} nodeFront
+ * The NodeFront of the grid container element for which the grid highlighter
+ * is shown for.
+ * @param {Boolean} highlighted
+ * If the grid should be updated to highlight or hide.
+ */
+ onHighlighterChange(nodeFront, highlighted) {
+ if (!this.isPanelVisible()) {
+ return;
+ }
+
+ const { grids } = this.store.getState();
+ const grid = grids.find(g => g.nodeFront === nodeFront);
+
+ if (!grid || grid.highlighted === highlighted) {
+ return;
+ }
+
+ this.store.dispatch(updateGridHighlighted(nodeFront, highlighted));
+ }
+
+ /**
+ * Handler for "new-root" event fired by the inspector, which indicates a page
+ * navigation. Updates grid panel contents.
+ */
+ onNavigate() {
+ if (this.isPanelVisible()) {
+ this.updateGridPanel();
+ }
+ }
+
+ /**
+ * Handler for reflow events fired by the inspector when a node is selected. On reflows,
+ * update the grid panel content, because the shape or number of grids on the page may
+ * have changed.
+ *
+ * Note that there may be frequent reflows on the page and that not all of them actually
+ * cause the grids to change. So, we want to limit how many times we update the grid
+ * panel to only reflows that actually either change the list of grids, or those that
+ * change the current outlined grid.
+ * To achieve this, this function compares the list of grid containers from before and
+ * after the reflow, as well as the grid fragment data on the currently highlighted
+ * grid.
+ */
+ async onReflow() {
+ try {
+ if (!this.isPanelVisible()) {
+ return;
+ }
+
+ // The list of grids currently displayed.
+ const { grids } = this.store.getState();
+
+ // The new list of grids from the server.
+ const newGridFronts = await this.getGrids();
+
+ // In some cases, the nodes for current grids may have been removed from the DOM in
+ // which case we need to update.
+ if (grids.length && grids.some(grid => !grid.nodeFront.actorID)) {
+ await this.updateGridPanel(newGridFronts);
+ return;
+ }
+
+ // Get the node front(s) from the current grid(s) so we can compare them to them to
+ // the node(s) of the new grids.
+ const oldNodeFronts = grids.map(grid => grid.nodeFront.actorID);
+ const newNodeFronts = newGridFronts
+ .filter(grid => grid.containerNode)
+ .map(grid => grid.containerNodeFront.actorID);
+
+ if (
+ grids.length === newGridFronts.length &&
+ oldNodeFronts.sort().join(",") == newNodeFronts.sort().join(",") &&
+ !this.haveCurrentFragmentsChanged(newGridFronts)
+ ) {
+ // Same list of containers and the geometry of all the displayed grids remained the
+ // same, we can safely abort.
+ return;
+ }
+
+ // Either the list of containers or the current fragments have changed, do update.
+ await this.updateGridPanel(newGridFronts);
+ } catch (e) {
+ this._throwUnlessDestroyed(
+ e,
+ "Inspector destroyed while executing onReflow callback"
+ );
+ }
+ }
+
+ /**
+ * Handler for a change in the grid overlay color picker for a grid container.
+ *
+ * @param {NodeFront} node
+ * The NodeFront of the grid container element for which the grid color is
+ * being updated.
+ * @param {String} color
+ * A hex string representing the color to use.
+ */
+ async onSetGridOverlayColor(node, color) {
+ this.store.dispatch(updateGridColor(node, color));
+
+ const { grids } = this.store.getState();
+ const currentUrl = this.inspector.currentTarget.url;
+ // Get the hostname, if there is no hostname, fall back on protocol
+ // ex: `data:` uri, and `about:` pages
+ const hostname =
+ parseURL(currentUrl).hostname || parseURL(currentUrl).protocol;
+ const customGridColors =
+ (await asyncStorage.getItem("gridInspectorHostColors")) || {};
+
+ for (const grid of grids) {
+ if (grid.nodeFront === node) {
+ if (!customGridColors[hostname]) {
+ customGridColors[hostname] = [];
+ }
+ // Update the custom color for the grid in this position.
+ customGridColors[hostname][grid.id] = color;
+ await asyncStorage.setItem("gridInspectorHostColors", customGridColors);
+
+ if (!this.isPanelVisible()) {
+ // This call might fail if called asynchrously after the toolbox is finished
+ // closing.
+ return;
+ }
+
+ // If the grid for which the color was updated currently has a highlighter, update
+ // the color. If the node is not explicitly highlighted, we assume it's the
+ // parent grid for a subgrid.
+ if (this.highlighters.gridHighlighters.has(node)) {
+ this.highlighters.showGridHighlighter(node);
+ } else {
+ this.highlighters.showParentGridHighlighter(node);
+ }
+ }
+ }
+ }
+
+ /**
+ * Handler for the inspector sidebar "select" event. Starts tracking reflows
+ * if the layout panel is visible. Otherwise, stop tracking reflows.
+ * Finally, refresh the layout view if it is visible.
+ */
+ onSidebarSelect() {
+ if (!this.isPanelVisible()) {
+ this.inspector.off("reflow-in-selected-target", this.onReflow);
+ return;
+ }
+
+ this.inspector.on("reflow-in-selected-target", this.onReflow);
+ this.updateGridPanel();
+ }
+
+ /**
+ * Handler for a change in the input checkboxes in the GridList component.
+ * Toggles on/off the grid highlighter for the provided grid container element.
+ *
+ * @param {NodeFront} node
+ * The NodeFront of the grid container element for which the grid
+ * highlighter is toggled on/off for.
+ */
+ onToggleGridHighlighter(node) {
+ const { grids } = this.store.getState();
+ const grid = grids.find(g => g.nodeFront === node);
+ this.store.dispatch(updateGridHighlighted(node, !grid.highlighted));
+ this.highlighters.toggleGridHighlighter(node, "grid");
+ }
+
+ /**
+ * Handler for a change in the show grid areas checkbox in the GridDisplaySettings
+ * component. Toggles on/off the option to show the grid areas in the grid highlighter.
+ * Refreshes the shown grid highlighter for the grids currently highlighted.
+ *
+ * @param {Boolean} enabled
+ * Whether or not the grid highlighter should show the grid areas.
+ */
+ onToggleShowGridAreas(enabled) {
+ this.store.dispatch(updateShowGridAreas(enabled));
+ Services.prefs.setBoolPref(SHOW_GRID_AREAS, enabled);
+
+ if (enabled) {
+ this.telemetry.scalarSet(TELEMETRY_GRID_AREAS_OVERLAY_CHECKED, 1);
+ }
+
+ const { grids } = this.store.getState();
+
+ for (const grid of grids) {
+ if (grid.highlighted) {
+ this.highlighters.showGridHighlighter(grid.nodeFront);
+ }
+ }
+ }
+
+ /**
+ * Handler for a change in the show grid line numbers checkbox in the
+ * GridDisplaySettings component. Toggles on/off the option to show the grid line
+ * numbers in the grid highlighter. Refreshes the shown grid highlighter for the
+ * grids currently highlighted.
+ *
+ * @param {Boolean} enabled
+ * Whether or not the grid highlighter should show the grid line numbers.
+ */
+ onToggleShowGridLineNumbers(enabled) {
+ this.store.dispatch(updateShowGridLineNumbers(enabled));
+ Services.prefs.setBoolPref(SHOW_GRID_LINE_NUMBERS, enabled);
+
+ if (enabled) {
+ this.telemetry.scalarSet(TELEMETRY_GRID_LINE_NUMBERS_CHECKED, 1);
+ }
+
+ const { grids } = this.store.getState();
+
+ for (const grid of grids) {
+ if (grid.highlighted) {
+ this.highlighters.showGridHighlighter(grid.nodeFront);
+ }
+ }
+ }
+
+ /**
+ * Handler for a change in the extend grid lines infinitely checkbox in the
+ * GridDisplaySettings component. Toggles on/off the option to extend the grid
+ * lines infinitely in the grid highlighter. Refreshes the shown grid highlighter
+ * for grids currently highlighted.
+ *
+ * @param {Boolean} enabled
+ * Whether or not the grid highlighter should extend grid lines infinitely.
+ */
+ onToggleShowInfiniteLines(enabled) {
+ this.store.dispatch(updateShowInfiniteLines(enabled));
+ Services.prefs.setBoolPref(SHOW_INFINITE_LINES_PREF, enabled);
+
+ if (enabled) {
+ this.telemetry.scalarSet(TELEMETRY_INFINITE_LINES_CHECKED, 1);
+ }
+
+ const { grids } = this.store.getState();
+
+ for (const grid of grids) {
+ if (grid.highlighted) {
+ this.highlighters.showGridHighlighter(grid.nodeFront);
+ }
+ }
+ }
+
+ /**
+ * Some grid-inspector methods are highly asynchronous and might still run
+ * after the inspector was destroyed. Swallow errors if the grid inspector is
+ * already destroyed, throw otherwise.
+ *
+ * @param {Error} error
+ * The original error object.
+ * @param {String} message
+ * The message to log in case the inspector is already destroyed and
+ * the error is swallowed.
+ */
+ _throwUnlessDestroyed(error, message) {
+ if (!this.inspector) {
+ console.warn(message);
+ } else {
+ // If the grid inspector was not destroyed, this is an unexpected error.
+ throw error;
+ }
+ }
+
+ /**
+ * Set z-index of each grids so that nested subgrids are always above their parent grid
+ * container.
+ *
+ * @param {Array} grids
+ * A list of grid data.
+ * @param {Object} parent
+ * A grid data of parent.
+ * @param {Number} zIndex
+ * z-index for the parent.
+ */
+ _updateZOrder(grids, parent, zIndex = 0) {
+ parent.zIndex = zIndex;
+
+ for (const childIndex of parent.subgrids) {
+ // Recurse into children grids.
+ this._updateZOrder(grids, grids[childIndex], zIndex + 1);
+ }
+ }
+}
+
+module.exports = GridInspector;