grid-inspector.js (24281B)
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/. */ 4 5 "use strict"; 6 7 const flags = require("resource://devtools/shared/flags.js"); 8 const { throttle } = require("resource://devtools/shared/throttle.js"); 9 10 const gridsReducer = require("resource://devtools/client/inspector/grids/reducers/grids.js"); 11 const highlighterSettingsReducer = require("resource://devtools/client/inspector/grids/reducers/highlighter-settings.js"); 12 const { 13 updateGridColor, 14 updateGridHighlighted, 15 updateGrids, 16 } = require("resource://devtools/client/inspector/grids/actions/grids.js"); 17 const { 18 updateShowGridAreas, 19 updateShowGridLineNumbers, 20 updateShowInfiniteLines, 21 } = require("resource://devtools/client/inspector/grids/actions/highlighter-settings.js"); 22 23 loader.lazyRequireGetter( 24 this, 25 "compareFragmentsGeometry", 26 "resource://devtools/client/inspector/grids/utils/utils.js", 27 true 28 ); 29 loader.lazyRequireGetter( 30 this, 31 "parseURL", 32 "resource://devtools/client/shared/source-utils.js", 33 true 34 ); 35 loader.lazyRequireGetter( 36 this, 37 "asyncStorage", 38 "resource://devtools/shared/async-storage.js" 39 ); 40 41 const SHOW_GRID_AREAS = "devtools.gridinspector.showGridAreas"; 42 const SHOW_GRID_LINE_NUMBERS = "devtools.gridinspector.showGridLineNumbers"; 43 const SHOW_INFINITE_LINES_PREF = "devtools.gridinspector.showInfiniteLines"; 44 45 // Default grid colors. 46 const GRID_COLORS = [ 47 "#9400FF", 48 "#DF00A9", 49 "#0A84FF", 50 "#12BC00", 51 "#EA8000", 52 "#00B0BD", 53 "#D70022", 54 "#4B42FF", 55 "#B5007F", 56 "#058B00", 57 "#A47F00", 58 "#005A71", 59 ]; 60 61 class GridInspector { 62 constructor(inspector, window) { 63 this.document = window.document; 64 this.inspector = inspector; 65 this.store = inspector.store; 66 this.telemetry = inspector.telemetry; 67 68 // Maximum number of grid highlighters that can be displayed. 69 this.maxHighlighters = Services.prefs.getIntPref( 70 "devtools.gridinspector.maxHighlighters" 71 ); 72 73 this.store.injectReducer("grids", gridsReducer); 74 this.store.injectReducer("highlighterSettings", highlighterSettingsReducer); 75 76 this.onHighlighterShown = this.onHighlighterShown.bind(this); 77 this.onHighlighterHidden = this.onHighlighterHidden.bind(this); 78 this.onNavigate = this.onNavigate.bind(this); 79 this.onReflow = throttle(this.onReflow, 500, this); 80 this.onSetGridOverlayColor = this.onSetGridOverlayColor.bind(this); 81 this.onSidebarSelect = this.onSidebarSelect.bind(this); 82 this.onToggleGridHighlighter = this.onToggleGridHighlighter.bind(this); 83 this.onToggleShowGridAreas = this.onToggleShowGridAreas.bind(this); 84 this.onToggleShowGridLineNumbers = 85 this.onToggleShowGridLineNumbers.bind(this); 86 this.onToggleShowInfiniteLines = this.onToggleShowInfiniteLines.bind(this); 87 this.updateGridPanel = this.updateGridPanel.bind(this); 88 this.listenForGridHighlighterEvents = 89 this.listenForGridHighlighterEvents.bind(this); 90 91 this.init(); 92 } 93 94 get highlighters() { 95 if (!this._highlighters) { 96 this._highlighters = this.inspector.highlighters; 97 } 98 99 return this._highlighters; 100 } 101 102 /** 103 * Initializes the grid inspector by fetching the LayoutFront from the walker and 104 * loading the highlighter settings. 105 */ 106 async init() { 107 if (!this.inspector) { 108 return; 109 } 110 111 if (flags.testing) { 112 // In tests, we start listening immediately to avoid having to simulate a mousemove. 113 this.listenForGridHighlighterEvents(); 114 } else { 115 this.document.addEventListener( 116 "mousemove", 117 this.listenForGridHighlighterEvents, 118 { 119 once: true, 120 } 121 ); 122 } 123 124 this.inspector.sidebar.on("select", this.onSidebarSelect); 125 this.inspector.on("new-root", this.onNavigate); 126 127 this.onSidebarSelect(); 128 } 129 130 listenForGridHighlighterEvents() { 131 this.highlighters.on("grid-highlighter-hidden", this.onHighlighterHidden); 132 this.highlighters.on("grid-highlighter-shown", this.onHighlighterShown); 133 } 134 135 /** 136 * Get the LayoutActor fronts for all interesting targets where we have inspectors. 137 * 138 * @return {Array} The list of LayoutActor fronts 139 */ 140 async getLayoutFronts() { 141 const inspectorFronts = await this.inspector.getAllInspectorFronts(); 142 const layoutFronts = await Promise.all( 143 inspectorFronts.map(({ walker }) => walker.getLayoutInspector()) 144 ); 145 return layoutFronts.filter(front => !front.isDestroyed()); 146 } 147 148 /** 149 * Destruction function called when the inspector is destroyed. Removes event listeners 150 * and cleans up references. 151 */ 152 destroy() { 153 if (this._highlighters) { 154 this.highlighters.off( 155 "grid-highlighter-hidden", 156 this.onHighlighterHidden 157 ); 158 this.highlighters.off("grid-highlighter-shown", this.onHighlighterShown); 159 } 160 this.document.removeEventListener( 161 "mousemove", 162 this.listenForGridHighlighterEvents 163 ); 164 165 this.inspector.sidebar.off("select", this.onSidebarSelect); 166 this.inspector.off("new-root", this.onNavigate); 167 168 this.inspector.off("reflow", this.onReflow); 169 170 this._highlighters = null; 171 this.document = null; 172 this.inspector = null; 173 this.store = null; 174 } 175 176 getComponentProps() { 177 return { 178 onSetGridOverlayColor: this.onSetGridOverlayColor, 179 onToggleGridHighlighter: this.onToggleGridHighlighter, 180 onToggleShowGridAreas: this.onToggleShowGridAreas, 181 onToggleShowGridLineNumbers: this.onToggleShowGridLineNumbers, 182 onToggleShowInfiniteLines: this.onToggleShowInfiniteLines, 183 }; 184 } 185 186 /** 187 * Returns the initial color linked to a grid container. Will attempt to check the 188 * current grid highlighter state and the store. 189 * 190 * @param {NodeFront} nodeFront 191 * The NodeFront for which we need the color. 192 * @param {string} customColor 193 * The color fetched from the custom palette, if it exists. 194 * @param {string} fallbackColor 195 * The color to use if no color could be found for the node front. 196 * @return {string} color 197 * The color to use. 198 */ 199 getInitialGridColor(nodeFront, customColor, fallbackColor) { 200 const highlighted = this.highlighters.gridHighlighters.has(nodeFront); 201 202 let color; 203 if (customColor) { 204 color = customColor; 205 } else if ( 206 highlighted && 207 this.highlighters.state.grids.has(nodeFront.actorID) 208 ) { 209 // If the node front is currently highlighted, use the color from the highlighter 210 // options. 211 color = this.highlighters.state.grids.get(nodeFront.actorID).options 212 .color; 213 } else { 214 // Otherwise use the color defined in the store for this node front. 215 color = this.getGridColorForNodeFront(nodeFront); 216 } 217 218 return color || fallbackColor; 219 } 220 221 /** 222 * Returns the color set for the grid highlighter associated with the provided 223 * nodeFront. 224 * 225 * @param {NodeFront} nodeFront 226 * The NodeFront for which we need the color. 227 */ 228 getGridColorForNodeFront(nodeFront) { 229 const { grids } = this.store.getState(); 230 231 for (const grid of grids) { 232 if (grid.nodeFront === nodeFront) { 233 return grid.color; 234 } 235 } 236 237 return null; 238 } 239 240 /** 241 * Given a list of new grid fronts, and if there are highlighted grids, check 242 * if their fragments have changed. 243 * 244 * @param {Array} newGridFronts 245 * A list of GridFront objects. 246 * @return {boolean} 247 */ 248 haveCurrentFragmentsChanged(newGridFronts) { 249 const gridHighlighters = this.highlighters.gridHighlighters; 250 251 if (!gridHighlighters.size) { 252 return false; 253 } 254 255 const gridFronts = newGridFronts.filter(g => 256 gridHighlighters.has(g.containerNodeFront) 257 ); 258 if (!gridFronts.length) { 259 return false; 260 } 261 262 const { grids } = this.store.getState(); 263 264 for (const node of gridHighlighters.keys()) { 265 const oldFragments = grids.find(g => g.nodeFront === node).gridFragments; 266 const newFragments = newGridFronts.find( 267 g => g.containerNodeFront === node 268 ).gridFragments; 269 270 if (!compareFragmentsGeometry(oldFragments, newFragments)) { 271 return true; 272 } 273 } 274 275 return false; 276 } 277 278 /** 279 * Returns true if the layout panel is visible, and false otherwise. 280 */ 281 isPanelVisible() { 282 return ( 283 this.inspector && 284 this.inspector.toolbox && 285 this.inspector.sidebar && 286 this.inspector.toolbox.currentToolId === "inspector" && 287 this.inspector.sidebar.getCurrentTabID() === "layoutview" 288 ); 289 } 290 291 /** 292 * Updates the grid panel by dispatching the new grid data. This is called when the 293 * layout view becomes visible or the view needs to be updated with new grid data. 294 */ 295 async updateGridPanel() { 296 // Stop refreshing if the inspector or store is already destroyed. 297 if (!this.inspector || !this.store) { 298 return; 299 } 300 301 try { 302 await this._updateGridPanel(); 303 } catch (e) { 304 this._throwUnlessDestroyed( 305 e, 306 "Inspector destroyed while executing updateGridPanel" 307 ); 308 } 309 } 310 311 async _updateGridPanel() { 312 const gridFronts = await this.getGrids(); 313 314 if (!gridFronts.length) { 315 try { 316 this.store.dispatch(updateGrids([])); 317 this.inspector.emit("grid-panel-updated"); 318 return; 319 } catch (e) { 320 // This call might fail if called asynchrously after the toolbox is finished 321 // closing. 322 return; 323 } 324 } 325 326 const currentUrl = this.inspector.currentTarget.url; 327 328 // Log how many CSS Grid elements DevTools sees. 329 if (currentUrl != this.inspector.previousURL) { 330 Glean.devtoolsInspector.numberOfCssGridsInAPage.accumulateSingleSample( 331 gridFronts.length 332 ); 333 this.inspector.previousURL = currentUrl; 334 } 335 336 // Get the hostname, if there is no hostname, fall back on protocol 337 // ex: `data:` uri, and `about:` pages 338 const hostname = 339 parseURL(currentUrl).hostname || parseURL(currentUrl).protocol; 340 const customColors = 341 (await asyncStorage.getItem("gridInspectorHostColors")) || {}; 342 343 const grids = []; 344 for (let i = 0; i < gridFronts.length; i++) { 345 const grid = gridFronts[i]; 346 let nodeFront = grid.containerNodeFront; 347 348 // If the GridFront didn't yet have access to the NodeFront for its container, then 349 // get it from the walker. This happens when the walker hasn't yet seen this 350 // particular DOM Node in the tree yet, or when we are connected to an older server. 351 if (!nodeFront) { 352 try { 353 nodeFront = await grid.walkerFront.getNodeFromActor(grid.actorID, [ 354 "containerEl", 355 ]); 356 } catch (e) { 357 // This call might fail if called asynchrously after the toolbox is finished 358 // closing. 359 return; 360 } 361 } 362 363 const colorForHost = customColors[hostname] 364 ? customColors[hostname][i] 365 : null; 366 const fallbackColor = GRID_COLORS[i % GRID_COLORS.length]; 367 const color = this.getInitialGridColor( 368 nodeFront, 369 colorForHost, 370 fallbackColor 371 ); 372 const highlighted = this.highlighters.gridHighlighters.has(nodeFront); 373 const disabled = 374 !highlighted && 375 this.maxHighlighters > 1 && 376 this.highlighters.gridHighlighters.size === this.maxHighlighters; 377 const isSubgrid = grid.isSubgrid; 378 const gridData = { 379 id: i, 380 actorID: grid.actorID, 381 color, 382 disabled, 383 direction: grid.direction, 384 gridFragments: grid.gridFragments, 385 highlighted, 386 isSubgrid, 387 nodeFront, 388 parentNodeActorID: null, 389 subgrids: [], 390 writingMode: grid.writingMode, 391 }; 392 393 if (isSubgrid) { 394 let parentGridNodeFront; 395 396 try { 397 parentGridNodeFront = 398 await nodeFront.walkerFront.getParentGridNode(nodeFront); 399 } catch (e) { 400 // This call might fail if called asynchrously after the toolbox is finished 401 // closing. 402 return; 403 } 404 405 if (!parentGridNodeFront) { 406 return; 407 } 408 409 const parentIndex = grids.findIndex( 410 g => g.nodeFront.actorID === parentGridNodeFront.actorID 411 ); 412 gridData.parentNodeActorID = parentGridNodeFront.actorID; 413 grids[parentIndex].subgrids.push(gridData.id); 414 } 415 416 grids.push(gridData); 417 } 418 419 // We need to make sure that nested subgrids are displayed above their parent grid 420 // containers, so update the z-index of each grid before rendering them. 421 for (const root of grids.filter(g => !g.parentNodeActorID)) { 422 this._updateZOrder(grids, root); 423 } 424 425 this.store.dispatch(updateGrids(grids)); 426 this.inspector.emit("grid-panel-updated"); 427 } 428 429 /** 430 * Get all GridFront instances from the server(s). 431 * 432 * @return {Array} The list of GridFronts 433 */ 434 async getGrids() { 435 const promises = []; 436 try { 437 const layoutFronts = await this.getLayoutFronts(); 438 for (const layoutFront of layoutFronts) { 439 promises.push(layoutFront.getAllGrids()); 440 } 441 } catch (e) { 442 // This call might fail if called asynchrously after the toolbox is finished closing 443 } 444 445 const gridFronts = (await Promise.all(promises)).flat(); 446 return gridFronts; 447 } 448 449 /** 450 * Handler for "grid-highlighter-shown" events emitted from the 451 * HighlightersOverlay. Passes nodefront and event name to handleHighlighterChange. 452 * Required since on and off events need the same reference object. 453 * 454 * @param {NodeFront} nodeFront 455 * The NodeFront of the grid container element for which the grid 456 * highlighter is shown for. 457 */ 458 onHighlighterShown(nodeFront) { 459 this.onHighlighterChange(nodeFront, true); 460 } 461 462 /** 463 * Handler for "grid-highlighter-hidden" events emitted from the 464 * HighlightersOverlay. Passes nodefront and event name to handleHighlighterChange. 465 * Required since on and off events need the same reference object. 466 * 467 * @param {NodeFront} nodeFront 468 * The NodeFront of the grid container element for which the grid highlighter 469 * is hidden for. 470 */ 471 onHighlighterHidden(nodeFront) { 472 this.onHighlighterChange(nodeFront, false); 473 } 474 475 /** 476 * Handler for "grid-highlighter-shown" and "grid-highlighter-hidden" events emitted 477 * from the HighlightersOverlay. Updates the NodeFront's grid highlighted state. 478 * 479 * @param {NodeFront} nodeFront 480 * The NodeFront of the grid container element for which the grid highlighter 481 * is shown for. 482 * @param {boolean} highlighted 483 * If the grid should be updated to highlight or hide. 484 */ 485 onHighlighterChange(nodeFront, highlighted) { 486 if (!this.isPanelVisible()) { 487 return; 488 } 489 490 const { grids } = this.store.getState(); 491 const grid = grids.find(g => g.nodeFront === nodeFront); 492 493 if (!grid || grid.highlighted === highlighted) { 494 return; 495 } 496 497 this.store.dispatch(updateGridHighlighted(nodeFront, highlighted)); 498 } 499 500 /** 501 * Handler for "new-root" event fired by the inspector, which indicates a page 502 * navigation. Updates grid panel contents. 503 */ 504 onNavigate() { 505 if (this.isPanelVisible()) { 506 this.updateGridPanel(); 507 } 508 } 509 510 /** 511 * Handler for reflow events fired by the inspector when a node is selected. On reflows, 512 * update the grid panel content, because the shape or number of grids on the page may 513 * have changed. 514 * 515 * Note that there may be frequent reflows on the page and that not all of them actually 516 * cause the grids to change. So, we want to limit how many times we update the grid 517 * panel to only reflows that actually either change the list of grids, or those that 518 * change the current outlined grid. 519 * To achieve this, this function compares the list of grid containers from before and 520 * after the reflow, as well as the grid fragment data on the currently highlighted 521 * grid. 522 */ 523 async onReflow() { 524 try { 525 if (!this.isPanelVisible()) { 526 return; 527 } 528 529 // The list of grids currently displayed. 530 const { grids } = this.store.getState(); 531 532 // The new list of grids from the server. 533 const newGridFronts = await this.getGrids(); 534 535 // In some cases, the nodes for current grids may have been removed from the DOM in 536 // which case we need to update. 537 if (grids.length && grids.some(grid => !grid.nodeFront.actorID)) { 538 await this.updateGridPanel(newGridFronts); 539 return; 540 } 541 542 // Get the node front(s) from the current grid(s) so we can compare them to them to 543 // the node(s) of the new grids. 544 const oldNodeFronts = grids.map(grid => grid.nodeFront.actorID); 545 const newNodeFronts = newGridFronts 546 .filter(grid => grid.containerNode) 547 .map(grid => grid.containerNodeFront.actorID); 548 549 if ( 550 grids.length === newGridFronts.length && 551 oldNodeFronts.sort().join(",") == newNodeFronts.sort().join(",") && 552 !this.haveCurrentFragmentsChanged(newGridFronts) 553 ) { 554 // Same list of containers and the geometry of all the displayed grids remained the 555 // same, we can safely abort. 556 return; 557 } 558 559 // Either the list of containers or the current fragments have changed, do update. 560 await this.updateGridPanel(newGridFronts); 561 } catch (e) { 562 this._throwUnlessDestroyed( 563 e, 564 "Inspector destroyed while executing onReflow callback" 565 ); 566 } 567 } 568 569 /** 570 * Handler for a change in the grid overlay color picker for a grid container. 571 * 572 * @param {NodeFront} node 573 * The NodeFront of the grid container element for which the grid color is 574 * being updated. 575 * @param {string} color 576 * A hex string representing the color to use. 577 */ 578 async onSetGridOverlayColor(node, color) { 579 this.store.dispatch(updateGridColor(node, color)); 580 581 const { grids } = this.store.getState(); 582 const currentUrl = this.inspector.currentTarget.url; 583 // Get the hostname, if there is no hostname, fall back on protocol 584 // ex: `data:` uri, and `about:` pages 585 const hostname = 586 parseURL(currentUrl).hostname || parseURL(currentUrl).protocol; 587 const customGridColors = 588 (await asyncStorage.getItem("gridInspectorHostColors")) || {}; 589 590 for (const grid of grids) { 591 if (grid.nodeFront !== node) { 592 continue; 593 } 594 595 if (!customGridColors[hostname]) { 596 customGridColors[hostname] = []; 597 } 598 // Update the custom color for the grid in this position. 599 customGridColors[hostname][grid.id] = color; 600 await asyncStorage.setItem("gridInspectorHostColors", customGridColors); 601 602 if (!this.isPanelVisible()) { 603 // This call might fail if called asynchrously after the toolbox is finished 604 // closing. 605 return; 606 } 607 608 // If the grid for which the color was updated currently has a highlighter, update 609 // the color. 610 if (this.highlighters.gridHighlighters.has(node)) { 611 this.highlighters.showGridHighlighter(node); 612 continue; 613 } 614 615 // If the node is not explicitly highlighted, but is a parent grid which has an 616 // highlighted subgrid, we also want to update the color. 617 const subGrid = grids.find(({ id }) => grid.subgrids.includes(id)); 618 if (subGrid?.highlighted) { 619 this.highlighters.showParentGridHighlighter(node); 620 } 621 } 622 } 623 624 /** 625 * Handler for the inspector sidebar "select" event. Starts tracking reflows 626 * if the layout panel is visible. Otherwise, stop tracking reflows. 627 * Finally, refresh the layout view if it is visible. 628 */ 629 onSidebarSelect() { 630 if (!this.isPanelVisible()) { 631 this.inspector.off("reflow", this.onReflow); 632 return; 633 } 634 635 // The panel shows grids from all debugged document, so we need to listen for the 636 // `reflow` event (and not `reflow-in-selected-target`). 637 this.inspector.on("reflow", this.onReflow); 638 this.updateGridPanel(); 639 } 640 641 /** 642 * Handler for a change in the input checkboxes in the GridList component. 643 * Toggles on/off the grid highlighter for the provided grid container element. 644 * 645 * @param {NodeFront} node 646 * The NodeFront of the grid container element for which the grid 647 * highlighter is toggled on/off for. 648 */ 649 onToggleGridHighlighter(node) { 650 const { grids } = this.store.getState(); 651 const grid = grids.find(g => g.nodeFront === node); 652 this.store.dispatch(updateGridHighlighted(node, !grid.highlighted)); 653 this.highlighters.toggleGridHighlighter(node, "grid"); 654 } 655 656 /** 657 * Handler for a change in the show grid areas checkbox in the GridDisplaySettings 658 * component. Toggles on/off the option to show the grid areas in the grid highlighter. 659 * Refreshes the shown grid highlighter for the grids currently highlighted. 660 * 661 * @param {boolean} enabled 662 * Whether or not the grid highlighter should show the grid areas. 663 */ 664 onToggleShowGridAreas(enabled) { 665 this.store.dispatch(updateShowGridAreas(enabled)); 666 Services.prefs.setBoolPref(SHOW_GRID_AREAS, enabled); 667 668 const { grids } = this.store.getState(); 669 670 for (const grid of grids) { 671 if (grid.highlighted) { 672 this.highlighters.showGridHighlighter(grid.nodeFront); 673 } 674 } 675 } 676 677 /** 678 * Handler for a change in the show grid line numbers checkbox in the 679 * GridDisplaySettings component. Toggles on/off the option to show the grid line 680 * numbers in the grid highlighter. Refreshes the shown grid highlighter for the 681 * grids currently highlighted. 682 * 683 * @param {boolean} enabled 684 * Whether or not the grid highlighter should show the grid line numbers. 685 */ 686 onToggleShowGridLineNumbers(enabled) { 687 this.store.dispatch(updateShowGridLineNumbers(enabled)); 688 Services.prefs.setBoolPref(SHOW_GRID_LINE_NUMBERS, enabled); 689 690 const { grids } = this.store.getState(); 691 692 for (const grid of grids) { 693 if (grid.highlighted) { 694 this.highlighters.showGridHighlighter(grid.nodeFront); 695 } 696 } 697 } 698 699 /** 700 * Handler for a change in the extend grid lines infinitely checkbox in the 701 * GridDisplaySettings component. Toggles on/off the option to extend the grid 702 * lines infinitely in the grid highlighter. Refreshes the shown grid highlighter 703 * for grids currently highlighted. 704 * 705 * @param {boolean} enabled 706 * Whether or not the grid highlighter should extend grid lines infinitely. 707 */ 708 onToggleShowInfiniteLines(enabled) { 709 this.store.dispatch(updateShowInfiniteLines(enabled)); 710 Services.prefs.setBoolPref(SHOW_INFINITE_LINES_PREF, enabled); 711 712 const { grids } = this.store.getState(); 713 714 for (const grid of grids) { 715 if (grid.highlighted) { 716 this.highlighters.showGridHighlighter(grid.nodeFront); 717 } 718 } 719 } 720 721 /** 722 * Some grid-inspector methods are highly asynchronous and might still run 723 * after the inspector was destroyed. Swallow errors if the grid inspector is 724 * already destroyed, throw otherwise. 725 * 726 * @param {Error} error 727 * The original error object. 728 * @param {string} message 729 * The message to log in case the inspector is already destroyed and 730 * the error is swallowed. 731 */ 732 _throwUnlessDestroyed(error, message) { 733 if (!this.inspector) { 734 console.warn(message); 735 } else { 736 // If the grid inspector was not destroyed, this is an unexpected error. 737 throw error; 738 } 739 } 740 741 /** 742 * Set z-index of each grids so that nested subgrids are always above their parent grid 743 * container. 744 * 745 * @param {Array} grids 746 * A list of grid data. 747 * @param {object} parent 748 * A grid data of parent. 749 * @param {number} zIndex 750 * z-index for the parent. 751 */ 752 _updateZOrder(grids, parent, zIndex = 0) { 753 parent.zIndex = zIndex; 754 755 for (const childIndex of parent.subgrids) { 756 // Recurse into children grids. 757 this._updateZOrder(grids, grids[childIndex], zIndex + 1); 758 } 759 } 760 } 761 762 module.exports = GridInspector;