Bug 1317102 - Display multiple grid containers in the CSS Grid Inspector. r=pbro draft
authorGabriel Luong <gabriel.luong@gmail.com>
Mon, 28 May 2018 11:27:30 -0400
changeset 800591 c1d35cf38009b5ffae4d525677ebe1f29bb2e796
parent 800590 f0a6c51844657701662886c0102f6a4ef9d56835
push id111413
push userbmo:gl@mozilla.com
push dateMon, 28 May 2018 15:27:59 +0000
reviewerspbro
bugs1317102
milestone62.0a1
Bug 1317102 - Display multiple grid containers in the CSS Grid Inspector. r=pbro MozReview-Commit-ID: 6LN6QE7k2yX
devtools/client/inspector/grids/grid-inspector.js
devtools/client/inspector/grids/reducers/grids.js
devtools/client/inspector/rules/views/text-property-editor.js
devtools/client/inspector/shared/highlighters-overlay.js
--- a/devtools/client/inspector/grids/grid-inspector.js
+++ b/devtools/client/inspector/grids/grid-inspector.js
@@ -175,25 +175,25 @@ class GridInspector {
    *         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) {
     let highlighted = this._highlighters &&
-      nodeFront == this.highlighters.gridHighlighterShown;
+      this.highlighters.gridHighlightersShown.includes(nodeFront);
 
     let color;
     if (customColor) {
       color = customColor;
-    } else if (highlighted && this.highlighters.state.grid.options) {
+    } 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.grid.options.color;
+      color = this.highlighters.state.grids.get(nodeFront).options.color;
     } else {
       // Otherwise use the color defined in the store for this node front.
       color = this.getGridColorForNodeFront(nodeFront);
     }
 
     return color || fallbackColor;
   }
 
