Source code
Revision control
Copy as Markdown
Other Tools
/* 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
"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;