Bug 1342941 - Implement the geometry editor toggle in the new box model. r?zer0 draft
authorGabriel Luong <gabriel.luong@gmail.com>
Fri, 10 Mar 2017 13:46:24 -0500
changeset 496846 0e27464cf20e4c6c23f008d36485df8aa0395a64
parent 496845 30e3c78a7ebd166ecf71353fbe409f04a6265ca4
child 548712 64f1267aefd12b3fbd4c24897cf5d66b56a3a378
push id48705
push userbmo:gl@mozilla.com
push dateFri, 10 Mar 2017 18:47:07 +0000
reviewerszer0
bugs1342941
milestone55.0a1
Bug 1342941 - Implement the geometry editor toggle in the new box model. r?zer0 MozReview-Commit-ID: CuPfWjUybR2
devtools/client/inspector/boxmodel/actions/box-model.js
devtools/client/inspector/boxmodel/actions/index.js
devtools/client/inspector/boxmodel/box-model.js
devtools/client/inspector/boxmodel/components/BoxModel.js
devtools/client/inspector/boxmodel/components/BoxModelApp.js
devtools/client/inspector/boxmodel/components/BoxModelInfo.js
devtools/client/inspector/boxmodel/reducers/box-model.js
devtools/client/inspector/boxmodel/types.js
devtools/client/inspector/computed/computed.js
devtools/client/inspector/layout/layout.js
devtools/client/inspector/shared/highlighters-overlay.js
devtools/client/themes/boxmodel.css
devtools/server/actors/highlighters/geometry-editor.js
--- a/devtools/client/inspector/boxmodel/actions/box-model.js
+++ b/devtools/client/inspector/boxmodel/actions/box-model.js
@@ -1,21 +1,35 @@
 /* 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 {
+  UPDATE_GEOMETRY_EDITOR_ENABLED,
   UPDATE_LAYOUT,
 } = require("./index");
 
 module.exports = {
 
   /**
+   * Update the geometry editor's enabled state.
+   *
+   * @param  {Boolean} enabled
+   *         Whether or not the geometry editor is enabled or not.
+   */
+  updateGeometryEditorEnabled(enabled) {
+    return {
+      type: UPDATE_GEOMETRY_EDITOR_ENABLED,
+      enabled,
+    };
+  },
+
+  /**
    * Update the layout state with the new layout properties.
    */
   updateLayout(layout) {
     return {
       type: UPDATE_LAYOUT,
       layout,
     };
   },
--- a/devtools/client/inspector/boxmodel/actions/index.js
+++ b/devtools/client/inspector/boxmodel/actions/index.js
@@ -3,12 +3,15 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { createEnum } = require("devtools/client/shared/enum");
 
 createEnum([
 
+  // Update the geometry editor's enabled state.
+  "UPDATE_GEOMETRY_EDITOR_ENABLED",
+
   // Update the layout state with the latest layout properties.
   "UPDATE_LAYOUT",
 
 ], module.exports);
--- a/devtools/client/inspector/boxmodel/box-model.js
+++ b/devtools/client/inspector/boxmodel/box-model.js
@@ -5,42 +5,50 @@
 "use strict";
 
 const { Task } = require("devtools/shared/task");
 const { getCssProperties } = require("devtools/shared/fronts/css-properties");
 const { ReflowFront } = require("devtools/shared/fronts/reflow");
 
 const { InplaceEditor } = require("devtools/client/shared/inplace-editor");
 
