summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/boxmodel/components/BoxModelMain.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/inspector/boxmodel/components/BoxModelMain.js774
1 files changed, 774 insertions, 0 deletions
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;