diff options
Diffstat (limited to 'devtools/client/inspector/boxmodel/components/BoxModelMain.js')
-rw-r--r-- | devtools/client/inspector/boxmodel/components/BoxModelMain.js | 775 |
1 files changed, 775 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..d821f42fae --- /dev/null +++ b/devtools/client/inspector/boxmodel/components/BoxModelMain.js @@ -0,0 +1,775 @@ +/* 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; |