diff options
Diffstat (limited to '')
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", +) |