-const { updateLayout } = require("./actions/box-model");
+const {
+  updateGeometryEditorEnabled,
+  updateLayout,
+} = require("./actions/box-model");
 
 const EditingSession = require("./utils/editing-session");
 
 const NUMERIC = /^-?[\d\.]+$/;
 
 /**
  * A singleton instance of the box model controllers.
  *
  * @param  {Inspector} inspector
  *         An instance of the Inspector currently loaded in the toolbox.
  * @param  {Window} window
  *         The document window of the toolbox.
  */
 function BoxModel(inspector, window) {
   this.document = window.document;
+  this.highlighters = inspector.highlighters;
   this.inspector = inspector;
   this.store = inspector.store;
 
   this.updateBoxModel = this.updateBoxModel.bind(this);
 
   this.onHideBoxModelHighlighter = this.onHideBoxModelHighlighter.bind(this);
+  this.onHideGeometryEditor = this.onHideGeometryEditor.bind(this);
+  this.onMarkupViewLeave = this.onMarkupViewLeave.bind(this);
+  this.onMarkupViewNodeHover = this.onMarkupViewNodeHover.bind(this);
   this.onNewSelection = this.onNewSelection.bind(this);
   this.onShowBoxModelEditor = this.onShowBoxModelEditor.bind(this);
   this.onShowBoxModelHighlighter = this.onShowBoxModelHighlighter.bind(this);
   this.onSidebarSelect = this.onSidebarSelect.bind(this);
+  this.onToggleGeometryEditor = this.onToggleGeometryEditor.bind(this);
 
   this.inspector.selection.on("new-node-front", this.onNewSelection);
   this.inspector.sidebar.on("select", this.onSidebarSelect);
 }
 
 BoxModel.prototype = {
 
   /**
@@ -53,44 +61,46 @@ BoxModel.prototype = {
 
     if (this.reflowFront) {
       this.untrackReflows();
       this.reflowFront.destroy();
       this.reflowFront = null;
     }
 
     this.document = null;
+    this.highlighters = null;
     this.inspector = null;
     this.walker = null;
   },
 
   /**
    * Returns an object containing the box model's handler functions used in the box
    * model's React component props.
    */
   getComponentProps() {
     return {
       onHideBoxModelHighlighter: this.onHideBoxModelHighlighter,
       onShowBoxModelEditor: this.onShowBoxModelEditor,
       onShowBoxModelHighlighter: this.onShowBoxModelHighlighter,
+      onToggleGeometryEditor: this.onToggleGeometryEditor,
     };
   },
 
   /**
    * Returns true if the computed or layout panel is visible, and false otherwise.
    */
   isPanelVisible() {
     return this.inspector.toolbox && this.inspector.sidebar &&
            this.inspector.toolbox.currentToolId === "inspector" &&
            (this.inspector.sidebar.getCurrentTabID() === "layoutview" ||
             this.inspector.sidebar.getCurrentTabID() === "computedview");
   },
 
   /**
-   * Returns true if the layout panel is visible and the current node is valid to
+   * Returns true if the layout panel is visible and the current element is valid to
    * be displayed in the view.
    */
   isPanelVisibleAndNodeValid() {
     return this.isPanelVisible() &&
            this.inspector.selection.isConnected() &&
            this.inspector.selection.isElementNode();
   },
 
@@ -145,16 +155,23 @@ BoxModel.prototype = {
 
       let node = this.inspector.selection.nodeFront;
       let layout = yield this.inspector.pageStyle.getLayout(node, {
         autoMargins: true,
       });
       let styleEntries = yield this.inspector.pageStyle.getApplied(node, {});
       this.elementRules = styleEntries.map(e => e.rule);
 
+      // Update the layout properties with whether or not the element's position is
+      // editable with the geometry editor.
+      let isPositionEditable = yield this.inspector.pageStyle.isPositionEditable(node);
+      layout = Object.assign({}, layout, {
+        isPositionEditable,
+      });
+
       // Update the redux store with the latest layout properties and update the box
       // model view.
       this.store.dispatch(updateLayout(layout));
 
       // If a subsequent request has been made, wait for that one instead.
       if (this._lastRequest != lastRequest) {
         return this._lastRequest;
       }
@@ -166,40 +183,81 @@ BoxModel.prototype = {
 
       return null;
     }).bind(this)).catch(console.error);
 
     this._lastRequest = lastRequest;
   },
 
   /**
+   * Hides the box-model highlighter on the currently selected element.
+   */
+  onHideBoxModelHighlighter() {
+    let toolbox = this.inspector.toolbox;
+    toolbox.highlighterUtils.unhighlight();
+  },
+
+  /**
+   * Hides the geometry editor and updates the box moodel store with the new
+   * geometry editor enabled state.
+   */
+  onHideGeometryEditor() {
+    let { markup, selection, toolbox } = this.inspector;
+
+    this.highlighters.hideGeometryEditor();
+    this.store.dispatch(updateGeometryEditorEnabled(false));
+
+    toolbox.off("picker-started", this.onHideGeometryEditor);
+    selection.off("new-node-front", this.onHideGeometryEditor);
+    markup.off("leave", this.onMarkupViewLeave);
+    markup.off("node-hover", this.onMarkupViewNodeHover);
+  },
+
+  /**
+   * Handler function that re-shows the geometry editor for an element that already
+   * had the geometry editor enabled. This handler function is called on a "leave" event
+   * on the markup view.
+   */
+  onMarkupViewLeave() {
+    let state = this.store.getState();
+    let enabled = state.boxModel.geometryEditorEnabled;
+
+    if (!enabled) {
+      return;
+    }
+
+    let nodeFront = this.inspector.selection.nodeFront;
+    this.highlighters.showGeometryEditor(nodeFront);
+  },
+
+  /**
+   * Handler function that temporarily hides the geomery editor when the
+   * markup view has a "node-hover" event.
+   */
+  onMarkupViewNodeHover() {
+    this.highlighters.hideGeometryEditor();
+  },
+
+  /**
    * Selection 'new-node-front' event handler.
    */
