Bug 1345119 - Part 3: Display offset parent of absolutely positioned node in box model. r=gl
authorStanford Lockhart <lockhart@cs.dal.ca>
Fri, 17 Mar 2017 23:07:06 -0300
changeset 349606 130016ad0425
parent 349605 03e81669e18c
child 349607 37dac6a2af16
push id88463
push usergabriel.luong@gmail.com
push dateSat, 25 Mar 2017 22:23:20 +0000
treeherdermozilla-inbound@23d64298e8c6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgl
bugs1345119
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1345119 - Part 3: Display offset parent of absolutely positioned node in box model. r=gl MozReview-Commit-ID: 102vRTuIhEh
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/BoxModelMain.js
devtools/client/inspector/boxmodel/reducers/box-model.js
devtools/client/inspector/boxmodel/types.js
devtools/client/inspector/computed/computed.js
devtools/client/themes/boxmodel.css
devtools/server/actors/inspector.js
--- a/devtools/client/inspector/boxmodel/actions/box-model.js
+++ b/devtools/client/inspector/boxmodel/actions/box-model.js
@@ -2,16 +2,17 @@
  * 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,
+  UPDATE_OFFSET_PARENT,
 } = require("./index");
 
 module.exports = {
 
   /**
    * Update the geometry editor's enabled state.
    *
    * @param  {Boolean} enabled
@@ -29,9 +30,19 @@ module.exports = {
    */
   updateLayout(layout) {
     return {
       type: UPDATE_LAYOUT,
       layout,
     };
   },
 
+  /**
+   * Update the offset parent state with the new DOM node.
+   */
+  updateOffsetParent(offsetParent) {
+    return {
+      type: UPDATE_OFFSET_PARENT,
+      offsetParent,
+    };
+  }
+
 };