@@ -219,31 +219,31 @@ class GridInspector {
   /**
    * Retrieve the shared SwatchColorPicker instance.
    */
   getSwatchColorPickerTooltip() {
     return this.swatchColorPickerTooltip;
   }
 
   /**
-   * Given a list of new grid fronts, and if we have a currently highlighted grid, check
+   * Given a list of new grid fronts, and if there are highlighted grids, check
    * if its fragments have changed.
    *
    * @param  {Array} newGridFronts
    *         A list of GridFront objects.
    * @return {Boolean}
    */
   haveCurrentFragmentsChanged(newGridFronts) {
-    const currentNode = this.highlighters.gridHighlighterShown;
-    if (!currentNode) {
+    if (!this.highlighters.gridHighlightersShown) {
       return false;
     }
 
-    const newGridFront = newGridFronts.find(g => g.containerNodeFront === currentNode);
-    if (!newGridFront) {
+    const gridFronts = newGridFronts.find(g =>
+      this.highlighters.gridHighlightersShown.includes(g.containerNodeFront));
+    if (!gridFronts.length) {
       return false;
     }
 
     const { grids } = this.store.getState();
     const oldFragments = grids.find(g => g.nodeFront === currentNode).gridFragments;
     const newFragments = newGridFront.gridFragments;
 
     return !compareFragmentsGeometry(oldFragments, newFragments);
@@ -448,19 +448,18 @@ class GridInspector {
     // Otherwise, continue comparing with the new grids.
     const newNodeFronts = newGridFronts.filter(grid => grid.containerNodeFront)
                                        .map(grid => grid.containerNodeFront.actorID);
     if (grids.length === newGridFronts.length &&
         oldNodeFronts.sort().join(",") == newNodeFronts.sort().join(",")) {
       // Same list of containers, but let's check if the geometry of the current grid has
       // changed, if it hasn't we can safely abort.
       if (!this._highlighters ||
-          !this.highlighters.gridHighlighterShown ||
-          (this.highlighters.gridHighlighterShown &&
-           !this.haveCurrentFragmentsChanged(newGridFronts))) {
+          !this.highlighters.gridHighlightersShown.length ||
+          !this.haveCurrentFragmentsChanged(newGridFronts)) {
         return;
       }
     }
 
     // Either the list of containers or the current fragments have changed, do update.
     this.updateGridPanel(newGridFronts);
   }
 
--- a/devtools/client/inspector/grids/reducers/grids.js
+++ b/devtools/client/inspector/grids/reducers/grids.js
@@ -23,21 +23,21 @@ let reducers = {
       return g;
     });
 
     return newGrids;
   },
 
   [UPDATE_GRID_HIGHLIGHTED](grids, { nodeFront, highlighted }) {
     return grids.map(g => {
-      let isUpdatedNode = g.nodeFront === nodeFront;
+      if (g.nodeFront == nodeFront) {
+        g = Object.assign({}, g, { highlighted });
+      }
 
-      return Object.assign({}, g, {
-        highlighted: isUpdatedNode && highlighted
-      });
+      return g;
     });
   },
 
   [UPDATE_GRIDS](_, { grids }) {
     return grids;
   },
 
 };
--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -527,18 +527,18 @@ TextPropertyEditor.prototype = {
           this.ruleView.inspector.selection.nodeFront) {
         flexToggle.classList.add("active");
       }
     }
 
     let gridToggle = this.valueSpan.querySelector(".ruleview-grid");
     if (gridToggle) {
       gridToggle.setAttribute("title", l10n("rule.gridToggle.tooltip"));
-      if (this.ruleView.highlighters.gridHighlighterShown ===
-          this.ruleView.inspector.selection.nodeFront) {
+      if (this.ruleView.highlighters.gridHighlightersShown.includes(
+          this.ruleView.inspector.selection.nodeFront)) {
         gridToggle.classList.add("active");
       }
     }
 
     let shapeToggle = this.valueSpan.querySelector(".ruleview-shapeswatch");
     if (shapeToggle) {
       let mode = "css" + name.split("-").map(s => {
         return s[0].toUpperCase() + s.slice(1);
--- a/devtools/client/inspector/shared/highlighters-overlay.js
+++ b/devtools/client/inspector/shared/highlighters-overlay.js
@@ -28,48 +28,53 @@ const SHOW_INFINITE_LINES_PREF = "devtoo
  * Highlighters overlay is a singleton managing all highlighters in the Inspector.
  */
 class HighlightersOverlay {
   /**
    * @param  {Inspector} inspector
    *         Inspector toolbox panel.
    */
   constructor(inspector) {
-    /*
-    * Collection of instantiated highlighter actors like FlexboxHighlighter,
-    * CssGridHighlighter, ShapesHighlighter and GeometryEditorHighlighter.
-    */
+    /**
+     * Collection of instantiated highlighter actors like FlexboxHighlighter,
+     * ShapesHighlighter and GeometryEditorHighlighter.
+     */
     this.highlighters = {};
-    /*
-    * Collection of instantiated in-context editors, like ShapesInContextEditor, which
-    * behave like highlighters but with added editing capabilities that need to map value
-    * changes to properties in the Rule view.
-    */
+    // Collection of instantiated grid highlighter actors.
+    this.gridHighlighters = new Map();
+    /**
+     * Collection of instantiated in-context editors, like ShapesInContextEditor, which
+     * behave like highlighters but with added editing capabilities that need to map value
+     * changes to properties in the Rule view.
+     */
     this.editors = {};
     this.inspector = inspector;
     this.highlighterUtils = this.inspector.toolbox.highlighterUtils;
     this.store = this.inspector.store;
     this.telemetry = inspector.telemetry;
 
     // NodeFront of the flexbox container that is highlighted.
     this.flexboxHighlighterShown = null;
     // NodeFront of element that is highlighted by the geometry editor.
     this.geometryEditorHighlighterShown = null;
-    // NodeFront of the grid container that is highlighted.
-    this.gridHighlighterShown = null;
     // Name of the highlighter shown on mouse hover.
     this.hoveredHighlighterShown = null;
     // Name of the selector highlighter shown.
     this.selectorHighlighterShown = null;
     // NodeFront of the shape that is highlighted
     this.shapesHighlighterShown = null;
+
+    // Array of grid containers that are highlighted.
+    this.gridHighlightersShown = [];
+
     // Saved state to be restore on page navigation.
     this.state = {
       flexbox: {},
-      grid: {},
+      // Map of grid NodeFront actorID to the their stored grid options
+      grids: new Map(),
       shapes: {},
     };
 
     this.onClick = this.onClick.bind(this);
     this.onMarkupMutation = this.onMarkupMutation.bind(this);
     this.onMouseMove = this.onMouseMove.bind(this);
     this.onMouseOut = this.onMouseOut.bind(this);
     this.onWillNavigate = this.onWillNavigate.bind(this);
@@ -144,17 +149,17 @@ class HighlightersOverlay {
 
     dispatch(updateShowGridAreas(showGridAreas));
     dispatch(updateShowGridLineNumbers(showGridLineNumbers));
     dispatch(updateShowInfiniteLines(showInfinteLines));
   }
 
   /**
    * Toggle the shapes highlighter for the given node.
-
+   *
    * @param  {NodeFront} node
    *         The NodeFront of the element with a shape to highlight.
    * @param  {Object} options
    *         Object used for passing options to the shapes highlighter.
    * @param {TextProperty} textProperty
    *        TextProperty where to write changes.
    */
   async toggleShapesHighlighter(node, options, textProperty) {
@@ -338,17 +343,17 @@ class HighlightersOverlay {
    * @param  {NodeFront} node
    *         The NodeFront of the grid container element to highlight.
    * @param. {String|null} trigger
    *         String name matching "grid" or "rule" to indicate where the
    *         grid highlighter was toggled on from. "grid" represents the grid view
    *         "rule" represents the rule view.
    */
   async toggleGridHighlighter(node, trigger) {
-    if (node == this.gridHighlighterShown) {
+    if (this.gridHighlighters.has(node)) {
       await this.hideGridHighlighter(node);
       return;
     }
 
     await this.showGridHighlighter(node, {}, trigger);
   }
 
   /**
@@ -359,17 +364,17 @@ class HighlightersOverlay {
    * @param  {Object} options
    *         Object used for passing options to the grid highlighter.
    * @param. {String|null} trigger
    *         String name matching "grid" or "rule" to indicate where the
    *         grid highlighter was toggled on from. "grid" represents the grid view
    *         "rule" represents the rule view.
    */
   async showGridHighlighter(node, options, trigger) {
-    let highlighter = await this._getHighlighter("CssGridHighlighter");
+    let highlighter = await this._getGridHighlighter(node);
     if (!highlighter) {
       return;
     }
 
     options = Object.assign({}, options, this.getGridHighlighterSettings(node));
 
     let isShown = await highlighter.show(node, options);
     if (!isShown) {
@@ -383,49 +388,52 @@ class HighlightersOverlay {
     } else if (trigger == "rule") {
       this.telemetry.scalarAdd("devtools.rules.gridinspector.opened", 1);
     }
 
     try {
       // Save grid highlighter state.
       let { url } = this.inspector.target;
       let selector = await node.getUniqueSelector();
-      this.state.grid = { selector, options, url };
-      this.gridHighlighterShown = node;
+      this.state.grids.set(node.actorID, { selector, options, url });
+      this.gridHighlightersShown.push(node);
+
       // Emit the NodeFront of the grid container element that the grid highlighter was
       // shown for.
       this.emit("grid-highlighter-shown", node, options);
     } catch (e) {
       this._handleRejection(e);
     }
   }
 
   /**
    * Hide the grid highlighter for the given grid container element.
    *
    * @param  {NodeFront} node
    *         The NodeFront of the grid container element to unhighlight.
    */
   async hideGridHighlighter(node) {
-    if (!this.gridHighlighterShown || !this.highlighters.CssGridHighlighter) {
+    if (!this.gridHighlighters.has(node)) {
       return;
     }
 
     this._toggleRuleViewIcon(node, false, ".ruleview-grid");
 
-    await this.highlighters.CssGridHighlighter.hide();
+    const highlighter = this.gridHighlighters.get(node);
+    await highlighter.finalize();
+
+    this.gridHighlightersShown = this.gridHighlightersShown.filter(item => item != node);
+    this.gridHighlighters.delete(node);
 
     // Emit the NodeFront of the grid container element that the grid highlighter was
     // hidden for.
-    const nodeFront = this.gridHighlighterShown;
-    this.gridHighlighterShown = null;
-    this.emit("grid-highlighter-hidden", nodeFront, this.state.grid.options);
+    this.emit("grid-highlighter-hidden", node,
+      this.state.grids.get(node.actorID).options);
 
-    // Erase grid highlighter state.
-    this.state.grid = {};
+    this.state.grids.delete(node.actorID);
   }
 
   /**
    * Show the box model highlighter for the given node.
    *
    * @param  {NodeFront} node
    *         The NodeFront of the element to highlight.
    * @param  {Object} options
@@ -519,17 +527,19 @@ class HighlightersOverlay {
     }
   }
 
   /**
    * Restores the saved grid highlighter state.
    */
   async restoreGridState() {
     try {
-      await this.restoreState("grid", this.state.grid, this.showGridHighlighter);
+      for (let gridState of this.state.grids.values()) {
+        await this.restoreState("grid", gridState, this.showGridHighlighter);
+      }
     } catch (e) {
       this._handleRejection(e);
     }
   }
 
   /**
    * Helper function called by restoreFlexboxState, restoreGridState.
    * Restores the saved highlighter state for the given highlighter
@@ -565,28 +575,28 @@ class HighlightersOverlay {
       await showFunction(nodeFront, options);
       this.emit(`${name}-state-restored`, { restored: true });
     }
 
     this.emit(`${name}-state-restored`, { restored: false });
   }
 
   /**
-  * Get an instance of an in-context editor for the given type.
-  *
-  * In-context editors behave like highlighters but with added editing capabilities which
-  * need to write value changes back to something, like to properties in the Rule view.
-  * They typically exist in the context of the page, like the ShapesInContextEditor.
-  *
-  * @param  {String} type
-  *         Type of in-context editor. Currently supported: "shapesEditor"
-  *
-  * @return {Object|null}
-  *         Reference to instance for given type of in-context editor or null.
-  */
+   * Get an instance of an in-context editor for the given type.
+   *
+   * In-context editors behave like highlighters but with added editing capabilities which
+   *  need to write value changes back to something, like to properties in the Rule view.
+   * They typically exist in the context of the page, like the ShapesInContextEditor.
+   *
+   * @param  {String} type
+   *         Type of in-context editor. Currently supported: "shapesEditor"
+   *
+   * @return {Object|null}
+   *         Reference to instance for given type of in-context editor or null.
+   */
   async getInContextEditor(type) {
     if (this.editors[type]) {
       return this.editors[type];
     }
 
     let editor;
 
     switch (type) {
@@ -632,16 +642,48 @@ class HighlightersOverlay {
       // Ignore any error
     }
 
     if (!highlighter) {
       return null;
     }
 
     this.highlighters[type] = highlighter;
+
+    return highlighter;
+  }
+
+  /**
+   * Get a grid highlighter front for a given node. It will initialize a new grid
+   * highlighter for every unique node.
+   *
+   * @param  {NodeFront} node
+   *         The NodeFront of the grid container element to highlight.
+   * @return {Promise} that resolves to the grid highlighter front.
+   */
+  async _getGridHighlighter(node) {
+    if (this.gridHighlighters.has(node)) {
+      return this.gridHighlighters.get(node);
+    }
+
+    let utils = this.highlighterUtils;
+    let highlighter;
+
+    try {
+      highlighter = await utils.getHighlighterByType("CssGridHighlighter");
+    } catch (e) {
+      // Ignore any error
+    }
+
+    if (!highlighter) {
+      return null;
+    }
+
+    this.gridHighlighters.set(node, highlighter);
+
     return highlighter;
   }
 
   _handleRejection(error) {
     if (!this.destroyed) {
       console.error(error);
     }
   }
@@ -914,88 +956,104 @@ class HighlightersOverlay {
   async onMarkupMutation(mutations) {
     let hasInterestingMutation = mutations.some(mut => mut.type === "childList");
     if (!hasInterestingMutation) {
       // Bail out if the mutations did not remove nodes, or if no grid highlighter is
       // displayed.
       return;
     }
 
+    for (let grid of this.gridHighlightersShown) {
+      this._hideHighlighterIfDeadNode(grid, this.hideGridHighlighter);
+    }
+
     this._hideHighlighterIfDeadNode(this.flexboxHighlighterShown,
       this.hideFlexboxHighlighter);
-    this._hideHighlighterIfDeadNode(this.gridHighlighterShown,
-      this.hideGridHighlighter);
     this._hideHighlighterIfDeadNode(this.shapesHighlighterShown,
       this.hideShapesHighlighter);
   }
 
   /**
    * Clear saved highlighter shown properties on will-navigate.
    */
   onWillNavigate() {
+    this.destroyEditors();
+
+    this.gridHighlightersShown = [];
+
     this.boxModelHighlighterShown = null;
     this.flexboxHighlighterShown = null;
     this.geometryEditorHighlighterShown = null;
-    this.gridHighlighterShown = null;
     this.hoveredHighlighterShown = null;
     this.selectorHighlighterShown = null;
     this.shapesHighlighterShown = null;
-    this.destroyEditors();
   }
 
   /**
-  * Destroy and clean-up all instances of in-context editors.
-  */
+   * Destroy and clean-up all instances of in-context editors.
+   */
   destroyEditors() {
     for (let type in this.editors) {
       this.editors[type].off("show");
       this.editors[type].off("hide");
       this.editors[type].destroy();
     }
 
-    this.editors = {};
+    this.editors = null;
   }
 
   /**
-  * Destroy and clean-up all instances of highlighters.
-  */
+   *
+   */
+  destroyGridHighlighters() {
+    for (let highlighter of this.gridHighlighters.values()) {
+      highlighter.finalize();
+    }
+
+    this.gridHighlighters.clear();
+    this.gridHighlighters = null;
+  }
+
+  /**
+   * Destroy and clean-up all instances of highlighters.
+   */
   destroyHighlighters() {
     for (let type in this.highlighters) {
       if (this.highlighters[type]) {
         this.highlighters[type].finalize();
         this.highlighters[type] = null;
       }
     }
 
     this.highlighters = null;
   }
 
   /**
    * Destroy this overlay instance, removing it from the view and destroying
    * all initialized highlighters.
    */
   destroy() {
-    this.destroyHighlighters();
-    this.destroyEditors();
-
-    // Remove inspector events.
     this.inspector.off("markupmutation", this.onMarkupMutation);
     this.inspector.target.off("will-navigate", this.onWillNavigate);
 
+    this.destroyEditors();
+    this.destroyGridHighlighters();
+    this.destroyHighlighters();
+
     this._lastHovered = null;
 
     this.inspector = null;
     this.highlighterUtils = null;
     this.state = null;
     this.store = null;
 
     this.boxModelHighlighterShown = null;
     this.flexboxHighlighterShown = null;
     this.geometryEditorHighlighterShown = null;
-    this.gridHighlighterShown = null;
+    this.gridHighlightersShown = null;
     this.hoveredHighlighterShown = null;
     this.selectorHighlighterShown = null;
     this.shapesHighlighterShown = null;
 
     this.destroyed = true;
   }
 }