-  onNewSelection: function () {
+  onNewSelection() {
     if (!this.isPanelVisibleAndNodeValid()) {
       return;
     }
 
     if (this.inspector.selection.isConnected() &&
         this.inspector.selection.isElementNode()) {
       this.trackReflows();
     }
 
     this.updateBoxModel("new-selection");
   },
 
   /**
-   * Hides the box-model highlighter on the currently selected element.
-   */
-  onHideBoxModelHighlighter() {
-    let toolbox = this.inspector.toolbox;
-    toolbox.highlighterUtils.unhighlight();
-  },
-
-  /**
    * Shows the inplace editor when a box model editable value is clicked on the
    * box model panel.
    *
    * @param  {DOMNode} element
    *         The element that was clicked.
    * @param  {Event} event
    *         The event object.
    * @param  {String} property
@@ -291,11 +349,39 @@ BoxModel.prototype = {
     if (this.inspector.selection.isConnected() &&
         this.inspector.selection.isElementNode()) {
       this.trackReflows();
     }
 
     this.updateBoxModel();
   },
 
+  /**
+   * Toggles on/off the geometry editor for the current element when the geometry editor
+   * toggle button is clicked.
+   */
+  onToggleGeometryEditor() {
+    let { markup, selection, toolbox } = this.inspector;
+    let nodeFront = this.inspector.selection.nodeFront;
+    let state = this.store.getState();
+    let enabled = !state.boxModel.geometryEditorEnabled;
+
+    this.highlighters.toggleGeometryHighlighter(nodeFront);
+    this.store.dispatch(updateGeometryEditorEnabled(enabled));
+
+    if (enabled) {
+      // Hide completely the geometry editor if the picker is clicked or a new node front
+      toolbox.on("picker-started", this.onHideGeometryEditor);
+      selection.on("new-node-front", this.onHideGeometryEditor);
+      // Temporary hide the geometry editor
+      markup.on("leave", this.onMarkupViewLeave);
+      markup.on("node-hover", this.onMarkupViewNodeHover);
+    } else {
+      toolbox.off("picker-started", this.onHideGeometryEditor);
+      selection.off("new-node-front", this.onHideGeometryEditor);
+      markup.off("leave", this.onMarkupViewLeave);
+      markup.off("node-hover", this.onMarkupViewNodeHover);
+    }
+  },
+
 };
 
 module.exports = BoxModel;
