tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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;