--- a/devtools/client/inspector/boxmodel/actions/index.js
+++ b/devtools/client/inspector/boxmodel/actions/index.js
@@ -9,9 +9,12 @@ const { createEnum } = require("devtools
 createEnum([
 
   // Update the geometry editor's enabled state.
   "UPDATE_GEOMETRY_EDITOR_ENABLED",
 
   // Update the layout state with the latest layout properties.
   "UPDATE_LAYOUT",
 
+  // Update the offset parent state with the new DOM node.
+  "UPDATE_OFFSET_PARENT",
+
 ], module.exports);
--- a/devtools/client/inspector/boxmodel/box-model.js
+++ b/devtools/client/inspector/boxmodel/box-model.js
@@ -8,16 +8,17 @@ const { Task } = require("devtools/share
 const { getCssProperties } = require("devtools/shared/fronts/css-properties");
 const { ReflowFront } = require("devtools/shared/fronts/reflow");
 
 const { InplaceEditor } = require("devtools/client/shared/inplace-editor");
 
 const {
   updateGeometryEditorEnabled,
   updateLayout,
+  updateOffsetParent,
 } = require("./actions/box-model");
 
 const EditingSession = require("./utils/editing-session");
 
 const NUMERIC = /^-?[\d\.]+$/;
 
 /**
  * A singleton instance of the box model controllers.
@@ -162,16 +163,25 @@ BoxModel.prototype = {
 
       // 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,
       });
 
+      const actorCanGetOffSetParent
+        = yield this.inspector.target.actorHasMethod("domwalker", "getOffsetParent");
+
+      if (actorCanGetOffSetParent) {
+        // Update the redux store with the latest offset parent DOM node
+        let offsetParent = yield this.inspector.walker.getOffsetParent(node);
+        this.store.dispatch(updateOffsetParent(offsetParent));
+      }
+
       // 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;
       }
--- a/devtools/client/inspector/boxmodel/components/BoxModel.js
+++ b/devtools/client/inspector/boxmodel/components/BoxModel.js
@@ -14,44 +14,50 @@ const BoxModelProperties = createFactory
 const Types = require("../types");
 
 module.exports = createClass({
 
   displayName: "BoxModel",
 
   propTypes: {
     boxModel: PropTypes.shape(Types.boxModel).isRequired,
+    setSelectedNode: PropTypes.func.isRequired,
     showBoxModelProperties: PropTypes.bool.isRequired,
     onHideBoxModelHighlighter: PropTypes.func.isRequired,
     onShowBoxModelEditor: PropTypes.func.isRequired,
     onShowBoxModelHighlighter: PropTypes.func.isRequired,
+    onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
     onToggleGeometryEditor: PropTypes.func.isRequired,
   },
 
   mixins: [ addons.PureRenderMixin ],
 
   render() {
     let {
       boxModel,
+      setSelectedNode,
       showBoxModelProperties,
       onHideBoxModelHighlighter,
       onShowBoxModelEditor,
       onShowBoxModelHighlighter,
+      onShowBoxModelHighlighterForNode,
       onToggleGeometryEditor,
     } = this.props;
 
     return dom.div(
       {
         className: "boxmodel-container",
       },
       BoxModelMain({
         boxModel,
+        setSelectedNode,
         onHideBoxModelHighlighter,
         onShowBoxModelEditor,
         onShowBoxModelHighlighter,
+        onShowBoxModelHighlighterForNode,
       }),
       BoxModelInfo({
         boxModel,
         onToggleGeometryEditor,
       }),
       showBoxModelProperties ?
         BoxModelProperties({
           boxModel,
--- a/devtools/client/inspector/boxmodel/components/BoxModelApp.js
+++ b/devtools/client/inspector/boxmodel/components/BoxModelApp.js
@@ -20,20 +20,22 @@ const BOXMODEL_STRINGS_URI = "devtools/c
 const BOXMODEL_L10N = new LocalizationHelper(BOXMODEL_STRINGS_URI);
 
 const BoxModelApp = createClass({
 
   displayName: "BoxModelApp",
 
   propTypes: {
     boxModel: PropTypes.shape(Types.boxModel).isRequired,
+    setSelectedNode: PropTypes.func.isRequired,
     showBoxModelProperties: PropTypes.bool.isRequired,
     onHideBoxModelHighlighter: PropTypes.func.isRequired,
     onShowBoxModelEditor: PropTypes.func.isRequired,
     onShowBoxModelHighlighter: PropTypes.func.isRequired,
+    onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
     onToggleGeometryEditor: PropTypes.func.isRequired,
   },
 
   mixins: [ addons.PureRenderMixin ],
 
   render() {
     return Accordion({
       items: [
--- a/devtools/client/inspector/boxmodel/components/BoxModelMain.js
+++ b/devtools/client/inspector/boxmodel/components/BoxModelMain.js
@@ -5,34 +5,39 @@
 "use strict";
 
 const { addons, createClass, createFactory, DOM: dom, PropTypes } =
   require("devtools/client/shared/vendor/react");
 
 const { LocalizationHelper } = require("devtools/shared/l10n");
 
 const BoxModelEditable = createFactory(require("./BoxModelEditable"));
+// Reps
+const { REPS, MODE } = require("devtools/client/shared/components/reps/reps");
+const Rep = createFactory(REPS.Rep);
 
 const Types = require("../types");
 
 const BOXMODEL_STRINGS_URI = "devtools/client/locales/boxmodel.properties";
 const BOXMODEL_L10N = new LocalizationHelper(BOXMODEL_STRINGS_URI);
 
 const SHARED_STRINGS_URI = "devtools/client/locales/shared.properties";
 const SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI);
 
 module.exports = createClass({
 
   displayName: "BoxModelMain",
 
   propTypes: {
     boxModel: PropTypes.shape(Types.boxModel).isRequired,
+    setSelectedNode: PropTypes.func.isRequired,
     onHideBoxModelHighlighter: PropTypes.func.isRequired,
     onShowBoxModelEditor: PropTypes.func.isRequired,
     onShowBoxModelHighlighter: PropTypes.func.isRequired,
+    onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
   },
 
   mixins: [ addons.PureRenderMixin ],
 
   getBorderOrPaddingValue(property) {
     let { layout } = this.props.boxModel;
     return layout[property] ? parseFloat(layout[property]) : "-";
   },
@@ -87,34 +92,90 @@ module.exports = createClass({
     let { layout } = this.props.boxModel;
 
     if (layout.position === "static") {
       return "-";
     }
     return layout[property] ? parseFloat(layout[property]) : "-";
   },
 
+  /**
+   * While waiting for a reps fix in https://github.com/devtools-html/reps/issues/92,
+   * translate nodeFront to a grip-like object that can be used with an ElementNode rep.
+   *
+   * @params  {NodeFront} nodeFront
+   *          The NodeFront for which we want to create a grip-like object.
+   * @returns {Object} a grip-like object that can be used with Reps.
+   */
+  translateNodeFrontToGrip(nodeFront) {
+    let {
+      attributes
+    } = nodeFront;
+
+    // The main difference between NodeFront and grips is that attributes are treated as
+    // a map in grips and as an array in NodeFronts.
+    let attributesMap = {};
+    for (let { name, value } of attributes) {
+      attributesMap[name] = value;
+    }
+
+    return {
+      actor: nodeFront.actorID,
+      preview: {
+        attributes: attributesMap,
+        attributesLength: attributes.length,
+        // nodeName is already lowerCased in Node grips
+        nodeName: nodeFront.nodeName.toLowerCase(),
+        nodeType: nodeFront.nodeType,
+      }
+    };
+  },
+
   onHighlightMouseOver(event) {
     let region = event.target.getAttribute("data-box");
+
     if (!region) {
+      let el = event.target;
+
+      do {
+        el = el.parentNode;
+
+        if (el && el.getAttribute("data-box")) {
+          region = el.getAttribute("data-box");
+          break;
+        }
+      } while (el.parentNode);
+
       this.props.onHideBoxModelHighlighter();
     }
 
+    if (region === "offset-parent") {
+      this.props.onHideBoxModelHighlighter();
+      this.props.onShowBoxModelHighlighterForNode(this.props.boxModel.offsetParent);
+      return;
+    }
+
     this.props.onShowBoxModelHighlighter({
       region,
       showOnly: region,
       onlyRegionArea: true,
     });
   },
 
   render() {
-    let { boxModel, onShowBoxModelEditor } = this.props;
-    let { layout } = boxModel;
+    let {
+      boxModel,
+      setSelectedNode,
+      onShowBoxModelEditor,
+    } = this.props;
+    let { layout, offsetParent } = boxModel;
     let { height, width, position } = layout;
 
+    let displayOffsetParent = offsetParent && layout.position === "absolute";
+
     let borderTop = this.getBorderOrPaddingValue("border-top-width");
     let borderRight = this.getBorderOrPaddingValue("border-right-width");
     let borderBottom = this.getBorderOrPaddingValue("border-bottom-width");
     let borderLeft = this.getBorderOrPaddingValue("border-left-width");
 
     let paddingTop = this.getBorderOrPaddingValue("padding-top");
     let paddingRight = this.getBorderOrPaddingValue("padding-right");
     let paddingBottom = this.getBorderOrPaddingValue("padding-bottom");
@@ -170,16 +231,33 @@ module.exports = createClass({
       );
 
     return dom.div(
       {
         className: "boxmodel-main",
         onMouseOver: this.onHighlightMouseOver,
         onMouseOut: this.props.onHideBoxModelHighlighter,
       },
+      displayOffsetParent ?
+        dom.span(
+          {
+            className: "boxmodel-offset-parent",
+            "data-box": "offset-parent",
+          },
+          Rep(
+            {
+              defaultRep: offsetParent,
+              mode: MODE.TINY,
+              object: this.translateNodeFrontToGrip(offsetParent),
+              onInspectIconClick: () => setSelectedNode(offsetParent, "box-model"),
+            }
+          )
+        )
+        :
+        null,
       displayPosition ?
         dom.span(
           {
             className: "boxmodel-legend",
             "data-box": "position",
             title: BOXMODEL_L10N.getFormatStr("boxmodel.position", position),
           },
           BOXMODEL_L10N.getFormatStr("boxmodel.position", position)
--- a/devtools/client/inspector/boxmodel/reducers/box-model.js
+++ b/devtools/client/inspector/boxmodel/reducers/box-model.js
@@ -2,37 +2,45 @@
  * 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,
+  UPDATE_OFFSET_PARENT,
 } = require("../actions/index");
 
 const INITIAL_BOX_MODEL = {
   geometryEditorEnabled: false,
   layout: {},
+  offsetParent: null
 };
 
 let reducers = {
 
   [UPDATE_GEOMETRY_EDITOR_ENABLED](boxModel, { enabled }) {
     return Object.assign({}, boxModel, {
       geometryEditorEnabled: enabled,
     });
   },
 
   [UPDATE_LAYOUT](boxModel, { layout }) {
     return Object.assign({}, boxModel, {
       layout,
     });
   },
 
+  [UPDATE_OFFSET_PARENT](boxModel, { offsetParent }) {
+    return Object.assign({}, boxModel, {
+      offsetParent,
+    });
+  },
+
 };
 
 module.exports = function (boxModel = INITIAL_BOX_MODEL, action) {
   let reducer = reducers[action.type];
   if (!reducer) {
     return boxModel;
   }
   return reducer(boxModel, action);
--- a/devtools/client/inspector/boxmodel/types.js
+++ b/devtools/client/inspector/boxmodel/types.js
@@ -12,9 +12,12 @@ const { PropTypes } = require("devtools/
 exports.boxModel = {
 
   // Whether or not the geometry editor is enabled
   geometryEditorEnabled: PropTypes.boolean,
 
   // The layout information of the current selected node
   layout: PropTypes.object,
 
+  // The offset parent for the selected node
+  offsetParent: PropTypes.object,
+
 };
--- a/devtools/client/inspector/computed/computed.js
+++ b/devtools/client/inspector/computed/computed.js
@@ -613,30 +613,37 @@ CssComputedView.prototype = {
     this.inspector.emit("computed-view-sourcelinks-updated");
   },
 
   /**
    * Render the box model view.
    */
   createBoxModelView: function () {
     let {
+      setSelectedNode,
+      onShowBoxModelHighlighterForNode,
+    } = this.inspector.getCommonComponentProps();
+
+    let {
       onHideBoxModelHighlighter,
       onShowBoxModelEditor,
       onShowBoxModelHighlighter,
       onToggleGeometryEditor,
     } = this.inspector.boxmodel.getComponentProps();
 
     let provider = createElement(
       Provider,
       { store: this.store },
       BoxModelApp({
+        setSelectedNode,
         showBoxModelProperties: false,
         onHideBoxModelHighlighter,
         onShowBoxModelEditor,
         onShowBoxModelHighlighter,
+        onShowBoxModelHighlighterForNode,
         onToggleGeometryEditor,
       })
     );
     ReactDOM.render(provider, this.boxModelWrapper);
   },
 
   /**
    * The CSS as displayed by the UI.
--- a/devtools/client/themes/boxmodel.css
+++ b/devtools/client/themes/boxmodel.css
@@ -318,8 +318,17 @@
 .boxmodel-properties-header {
   display: flex;
   padding: 2px 0;
 }
 
 .boxmodel-properties-wrapper {
   padding: 0 9px;
 }
+
+/* Box Model Main - Offset Parent */
+
+.boxmodel-offset-parent {
+  position: absolute;
+  top: -20px;
+  right: -10px;
+  color: var(--theme-highlight-purple);
+}
--- a/devtools/server/actors/inspector.js
+++ b/devtools/server/actors/inspector.js
@@ -2644,25 +2644,23 @@ var WalkerActor = protocol.ActorClassWit
     if (!this.layoutActor) {
       this.layoutActor = new LayoutActor(this.conn, this.tabActor, this);
     }
 
     return this.layoutActor;
   },
 
   /**
-   * Get the offset parent of the node
-   * If the offset parent is statically positioned, there is no offset parent
-   * and null is returned.
-   * Returns the DOMNode for the offset parent if it exists
+   * Returns the offset parent DOMNode of the given node if it exists, otherwise, it
+   * returns null.
    */
-  getOffsetParent: function (domnode) {
-    let offsetParent = domnode.rawNode.offsetParent;
-
-    if (!offsetParent || CssLogic.getComputedStyle(offsetParent).position === "static") {
+  getOffsetParent: function (node) {
+    let offsetParent = node.rawNode.offsetParent;
+
+    if (!offsetParent) {
       return null;
     }
 
     return this._ref(offsetParent);
   },
 });
 
 /**