Bug 1345119 - Part 3: Display offset parent of absolutely positioned node in box model. r?jdescottes draft
authorStanford Lockhart <lockhart@cs.dal.ca>
Fri, 17 Mar 2017 23:07:06 -0300
changeset 502541 e2ec41d82878abf5e74cdf618c5d8938f68b954c
parent 502540 ef133157654a4876186b1a39d1f2dffad3c4c0b5
child 502542 0ba774d4b422a9aa062efb57f039493e566bdeaa
child 502543 d6de57cdc4209d0584cea08f286b2ae49cccd9e7
push id50319
push userbmo:lockhart@cs.dal.ca
push dateWed, 22 Mar 2017 01:00:22 +0000
reviewersjdescottes
bugs1345119
milestone55.0a1
Bug 1345119 - Part 3: Display offset parent of absolutely positioned node in box model. r?jdescottes 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
--- 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,22 @@ 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,
       });
 
+      if (yield this.inspector.target.actorHasMethod("domwalker", "getOffsetParent")) {
+        // 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
@@ -18,40 +18,46 @@ 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,
+    onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
     onToggleGeometryEditor: PropTypes.func.isRequired,
+    setSelectedNode: PropTypes.func.isRequired,
   },
 
   mixins: [ addons.PureRenderMixin ],
 
   render() {
     let {
       boxModel,
       showBoxModelProperties,
       onHideBoxModelHighlighter,
       onShowBoxModelEditor,
       onShowBoxModelHighlighter,
+      onShowBoxModelHighlighterForNode,
       onToggleGeometryEditor,
+      setSelectedNode,
     } = this.props;
 
     return dom.div(
       {
         className: "boxmodel-container",
       },
       BoxModelMain({
         boxModel,
         onHideBoxModelHighlighter,
         onShowBoxModelEditor,
         onShowBoxModelHighlighter,
+        onShowBoxModelHighlighterForNode,
+        setSelectedNode,
       }),
       BoxModelInfo({
         boxModel,
         onToggleGeometryEditor,
       }),
       showBoxModelProperties ?
         BoxModelProperties({
           boxModel,
--- a/devtools/client/inspector/boxmodel/components/BoxModelApp.js
+++ b/devtools/client/inspector/boxmodel/components/BoxModelApp.js
@@ -24,17 +24,19 @@ 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,
+    onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
     onToggleGeometryEditor: PropTypes.func.isRequired,
+    setSelectedNode: 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,16 +5,19 @@
 "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);
@@ -23,16 +26,18 @@ module.exports = createClass({
 
   displayName: "BoxModelMain",
 
   propTypes: {
     boxModel: PropTypes.shape(Types.boxModel).isRequired,
     onHideBoxModelHighlighter: PropTypes.func.isRequired,
     onShowBoxModelEditor: PropTypes.func.isRequired,
     onShowBoxModelHighlighter: PropTypes.func.isRequired,
+    onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
+    setSelectedNode: PropTypes.func.isRequired,
   },
 
   mixins: [ addons.PureRenderMixin ],
 
   getBorderOrPaddingValue(property) {
     let { layout } = this.props.boxModel;
     return layout[property] ? parseFloat(layout[property]) : "-";
   },
@@ -90,29 +95,79 @@ module.exports = createClass({
       return "-";
     }
     return layout[property] ? parseFloat(layout[property]) : "-";
   },
 
   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,
     });
   },
 
+  /**
+   * 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,
+      }
+    };
+  },
+
   render() {
-    let { boxModel, onShowBoxModelEditor } = this.props;
-    let { layout } = boxModel;
+    let {
+        boxModel,
+        onShowBoxModelEditor,
+        setSelectedNode,
+    } = this.props;
+    let { layout, offsetParent } = boxModel;
     let { height, width, position } = layout;
 
     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");
@@ -135,16 +190,33 @@ module.exports = createClass({
     width = this.getWidthValue(width);
 
     return dom.div(
       {
         className: "boxmodel-main",
         onMouseOver: this.onHighlightMouseOver,
         onMouseOut: this.props.onHideBoxModelHighlighter,
       },
+      offsetParent ?
+        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,31 +613,38 @@ 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({
         showBoxModelProperties: false,
         onHideBoxModelHighlighter,
         onShowBoxModelEditor,
         onShowBoxModelHighlighter,
+        onShowBoxModelHighlighterForNode,
         onToggleGeometryEditor,
+        setSelectedNode,
       })
     );
     ReactDOM.render(provider, this.boxModelWrapper);
   },
 
   /**
    * The CSS as displayed by the UI.
    */
--- a/devtools/client/themes/boxmodel.css
+++ b/devtools/client/themes/boxmodel.css
@@ -312,8 +312,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);
+}