summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/boxmodel/components
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/inspector/boxmodel/components/BoxModel.js97
-rw-r--r--devtools/client/inspector/boxmodel/components/BoxModelEditable.js109
-rw-r--r--devtools/client/inspector/boxmodel/components/BoxModelInfo.js79
-rw-r--r--devtools/client/inspector/boxmodel/components/BoxModelMain.js774
-rw-r--r--devtools/client/inspector/boxmodel/components/BoxModelProperties.js142
-rw-r--r--devtools/client/inspector/boxmodel/components/ComputedProperty.js123
-rw-r--r--devtools/client/inspector/boxmodel/components/moz.build14
7 files changed, 1338 insertions, 0 deletions
diff --git a/devtools/client/inspector/boxmodel/components/BoxModel.js b/devtools/client/inspector/boxmodel/components/BoxModel.js
new file mode 100644
index 0000000000..23d6fc0ed4
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/components/BoxModel.js
@@ -0,0 +1,97 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const BoxModelInfo = createFactory(
+ require("resource://devtools/client/inspector/boxmodel/components/BoxModelInfo.js")
+);
+const BoxModelMain = createFactory(
+ require("resource://devtools/client/inspector/boxmodel/components/BoxModelMain.js")
+);
+const BoxModelProperties = createFactory(
+ require("resource://devtools/client/inspector/boxmodel/components/BoxModelProperties.js")
+);
+
+const Types = require("resource://devtools/client/inspector/boxmodel/types.js");
+
+class BoxModel extends PureComponent {
+ static get propTypes() {
+ return {
+ boxModel: PropTypes.shape(Types.boxModel).isRequired,
+ dispatch: PropTypes.func.isRequired,
+ onShowBoxModelEditor: PropTypes.func.isRequired,
+ onShowRulePreviewTooltip: PropTypes.func.isRequired,
+ onToggleGeometryEditor: PropTypes.func.isRequired,
+ showBoxModelProperties: PropTypes.bool.isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ }
+
+ onKeyDown(event) {
+ const { target } = event;
+
+ if (target == this.boxModelContainer) {
+ this.boxModelMain.onKeyDown(event);
+ }
+ }
+
+ render() {
+ const {
+ boxModel,
+ dispatch,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ onToggleGeometryEditor,
+ setSelectedNode,
+ showBoxModelProperties,
+ } = this.props;
+
+ return dom.div(
+ {
+ className: "boxmodel-container",
+ tabIndex: 0,
+ ref: div => {
+ this.boxModelContainer = div;
+ },
+ onKeyDown: this.onKeyDown,
+ },
+ BoxModelMain({
+ boxModel,
+ boxModelContainer: this.boxModelContainer,
+ dispatch,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ ref: boxModelMain => {
+ this.boxModelMain = boxModelMain;
+ },
+ }),
+ BoxModelInfo({
+ boxModel,
+ onToggleGeometryEditor,
+ }),
+ showBoxModelProperties
+ ? BoxModelProperties({
+ boxModel,
+ dispatch,
+ setSelectedNode,
+ })
+ : null
+ );
+ }
+}
+
+module.exports = BoxModel;
diff --git a/devtools/client/inspector/boxmodel/components/BoxModelEditable.js b/devtools/client/inspector/boxmodel/components/BoxModelEditable.js
new file mode 100644
index 0000000000..c60a3da6be
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/components/BoxModelEditable.js
@@ -0,0 +1,109 @@
+/* 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 {
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ editableItem,
+} = require("resource://devtools/client/shared/inplace-editor.js");
+
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const SHARED_STRINGS_URI = "devtools/client/locales/shared.properties";
+const SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI);
+
+const LONG_TEXT_ROTATE_LIMIT = 3;
+const HIGHLIGHT_RULE_PREF = Services.prefs.getBoolPref(
+ "devtools.layout.boxmodel.highlightProperty"
+);
+
+class BoxModelEditable extends PureComponent {
+ static get propTypes() {
+ return {
+ box: PropTypes.string.isRequired,
+ direction: PropTypes.string,
+ focusable: PropTypes.bool.isRequired,
+ level: PropTypes.string,
+ onShowBoxModelEditor: PropTypes.func.isRequired,
+ onShowRulePreviewTooltip: PropTypes.func.isRequired,
+ property: PropTypes.string.isRequired,
+ textContent: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
+ .isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.onMouseOver = this.onMouseOver.bind(this);
+ }
+
+ componentDidMount() {
+ const { property, onShowBoxModelEditor } = this.props;
+
+ editableItem(
+ {
+ element: this.boxModelEditable,
+ },
+ (element, event) => {
+ onShowBoxModelEditor(element, event, property);
+ }
+ );
+ }
+
+ onMouseOver(event) {
+ const { onShowRulePreviewTooltip, property } = this.props;
+
+ if (event.shiftKey && HIGHLIGHT_RULE_PREF) {
+ onShowRulePreviewTooltip(event.target, property);
+ }
+ }
+
+ render() {
+ const { box, direction, focusable, level, property, textContent } =
+ this.props;
+
+ const rotate =
+ direction &&
+ (direction == "left" || direction == "right") &&
+ box !== "position" &&
+ textContent.toString().length > LONG_TEXT_ROTATE_LIMIT;
+
+ return dom.p(
+ {
+ className: `boxmodel-${box}
+ ${
+ direction
+ ? " boxmodel-" + direction
+ : "boxmodel-" + property
+ }
+ ${rotate ? " boxmodel-rotate" : ""}`,
+ id: property + "-id",
+ },
+ dom.span(
+ {
+ className: "boxmodel-editable",
+ "aria-label": SHARED_L10N.getFormatStr(
+ "boxModelEditable.accessibleLabel",
+ property,
+ textContent
+ ),
+ "data-box": box,
+ tabIndex: box === level && focusable ? 0 : -1,
+ title: property,
+ onMouseOver: this.onMouseOver,
+ ref: span => {
+ this.boxModelEditable = span;
+ },
+ },
+ textContent
+ )
+ );
+ }
+}
+
+module.exports = BoxModelEditable;
diff --git a/devtools/client/inspector/boxmodel/components/BoxModelInfo.js b/devtools/client/inspector/boxmodel/components/BoxModelInfo.js
new file mode 100644
index 0000000000..e64faba05a
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/components/BoxModelInfo.js
@@ -0,0 +1,79 @@
+/* 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 {
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+
+const Types = require("resource://devtools/client/inspector/boxmodel/types.js");
+
+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);
+
+class BoxModelInfo extends PureComponent {
+ static get propTypes() {
+ return {
+ boxModel: PropTypes.shape(Types.boxModel).isRequired,
+ onToggleGeometryEditor: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.onToggleGeometryEditor = this.onToggleGeometryEditor.bind(this);
+ }
+
+ onToggleGeometryEditor(e) {
+ this.props.onToggleGeometryEditor();
+ }
+
+ render() {
+ const { boxModel } = this.props;
+ const { geometryEditorEnabled, layout } = boxModel;
+ const { height = "-", isPositionEditable, position, width = "-" } = layout;
+
+ let buttonClass = "layout-geometry-editor devtools-button";
+ if (geometryEditorEnabled) {
+ buttonClass += " checked";
+ }
+
+ return dom.div(
+ {
+ className: "boxmodel-info",
+ role: "region",
+ "aria-label": SHARED_L10N.getFormatStr(
+ "boxModelInfo.accessibleLabel",
+ width,
+ height,
+ position
+ ),
+ },
+ dom.span(
+ { className: "boxmodel-element-size" },
+ SHARED_L10N.getFormatStr("dimensions", width, height)
+ ),
+ dom.section(
+ { className: "boxmodel-position-group" },
+ isPositionEditable
+ ? dom.button({
+ className: buttonClass,
+ title: BOXMODEL_L10N.getStr("boxmodel.geometryButton.tooltip"),
+ onClick: this.onToggleGeometryEditor,
+ })
+ : null,
+ dom.span({ className: "boxmodel-element-position" }, position)
+ )
+ );
+ }
+}
+
+module.exports = BoxModelInfo;
diff --git a/devtools/client/inspector/boxmodel/components/BoxModelMain.js b/devtools/client/inspector/boxmodel/components/BoxModelMain.js
new file mode 100644
index 0000000000..e7065f797a
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/components/BoxModelMain.js
@@ -0,0 +1,774 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const { KeyCodes } = require("resource://devtools/client/shared/keycodes.js");
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+
+const BoxModelEditable = createFactory(
+ require("resource://devtools/client/inspector/boxmodel/components/BoxModelEditable.js")
+);
+
+const Types = require("resource://devtools/client/inspector/boxmodel/types.js");
+
+const {
+ highlightSelectedNode,
+ unhighlightNode,
+} = require("resource://devtools/client/inspector/boxmodel/actions/box-model-highlighter.js");
+
+const SHARED_STRINGS_URI = "devtools/client/locales/shared.properties";
+const SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI);
+
+class BoxModelMain extends PureComponent {
+ static get propTypes() {
+ return {
+ boxModel: PropTypes.shape(Types.boxModel).isRequired,
+ boxModelContainer: PropTypes.object,
+ dispatch: PropTypes.func.isRequired,
+ onShowBoxModelEditor: PropTypes.func.isRequired,
+ onShowRulePreviewTooltip: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ activeDescendant: null,
+ focusable: false,
+ };
+
+ this.getActiveDescendant = this.getActiveDescendant.bind(this);
+ this.getBorderOrPaddingValue = this.getBorderOrPaddingValue.bind(this);
+ this.getContextBox = this.getContextBox.bind(this);
+ this.getDisplayPosition = this.getDisplayPosition.bind(this);
+ this.getHeightValue = this.getHeightValue.bind(this);
+ this.getMarginValue = this.getMarginValue.bind(this);
+ this.getPositionValue = this.getPositionValue.bind(this);
+ this.getWidthValue = this.getWidthValue.bind(this);
+ this.moveFocus = this.moveFocus.bind(this);
+ this.onHighlightMouseOver = this.onHighlightMouseOver.bind(this);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.onLevelClick = this.onLevelClick.bind(this);
+ this.setActive = this.setActive.bind(this);
+ }
+
+ componentDidUpdate() {
+ const displayPosition = this.getDisplayPosition();
+ const isContentBox = this.getContextBox();
+
+ this.layouts = {
+ position: new Map([
+ [KeyCodes.DOM_VK_ESCAPE, this.positionLayout],
+ [KeyCodes.DOM_VK_DOWN, this.marginLayout],
+ [KeyCodes.DOM_VK_RETURN, this.positionEditable],
+ [KeyCodes.DOM_VK_UP, null],
+ ["click", this.positionLayout],
+ ]),
+ margin: new Map([
+ [KeyCodes.DOM_VK_ESCAPE, this.marginLayout],
+ [KeyCodes.DOM_VK_DOWN, this.borderLayout],
+ [KeyCodes.DOM_VK_RETURN, this.marginEditable],
+ [KeyCodes.DOM_VK_UP, displayPosition ? this.positionLayout : null],
+ ["click", this.marginLayout],
+ ]),
+ border: new Map([
+ [KeyCodes.DOM_VK_ESCAPE, this.borderLayout],
+ [KeyCodes.DOM_VK_DOWN, this.paddingLayout],
+ [KeyCodes.DOM_VK_RETURN, this.borderEditable],
+ [KeyCodes.DOM_VK_UP, this.marginLayout],
+ ["click", this.borderLayout],
+ ]),
+ padding: new Map([
+ [KeyCodes.DOM_VK_ESCAPE, this.paddingLayout],
+ [KeyCodes.DOM_VK_DOWN, isContentBox ? this.contentLayout : null],
+ [KeyCodes.DOM_VK_RETURN, this.paddingEditable],
+ [KeyCodes.DOM_VK_UP, this.borderLayout],
+ ["click", this.paddingLayout],
+ ]),
+ content: new Map([
+ [KeyCodes.DOM_VK_ESCAPE, this.contentLayout],
+ [KeyCodes.DOM_VK_DOWN, null],
+ [KeyCodes.DOM_VK_RETURN, this.contentEditable],
+ [KeyCodes.DOM_VK_UP, this.paddingLayout],
+ ["click", this.contentLayout],
+ ]),
+ };
+ }
+
+ getActiveDescendant() {
+ let { activeDescendant } = this.state;
+
+ if (!activeDescendant) {
+ const displayPosition = this.getDisplayPosition();
+ const nextLayout = displayPosition
+ ? this.positionLayout
+ : this.marginLayout;
+ activeDescendant = nextLayout.getAttribute("data-box");
+ this.setActive(nextLayout);
+ }
+
+ return activeDescendant;
+ }
+
+ getBorderOrPaddingValue(property) {
+ const { layout } = this.props.boxModel;
+ return layout[property] ? parseFloat(layout[property]) : "-";
+ }
+
+ /**
+ * Returns true if the layout box sizing is context box and false otherwise.
+ */
+ getContextBox() {
+ const { layout } = this.props.boxModel;
+ return layout["box-sizing"] == "content-box";
+ }
+
+ /**
+ * Returns true if the position is displayed and false otherwise.
+ */
+ getDisplayPosition() {
+ const { layout } = this.props.boxModel;
+ return layout.position && layout.position != "static";
+ }
+
+ getHeightValue(property) {
+ if (property == undefined) {
+ return "-";
+ }
+
+ const { layout } = this.props.boxModel;
+
+ property -=
+ parseFloat(layout["border-top-width"]) +
+ parseFloat(layout["border-bottom-width"]) +
+ parseFloat(layout["padding-top"]) +
+ parseFloat(layout["padding-bottom"]);
+ property = parseFloat(property.toPrecision(6));
+
+ return property;
+ }
+
+ getMarginValue(property, direction) {
+ const { layout } = this.props.boxModel;
+ const autoMargins = layout.autoMargins || {};
+ let value = "-";
+
+ if (direction in autoMargins) {
+ value = autoMargins[direction];
+ } else if (layout[property]) {
+ const parsedValue = parseFloat(layout[property]);
+
+ if (Number.isNaN(parsedValue)) {
+ // Not a number. We use the raw string.
+ // Useful for pseudo-elements with auto margins since they
+ // don't appear in autoMargins.
+ value = layout[property];
+ } else {
+ value = parsedValue;
+ }
+ }
+
+ return value;
+ }
+
+ getPositionValue(property) {
+ const { layout } = this.props.boxModel;
+ let value = "-";
+
+ if (!layout[property]) {
+ return value;
+ }
+
+ const parsedValue = parseFloat(layout[property]);
+
+ if (Number.isNaN(parsedValue)) {
+ // Not a number. We use the raw string.
+ value = layout[property];
+ } else {
+ value = parsedValue;
+ }
+
+ return value;
+ }
+
+ getWidthValue(property) {
+ if (property == undefined) {
+ return "-";
+ }
+
+ const { layout } = this.props.boxModel;
+
+ property -=
+ parseFloat(layout["border-left-width"]) +
+ parseFloat(layout["border-right-width"]) +
+ parseFloat(layout["padding-left"]) +
+ parseFloat(layout["padding-right"]);
+ property = parseFloat(property.toPrecision(6));
+
+ return property;
+ }
+
+ /**
+ * Move the focus to the next/previous editable element of the current layout.
+ *
+ * @param {Element} target
+ * Node to be observed
+ * @param {Boolean} shiftKey
+ * Determines if shiftKey was pressed
+ */
+ moveFocus({ target, shiftKey }) {
+ const editBoxes = [
+ ...this.positionLayout.querySelectorAll("[data-box].boxmodel-editable"),
+ ];
+ const editingMode = target.tagName === "input";
+ // target.nextSibling is input field
+ let position = editingMode
+ ? editBoxes.indexOf(target.nextSibling)
+ : editBoxes.indexOf(target);
+
+ if (position === editBoxes.length - 1 && !shiftKey) {
+ position = 0;
+ } else if (position === 0 && shiftKey) {
+ position = editBoxes.length - 1;
+ } else {
+ shiftKey ? position-- : position++;
+ }
+
+ const editBox = editBoxes[position];
+ this.setActive(editBox);
+ editBox.focus();
+
+ if (editingMode) {
+ editBox.click();
+ }
+ }
+
+ /**
+ * Active level set to current layout.
+ *
+ * @param {Element} nextLayout
+ * Element of next layout that user has navigated to
+ */
+ setActive(nextLayout) {
+ const { boxModelContainer } = this.props;
+
+ // We set this attribute for testing purposes.
+ if (boxModelContainer) {
+ boxModelContainer.dataset.activeDescendantClassName =
+ nextLayout.className;
+ }
+
+ this.setState({
+ activeDescendant: nextLayout.getAttribute("data-box"),
+ });
+ }
+
+ 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.dispatch(unhighlightNode());
+ }
+
+ this.props.dispatch(
+ highlightSelectedNode({
+ region,
+ showOnly: region,
+ onlyRegionArea: true,
+ })
+ );
+
+ event.preventDefault();
+ }
+
+ /**
+ * Handle keyboard navigation and focus for box model layouts.
+ *
+ * Updates active layout on arrow key navigation
+ * Focuses next layout's editboxes on enter key
+ * Unfocuses current layout's editboxes when active layout changes
+ * Controls tabbing between editBoxes
+ *
+ * @param {Event} event
+ * The event triggered by a keypress on the box model
+ */
+ onKeyDown(event) {
+ const { target, keyCode } = event;
+ const isEditable = target._editable || target.editor;
+
+ const level = this.getActiveDescendant();
+ const editingMode = target.tagName === "input";
+
+ switch (keyCode) {
+ case KeyCodes.DOM_VK_RETURN:
+ if (!isEditable) {
+ this.setState({ focusable: true }, () => {
+ const editableBox = this.layouts[level].get(keyCode);
+ if (editableBox) {
+ editableBox.boxModelEditable.focus();
+ }
+ });
+ }
+ break;
+ case KeyCodes.DOM_VK_DOWN:
+ case KeyCodes.DOM_VK_UP:
+ if (!editingMode) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.setState({ focusable: false }, () => {
+ const nextLayout = this.layouts[level].get(keyCode);
+
+ if (!nextLayout) {
+ return;
+ }
+
+ this.setActive(nextLayout);
+
+ if (target?._editable) {
+ target.blur();
+ }
+
+ this.props.boxModelContainer.focus();
+ });
+ }
+ break;
+ case KeyCodes.DOM_VK_TAB:
+ if (isEditable) {
+ event.preventDefault();
+ this.moveFocus(event);
+ }
+ break;
+ case KeyCodes.DOM_VK_ESCAPE:
+ if (target._editable) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.setState({ focusable: false }, () => {
+ this.props.boxModelContainer.focus();
+ });
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Update active on mouse click.
+ *
+ * @param {Event} event
+ * The event triggered by a mouse click on the box model
+ */
+ onLevelClick(event) {
+ const { target } = event;
+ const displayPosition = this.getDisplayPosition();
+ const isContentBox = this.getContextBox();
+
+ // Avoid switching the active descendant to the position or content layout
+ // if those are not editable.
+ if (
+ (!displayPosition && target == this.positionLayout) ||
+ (!isContentBox && target == this.contentLayout)
+ ) {
+ return;
+ }
+
+ const nextLayout =
+ this.layouts[target.getAttribute("data-box")].get("click");
+ this.setActive(nextLayout);
+
+ if (target?._editable) {
+ target.blur();
+ }
+ }
+
+ render() {
+ const {
+ boxModel,
+ dispatch,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ } = this.props;
+ const { layout } = boxModel;
+ let { height, width } = layout;
+ const { activeDescendant: level, focusable } = this.state;
+
+ const borderTop = this.getBorderOrPaddingValue("border-top-width");
+ const borderRight = this.getBorderOrPaddingValue("border-right-width");
+ const borderBottom = this.getBorderOrPaddingValue("border-bottom-width");
+ const borderLeft = this.getBorderOrPaddingValue("border-left-width");
+
+ const paddingTop = this.getBorderOrPaddingValue("padding-top");
+ const paddingRight = this.getBorderOrPaddingValue("padding-right");
+ const paddingBottom = this.getBorderOrPaddingValue("padding-bottom");
+ const paddingLeft = this.getBorderOrPaddingValue("padding-left");
+
+ const displayPosition = this.getDisplayPosition();
+ const positionTop = this.getPositionValue("top");
+ const positionRight = this.getPositionValue("right");
+ const positionBottom = this.getPositionValue("bottom");
+ const positionLeft = this.getPositionValue("left");
+
+ const marginTop = this.getMarginValue("margin-top", "top");
+ const marginRight = this.getMarginValue("margin-right", "right");
+ const marginBottom = this.getMarginValue("margin-bottom", "bottom");
+ const marginLeft = this.getMarginValue("margin-left", "left");
+
+ height = this.getHeightValue(height);
+ width = this.getWidthValue(width);
+
+ const contentBox =
+ layout["box-sizing"] == "content-box"
+ ? dom.div(
+ { className: "boxmodel-size" },
+ BoxModelEditable({
+ box: "content",
+ focusable,
+ level,
+ property: "width",
+ ref: editable => {
+ this.contentEditable = editable;
+ },
+ textContent: width,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ dom.span({}, "\u00D7"),
+ BoxModelEditable({
+ box: "content",
+ focusable,
+ level,
+ property: "height",
+ textContent: height,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ })
+ )
+ : dom.p(
+ {
+ className: "boxmodel-size",
+ id: "boxmodel-size-id",
+ },
+ dom.span(
+ { title: "content" },
+ SHARED_L10N.getFormatStr("dimensions", width, height)
+ )
+ );
+
+ return dom.div(
+ {
+ className: "boxmodel-main devtools-monospace",
+ "data-box": "position",
+ ref: div => {
+ this.positionLayout = div;
+ },
+ onClick: this.onLevelClick,
+ onKeyDown: this.onKeyDown,
+ onMouseOver: this.onHighlightMouseOver,
+ onMouseOut: () => dispatch(unhighlightNode()),
+ },
+ displayPosition
+ ? dom.span(
+ {
+ className: "boxmodel-legend",
+ "data-box": "position",
+ title: "position",
+ },
+ "position"
+ )
+ : null,
+ dom.div(
+ { className: "boxmodel-box" },
+ dom.span(
+ {
+ className: "boxmodel-legend",
+ "data-box": "margin",
+ title: "margin",
+ role: "region",
+ "aria-level": "1", // margin, outermost box
+ "aria-owns":
+ "margin-top-id margin-right-id margin-bottom-id margin-left-id margins-div",
+ },
+ "margin"
+ ),
+ dom.div(
+ {
+ className: "boxmodel-margins",
+ id: "margins-div",
+ "data-box": "margin",
+ title: "margin",
+ ref: div => {
+ this.marginLayout = div;
+ },
+ },
+ dom.span(
+ {
+ className: "boxmodel-legend",
+ "data-box": "border",
+ title: "border",
+ role: "region",
+ "aria-level": "2", // margin -> border, second box
+ "aria-owns":
+ "border-top-width-id border-right-width-id border-bottom-width-id border-left-width-id borders-div",
+ },
+ "border"
+ ),
+ dom.div(
+ {
+ className: "boxmodel-borders",
+ id: "borders-div",
+ "data-box": "border",
+ title: "border",
+ ref: div => {
+ this.borderLayout = div;
+ },
+ },
+ dom.span(
+ {
+ className: "boxmodel-legend",
+ "data-box": "padding",
+ title: "padding",
+ role: "region",
+ "aria-level": "3", // margin -> border -> padding
+ "aria-owns":
+ "padding-top-id padding-right-id padding-bottom-id padding-left-id padding-div",
+ },
+ "padding"
+ ),
+ dom.div(
+ {
+ className: "boxmodel-paddings",
+ id: "padding-div",
+ "data-box": "padding",
+ title: "padding",
+ "aria-owns": "boxmodel-contents-id",
+ ref: div => {
+ this.paddingLayout = div;
+ },
+ },
+ dom.div({
+ className: "boxmodel-contents",
+ id: "boxmodel-contents-id",
+ "data-box": "content",
+ title: "content",
+ role: "region",
+ "aria-level": "4", // margin -> border -> padding -> content
+ "aria-label": SHARED_L10N.getFormatStr(
+ "boxModelSize.accessibleLabel",
+ width,
+ height
+ ),
+ "aria-owns": "boxmodel-size-id",
+ ref: div => {
+ this.contentLayout = div;
+ },
+ })
+ )
+ )
+ )
+ ),
+ displayPosition
+ ? BoxModelEditable({
+ box: "position",
+ direction: "top",
+ focusable,
+ level,
+ property: "position-top",
+ ref: editable => {
+ this.positionEditable = editable;
+ },
+ textContent: positionTop,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ })
+ : null,
+ displayPosition
+ ? BoxModelEditable({
+ box: "position",
+ direction: "right",
+ focusable,
+ level,
+ property: "position-right",
+ textContent: positionRight,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ })
+ : null,
+ displayPosition
+ ? BoxModelEditable({
+ box: "position",
+ direction: "bottom",
+ focusable,
+ level,
+ property: "position-bottom",
+ textContent: positionBottom,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ })
+ : null,
+ displayPosition
+ ? BoxModelEditable({
+ box: "position",
+ direction: "left",
+ focusable,
+ level,
+ property: "position-left",
+ textContent: positionLeft,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ })
+ : null,
+ BoxModelEditable({
+ box: "margin",
+ direction: "top",
+ focusable,
+ level,
+ property: "margin-top",
+ ref: editable => {
+ this.marginEditable = editable;
+ },
+ textContent: marginTop,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ BoxModelEditable({
+ box: "margin",
+ direction: "right",
+ focusable,
+ level,
+ property: "margin-right",
+ textContent: marginRight,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ BoxModelEditable({
+ box: "margin",
+ direction: "bottom",
+ focusable,
+ level,
+ property: "margin-bottom",
+ textContent: marginBottom,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ BoxModelEditable({
+ box: "margin",
+ direction: "left",
+ focusable,
+ level,
+ property: "margin-left",
+ textContent: marginLeft,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ BoxModelEditable({
+ box: "border",
+ direction: "top",
+ focusable,
+ level,
+ property: "border-top-width",
+ ref: editable => {
+ this.borderEditable = editable;
+ },
+ textContent: borderTop,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ BoxModelEditable({
+ box: "border",
+ direction: "right",
+ focusable,
+ level,
+ property: "border-right-width",
+ textContent: borderRight,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ BoxModelEditable({
+ box: "border",
+ direction: "bottom",
+ focusable,
+ level,
+ property: "border-bottom-width",
+ textContent: borderBottom,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ BoxModelEditable({
+ box: "border",
+ direction: "left",
+ focusable,
+ level,
+ property: "border-left-width",
+ textContent: borderLeft,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ BoxModelEditable({
+ box: "padding",
+ direction: "top",
+ focusable,
+ level,
+ property: "padding-top",
+ ref: editable => {
+ this.paddingEditable = editable;
+ },
+ textContent: paddingTop,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ BoxModelEditable({
+ box: "padding",
+ direction: "right",
+ focusable,
+ level,
+ property: "padding-right",
+ textContent: paddingRight,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ BoxModelEditable({
+ box: "padding",
+ direction: "bottom",
+ focusable,
+ level,
+ property: "padding-bottom",
+ textContent: paddingBottom,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ BoxModelEditable({
+ box: "padding",
+ direction: "left",
+ focusable,
+ level,
+ property: "padding-left",
+ textContent: paddingLeft,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ contentBox
+ );
+ }
+}
+
+module.exports = BoxModelMain;
diff --git a/devtools/client/inspector/boxmodel/components/BoxModelProperties.js b/devtools/client/inspector/boxmodel/components/BoxModelProperties.js
new file mode 100644
index 0000000000..8b314a46ed
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/components/BoxModelProperties.js
@@ -0,0 +1,142 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+
+const ComputedProperty = createFactory(
+ require("resource://devtools/client/inspector/boxmodel/components/ComputedProperty.js")
+);
+
+const Types = require("resource://devtools/client/inspector/boxmodel/types.js");
+
+const BOXMODEL_STRINGS_URI = "devtools/client/locales/boxmodel.properties";
+const BOXMODEL_L10N = new LocalizationHelper(BOXMODEL_STRINGS_URI);
+
+class BoxModelProperties extends PureComponent {
+ static get propTypes() {
+ return {
+ boxModel: PropTypes.shape(Types.boxModel).isRequired,
+ dispatch: PropTypes.func.isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ isOpen: true,
+ };
+
+ this.getReferenceElement = this.getReferenceElement.bind(this);
+ this.onToggleExpander = this.onToggleExpander.bind(this);
+ }
+
+ /**
+ * Various properties can display a reference element. E.g. position displays an offset
+ * parent if its value is other than fixed or static. Or z-index displays a stacking
+ * context, etc.
+ * This returns the right element if there needs to be one, and one was passed in the
+ * props.
+ *
+ * @return {Object} An object with 2 properties:
+ * - referenceElement {NodeFront}
+ * - referenceElementType {String}
+ */
+ getReferenceElement(propertyName) {
+ const value = this.props.boxModel.layout[propertyName];
+
+ if (
+ propertyName === "position" &&
+ value !== "static" &&
+ value !== "fixed" &&
+ this.props.boxModel.offsetParent
+ ) {
+ return {
+ referenceElement: this.props.boxModel.offsetParent,
+ referenceElementType: BOXMODEL_L10N.getStr("boxmodel.offsetParent"),
+ };
+ }
+
+ return {};
+ }
+
+ onToggleExpander(event) {
+ this.setState({
+ isOpen: !this.state.isOpen,
+ });
+ event.stopPropagation();
+ }
+
+ render() {
+ const { boxModel, dispatch, setSelectedNode } = this.props;
+ const { layout } = boxModel;
+
+ const layoutInfo = [
+ "box-sizing",
+ "display",
+ "float",
+ "line-height",
+ "position",
+ "z-index",
+ ];
+
+ const properties = layoutInfo.map(info => {
+ const { referenceElement, referenceElementType } =
+ this.getReferenceElement(info);
+
+ return ComputedProperty({
+ dispatch,
+ key: info,
+ name: info,
+ referenceElement,
+ referenceElementType,
+ setSelectedNode,
+ value: layout[info],
+ });
+ });
+
+ return dom.div(
+ { className: "layout-properties" },
+ dom.div(
+ {
+ className: "layout-properties-header",
+ role: "heading",
+ "aria-level": "3",
+ onDoubleClick: this.onToggleExpander,
+ },
+ dom.span({
+ className: "layout-properties-expander theme-twisty",
+ open: this.state.isOpen,
+ role: "button",
+ "aria-label": BOXMODEL_L10N.getStr(
+ this.state.isOpen
+ ? "boxmodel.propertiesHideLabel"
+ : "boxmodel.propertiesShowLabel"
+ ),
+ onClick: this.onToggleExpander,
+ }),
+ BOXMODEL_L10N.getStr("boxmodel.propertiesLabel")
+ ),
+ dom.div(
+ {
+ className: "layout-properties-wrapper devtools-monospace",
+ hidden: !this.state.isOpen,
+ role: "table",
+ },
+ properties
+ )
+ );
+ }
+}
+
+module.exports = BoxModelProperties;
diff --git a/devtools/client/inspector/boxmodel/components/ComputedProperty.js b/devtools/client/inspector/boxmodel/components/ComputedProperty.js
new file mode 100644
index 0000000000..330ae7512e
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/components/ComputedProperty.js
@@ -0,0 +1,123 @@
+/* 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 {
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+
+loader.lazyRequireGetter(
+ this,
+ "getNodeRep",
+ "resource://devtools/client/inspector/shared/node-reps.js"
+);
+
+const {
+ highlightNode,
+ unhighlightNode,
+} = require("resource://devtools/client/inspector/boxmodel/actions/box-model-highlighter.js");
+
+const BOXMODEL_STRINGS_URI = "devtools/client/locales/boxmodel.properties";
+const BOXMODEL_L10N = new LocalizationHelper(BOXMODEL_STRINGS_URI);
+
+class ComputedProperty extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ name: PropTypes.string.isRequired,
+ referenceElement: PropTypes.object,
+ referenceElementType: PropTypes.string,
+ setSelectedNode: PropTypes.func,
+ value: PropTypes.string,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.renderReferenceElementPreview =
+ this.renderReferenceElementPreview.bind(this);
+ }
+
+ renderReferenceElementPreview() {
+ const {
+ dispatch,
+ referenceElement,
+ referenceElementType,
+ setSelectedNode,
+ } = this.props;
+
+ if (!referenceElement) {
+ return null;
+ }
+
+ return dom.div(
+ { className: "reference-element" },
+ dom.span(
+ {
+ className: "reference-element-type",
+ role: "button",
+ title: BOXMODEL_L10N.getStr("boxmodel.offsetParent.title"),
+ },
+ referenceElementType
+ ),
+ getNodeRep(referenceElement, {
+ onInspectIconClick: () =>
+ setSelectedNode(referenceElement, { reason: "box-model" }),
+ onDOMNodeMouseOver: () => dispatch(highlightNode(referenceElement)),
+ onDOMNodeMouseOut: () => dispatch(unhighlightNode()),
+ })
+ );
+ }
+
+ render() {
+ const { name, value } = this.props;
+
+ return dom.div(
+ {
+ className: "computed-property-view",
+ role: "row",
+ "data-property-name": name,
+ ref: container => {
+ this.container = container;
+ },
+ },
+ dom.div(
+ {
+ className: "computed-property-name-container",
+ role: "presentation",
+ },
+ dom.div(
+ {
+ className: "computed-property-name theme-fg-color3",
+ role: "cell",
+ title: name,
+ },
+ name
+ )
+ ),
+ dom.div(
+ {
+ className: "computed-property-value-container",
+ role: "presentation",
+ },
+ dom.div(
+ {
+ className: "computed-property-value theme-fg-color1",
+ dir: "ltr",
+ role: "cell",
+ },
+ value
+ ),
+ this.renderReferenceElementPreview()
+ )
+ );
+ }
+}
+
+module.exports = ComputedProperty;
diff --git a/devtools/client/inspector/boxmodel/components/moz.build b/devtools/client/inspector/boxmodel/components/moz.build
new file mode 100644
index 0000000000..ed57c93eb7
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/components/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "BoxModel.js",
+ "BoxModelEditable.js",
+ "BoxModelInfo.js",
+ "BoxModelMain.js",
+ "BoxModelProperties.js",
+ "ComputedProperty.js",
+)