--- a/devtools/client/inspector/boxmodel/components/BoxModel.js
+++ b/devtools/client/inspector/boxmodel/components/BoxModel.js
@@ -18,41 +18,44 @@ module.exports = createClass({
   displayName: "BoxModel",
 
   propTypes: {
     boxModel: PropTypes.shape(Types.boxModel).isRequired,
     showBoxModelProperties: PropTypes.bool.isRequired,
     onHideBoxModelHighlighter: PropTypes.func.isRequired,
     onShowBoxModelEditor: PropTypes.func.isRequired,
     onShowBoxModelHighlighter: PropTypes.func.isRequired,
+    onToggleGeometryEditor: PropTypes.func.isRequired,
   },
 
   mixins: [ addons.PureRenderMixin ],
 
   render() {
     let {
       boxModel,
       showBoxModelProperties,
       onHideBoxModelHighlighter,
       onShowBoxModelEditor,
       onShowBoxModelHighlighter,
+      onToggleGeometryEditor,
     } = this.props;
 
     return dom.div(
       {
         className: "boxmodel-container",
       },
       BoxModelMain({
         boxModel,
         onHideBoxModelHighlighter,
         onShowBoxModelEditor,
         onShowBoxModelHighlighter,
       }),
       BoxModelInfo({
         boxModel,
+        onToggleGeometryEditor,
       }),
       showBoxModelProperties ?
         BoxModelProperties({
           boxModel,
         })
         :
         null
     );
--- a/devtools/client/inspector/boxmodel/components/BoxModelApp.js
+++ b/devtools/client/inspector/boxmodel/components/BoxModelApp.js
@@ -24,16 +24,17 @@ const BoxModelApp = createClass({
   displayName: "BoxModelApp",
 
   propTypes: {
     boxModel: PropTypes.shape(Types.boxModel).isRequired,
     showBoxModelProperties: PropTypes.bool.isRequired,
     onHideBoxModelHighlighter: PropTypes.func.isRequired,
     onShowBoxModelEditor: PropTypes.func.isRequired,
     onShowBoxModelHighlighter: PropTypes.func.isRequired,
+    onToggleGeometryEditor: PropTypes.func.isRequired,
   },
 
   mixins: [ addons.PureRenderMixin ],
 
   render() {
     return Accordion({
       items: [
         {
--- a/devtools/client/inspector/boxmodel/components/BoxModelInfo.js
+++ b/devtools/client/inspector/boxmodel/components/BoxModelInfo.js
@@ -17,43 +17,62 @@ const SHARED_STRINGS_URI = "devtools/cli
 const SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI);
 
 module.exports = createClass({
 
   displayName: "BoxModelInfo",
 
   propTypes: {
     boxModel: PropTypes.shape(Types.boxModel).isRequired,
+    onToggleGeometryEditor: PropTypes.func.isRequired,
   },
 
   mixins: [ addons.PureRenderMixin ],
 
+  onToggleGeometryEditor(e) {
+    this.props.onToggleGeometryEditor();
+  },
+
   render() {
     let { boxModel } = this.props;
-    let { layout } = boxModel;
-    let { width, height, position } = layout;
+    let { geometryEditorEnabled, layout } = boxModel;
+    let {
+      height,
+      isPositionEditable,
+      position,
+      width,
+    } = layout;
+
+    let buttonClass = "layout-geometry-editor devtools-button";
+    if (geometryEditorEnabled) {
+      buttonClass += " checked";
+    }
 
     return dom.div(
       {
         className: "boxmodel-info",
       },
       dom.span(
         {
           className: "boxmodel-element-size",
         },
         SHARED_L10N.getFormatStr("dimensions", width, height)
       ),
       dom.section(
         {
           className: "boxmodel-position-group",
         },
-        dom.button({
-          className: "layout-geometry-editor devtools-button",
-          title: BOXMODEL_L10N.getStr("boxmodel.geometryButton.tooltip"),
-        }),
+        isPositionEditable ?
+          dom.button({
+            className: buttonClass,
+            title: BOXMODEL_L10N.getStr("boxmodel.geometryButton.tooltip"),
+            onClick: this.onToggleGeometryEditor,
+          })
+          :
+          null,
         dom.span(
           {
             className: "boxmodel-element-position",
           },
           position
         )
       )
     );
--- a/devtools/client/inspector/boxmodel/reducers/box-model.js
+++ b/devtools/client/inspector/boxmodel/reducers/box-model.js
@@ -1,24 +1,32 @@
 /* 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 {
+  UPDATE_GEOMETRY_EDITOR_ENABLED,
   UPDATE_LAYOUT,
 } = require("../actions/index");
 
 const INITIAL_BOX_MODEL = {
+  geometryEditorEnabled: false,
   layout: {},
 };
 
 let reducers = {
 
+  [UPDATE_GEOMETRY_EDITOR_ENABLED](boxModel, { enabled }) {
+    return Object.assign({}, boxModel, {
+      geometryEditorEnabled: enabled,
+    });
+  },
+
   [UPDATE_LAYOUT](boxModel, { layout }) {
     return Object.assign({}, boxModel, {
       layout,
     });
   },
 
 };
 
--- a/devtools/client/inspector/boxmodel/types.js
+++ b/devtools/client/inspector/boxmodel/types.js
@@ -6,12 +6,15 @@
 
 const { PropTypes } = require("devtools/client/shared/vendor/react");
 
 /**
  * The box model data for the current selected node.
  */
 exports.boxModel = {
 
+  // Whether or not the geometry editor is enabled
+  geometryEditorEnabled: PropTypes.boolean,
+
   // The layout information of the current selected node
   layout: PropTypes.object,
 
 };
--- a/devtools/client/inspector/computed/computed.js
+++ b/devtools/client/inspector/computed/computed.js
@@ -616,26 +616,28 @@ CssComputedView.prototype = {
   /**
    * Render the box model view.
    */
   createBoxModelView: function () {
     let {
       onHideBoxModelHighlighter,
       onShowBoxModelEditor,
       onShowBoxModelHighlighter,
+      onToggleGeometryEditor,
     } = this.inspector.boxmodel.getComponentProps();
 
     let provider = createElement(
       Provider,
       { store: this.store },
       BoxModelApp({
         showBoxModelProperties: false,
         onHideBoxModelHighlighter,
         onShowBoxModelEditor,
         onShowBoxModelHighlighter,
+        onToggleGeometryEditor,
       })
     );
     ReactDOM.render(provider, this.boxModelWrapper);
   },
 
   /**
    * The CSS as displayed by the UI.
    */
--- a/devtools/client/inspector/layout/layout.js
+++ b/devtools/client/inspector/layout/layout.js
@@ -31,16 +31,17 @@ LayoutView.prototype = {
     if (!this.inspector) {
       return;
     }
 
     let {
       onHideBoxModelHighlighter,
       onShowBoxModelEditor,
       onShowBoxModelHighlighter,
+      onToggleGeometryEditor,
     } = this.inspector.boxmodel.getComponentProps();
 
     let {
       getSwatchColorPickerTooltip,
       setSelectedNode,
       onSetGridOverlayColor,
       onShowBoxModelHighlighterForNode,
       onShowGridAreaHighlight,
@@ -67,16 +68,17 @@ LayoutView.prototype = {
 
       onHideBoxModelHighlighter,
       onSetGridOverlayColor,
       onShowBoxModelEditor,
       onShowBoxModelHighlighter,
       onShowBoxModelHighlighterForNode,
       onShowGridAreaHighlight,
       onShowGridCellHighlight,
+      onToggleGeometryEditor,
       onToggleGridHighlighter,
       onToggleShowGridLineNumbers,
       onToggleShowInfiniteLines,
     });
 
     let provider = createElement(Provider, {
       store: this.store,
       id: "layoutview",
--- a/devtools/client/inspector/shared/highlighters-overlay.js
+++ b/devtools/client/inspector/shared/highlighters-overlay.js
@@ -22,16 +22,18 @@ const DEFAULT_GRID_COLOR = "#4B0082";
 function HighlightersOverlay(inspector) {
   this.inspector = inspector;
   this.highlighters = {};
   this.highlighterUtils = this.inspector.toolbox.highlighterUtils;
 
   // Only initialize the overlay if at least one of the highlighter types is supported.
   this.supportsHighlighters = this.highlighterUtils.supportsCustomHighlighters();
 
+  // 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;
   // Saved state to be restore on page navigation.
   this.state = {
@@ -171,16 +173,67 @@ HighlightersOverlay.prototype = {
       this.state.grid.options);
     this.gridHighlighterShown = null;
 
     // Erase grid highlighter state.
     this.state.grid = {};
   }),
 
   /**
+   * Toggle the geometry editor highlighter for the given element.
+   *
+   * @param {NodeFront} node
+   *        The NodeFront of the element to highlight.
+   */
+  toggleGeometryHighlighter: Task.async(function* (node) {
+    if (node == this.geometryEditorHighlighterShown) {
+      yield this.hideGeometryEditor();
+      return;
+    }
+
+    yield this.showGeometryEditor(node);
+  }),
+
+  /**
+   * Show the geometry editor highlightor for the given element.
+   *
+   * @param {NodeFront} node
+   *        THe NodeFront of the element to highlight.
+   */
+  showGeometryEditor: Task.async(function* (node) {
+    let highlighter = yield this._getHighlighter("GeometryEditorHighlighter");
+    if (!highlighter) {
+      return;
+    }
+
+    let isShown = yield highlighter.show(node);
+    if (!isShown) {
+      return;
+    }
+
+    this.emit("geometry-editor-highlighter-shown");
+    this.geometryEditorHighlighterShown = node;
+  }),
+
+  /**
+   * Hide the geometry editor highlighter.
+   */
+  hideGeometryEditor: Task.async(function* () {
+    if (!this.geometryEditorHighlighterShown ||
+        !this.highlighters.GeometryEditorHighlighter) {
+      return;
+    }
+
+    yield this.highlighters.GeometryEditorHighlighter.hide();
+
+    this.emit("geometry-editor-highlighter-hidden");
+    this.geometryEditorHighlighterShown = null;
+  }),
+
+  /**
    * Restore the saved highlighter states.
    *
    * @return {Promise} that resolves when the highlighter state was restored, and the
    *          expected highlighters are displayed.
    */
   restoreState: Task.async(function* () {
     let { selector, options, url } = this.state.grid;
 
@@ -385,16 +438,17 @@ HighlightersOverlay.prototype = {
       this._handleRejection(e);
     }
   }),
 
   /**
    * Clear saved highlighter shown properties on will-navigate.
    */
   onWillNavigate: function () {
+    this.geometryEditorHighlighterShown = null;
     this.gridHighlighterShown = null;
     this.hoveredHighlighterShown = null;
     this.selectorHighlighterShown = null;
 
     // The inspector panel should emit the new-root event when it is ready after navigate.
     this.onInspectorNewRoot = this.inspector.once("new-root");
   },
 
@@ -415,16 +469,19 @@ HighlightersOverlay.prototype = {
     this.inspector.target.off("will-navigate", this.onWillNavigate);
 
     this._lastHovered = null;
 
     this.inspector = null;
     this.highlighters = null;
     this.highlighterUtils = null;
     this.supportsHighlighters = null;
+
+    this.geometryEditorHighlighterShown = null;
     this.gridHighlighterShown = null;
     this.hoveredHighlighterShown = null;
     this.selectorHighlighterShown = null;
+
     this.destroyed = true;
   }
 };
 
 module.exports = HighlightersOverlay;
--- a/devtools/client/themes/boxmodel.css
+++ b/devtools/client/themes/boxmodel.css
@@ -17,20 +17,16 @@
 
 .boxmodel-header,
 .boxmodel-info {
   display: flex;
   align-items: center;
   padding: 4px 17px;
 }
 
-.layout-geometry-editor {
-  visibility: hidden;
-}
-
 .layout-geometry-editor::before {
   background: url(images/geometry-editor.svg) no-repeat center center / 16px 16px;
 }
 
 /* Main: contains the box-model regions */
 
 .boxmodel-main {
   position: relative;
--- a/devtools/server/actors/highlighters/geometry-editor.js
+++ b/devtools/server/actors/highlighters/geometry-editor.js
@@ -219,16 +219,20 @@ function GeometryEditorHighlighter(highl
   // Register the mousedown event for each Geometry Editor's handler.
   // Those events are automatically removed when the markup is destroyed.
   let onMouseDown = this.handleEvent.bind(this);
 
   for (let side of GeoProp.SIDES) {
     this.getElement("handler-" + side)
       .addEventListener("mousedown", onMouseDown);
   }
+
+  this.onWillNavigate = this.onWillNavigate.bind(this);
+
+  this.highlighterEnv.on("will-navigate", this.onWillNavigate);
 }
 
 GeometryEditorHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
   typeName: "GeometryEditorHighlighter",
 
   ID_CLASS_PREFIX: "geometry-editor-",
 
   _buildMarkup: function () {
@@ -694,11 +698,17 @@ GeometryEditorHighlighter.prototype = ex
       }
     }
     let labelCross = crossPos;
     labelEl.setAttribute("transform", GeoProp.isHorizontal(side)
                          ? "translate(" + labelMain + " " + labelCross + ")"
                          : "translate(" + labelCross + " " + labelMain + ")");
     labelEl.removeAttribute("hidden");
     labelTextEl.setTextContent(labelValue);
-  }
+  },
+
+  onWillNavigate({ isTopLevel }) {
+    if (isTopLevel) {
+      this.hide();
+    }
+  },
 });
 exports.GeometryEditorHighlighter = GeometryEditorHighlighter;