diff options
Diffstat (limited to 'devtools/client/inspector/boxmodel')
45 files changed, 4916 insertions, 0 deletions
diff --git a/devtools/client/inspector/boxmodel/actions/box-model-highlighter.js b/devtools/client/inspector/boxmodel/actions/box-model-highlighter.js new file mode 100644 index 0000000000..9033dc46ae --- /dev/null +++ b/devtools/client/inspector/boxmodel/actions/box-model-highlighter.js @@ -0,0 +1,86 @@ +/* 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"; + +/** + * This module exports thunks. + * Thunks are functions that can be dispatched to the Inspector Redux store. + * + * These functions receive one object with options that contains: + * - dispatch() => function to dispatch Redux actions to the store + * - getState() => function to get the current state of the entire Inspector Redux store + * - inspector => object instance of Inspector client + * + * They provide a shortcut for React components to invoke the box model highlighter + * without having to know where the highlighter exists. + */ + +module.exports = { + /** + * Show the box model highlighter for the currently selected node front. + * The selected node is obtained from the Selection instance on the Inspector. + * + * @param {Object} options + * Optional configuration options passed to the box model highlighter + */ + highlightSelectedNode(options = {}) { + return async thunkOptions => { + const { inspector } = thunkOptions; + if (!inspector || inspector._destroyed) { + return; + } + + const { nodeFront } = inspector.selection; + if (!nodeFront) { + return; + } + + await inspector.highlighters.showHighlighterTypeForNode( + inspector.highlighters.TYPES.BOXMODEL, + nodeFront, + options + ); + }; + }, + + /** + * Show the box model highlighter for the given node front. + * + * @param {NodeFront} nodeFront + * Node that should be highlighted. + * @param {Object} options + * Optional configuration options passed to the box model highlighter + */ + highlightNode(nodeFront, options = {}) { + return async thunkOptions => { + const { inspector } = thunkOptions; + if (!inspector || inspector._destroyed) { + return; + } + + await inspector.highlighters.showHighlighterTypeForNode( + inspector.highlighters.TYPES.BOXMODEL, + nodeFront, + options + ); + }; + }, + + /** + * Hide the box model highlighter for any highlighted node. + */ + unhighlightNode() { + return async thunkOptions => { + const { inspector } = thunkOptions; + if (!inspector || inspector._destroyed) { + return; + } + + await inspector.highlighters.hideHighlighterType( + inspector.highlighters.TYPES.BOXMODEL + ); + }; + }, +}; diff --git a/devtools/client/inspector/boxmodel/actions/box-model.js b/devtools/client/inspector/boxmodel/actions/box-model.js new file mode 100644 index 0000000000..322cf6cb00 --- /dev/null +++ b/devtools/client/inspector/boxmodel/actions/box-model.js @@ -0,0 +1,46 @@ +/* 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 { + UPDATE_GEOMETRY_EDITOR_ENABLED, + UPDATE_LAYOUT, + UPDATE_OFFSET_PARENT, +} = require("resource://devtools/client/inspector/boxmodel/actions/index.js"); + +module.exports = { + /** + * Updates the geometry editor's enabled state. + * + * @param {Boolean} enabled + * Whether or not the geometry editor is enabled or not. + */ + updateGeometryEditorEnabled(enabled) { + return { + type: UPDATE_GEOMETRY_EDITOR_ENABLED, + enabled, + }; + }, + + /** + * Updates the layout state with the new layout properties. + */ + updateLayout(layout) { + return { + type: UPDATE_LAYOUT, + layout, + }; + }, + + /** + * Updates the offset parent state with the new DOM node. + */ + updateOffsetParent(offsetParent) { + return { + type: UPDATE_OFFSET_PARENT, + offsetParent, + }; + }, +}; diff --git a/devtools/client/inspector/boxmodel/actions/index.js b/devtools/client/inspector/boxmodel/actions/index.js new file mode 100644 index 0000000000..813b4e9e11 --- /dev/null +++ b/devtools/client/inspector/boxmodel/actions/index.js @@ -0,0 +1,21 @@ +/* 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 { createEnum } = require("resource://devtools/client/shared/enum.js"); + +createEnum( + [ + // Updates the geometry editor's enabled state. + "UPDATE_GEOMETRY_EDITOR_ENABLED", + + // Updates the layout state with the latest layout properties. + "UPDATE_LAYOUT", + + // Updates the offset parent state with the new DOM node. + "UPDATE_OFFSET_PARENT", + ], + module.exports +); diff --git a/devtools/client/inspector/boxmodel/actions/moz.build b/devtools/client/inspector/boxmodel/actions/moz.build new file mode 100644 index 0000000000..7ee681b35c --- /dev/null +++ b/devtools/client/inspector/boxmodel/actions/moz.build @@ -0,0 +1,11 @@ +# -*- 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( + "box-model-highlighter.js", + "box-model.js", + "index.js", +) diff --git a/devtools/client/inspector/boxmodel/box-model.js b/devtools/client/inspector/boxmodel/box-model.js new file mode 100644 index 0000000000..c6870ef016 --- /dev/null +++ b/devtools/client/inspector/boxmodel/box-model.js @@ -0,0 +1,446 @@ +/* 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 boxModelReducer = require("resource://devtools/client/inspector/boxmodel/reducers/box-model.js"); +const { + updateGeometryEditorEnabled, + updateLayout, + updateOffsetParent, +} = require("resource://devtools/client/inspector/boxmodel/actions/box-model.js"); + +loader.lazyRequireGetter( + this, + "EditingSession", + "resource://devtools/client/inspector/boxmodel/utils/editing-session.js" +); +loader.lazyRequireGetter( + this, + "InplaceEditor", + "resource://devtools/client/shared/inplace-editor.js", + true +); +loader.lazyRequireGetter( + this, + "RulePreviewTooltip", + "resource://devtools/client/shared/widgets/tooltip/RulePreviewTooltip.js" +); + +const NUMERIC = /^-?[\d\.]+$/; + +/** + * A singleton instance of the box model controllers. + * + * @param {Inspector} inspector + * An instance of the Inspector currently loaded in the toolbox. + * @param {Window} window + * The document window of the toolbox. + */ +function BoxModel(inspector, window) { + this.document = window.document; + this.inspector = inspector; + this.store = inspector.store; + + this.store.injectReducer("boxModel", boxModelReducer); + + this.updateBoxModel = this.updateBoxModel.bind(this); + + this.onHideGeometryEditor = this.onHideGeometryEditor.bind(this); + this.onMarkupViewLeave = this.onMarkupViewLeave.bind(this); + this.onMarkupViewNodeHover = this.onMarkupViewNodeHover.bind(this); + this.onNewSelection = this.onNewSelection.bind(this); + this.onShowBoxModelEditor = this.onShowBoxModelEditor.bind(this); + this.onShowRulePreviewTooltip = this.onShowRulePreviewTooltip.bind(this); + this.onSidebarSelect = this.onSidebarSelect.bind(this); + this.onToggleGeometryEditor = this.onToggleGeometryEditor.bind(this); + + this.inspector.selection.on("new-node-front", this.onNewSelection); + this.inspector.sidebar.on("select", this.onSidebarSelect); +} + +BoxModel.prototype = { + /** + * Destruction function called when the inspector is destroyed. Removes event listeners + * and cleans up references. + */ + destroy() { + this.inspector.selection.off("new-node-front", this.onNewSelection); + this.inspector.sidebar.off("select", this.onSidebarSelect); + + if (this._geometryEditorEventsAbortController) { + this._geometryEditorEventsAbortController.abort(); + this._geometryEditorEventsAbortController = null; + } + + if (this._tooltip) { + this._tooltip.destroy(); + } + + this.untrackReflows(); + + this.elementRules = null; + this._highlighters = null; + this._tooltip = null; + this.document = null; + this.inspector = null; + }, + + get highlighters() { + if (!this._highlighters) { + // highlighters is a lazy getter in the inspector. + this._highlighters = this.inspector.highlighters; + } + + return this._highlighters; + }, + + get rulePreviewTooltip() { + if (!this._tooltip) { + this._tooltip = new RulePreviewTooltip(this.inspector.toolbox.doc); + } + + return this._tooltip; + }, + + /** + * Returns an object containing the box model's handler functions used in the box + * model's React component props. + */ + getComponentProps() { + return { + onShowBoxModelEditor: this.onShowBoxModelEditor, + onShowRulePreviewTooltip: this.onShowRulePreviewTooltip, + onToggleGeometryEditor: this.onToggleGeometryEditor, + }; + }, + + /** + * Returns true if the layout panel is visible, and false otherwise. + */ + isPanelVisible() { + return ( + this.inspector.toolbox && + this.inspector.sidebar && + this.inspector.toolbox.currentToolId === "inspector" && + this.inspector.sidebar.getCurrentTabID() === "layoutview" + ); + }, + + /** + * Returns true if the layout panel is visible and the current element is valid to + * be displayed in the view. + */ + isPanelVisibleAndNodeValid() { + return ( + this.isPanelVisible() && + this.inspector.selection.isConnected() && + this.inspector.selection.isElementNode() + ); + }, + + /** + * Starts listening to reflows in the current tab. + */ + trackReflows() { + this.inspector.on("reflow-in-selected-target", this.updateBoxModel); + }, + + /** + * Stops listening to reflows in the current tab. + */ + untrackReflows() { + this.inspector.off("reflow-in-selected-target", this.updateBoxModel); + }, + + /** + * Updates the box model panel by dispatching the new layout data. + * + * @param {String} reason + * Optional string describing the reason why the boxmodel is updated. + */ + updateBoxModel(reason) { + this._updateReasons = this._updateReasons || []; + if (reason) { + this._updateReasons.push(reason); + } + + const lastRequest = async function () { + if ( + !this.inspector || + !this.isPanelVisible() || + !this.inspector.selection.isConnected() || + !this.inspector.selection.isElementNode() + ) { + return null; + } + + const { nodeFront } = this.inspector.selection; + const inspectorFront = this.getCurrentInspectorFront(); + const { pageStyle } = inspectorFront; + + let layout = await pageStyle.getLayout(nodeFront, { + autoMargins: true, + }); + + const styleEntries = await pageStyle.getApplied(nodeFront, { + // We don't need styles applied to pseudo elements of the current node. + skipPseudo: true, + }); + this.elementRules = styleEntries.map(e => e.rule); + + // Update the layout properties with whether or not the element's position is + // editable with the geometry editor. + const isPositionEditable = await pageStyle.isPositionEditable(nodeFront); + + layout = Object.assign({}, layout, { + isPositionEditable, + }); + + // Update the redux store with the latest offset parent DOM node + const offsetParent = await inspectorFront.walker.getOffsetParent( + nodeFront + ); + this.store.dispatch(updateOffsetParent(offsetParent)); + + // Update the redux store with the latest layout properties and update the box + // model view. + this.store.dispatch(updateLayout(layout)); + + // If a subsequent request has been made, wait for that one instead. + if (this._lastRequest != lastRequest) { + return this._lastRequest; + } + + this.inspector.emit("boxmodel-view-updated", this._updateReasons); + + this._lastRequest = null; + this._updateReasons = []; + + return null; + } + .bind(this)() + .catch(error => { + // If we failed because we were being destroyed while waiting for a request, ignore. + if (this.document) { + console.error(error); + } + }); + + this._lastRequest = lastRequest; + }, + + /** + * Hides the geometry editor and updates the box moodel store with the new + * geometry editor enabled state. + */ + onHideGeometryEditor() { + this.highlighters.hideGeometryEditor(); + this.store.dispatch(updateGeometryEditorEnabled(false)); + + if (this._geometryEditorEventsAbortController) { + this._geometryEditorEventsAbortController.abort(); + this._geometryEditorEventsAbortController = null; + } + }, + + /** + * Handler function that re-shows the geometry editor for an element that already + * had the geometry editor enabled. This handler function is called on a "leave" event + * on the markup view. + */ + onMarkupViewLeave() { + const state = this.store.getState(); + const enabled = state.boxModel.geometryEditorEnabled; + + if (!enabled) { + return; + } + + const nodeFront = this.inspector.selection.nodeFront; + this.highlighters.showGeometryEditor(nodeFront); + }, + + /** + * Handler function that temporarily hides the geomery editor when the + * markup view has a "node-hover" event. + */ + onMarkupViewNodeHover() { + this.highlighters.hideGeometryEditor(); + }, + + /** + * Selection 'new-node-front' event handler. + */ + onNewSelection() { + if (!this.isPanelVisibleAndNodeValid()) { + return; + } + + if ( + this.inspector.selection.isConnected() && + this.inspector.selection.isElementNode() + ) { + this.trackReflows(); + } + + this.updateBoxModel("new-selection"); + }, + + /** + * Shows the RulePreviewTooltip when a box model editable value is hovered on the + * box model panel. + * + * @param {Element} target + * The target element. + * @param {String} property + * The name of the property. + */ + onShowRulePreviewTooltip(target, property) { + const { highlightProperty } = this.inspector.getPanel("ruleview").view; + const isHighlighted = highlightProperty(property); + + // Only show the tooltip if the property is not highlighted. + // TODO: In the future, use an associated ruleId for toggling the tooltip instead of + // the Boolean returned from highlightProperty. + if (!isHighlighted) { + this.rulePreviewTooltip.show(target); + } + }, + + /** + * Shows the inplace editor when a box model editable value is clicked on the + * box model panel. + * + * @param {DOMNode} element + * The element that was clicked. + * @param {Event} event + * The event object. + * @param {String} property + * The name of the property. + */ + onShowBoxModelEditor(element, event, property) { + const session = new EditingSession({ + inspector: this.inspector, + doc: this.document, + elementRules: this.elementRules, + }); + const initialValue = session.getProperty(property); + + const editor = new InplaceEditor( + { + element, + initial: initialValue, + contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE, + property: { + name: property, + }, + start: self => { + self.elt.parentNode.classList.add("boxmodel-editing"); + }, + change: value => { + if (NUMERIC.test(value)) { + value += "px"; + } + + const properties = [{ name: property, value }]; + + if (property.substring(0, 7) == "border-") { + const bprop = property.substring(0, property.length - 5) + "style"; + const style = session.getProperty(bprop); + if (!style || style == "none" || style == "hidden") { + properties.push({ name: bprop, value: "solid" }); + } + } + + if (property.substring(0, 9) == "position-") { + properties[0].name = property.substring(9); + } + + session.setProperties(properties).catch(console.error); + }, + done: (value, commit) => { + editor.elt.parentNode.classList.remove("boxmodel-editing"); + if (!commit) { + session.revert().then(() => { + session.destroy(); + }, console.error); + return; + } + + this.updateBoxModel("editable-value-change"); + }, + cssProperties: this.inspector.cssProperties, + }, + event + ); + }, + + /** + * Handler for the inspector sidebar select event. Starts tracking reflows if the + * layout panel is visible. Otherwise, stop tracking reflows. Finally, refresh the box + * model view if it is visible. + */ + onSidebarSelect() { + if (!this.isPanelVisible()) { + this.untrackReflows(); + return; + } + + if ( + this.inspector.selection.isConnected() && + this.inspector.selection.isElementNode() + ) { + this.trackReflows(); + } + + this.updateBoxModel(); + }, + + /** + * Toggles on/off the geometry editor for the current element when the geometry editor + * toggle button is clicked. + */ + onToggleGeometryEditor() { + const { markup, selection, toolbox } = this.inspector; + const nodeFront = this.inspector.selection.nodeFront; + const state = this.store.getState(); + const enabled = !state.boxModel.geometryEditorEnabled; + + this.highlighters.toggleGeometryHighlighter(nodeFront); + this.store.dispatch(updateGeometryEditorEnabled(enabled)); + + if (enabled) { + this._geometryEditorEventsAbortController = new AbortController(); + const eventListenersConfig = { + signal: this._geometryEditorEventsAbortController.signal, + }; + // Hide completely the geometry editor if: + // - the picker is clicked + // - or if a new node is selected + toolbox.nodePicker.on( + "picker-started", + this.onHideGeometryEditor, + eventListenersConfig + ); + selection.on( + "new-node-front", + this.onHideGeometryEditor, + eventListenersConfig + ); + // Temporarily hide the geometry editor + markup.on("leave", this.onMarkupViewLeave, eventListenersConfig); + markup.on("node-hover", this.onMarkupViewNodeHover, eventListenersConfig); + } else if (this._geometryEditorEventsAbortController) { + this._geometryEditorEventsAbortController.abort(); + this._geometryEditorEventsAbortController = null; + } + }, + + getCurrentInspectorFront() { + return this.inspector.selection.nodeFront.inspectorFront; + }, +}; + +module.exports = BoxModel; 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", +) diff --git a/devtools/client/inspector/boxmodel/moz.build b/devtools/client/inspector/boxmodel/moz.build new file mode 100644 index 0000000000..2791728136 --- /dev/null +++ b/devtools/client/inspector/boxmodel/moz.build @@ -0,0 +1,19 @@ +# -*- 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/. + +DIRS += [ + "actions", + "components", + "reducers", + "utils", +] + +DevToolsModules( + "box-model.js", + "types.js", +) + +BROWSER_CHROME_MANIFESTS += ["test/browser.ini"] diff --git a/devtools/client/inspector/boxmodel/reducers/box-model.js b/devtools/client/inspector/boxmodel/reducers/box-model.js new file mode 100644 index 0000000000..ea3863e56a --- /dev/null +++ b/devtools/client/inspector/boxmodel/reducers/box-model.js @@ -0,0 +1,45 @@ +/* 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 { + UPDATE_GEOMETRY_EDITOR_ENABLED, + UPDATE_LAYOUT, + UPDATE_OFFSET_PARENT, +} = require("resource://devtools/client/inspector/boxmodel/actions/index.js"); + +const INITIAL_BOX_MODEL = { + geometryEditorEnabled: false, + layout: {}, + offsetParent: null, +}; + +const reducers = { + [UPDATE_GEOMETRY_EDITOR_ENABLED](boxModel, { enabled }) { + return Object.assign({}, boxModel, { + geometryEditorEnabled: enabled, + }); + }, + + [UPDATE_LAYOUT](boxModel, { layout }) { + return Object.assign({}, boxModel, { + layout, + }); + }, + + [UPDATE_OFFSET_PARENT](boxModel, { offsetParent }) { + return Object.assign({}, boxModel, { + offsetParent, + }); + }, +}; + +module.exports = function (boxModel = INITIAL_BOX_MODEL, action) { + const reducer = reducers[action.type]; + if (!reducer) { + return boxModel; + } + return reducer(boxModel, action); +}; diff --git a/devtools/client/inspector/boxmodel/reducers/moz.build b/devtools/client/inspector/boxmodel/reducers/moz.build new file mode 100644 index 0000000000..fe216631de --- /dev/null +++ b/devtools/client/inspector/boxmodel/reducers/moz.build @@ -0,0 +1,9 @@ +# -*- 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( + "box-model.js", +) diff --git a/devtools/client/inspector/boxmodel/test/browser.ini b/devtools/client/inspector/boxmodel/test/browser.ini new file mode 100644 index 0000000000..3296d1579a --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser.ini @@ -0,0 +1,40 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + doc_boxmodel_iframe1.html + doc_boxmodel_iframe2.html + head.js + !/devtools/client/inspector/test/head.js + !/devtools/client/inspector/test/shared-head.js + !/devtools/client/shared/test/shared-head.js + !/devtools/client/shared/test/telemetry-test-helpers.js + !/devtools/client/shared/test/highlighter-test-actor.js + +[browser_boxmodel.js] +[browser_boxmodel_edit-position-visible-position-change.js] +[browser_boxmodel_editablemodel.js] +[browser_boxmodel_editablemodel_allproperties.js] +disabled=too many intermittent failures (bug 1009322) +[browser_boxmodel_editablemodel_bluronclick.js] +[browser_boxmodel_editablemodel_border.js] +[browser_boxmodel_editablemodel_pseudo.js] +[browser_boxmodel_editablemodel_stylerules.js] +[browser_boxmodel_guides.js] +[browser_boxmodel_jump-to-rule-on-hover.js] +[browser_boxmodel_layout-accordion-state.js] +[browser_boxmodel_navigation.js] +[browser_boxmodel_offsetparent.js] +[browser_boxmodel_positions.js] +[browser_boxmodel_properties.js] +[browser_boxmodel_pseudo-element.js] +[browser_boxmodel_rotate-labels-on-sides.js] +[browser_boxmodel_show-tooltip-for-unassociated-rule.js] +[browser_boxmodel_sync.js] +[browser_boxmodel_tooltips.js] +skip-if = true # Bug 1336198 +[browser_boxmodel_update-after-navigation.js] +skip-if = (os == 'linux' || os == 'win') && bits == 64 #Bug 1582395 +[browser_boxmodel_update-after-reload.js] +[browser_boxmodel_update-in-iframes.js] +disabled=Bug 1020038 boxmodel-view updates for iframe elements changes diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel.js new file mode 100644 index 0000000000..f5017a5f70 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel.js @@ -0,0 +1,201 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the box model displays the right values and that it updates when +// the node's style is changed + +// Expected values: +var res1 = [ + { + selector: ".boxmodel-element-size", + value: "160" + "\u00D7" + "160.117", + }, + { + selector: ".boxmodel-size > .boxmodel-width", + value: "100", + }, + { + selector: ".boxmodel-size > .boxmodel-height", + value: "100.117", + }, + { + selector: ".boxmodel-position.boxmodel-top > span", + value: "42", + }, + { + selector: ".boxmodel-position.boxmodel-left > span", + value: "42", + }, + { + selector: ".boxmodel-margin.boxmodel-top > span", + value: "30", + }, + { + selector: ".boxmodel-margin.boxmodel-left > span", + value: "auto", + }, + { + selector: ".boxmodel-margin.boxmodel-bottom > span", + value: "30", + }, + { + selector: ".boxmodel-margin.boxmodel-right > span", + value: "auto", + }, + { + selector: ".boxmodel-padding.boxmodel-top > span", + value: "20", + }, + { + selector: ".boxmodel-padding.boxmodel-left > span", + value: "20", + }, + { + selector: ".boxmodel-padding.boxmodel-bottom > span", + value: "20", + }, + { + selector: ".boxmodel-padding.boxmodel-right > span", + value: "20", + }, + { + selector: ".boxmodel-border.boxmodel-top > span", + value: "10", + }, + { + selector: ".boxmodel-border.boxmodel-left > span", + value: "10", + }, + { + selector: ".boxmodel-border.boxmodel-bottom > span", + value: "10", + }, + { + selector: ".boxmodel-border.boxmodel-right > span", + value: "10", + }, +]; + +var res2 = [ + { + selector: ".boxmodel-element-size", + value: "190" + "\u00D7" + "210", + }, + { + selector: ".boxmodel-size > .boxmodel-width", + value: "100", + }, + { + selector: ".boxmodel-size > .boxmodel-height", + value: "150", + }, + { + selector: ".boxmodel-position.boxmodel-top > span", + value: "50", + }, + { + selector: ".boxmodel-position.boxmodel-left > span", + value: "42", + }, + { + selector: ".boxmodel-margin.boxmodel-top > span", + value: "30", + }, + { + selector: ".boxmodel-margin.boxmodel-left > span", + value: "auto", + }, + { + selector: ".boxmodel-margin.boxmodel-bottom > span", + value: "30", + }, + { + selector: ".boxmodel-margin.boxmodel-right > span", + value: "auto", + }, + { + selector: ".boxmodel-padding.boxmodel-top > span", + value: "20", + }, + { + selector: ".boxmodel-padding.boxmodel-left > span", + value: "20", + }, + { + selector: ".boxmodel-padding.boxmodel-bottom > span", + value: "20", + }, + { + selector: ".boxmodel-padding.boxmodel-right > span", + value: "50", + }, + { + selector: ".boxmodel-border.boxmodel-top > span", + value: "10", + }, + { + selector: ".boxmodel-border.boxmodel-left > span", + value: "10", + }, + { + selector: ".boxmodel-border.boxmodel-bottom > span", + value: "10", + }, + { + selector: ".boxmodel-border.boxmodel-right > span", + value: "10", + }, +]; + +add_task(async function () { + const style = + "div { position: absolute; top: 42px; left: 42px; " + + "height: 100.111px; width: 100px; border: 10px solid black; " + + "padding: 20px; margin: 30px auto;}"; + const html = "<style>" + style + "</style><div></div>"; + + await addTab("data:text/html," + encodeURIComponent(html)); + const { inspector, boxmodel } = await openLayoutView(); + await selectNode("div", inspector); + + await testInitialValues(inspector, boxmodel); + await testChangingValues(inspector, boxmodel); +}); + +function testInitialValues(inspector, boxmodel) { + info("Test that the initial values of the box model are correct"); + const doc = boxmodel.document; + + for (let i = 0; i < res1.length; i++) { + const elt = doc.querySelector(res1[i].selector); + is( + elt.textContent, + res1[i].value, + res1[i].selector + " has the right value." + ); + } +} + +async function testChangingValues(inspector, boxmodel) { + info("Test that changing the document updates the box model"); + const doc = boxmodel.document; + + const onUpdated = waitForUpdate(inspector); + await setContentPageElementAttribute( + "div", + "style", + "height:150px;padding-right:50px;top:50px" + ); + await onUpdated; + + for (let i = 0; i < res2.length; i++) { + const elt = doc.querySelector(res2[i].selector); + is( + elt.textContent, + res2[i].value, + res2[i].selector + " has the right value after style update." + ); + } +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_edit-position-visible-position-change.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_edit-position-visible-position-change.js new file mode 100644 index 0000000000..0cae75aaaf --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_edit-position-visible-position-change.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the 'Edit Position' button is still visible after +// layout is changed. +// see bug 1398722 + +const TEST_URI = ` + <div id="mydiv" style="background:tomato; + position:absolute; + top:10px; + left:10px; + width:100px; + height:100px"> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + + await selectNode("#mydiv", inspector); + let editPositionButton = boxmodel.document.querySelector( + ".layout-geometry-editor" + ); + + ok( + isNodeVisible(editPositionButton), + "Edit Position button is visible initially" + ); + + const positionLeftTextbox = boxmodel.document.querySelector( + ".boxmodel-editable[title=position-left]" + ); + ok(isNodeVisible(positionLeftTextbox), "Position-left edit box exists"); + + info("Change the value of position-left and submit"); + const onUpdate = waitForUpdate(inspector); + EventUtils.synthesizeMouseAtCenter( + positionLeftTextbox, + {}, + boxmodel.document.defaultView + ); + EventUtils.synthesizeKey("8", {}, boxmodel.document.defaultView); + EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView); + + await onUpdate; + editPositionButton = boxmodel.document.querySelector( + ".layout-geometry-editor" + ); + ok( + isNodeVisible(editPositionButton), + "Edit Position button is still visible after layout change" + ); +}); diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel.js new file mode 100644 index 0000000000..262a28cc5f --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel.js @@ -0,0 +1,279 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that editing the box-model values works as expected and test various +// key bindings + +const TEST_URI = + "<style>" + + "div { margin: 10px; padding: 3px }" + + "#div1 { margin-top: 5px }" + + "#div2 { border-bottom: 1em solid black; }" + + "#div3 { padding: 2em; }" + + "#div4 { margin: 1px; }" + + "</style>" + + "<div id='div1'></div><div id='div2'></div>" + + "<div id='div3'></div><div id='div4'></div>"; + +add_task(async function () { + // Make sure the toolbox is tall enough to have empty space below the + // boxmodel-container. + await pushPref("devtools.toolbox.footer.height", 500); + + const tab = await addTab("data:text/html," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + + const browser = tab.linkedBrowser; + await testEditingMargins(inspector, boxmodel, browser); + await testKeyBindings(inspector, boxmodel, browser); + await testEscapeToUndo(inspector, boxmodel, browser); + await testDeletingValue(inspector, boxmodel, browser); + await testRefocusingOnClick(inspector, boxmodel, browser); +}); + +async function testEditingMargins(inspector, boxmodel, browser) { + info( + "Test that editing margin dynamically updates the document, pressing " + + "escape cancels the changes" + ); + + is( + await getStyle(browser, "#div1", "margin-top"), + "", + "Should be no margin-top on the element." + ); + await selectNode("#div1", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-margin.boxmodel-top > span" + ); + await waitForElementTextContent(span, "5"); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "5px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("3", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is( + await getStyle(browser, "#div1", "margin-top"), + "3px", + "Should have updated the margin." + ); + + EventUtils.synthesizeKey("VK_ESCAPE", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is( + await getStyle(browser, "#div1", "margin-top"), + "", + "Should be no margin-top on the element." + ); + + await waitForElementTextContent(span, "5"); +} + +async function testKeyBindings(inspector, boxmodel, browser) { + info( + "Test that arrow keys work correctly and pressing enter commits the " + + "changes" + ); + + is( + await getStyle(browser, "#div1", "margin-left"), + "", + "Should be no margin-top on the element." + ); + await selectNode("#div1", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-margin.boxmodel-left > span" + ); + is(span.textContent, "10", "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "10px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_UP", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "11px", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div1", "margin-left"), + "11px", + "Should have updated the margin." + ); + + EventUtils.synthesizeKey("VK_DOWN", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "10px", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div1", "margin-left"), + "10px", + "Should have updated the margin." + ); + + EventUtils.synthesizeKey( + "VK_UP", + { shiftKey: true }, + boxmodel.document.defaultView + ); + await waitForUpdate(inspector); + + is(editor.value, "20px", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div1", "margin-left"), + "20px", + "Should have updated the margin." + ); + EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView); + + is( + await getStyle(browser, "#div1", "margin-left"), + "20px", + "Should be the right margin-top on the element." + ); + + await waitForElementTextContent(span, "20"); +} + +async function testEscapeToUndo(inspector, boxmodel, browser) { + info( + "Test that deleting the value removes the property but escape undoes " + + "that" + ); + + is( + await getStyle(browser, "#div1", "margin-left"), + "20px", + "Should be the right margin-top on the element." + ); + await selectNode("#div1", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-margin.boxmodel-left > span" + ); + is(span.textContent, "20", "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "20px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_DELETE", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div1", "margin-left"), + "", + "Should have updated the margin." + ); + + EventUtils.synthesizeKey("VK_ESCAPE", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is( + await getStyle(browser, "#div1", "margin-left"), + "20px", + "Should be the right margin-top on the element." + ); + is(span.textContent, "20", "Should have the right value in the box model."); +} + +async function testDeletingValue(inspector, boxmodel, browser) { + info("Test that deleting the value removes the property"); + + await setStyle(browser, "#div1", "marginRight", "15px"); + await waitForUpdate(inspector); + + await selectNode("#div1", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-margin.boxmodel-right > span" + ); + is(span.textContent, "15", "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "15px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_DELETE", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div1", "margin-right"), + "", + "Should have updated the margin." + ); + + EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView); + + is( + await getStyle(browser, "#div1", "margin-right"), + "", + "Should be the right margin-top on the element." + ); + await waitForElementTextContent(span, "10"); +} + +async function testRefocusingOnClick(inspector, boxmodel, browser) { + info("Test that clicking in the editor input does not remove focus"); + + await selectNode("#div4", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-margin.boxmodel-top > span" + ); + is(span.textContent, "1", "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + + info("Click in the already opened editor input"); + EventUtils.synthesizeMouseAtCenter(editor, {}, boxmodel.document.defaultView); + is( + editor, + boxmodel.document.activeElement, + "Inplace editor input should still have focus." + ); + + info("Check the input can still be used as expected"); + EventUtils.synthesizeKey("VK_UP", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "2px", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div4", "margin-top"), + "2px", + "Should have updated the margin." + ); + EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView); + + is( + await getStyle(browser, "#div4", "margin-top"), + "2px", + "Should be the right margin-top on the element." + ); + await waitForElementTextContent(span, "2"); +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_allproperties.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_allproperties.js new file mode 100644 index 0000000000..a465d50e4f --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_allproperties.js @@ -0,0 +1,191 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test editing box model values when all values are set + +const TEST_URI = + "<style>" + + "div { margin: 10px; padding: 3px }" + + "#div1 { margin-top: 5px }" + + "#div2 { border-bottom: 1em solid black; }" + + "#div3 { padding: 2em; }" + + "</style>" + + "<div id='div1'></div><div id='div2'></div><div id='div3'></div>"; + +add_task(async function () { + const tab = await addTab("data:text/html," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + + const browser = tab.linkedBrowser; + await testEditing(inspector, boxmodel, browser); + await testEditingAndCanceling(inspector, boxmodel, browser); + await testDeleting(inspector, boxmodel, browser); + await testDeletingAndCanceling(inspector, boxmodel, browser); +}); + +async function testEditing(inspector, boxmodel, browser) { + info("When all properties are set on the node editing one should work"); + + await setStyle(browser, "#div1", "padding", "5px"); + await waitForUpdate(inspector); + + await selectNode("#div1", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-padding.boxmodel-bottom > span" + ); + await waitForElementTextContent(span, "5"); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "5px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("7", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "7", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div1", "padding-bottom"), + "7px", + "Should have updated the padding" + ); + + EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView); + + is( + await getStyle(browser, "#div1", "padding-bottom"), + "7px", + "Should be the right padding." + ); + await waitForElementTextContent(span, "7"); +} + +async function testEditingAndCanceling(inspector, boxmodel, browser) { + info( + "When all properties are set on the node editing one and then " + + "cancelling with ESCAPE should work" + ); + + await setStyle(browser, "#div1", "padding", "5px"); + await waitForUpdate(inspector); + + await selectNode("#div1", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-padding.boxmodel-left > span" + ); + await waitForElementTextContent(span, "5"); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "5px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("8", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "8", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div1", "padding-left"), + "8px", + "Should have updated the padding" + ); + + EventUtils.synthesizeKey("VK_ESCAPE", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is( + await getStyle(browser, "#div1", "padding-left"), + "5px", + "Should be the right padding." + ); + await waitForElementTextContent(span, "5"); +} + +async function testDeleting(inspector, boxmodel, browser) { + info("When all properties are set on the node deleting one should work"); + + await selectNode("#div1", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-padding.boxmodel-left > span" + ); + await waitForElementTextContent(span, "5"); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "5px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_DELETE", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div1", "padding-left"), + "", + "Should have updated the padding" + ); + + EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView); + + is( + await getStyle(browser, "#div1", "padding-left"), + "", + "Should be the right padding." + ); + await waitForElementTextContent(span, "3"); +} + +async function testDeletingAndCanceling(inspector, boxmodel, browser) { + info( + "When all properties are set on the node deleting one then cancelling " + + "should work" + ); + + await setStyle(browser, "#div1", "padding", "5px"); + await waitForUpdate(inspector); + + await selectNode("#div1", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-padding.boxmodel-left > span" + ); + await waitForElementTextContent(span, "5"); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "5px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_DELETE", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div1", "padding-left"), + "", + "Should have updated the padding" + ); + + EventUtils.synthesizeKey("VK_ESCAPE", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is( + await getStyle(browser, "#div1", "padding-left"), + "5px", + "Should be the right padding." + ); + await waitForElementTextContent(span, "5"); +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_bluronclick.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_bluronclick.js new file mode 100644 index 0000000000..3e40eda951 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_bluronclick.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that inplace editors can be blurred by clicking outside of the editor. + +const TEST_URI = `<style> + #div1 { + margin: 10px; + padding: 3px; + } + </style> + <div id="div1"></div>`; + +add_task(async function () { + // Make sure the toolbox is tall enough to have empty space below the + // boxmodel-container. + await pushPref("devtools.toolbox.footer.height", 500); + + await addTab("data:text/html," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + + await selectNode("#div1", inspector); + await testClickingOutsideEditor(boxmodel); + await testClickingBelowContainer(boxmodel); +}); + +async function testClickingOutsideEditor(boxmodel) { + info("Test that clicking outside the editor blurs it"); + const span = boxmodel.document.querySelector( + ".boxmodel-margin.boxmodel-top > span" + ); + await waitForElementTextContent(span, "10"); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + + info("Click next to the opened editor input."); + const onBlur = once(editor, "blur"); + const rect = editor.getBoundingClientRect(); + EventUtils.synthesizeMouse( + editor, + rect.width + 10, + rect.height / 2, + {}, + boxmodel.document.defaultView + ); + await onBlur; + + is( + boxmodel.document.querySelector(".styleinspector-propertyeditor"), + null, + "Inplace editor has been removed." + ); +} + +async function testClickingBelowContainer(boxmodel) { + info("Test that clicking below the box-model container blurs it"); + const span = boxmodel.document.querySelector( + ".boxmodel-margin.boxmodel-top > span" + ); + await waitForElementTextContent(span, "10"); + + info( + "Test that clicking below the boxmodel-container blurs the opened editor" + ); + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + + const onBlur = once(editor, "blur"); + const container = boxmodel.document.querySelector(".boxmodel-container"); + // Using getBoxQuads here because getBoundingClientRect (and therefore synthesizeMouse) + // use an erroneous height of ~50px for the boxmodel-container. + const bounds = container + .getBoxQuads({ relativeTo: boxmodel.document })[0] + .getBounds(); + EventUtils.synthesizeMouseAtPoint( + bounds.left + 10, + bounds.top + bounds.height + 10, + {}, + boxmodel.document.defaultView + ); + await onBlur; + + is( + boxmodel.document.querySelector(".styleinspector-propertyeditor"), + null, + "Inplace editor has been removed." + ); +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_border.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_border.js new file mode 100644 index 0000000000..98372cabbc --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_border.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that editing the border value in the box model applies the border style + +const TEST_URI = + "<style>" + + "div { margin: 10px; padding: 3px }" + + "#div1 { margin-top: 5px }" + + "#div2 { border-bottom: 1em solid black; }" + + "#div3 { padding: 2em; }" + + "</style>" + + "<div id='div1'></div><div id='div2'></div><div id='div3'></div>"; + +add_task(async function () { + const tab = await addTab("data:text/html," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + + const browser = tab.linkedBrowser; + is( + await getStyle(browser, "#div1", "border-top-width"), + "", + "Should have the right border" + ); + is( + await getStyle(browser, "#div1", "border-top-style"), + "", + "Should have the right border" + ); + await selectNode("#div1", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-border.boxmodel-top > span" + ); + await waitForElementTextContent(span, "0"); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "0", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("1", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "1", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div1", "border-top-width"), + "1px", + "Should have the right border" + ); + is( + await getStyle(browser, "#div1", "border-top-style"), + "solid", + "Should have the right border" + ); + + EventUtils.synthesizeKey("VK_ESCAPE", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is( + await getStyle(browser, "#div1", "border-top-width"), + "", + "Should be the right padding." + ); + is( + await getStyle(browser, "#div1", "border-top-style"), + "", + "Should have the right border" + ); + await waitForElementTextContent(span, "0"); +}); diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_pseudo.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_pseudo.js new file mode 100644 index 0000000000..b2f96fc522 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_pseudo.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that pseudo elements have no side effect on the box model widget for their +// container. See bug 1350499. + +const TEST_URI = `<style> + .test::before { + content: 'before'; + margin-top: 5px; + padding-top: 5px; + width: 5px; + } + </style> + <div style='width:200px;'> + <div class=test></div> + </div>`; + +add_task(async function () { + const tab = await addTab("data:text/html," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + const browser = tab.linkedBrowser; + + await selectNode(".test", inspector); + + // No margin-top defined. + info("Test that margins are not impacted by a pseudo element"); + is( + await getStyle(browser, ".test", "margin-top"), + "", + "margin-top is correct" + ); + await checkValueInBoxModel( + ".boxmodel-margin.boxmodel-top", + "0", + boxmodel.document + ); + + // No padding-top defined. + info("Test that paddings are not impacted by a pseudo element"); + is( + await getStyle(browser, ".test", "padding-top"), + "", + "padding-top is correct" + ); + await checkValueInBoxModel( + ".boxmodel-padding.boxmodel-top", + "0", + boxmodel.document + ); + + // Width should be driven by the parent div. + info("Test that dimensions are not impacted by a pseudo element"); + is(await getStyle(browser, ".test", "width"), "", "width is correct"); + await checkValueInBoxModel( + ".boxmodel-content.boxmodel-width", + "200", + boxmodel.document + ); +}); + +async function checkValueInBoxModel(selector, expectedValue, doc) { + const span = doc.querySelector(selector + " > span"); + await waitForElementTextContent(span, expectedValue); + + EventUtils.synthesizeMouseAtCenter(span, {}, doc.defaultView); + const editor = doc.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, expectedValue, "Should have the right value in the editor."); + + const onBlur = once(editor, "blur"); + EventUtils.synthesizeKey("VK_RETURN", {}, doc.defaultView); + await onBlur; +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_stylerules.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_stylerules.js new file mode 100644 index 0000000000..06b2467d1d --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_stylerules.js @@ -0,0 +1,153 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that units are displayed correctly when editing values in the box model +// and that values are retrieved and parsed correctly from the back-end + +const TEST_URI = + "<style>" + + "div { margin: 10px; padding: 3px }" + + "#div1 { margin-top: 5px }" + + "#div2 { border-bottom: 1em solid black; }" + + "#div3 { padding: 2em; }" + + "</style>" + + "<div id='div1'></div><div id='div2'></div><div id='div3'></div>"; + +add_task(async function () { + const tab = await addTab("data:text/html," + encodeURIComponent(TEST_URI)); + const browser = tab.linkedBrowser; + const { inspector, boxmodel } = await openLayoutView(); + + await testUnits(inspector, boxmodel, browser); + await testValueComesFromStyleRule(inspector, boxmodel, browser); + await testShorthandsAreParsed(inspector, boxmodel, browser); +}); + +async function testUnits(inspector, boxmodel, browser) { + info("Test that entering units works"); + + is( + await getStyle(browser, "#div1", "padding-top"), + "", + "Should have the right padding" + ); + await selectNode("#div1", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-padding.boxmodel-top > span" + ); + await waitForElementTextContent(span, "3"); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "3px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("1", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + EventUtils.synthesizeKey("e", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is( + await getStyle(browser, "#div1", "padding-top"), + "", + "An invalid value is handled cleanly" + ); + + EventUtils.synthesizeKey("m", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "1em", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div1", "padding-top"), + "1em", + "Should have updated the padding." + ); + + EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView); + + is( + await getStyle(browser, "#div1", "padding-top"), + "1em", + "Should be the right padding." + ); + await waitForElementTextContent(span, "16"); +} + +async function testValueComesFromStyleRule(inspector, boxmodel, browser) { + info("Test that we pick up the value from a higher style rule"); + + is( + await getStyle(browser, "#div2", "border-bottom-width"), + "", + "Should have the right border-bottom-width" + ); + await selectNode("#div2", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-border.boxmodel-bottom > span" + ); + await waitForElementTextContent(span, "16"); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "1em", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("0", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + + is(editor.value, "0", "Should have the right value in the editor."); + is( + await getStyle(browser, "#div2", "border-bottom-width"), + "0px", + "Should have updated the border." + ); + + EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView); + + is( + await getStyle(browser, "#div2", "border-bottom-width"), + "0px", + "Should be the right border-bottom-width." + ); + await waitForElementTextContent(span, "0"); +} + +async function testShorthandsAreParsed(inspector, boxmodel, browser) { + info("Test that shorthand properties are parsed correctly"); + + is( + await getStyle(browser, "#div3", "padding-right"), + "", + "Should have the right padding" + ); + await selectNode("#div3", inspector); + + const span = boxmodel.document.querySelector( + ".boxmodel-padding.boxmodel-right > span" + ); + await waitForElementTextContent(span, "32"); + + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + ok(editor, "Should have opened the editor."); + is(editor.value, "2em", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView); + + is( + await getStyle(browser, "#div3", "padding-right"), + "", + "Should be the right padding." + ); + await waitForElementTextContent(span, "32"); +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_guides.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_guides.js new file mode 100644 index 0000000000..341b8f525a --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_guides.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that hovering over regions in the box-model shows the highlighter with +// the right options. +// Tests that actually check the highlighter is displayed and correct are in the +// devtools/inspector/test folder. This test only cares about checking that the +// box model view does call the highlighter, and it does so by mocking it. + +const STYLE = + "div { position: absolute; top: 50px; left: 50px; " + + "height: 10px; width: 10px; border: 10px solid black; " + + "padding: 10px; margin: 10px;}"; +const HTML = "<style>" + STYLE + "</style><div></div>"; +const TEST_URL = "data:text/html;charset=utf-8," + encodeURIComponent(HTML); + +add_task(async function () { + await addTab(TEST_URL); + const { inspector, boxmodel } = await openLayoutView(); + await selectNode("div", inspector); + + let elt = boxmodel.document.querySelector(".boxmodel-margins"); + await testGuideOnLayoutHover(elt, "margin", inspector); + + elt = boxmodel.document.querySelector(".boxmodel-borders"); + await testGuideOnLayoutHover(elt, "border", inspector); + + elt = boxmodel.document.querySelector(".boxmodel-paddings"); + await testGuideOnLayoutHover(elt, "padding", inspector); + + elt = boxmodel.document.querySelector(".boxmodel-content"); + await testGuideOnLayoutHover(elt, "content", inspector); +}); + +async function testGuideOnLayoutHover(elt, expectedRegion, inspector) { + const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); + const onHighlighterShown = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + + info("Synthesizing mouseover on the boxmodel-view"); + EventUtils.synthesizeMouse( + elt, + 50, + 2, + { type: "mouseover" }, + elt.ownerDocument.defaultView + ); + + info("Waiting for the node-highlight event from the toolbox"); + const { nodeFront, options } = await onHighlighterShown; + + // Wait for the next event tick to make sure the remaining part of the + // test is executed after finishing synthesizing mouse event. + await new Promise(executeSoon); + + is( + nodeFront, + inspector.selection.nodeFront, + "The right nodeFront was highlighted" + ); + is( + options.region, + expectedRegion, + "Region " + expectedRegion + " was highlighted" + ); +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_jump-to-rule-on-hover.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_jump-to-rule-on-hover.js new file mode 100644 index 0000000000..74f382259e --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_jump-to-rule-on-hover.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that hovering over a box model value will jump to its source CSS rule in the +// rules view when the shift key is pressed. + +const TEST_URI = `<style> + #box { + margin: 5px; + } + </style> + <div id="box"></div>`; + +add_task(async function () { + await pushPref("devtools.layout.boxmodel.highlightProperty", true); + await addTab("data:text/html," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + await selectNode("#box", inspector); + + info( + "Test that hovering over margin-top value highlights the property in rules view." + ); + const ruleView = await inspector.getPanel("ruleview").view; + const el = boxmodel.document.querySelector( + ".boxmodel-margin.boxmodel-top > span" + ); + + info("Wait for mouse to hover over margin-top element."); + const onHighlightProperty = ruleView.once("scrolled-to-element"); + EventUtils.synthesizeMouseAtCenter( + el, + { type: "mousemove", shiftKey: true }, + boxmodel.document.defaultView + ); + await onHighlightProperty; + + info("Check that margin-top is visible in the rule view."); + const { rules, styleWindow } = ruleView; + const marginTop = rules[1].textProps[0].computed[0]; + ok( + isInViewport(marginTop.element, styleWindow), + "margin-top is visible in the rule view." + ); +}); + +function isInViewport(element, win) { + const { top, left, bottom, right } = element.getBoundingClientRect(); + return ( + top >= 0 && + bottom <= win.innerHeight && + left >= 0 && + right <= win.innerWidth + ); +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_layout-accordion-state.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_layout-accordion-state.js new file mode 100644 index 0000000000..d6802f4e5d --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_layout-accordion-state.js @@ -0,0 +1,103 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the box model's accordion state is persistent through hide/show in the +// layout view. + +const TEST_URI = ` + <style> + #div1 { + margin: 10px; + padding: 3px; + } + </style> + <div id="div1"></div> +`; + +const BOXMODEL_OPENED_PREF = "devtools.layout.boxmodel.opened"; +const ACCORDION_HEADER_SELECTOR = ".accordion-header"; +const ACCORDION_CONTENT_SELECTOR = ".accordion-content"; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel, toolbox } = await openLayoutView(); + const { document: doc } = boxmodel; + + await testAccordionStateAfterClickingHeader(doc); + await testAccordionStateAfterSwitchingSidebars(inspector, doc); + await testAccordionStateAfterReopeningLayoutView(toolbox); + + Services.prefs.clearUserPref(BOXMODEL_OPENED_PREF); +}); + +function testAccordionStateAfterClickingHeader(doc) { + const item = doc.querySelector("#layout-section-boxmodel"); + const header = item.querySelector(ACCORDION_HEADER_SELECTOR); + const content = item.querySelector(ACCORDION_CONTENT_SELECTOR); + + info("Checking initial state of the box model panel."); + ok( + !content.hidden && content.childElementCount > 0, + "The box model panel content is visible." + ); + ok( + Services.prefs.getBoolPref(BOXMODEL_OPENED_PREF), + `${BOXMODEL_OPENED_PREF} is pref on by default.` + ); + + info("Clicking the box model header to hide the box model panel."); + header.click(); + + info("Checking the new state of the box model panel."); + ok(content.hidden, "The box model panel content is hidden."); + ok( + !Services.prefs.getBoolPref(BOXMODEL_OPENED_PREF), + `${BOXMODEL_OPENED_PREF} is pref off.` + ); +} + +function testAccordionStateAfterSwitchingSidebars(inspector, doc) { + info( + "Checking the box model accordion state is persistent after switching sidebars." + ); + + info("Selecting the computed view."); + inspector.sidebar.select("computedview"); + + info("Selecting the layout view."); + inspector.sidebar.select("layoutview"); + + info("Checking the state of the box model panel."); + const item = doc.querySelector("#layout-section-boxmodel"); + const content = item.querySelector(ACCORDION_CONTENT_SELECTOR); + + ok(content.hidden, "The box model panel content is hidden."); + ok( + !Services.prefs.getBoolPref(BOXMODEL_OPENED_PREF), + `${BOXMODEL_OPENED_PREF} is pref off.` + ); +} + +async function testAccordionStateAfterReopeningLayoutView(toolbox) { + info( + "Checking the box model accordion state is persistent after closing and " + + "re-opening the layout view." + ); + + info("Closing the toolbox."); + await toolbox.destroy(); + + info("Re-opening the layout view."); + const { boxmodel } = await openLayoutView(); + const item = boxmodel.document.querySelector("#layout-section-boxmodel"); + const content = item.querySelector(ACCORDION_CONTENT_SELECTOR); + + info("Checking the state of the box model panel."); + ok(content.hidden, "The box model panel content is hidden."); + ok( + !Services.prefs.getBoolPref(BOXMODEL_OPENED_PREF), + `${BOXMODEL_OPENED_PREF} is pref off.` + ); +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_navigation.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_navigation.js new file mode 100644 index 0000000000..7274092a1a --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_navigation.js @@ -0,0 +1,200 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that keyboard and mouse navigation updates aria-active and focus +// of elements. + +const TEST_URI = ` + <style> + div { position: absolute; top: 42px; left: 42px; + height: 100.111px; width: 100px; border: 10px solid black; + padding: 20px; margin: 30px auto;} + </style><div></div> +`; + +add_task(async function () { + await addTab("data:text/html," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + await selectNode("div", inspector); + + await testInitialFocus(inspector, boxmodel); + await testChangingLevels(inspector, boxmodel); + await testTabbingThroughItems(inspector, boxmodel); + await testChangingLevelsByClicking(inspector, boxmodel); +}); + +function testInitialFocus(inspector, boxmodel) { + info("Test that the focus is(on margin layout."); + const doc = boxmodel.document; + const container = doc.querySelector(".boxmodel-container"); + container.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + + is( + container.dataset.activeDescendantClassName, + "boxmodel-main devtools-monospace", + "Should be set to the position layout." + ); +} + +function testChangingLevels(inspector, boxmodel) { + info("Test that using arrow keys updates level."); + const doc = boxmodel.document; + const container = doc.querySelector(".boxmodel-container"); + container.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + EventUtils.synthesizeKey("KEY_Escape"); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + is( + container.dataset.activeDescendantClassName, + "boxmodel-margins", + "Should be set to the margin layout." + ); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + is( + container.dataset.activeDescendantClassName, + "boxmodel-borders", + "Should be set to the border layout." + ); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + is( + container.dataset.activeDescendantClassName, + "boxmodel-paddings", + "Should be set to the padding layout." + ); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + is( + container.dataset.activeDescendantClassName, + "boxmodel-contents", + "Should be set to the content layout." + ); + + EventUtils.synthesizeKey("KEY_ArrowUp"); + is( + container.dataset.activeDescendantClassName, + "boxmodel-paddings", + "Should be set to the padding layout." + ); + + EventUtils.synthesizeKey("KEY_ArrowUp"); + is( + container.dataset.activeDescendantClassName, + "boxmodel-borders", + "Should be set to the border layout." + ); + + EventUtils.synthesizeKey("KEY_ArrowUp"); + is( + container.dataset.activeDescendantClassName, + "boxmodel-margins", + "Should be set to the margin layout." + ); + + EventUtils.synthesizeKey("KEY_ArrowUp"); + is( + container.dataset.activeDescendantClassName, + "boxmodel-main devtools-monospace", + "Should be set to the position layout." + ); +} + +function testTabbingThroughItems(inspector, boxmodel) { + info("Test that using Tab key moves focus to next/previous input field."); + const doc = boxmodel.document; + const container = doc.querySelector(".boxmodel-container"); + container.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + + const editBoxes = [...doc.querySelectorAll("[data-box].boxmodel-editable")]; + + const editBoxesInfo = [ + { name: "position-top", itemId: "position-top-id" }, + { name: "position-right", itemId: "position-right-id" }, + { name: "position-bottom", itemId: "position-bottom-id" }, + { name: "position-left", itemId: "position-left-id" }, + { name: "margin-top", itemId: "margin-top-id" }, + { name: "margin-right", itemId: "margin-right-id" }, + { name: "margin-bottom", itemId: "margin-bottom-id" }, + { name: "margin-left", itemId: "margin-left-id" }, + { name: "border-top-width", itemId: "border-top-width-id" }, + { name: "border-right-width", itemId: "border-right-width-id" }, + { name: "border-bottom-width", itemId: "border-bottom-width-id" }, + { name: "border-left-width", itemId: "border-left-width-id" }, + { name: "padding-top", itemId: "padding-top-id" }, + { name: "padding-right", itemId: "padding-right-id" }, + { name: "padding-bottom", itemId: "padding-bottom-id" }, + { name: "padding-left", itemId: "padding-left-id" }, + { name: "width", itemId: "width-id" }, + { name: "height", itemId: "height-id" }, + ]; + + // Check whether tabbing through box model items works + // Note that the test checks whether wrapping around the box model works + // by letting the loop run beyond the number of indexes to start with + // the first item again. + for (let i = 0; i <= editBoxesInfo.length; i++) { + const itemIndex = i % editBoxesInfo.length; + const editBoxInfo = editBoxesInfo[itemIndex]; + is( + editBoxes[itemIndex].parentElement.id, + editBoxInfo.itemId, + `${editBoxInfo.name} item is current` + ); + is( + editBoxes[itemIndex].previousElementSibling?.localName, + "input", + `Input shown for ${editBoxInfo.name} item` + ); + + // Pressing Tab should not be synthesized for the last item to + // wrap to the very last item again when tabbing in reversed order. + if (i < editBoxesInfo.length) { + EventUtils.synthesizeKey("KEY_Tab"); + } + } + + // Check whether reversed tabbing through box model items works + for (let i = editBoxesInfo.length; i >= 0; i--) { + const itemIndex = i % editBoxesInfo.length; + const editBoxInfo = editBoxesInfo[itemIndex]; + is( + editBoxes[itemIndex].parentElement.id, + editBoxInfo.itemId, + `${editBoxInfo.name} item is current` + ); + is( + editBoxes[itemIndex].previousElementSibling?.localName, + "input", + `Input shown for ${editBoxInfo.name} item` + ); + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + } +} + +function testChangingLevelsByClicking(inspector, boxmodel) { + info("Test that clicking on levels updates level."); + const doc = boxmodel.document; + const container = doc.querySelector(".boxmodel-container"); + container.focus(); + + const marginLayout = doc.querySelector(".boxmodel-margins"); + const borderLayout = doc.querySelector(".boxmodel-borders"); + const paddingLayout = doc.querySelector(".boxmodel-paddings"); + const contentLayout = doc.querySelector(".boxmodel-contents"); + const layouts = [contentLayout, paddingLayout, borderLayout, marginLayout]; + + layouts.forEach(layout => { + layout.click(); + is( + container.dataset.activeDescendantClassName, + layout.className, + `Should be set to ${layout.getAttribute("data-box")} layout.` + ); + }); +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_offsetparent.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_offsetparent.js new file mode 100644 index 0000000000..000d548bdd --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_offsetparent.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the box model displays the right values for the offset parent and that it +// updates when the node's style is changed + +const TEST_URI = ` + <div id="relative_parent" style="position: relative"> + <div id="absolute_child" style="position: absolute"></div> + </div> + <div id="static"></div> + <div id="no_parent" style="position: absolute"></div> + <div id="fixed" style="position: fixed"></div> +`; + +const OFFSET_PARENT_SELECTOR = + ".computed-property-value-container .objectBox-node"; + +const res1 = [ + { + selector: "#absolute_child", + offsetParentValue: "div#relative_parent", + }, + { + selector: "#no_parent", + offsetParentValue: "body", + }, + { + selector: "#relative_parent", + offsetParentValue: "body", + }, + { + selector: "#static", + offsetParentValue: null, + }, + { + selector: "#fixed", + offsetParentValue: null, + }, +]; + +const updates = [ + { + selector: "#absolute_child", + update: "position: static", + }, +]; + +const res2 = [ + { + selector: "#absolute_child", + offsetParentValue: null, + }, +]; + +add_task(async function () { + await addTab("data:text/html," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + + await testInitialValues(inspector, boxmodel); + await testChangingValues(inspector, boxmodel); +}); + +async function testInitialValues(inspector, boxmodel) { + info( + "Test that the initial values of the box model offset parent are correct" + ); + const viewdoc = boxmodel.document; + + for (const { selector, offsetParentValue } of res1) { + await selectNode(selector, inspector); + + const elt = viewdoc.querySelector(OFFSET_PARENT_SELECTOR); + is( + elt && elt.textContent, + offsetParentValue, + selector + " has the right value." + ); + } +} + +async function testChangingValues(inspector, boxmodel) { + info("Test that changing the document updates the box model"); + const viewdoc = boxmodel.document; + + for (const { selector, update } of updates) { + const onUpdated = waitForUpdate(inspector); + await setContentPageElementAttribute(selector, "style", update); + await onUpdated; + } + + for (const { selector, offsetParentValue } of res2) { + await selectNode(selector, inspector); + + const elt = viewdoc.querySelector(OFFSET_PARENT_SELECTOR); + is( + elt && elt.textContent, + offsetParentValue, + selector + " has the right value after style update." + ); + } +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_positions.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_positions.js new file mode 100644 index 0000000000..ca182d7130 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_positions.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the box model displays the right values for positions. + +const TEST_URI = ` + <style type='text/css'> + div { + position: absolute; + left: 0; + margin: 0; + padding: 0; + display: none; + height: 100px; + width: 100px; + border: 10px solid black; + } + </style> + <div>Test Node</div> +`; + +// Expected values: +const res1 = [ + { + selector: ".boxmodel-position.boxmodel-top > span", + value: "auto", + }, + { + selector: ".boxmodel-position.boxmodel-right > span", + value: "auto", + }, + { + selector: ".boxmodel-position.boxmodel-bottom > span", + value: "auto", + }, + { + selector: ".boxmodel-position.boxmodel-left > span", + value: "0", + }, +]; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + const node = await getNodeFront("div", inspector); + const children = await inspector.markup.walker.children(node); + const beforeElement = children.nodes[0]; + + await selectNode(beforeElement, inspector); + await testPositionValues(inspector, boxmodel); +}); + +function testPositionValues(inspector, boxmodel) { + info("Test that the position values of the box model are correct"); + const doc = boxmodel.document; + + for (let i = 0; i < res1.length; i++) { + const elt = doc.querySelector(res1[i].selector); + is( + elt.textContent, + res1[i].value, + res1[i].selector + " has the right value." + ); + } +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_properties.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_properties.js new file mode 100644 index 0000000000..6ebf5ab761 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_properties.js @@ -0,0 +1,129 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the box model properties list displays the right values +// and that it updates when the node's style is changed. + +const TEST_URI = ` + <style type='text/css'> + div { + box-sizing: border-box; + display: block; + float: left; + line-height: 20px; + position: relative; + z-index: 2; + height: 100px; + width: 100px; + border: 10px solid black; + padding: 20px; + margin: 30px auto; + } + </style> + <div>Test Node</div> +`; + +const res1 = [ + { + property: "box-sizing", + value: "border-box", + }, + { + property: "display", + value: "block", + }, + { + property: "float", + value: "left", + }, + { + property: "line-height", + value: "20px", + }, + { + property: "position", + value: "relative", + }, + { + property: "z-index", + value: "2", + }, +]; + +const res2 = [ + { + property: "box-sizing", + value: "content-box", + }, + { + property: "display", + value: "block", + }, + { + property: "float", + value: "right", + }, + { + property: "line-height", + value: "10px", + }, + { + property: "position", + value: "static", + }, + { + property: "z-index", + value: "5", + }, +]; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + await selectNode("div", inspector); + + await testInitialValues(inspector, boxmodel); + await testChangingValues(inspector, boxmodel); +}); + +function testInitialValues(inspector, boxmodel) { + info("Test that the initial values of the box model are correct"); + const doc = boxmodel.document; + + for (const { property, value } of res1) { + const elt = doc.querySelector(getPropertySelector(property)); + is(elt.textContent, value, property + " has the right value."); + } +} + +async function testChangingValues(inspector, boxmodel) { + info("Test that changing the document updates the box model"); + const doc = boxmodel.document; + + const onUpdated = waitForUpdate(inspector); + await setContentPageElementAttribute( + "div", + "style", + "box-sizing:content-box;float:right;" + + "line-height:10px;position:static;z-index:5;" + ); + await onUpdated; + + for (const { property, value } of res2) { + const elt = doc.querySelector(getPropertySelector(property)); + is( + elt.textContent, + value, + property + " has the right value after style update." + ); + } +} + +function getPropertySelector(propertyName) { + return ( + `.boxmodel-container .computed-property-view` + + `[data-property-name=${propertyName}] .computed-property-value` + ); +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_pseudo-element.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_pseudo-element.js new file mode 100644 index 0000000000..046ee067ba --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_pseudo-element.js @@ -0,0 +1,122 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the box model displays the right values for a pseudo-element. + +const TEST_URI = ` + <style type='text/css'> + div { + box-sizing: border-box; + display: block; + float: left; + line-height: 20px; + position: relative; + z-index: 2; + height: 100px; + width: 100px; + border: 10px solid black; + padding: 20px; + margin: 30px auto; + } + + div::before { + content: 'before'; + display: block; + width: 32px; + height: 32px; + margin: 0 auto 6px; + } + </style> + <div>Test Node</div> +`; + +// Expected values: +const res1 = [ + { + selector: ".boxmodel-element-size", + value: "32" + "\u00D7" + "32", + }, + { + selector: ".boxmodel-size > .boxmodel-width", + value: "32", + }, + { + selector: ".boxmodel-size > .boxmodel-height", + value: "32", + }, + { + selector: ".boxmodel-margin.boxmodel-top > span", + value: "0", + }, + { + selector: ".boxmodel-margin.boxmodel-left > span", + value: "4", // (100 - (10 * 2) - (20 * 2) - 32) / 2 + }, + { + selector: ".boxmodel-margin.boxmodel-bottom > span", + value: "6", + }, + { + selector: ".boxmodel-margin.boxmodel-right > span", + value: "4", // (100 - (10 * 2) - (20 * 2) - 32) / 2 + }, + { + selector: ".boxmodel-padding.boxmodel-top > span", + value: "0", + }, + { + selector: ".boxmodel-padding.boxmodel-left > span", + value: "0", + }, + { + selector: ".boxmodel-padding.boxmodel-bottom > span", + value: "0", + }, + { + selector: ".boxmodel-padding.boxmodel-right > span", + value: "0", + }, + { + selector: ".boxmodel-border.boxmodel-top > span", + value: "0", + }, + { + selector: ".boxmodel-border.boxmodel-left > span", + value: "0", + }, + { + selector: ".boxmodel-border.boxmodel-bottom > span", + value: "0", + }, + { + selector: ".boxmodel-border.boxmodel-right > span", + value: "0", + }, +]; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + const node = await getNodeFront("div", inspector); + const children = await inspector.markup.walker.children(node); + const beforeElement = children.nodes[0]; + + await selectNode(beforeElement, inspector); + await testInitialValues(inspector, boxmodel); +}); + +function testInitialValues(inspector, boxmodel) { + info("Test that the initial values of the box model are correct"); + const doc = boxmodel.document; + + for (let i = 0; i < res1.length; i++) { + const elt = doc.querySelector(res1[i].selector); + is( + elt.textContent, + res1[i].value, + res1[i].selector + " has the right value." + ); + } +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_rotate-labels-on-sides.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_rotate-labels-on-sides.js new file mode 100644 index 0000000000..f84ca9d8b0 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_rotate-labels-on-sides.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that longer values are rotated on the side + +const res1 = [ + { selector: ".boxmodel-margin.boxmodel-top > span", value: 30 }, + { selector: ".boxmodel-margin.boxmodel-left > span", value: "auto" }, + { selector: ".boxmodel-margin.boxmodel-bottom > span", value: 30 }, + { selector: ".boxmodel-margin.boxmodel-right > span", value: "auto" }, + { selector: ".boxmodel-padding.boxmodel-top > span", value: 20 }, + { selector: ".boxmodel-padding.boxmodel-left > span", value: 2000000 }, + { selector: ".boxmodel-padding.boxmodel-bottom > span", value: 20 }, + { selector: ".boxmodel-padding.boxmodel-right > span", value: 20 }, + { selector: ".boxmodel-border.boxmodel-top > span", value: 10 }, + { selector: ".boxmodel-border.boxmodel-left > span", value: 10 }, + { selector: ".boxmodel-border.boxmodel-bottom > span", value: 10 }, + { selector: ".boxmodel-border.boxmodel-right > span", value: 10 }, +]; + +const TEST_URI = encodeURIComponent( + [ + "<style>", + "div { border:10px solid black; padding: 20px 20px 20px 2000000px; " + + "margin: 30px auto; }", + "</style>", + "<div></div>", + ].join("") +); +const LONG_TEXT_ROTATE_LIMIT = 3; + +add_task(async function () { + await addTab("data:text/html," + TEST_URI); + const { inspector, boxmodel } = await openLayoutView(); + await selectNode("div", inspector); + + for (let i = 0; i < res1.length; i++) { + const elt = boxmodel.document.querySelector(res1[i].selector); + const isLong = elt.textContent.length > LONG_TEXT_ROTATE_LIMIT; + const classList = elt.parentNode.classList; + const canBeRotated = + classList.contains("boxmodel-left") || + classList.contains("boxmodel-right"); + const isRotated = classList.contains("boxmodel-rotate"); + + is( + canBeRotated && isLong, + isRotated, + res1[i].selector + " correctly rotated." + ); + } +}); diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_show-tooltip-for-unassociated-rule.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_show-tooltip-for-unassociated-rule.js new file mode 100644 index 0000000000..ffb911b342 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_show-tooltip-for-unassociated-rule.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const L10N = new LocalizationHelper( + "devtools/client/locales/inspector.properties" +); + +// Test that hovering over a box model value with no associated rule will show a tooltip +// saying: "No associated rule". + +const TEST_URI = `<style> + #box {} + </style> + <div id="box"></div>`; + +add_task(async function () { + await pushPref("devtools.layout.boxmodel.highlightProperty", true); + await addTab("data:text/html," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + const { rulePreviewTooltip } = boxmodel; + await selectNode("#box", inspector); + + info( + "Test that hovering over margin-top shows tooltip showing 'No associated rule'." + ); + const el = boxmodel.document.querySelector( + ".boxmodel-margin.boxmodel-top > span" + ); + + info("Wait for mouse to hover over margin-top element."); + const onRulePreviewTooltipShown = rulePreviewTooltip._tooltip.once( + "shown", + () => { + ok(true, "Tooltip shown."); + is( + rulePreviewTooltip.message.textContent, + L10N.getStr("rulePreviewTooltip.noAssociatedRule"), + `Tooltip shows ${L10N.getStr("rulePreviewTooltip.noAssociatedRule")}` + ); + } + ); + EventUtils.synthesizeMouseAtCenter( + el, + { type: "mousemove", shiftKey: true }, + boxmodel.document.defaultView + ); + await onRulePreviewTooltipShown; +}); diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_sync.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_sync.js new file mode 100644 index 0000000000..fed1e85519 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_sync.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test editing box model syncs with the rule view. + +const TEST_URI = "<p>hello</p>"; + +add_task(async function () { + await addTab("data:text/html," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + + info("When a property is edited, it should sync in the rule view"); + + await selectNode("p", inspector); + + info("Modify padding-bottom in box model view"); + const span = boxmodel.document.querySelector( + ".boxmodel-padding.boxmodel-bottom > span" + ); + EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView); + const editor = boxmodel.document.querySelector( + ".styleinspector-propertyeditor" + ); + + const onRuleViewRefreshed = once(inspector, "rule-view-refreshed"); + EventUtils.synthesizeKey("7", {}, boxmodel.document.defaultView); + await waitForUpdate(inspector); + await onRuleViewRefreshed; + is(editor.value, "7", "Should have the right value in the editor."); + EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView); + + info("Check that the property was synced with the rule view"); + const ruleView = selectRuleView(inspector); + const ruleEditor = getRuleViewRuleEditor(ruleView, 0); + const textProp = ruleEditor.rule.textProps[0]; + is(textProp.value, "7px", "The property has the right value"); +}); diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_tooltips.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_tooltips.js new file mode 100644 index 0000000000..de1ab24936 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_tooltips.js @@ -0,0 +1,166 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the regions in the box model view have tooltips, and that individual +// values too. Also test that values that are set from a css rule have tooltips +// referencing the rule. + +const TEST_URI = + "<style>" + + "#div1 { color: red; margin: 3em; }\n" + + "#div2 { border-bottom: 1px solid black; background: red; }\n" + + "html, body, #div3 { box-sizing: border-box; padding: 0 2em; }" + + "</style>" + + "<div id='div1'></div><div id='div2'></div><div id='div3'></div>"; + +// Test data for the tooltips over individual values. +// Each entry should contain: +// - selector: The selector for the node to be selected before starting to test +// - values: An array containing objects for each of the values that are defined +// by css rules. Each entry should contain: +// - name: the name of the property that is set by the css rule +// - ruleSelector: the selector of the rule +// - styleSheetLocation: the fileName:lineNumber +const VALUES_TEST_DATA = [ + { + selector: "#div1", + values: [ + { + name: "margin-top", + ruleSelector: "#div1", + styleSheetLocation: "inline:1", + }, + { + name: "margin-right", + ruleSelector: "#div1", + styleSheetLocation: "inline:1", + }, + { + name: "margin-bottom", + ruleSelector: "#div1", + styleSheetLocation: "inline:1", + }, + { + name: "margin-left", + ruleSelector: "#div1", + styleSheetLocation: "inline:1", + }, + ], + }, + { + selector: "#div2", + values: [ + { + name: "border-bottom-width", + ruleSelector: "#div2", + styleSheetLocation: "inline:2", + }, + ], + }, + { + selector: "#div3", + values: [ + { + name: "padding-top", + ruleSelector: "html, body, #div3", + styleSheetLocation: "inline:3", + }, + { + name: "padding-right", + ruleSelector: "html, body, #div3", + styleSheetLocation: "inline:3", + }, + { + name: "padding-bottom", + ruleSelector: "html, body, #div3", + styleSheetLocation: "inline:3", + }, + { + name: "padding-left", + ruleSelector: "html, body, #div3", + styleSheetLocation: "inline:3", + }, + ], + }, +]; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, boxmodel } = await openLayoutView(); + + info("Checking the regions tooltips"); + + ok( + boxmodel.document.querySelector(".boxmodel-margins").hasAttribute("title"), + "The margin region has a tooltip" + ); + is( + boxmodel.document.querySelector(".boxmodel-margins").getAttribute("title"), + "margin", + "The margin region has the correct tooltip content" + ); + + ok( + boxmodel.document.querySelector(".boxmodel-borders").hasAttribute("title"), + "The border region has a tooltip" + ); + is( + boxmodel.document.querySelector(".boxmodel-borders").getAttribute("title"), + "border", + "The border region has the correct tooltip content" + ); + + ok( + boxmodel.document.querySelector(".boxmodel-paddings").hasAttribute("title"), + "The padding region has a tooltip" + ); + is( + boxmodel.document.querySelector(".boxmodel-paddings").getAttribute("title"), + "padding", + "The padding region has the correct tooltip content" + ); + + ok( + boxmodel.document.querySelector(".boxmodel-content").hasAttribute("title"), + "The content region has a tooltip" + ); + is( + boxmodel.document.querySelector(".boxmodel-content").getAttribute("title"), + "content", + "The content region has the correct tooltip content" + ); + + for (const { selector, values } of VALUES_TEST_DATA) { + info("Selecting " + selector + " and checking the values tooltips"); + await selectNode(selector, inspector); + + info("Iterate over all values"); + for (const key in boxmodel.map) { + if (key === "position") { + continue; + } + + const name = boxmodel.map[key].property; + const expectedTooltipData = values.find(o => o.name === name); + const el = boxmodel.document.querySelector(boxmodel.map[key].selector); + + ok(el.hasAttribute("title"), "The " + name + " value has a tooltip"); + + if (expectedTooltipData) { + info("The " + name + " value comes from a css rule"); + const expectedTooltip = + name + + "\n" + + expectedTooltipData.ruleSelector + + "\n" + + expectedTooltipData.styleSheetLocation; + is(el.getAttribute("title"), expectedTooltip, "The tooltip is correct"); + } else { + info("The " + name + " isn't set by a css rule"); + is(el.getAttribute("title"), name, "The tooltip is correct"); + } + } + } +}); diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-after-navigation.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-after-navigation.js new file mode 100644 index 0000000000..4cd83590b0 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-after-navigation.js @@ -0,0 +1,96 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the box model view continues to work after a page navigation and that +// it also works after going back + +const IFRAME1 = URL_ROOT_SSL + "doc_boxmodel_iframe1.html"; +const IFRAME2 = URL_ROOT_SSL + "doc_boxmodel_iframe2.html"; + +add_task(async function () { + const tab = await addTab(IFRAME1); + const browser = tab.linkedBrowser; + const { inspector, boxmodel } = await openLayoutView(); + + await testFirstPage(inspector, boxmodel, browser); + + info("Navigate to the second page"); + let onMarkupLoaded = waitForMarkupLoaded(inspector); + await navigateTo(IFRAME2); + await onMarkupLoaded; + + await testSecondPage(inspector, boxmodel, browser); + + info("Go back to the first page"); + onMarkupLoaded = waitForMarkupLoaded(inspector); + gBrowser.goBack(); + await onMarkupLoaded; + + await testBackToFirstPage(inspector, boxmodel, browser); +}); + +async function testFirstPage(inspector, boxmodel, browser) { + info("Test that the box model view works on the first page"); + + await selectNode("p", inspector); + + info("Checking that the box model view shows the right value"); + const paddingElt = boxmodel.document.querySelector( + ".boxmodel-padding.boxmodel-top > span" + ); + is(paddingElt.textContent, "50"); + + info("Listening for box model view changes and modifying the padding"); + const onUpdated = waitForUpdate(inspector); + await setStyle(browser, "p", "padding", "20px"); + await onUpdated; + ok(true, "Box model view got updated"); + + info("Checking that the box model view shows the right value after update"); + is(paddingElt.textContent, "20"); +} + +async function testSecondPage(inspector, boxmodel, browser) { + info("Test that the box model view works on the second page"); + + await selectNode("p", inspector); + + info("Checking that the box model view shows the right value"); + const sizeElt = boxmodel.document.querySelector(".boxmodel-size > span"); + is(sizeElt.textContent, "100" + "\u00D7" + "100"); + + info("Listening for box model view changes and modifying the size"); + const onUpdated = waitForUpdate(inspector); + await setStyle(browser, "p", "width", "200px"); + await onUpdated; + ok(true, "Box model view got updated"); + + info("Checking that the box model view shows the right value after update"); + is(sizeElt.textContent, "200" + "\u00D7" + "100"); +} + +async function testBackToFirstPage(inspector, boxmodel, browser) { + info("Test that the box model view works on the first page after going back"); + + await selectNode("p", inspector); + + info( + "Checking that the box model view shows the right value, which is the" + + "modified value from step one because of the bfcache" + ); + const paddingElt = boxmodel.document.querySelector( + ".boxmodel-padding.boxmodel-top > span" + ); + is(paddingElt.textContent, "20"); + + info("Listening for box model view changes and modifying the padding"); + const onUpdated = waitForUpdate(inspector); + await setStyle(browser, "p", "padding", "100px"); + await onUpdated; + ok(true, "Box model view got updated"); + + info("Checking that the box model view shows the right value after update"); + is(paddingElt.textContent, "100"); +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-after-reload.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-after-reload.js new file mode 100644 index 0000000000..f6d309c8e2 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-after-reload.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the box model view continues to work after the page is reloaded + +add_task(async function () { + const tab = await addTab(URL_ROOT + "doc_boxmodel_iframe1.html"); + const browser = tab.linkedBrowser; + const { inspector, boxmodel } = await openLayoutView(); + + info("Test that the box model view works on the first page"); + await assertBoxModelView(inspector, boxmodel, browser); + + info("Reload the page"); + const onMarkupLoaded = waitForMarkupLoaded(inspector); + await reloadBrowser(); + await onMarkupLoaded; + + info("Test that the box model view works on the reloaded page"); + await assertBoxModelView(inspector, boxmodel, browser); +}); + +async function assertBoxModelView(inspector, boxmodel, browser) { + await selectNode("p", inspector); + + info("Checking that the box model view shows the right value"); + const paddingElt = boxmodel.document.querySelector( + ".boxmodel-padding.boxmodel-top > span" + ); + is(paddingElt.textContent, "50"); + + info("Listening for box model view changes and modifying the padding"); + const onUpdated = waitForUpdate(inspector); + await setStyle(browser, "p", "padding", "20px"); + await onUpdated; + ok(true, "Box model view got updated"); + + info("Checking that the box model view shows the right value after update"); + is(paddingElt.textContent, "20"); +} diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-in-iframes.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-in-iframes.js new file mode 100644 index 0000000000..eee72f696c --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-in-iframes.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the box model view for elements within iframes also updates when they +// change + +add_task(async function () { + const tab = await addTab(URL_ROOT + "doc_boxmodel_iframe1.html"); + const browser = tab.linkedBrowser; + const { inspector, boxmodel } = await openLayoutView(); + + await testResizingInIframe(inspector, boxmodel, browser); + await testReflowsAfterIframeDeletion(inspector, boxmodel, browser); +}); + +async function testResizingInIframe(inspector, boxmodel, browser) { + info("Test that resizing an element in an iframe updates its box model"); + + info("Selecting the nested test node"); + await selectNodeInFrames(["iframe", "iframe", "div"], inspector); + + info("Checking that the box model view shows the right value"); + const sizeElt = boxmodel.document.querySelector(".boxmodel-size > span"); + is(sizeElt.textContent, "400\u00D7200"); + + info("Listening for box model view changes and modifying its size"); + const onUpdated = waitForUpdate(inspector); + await setStyleInNestedIframe(browser, "div", "width", "200px"); + await onUpdated; + ok(true, "Box model view got updated"); + + info("Checking that the box model view shows the right value after update"); + is(sizeElt.textContent, "200\u00D7200"); +} + +async function testReflowsAfterIframeDeletion(inspector, boxmodel, browser) { + info( + "Test reflows are still sent to the box model view after deleting an " + + "iframe" + ); + + info("Deleting the iframe2"); + const onInspectorUpdated = inspector.once("inspector-updated"); + await removeNestedIframe(browser); + await onInspectorUpdated; + + info("Selecting the test node in iframe1"); + await selectNodeInFrames(["iframe", "p"], inspector); + + info("Checking that the box model view shows the right value"); + const sizeElt = boxmodel.document.querySelector(".boxmodel-size > span"); + is(sizeElt.textContent, "100\u00D7100"); + + info("Listening for box model view changes and modifying its size"); + const onUpdated = waitForUpdate(inspector); + await setStyleInIframe(browser, "p", "width", "200px"); + await onUpdated; + ok(true, "Box model view got updated"); + + info("Checking that the box model view shows the right value after update"); + is(sizeElt.textContent, "200\u00D7100"); +} + +async function setStyleInIframe(browser, selector, propertyName, value) { + const context = await getBrowsingContextInFrames(browser, ["iframe"]); + return setStyle(context, selector, propertyName, value); +} + +async function setStyleInNestedIframe(browser, selector, propertyName, value) { + const context = await getBrowsingContextInFrames(browser, [ + "iframe", + "iframe", + ]); + return setStyle(context, selector, propertyName, value); +} + +async function removeNestedIframe(browser) { + const context = await getBrowsingContextInFrames(browser, ["iframe"]); + await SpecialPowers.spawn(context, [], () => + content.document.querySelector("iframe").remove() + ); +} diff --git a/devtools/client/inspector/boxmodel/test/doc_boxmodel_iframe1.html b/devtools/client/inspector/boxmodel/test/doc_boxmodel_iframe1.html new file mode 100644 index 0000000000..eef48ce079 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/doc_boxmodel_iframe1.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<p style="padding:50px;color:#f06;">Root page</p> +<iframe src="doc_boxmodel_iframe2.html"></iframe> diff --git a/devtools/client/inspector/boxmodel/test/doc_boxmodel_iframe2.html b/devtools/client/inspector/boxmodel/test/doc_boxmodel_iframe2.html new file mode 100644 index 0000000000..0fa6dc02e9 --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/doc_boxmodel_iframe2.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<p style="width:100px;height:100px;background:red;box-sizing:border-box">iframe 1</p> +<iframe src="data:text/html,<div style='width:400px;height:200px;background:yellow;box-sizing:border-box'>iframe 2</div>"></iframe> diff --git a/devtools/client/inspector/boxmodel/test/head.js b/devtools/client/inspector/boxmodel/test/head.js new file mode 100644 index 0000000000..f15424acfb --- /dev/null +++ b/devtools/client/inspector/boxmodel/test/head.js @@ -0,0 +1,122 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ + +"use strict"; + +// Import the inspector's head.js first (which itself imports shared-head.js). +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js", + this +); + +Services.prefs.setIntPref("devtools.toolbox.footer.height", 350); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.toolbox.footer.height"); +}); + +/** + * Is the given node visible in the page (rendered in the frame tree). + * @param {DOMNode} + * @return {Boolean} + */ +function isNodeVisible(node) { + return !!node.getClientRects().length; +} + +/** + * Wait for the boxmodel-view-updated event. + * + * @param {InspectorPanel} inspector + * The instance of InspectorPanel currently loaded in the toolbox. + * @param {Boolean} waitForSelectionUpdate + * Should the boxmodel-view-updated event come from a new selection. + * @return {Promise} a promise + */ +async function waitForUpdate(inspector, waitForSelectionUpdate) { + /** + * While the highlighter is visible (mouse over the fields of the box model editor), + * reflow events are prevented; see ReflowActor -> setIgnoreLayoutChanges() + * The box model view updates in response to reflow events. + * To ensure reflow events are fired, hide the highlighter. + */ + await inspector.highlighters.hideHighlighterType( + inspector.highlighters.TYPES.BOXMODEL + ); + + return new Promise(resolve => { + inspector.on("boxmodel-view-updated", function onUpdate(reasons) { + // Wait for another update event if we are waiting for a selection related event. + if (waitForSelectionUpdate && !reasons.includes("new-selection")) { + return; + } + + inspector.off("boxmodel-view-updated", onUpdate); + resolve(); + }); + }); +} + +/** + * Wait for both boxmode-view-updated and markuploaded events. + * + * @return {Promise} a promise that resolves when both events have been received. + */ +function waitForMarkupLoaded(inspector) { + return Promise.all([ + waitForUpdate(inspector), + inspector.once("markuploaded"), + ]); +} + +function getStyle(browser, selector, propertyName) { + return SpecialPowers.spawn( + browser, + [selector, propertyName], + async function (_selector, _propertyName) { + return content.document + .querySelector(_selector) + .style.getPropertyValue(_propertyName); + } + ); +} + +function setStyle(browser, selector, propertyName, value) { + return SpecialPowers.spawn( + browser, + [selector, propertyName, value], + async function (_selector, _propertyName, _value) { + content.document.querySelector(_selector).style[_propertyName] = _value; + } + ); +} + +/** + * The box model doesn't participate in the inspector's update mechanism, so simply + * calling the default selectNode isn't enough to guarantee that the box model view has + * finished updating. We also need to wait for the "boxmodel-view-updated" event. + */ +var _selectNode = selectNode; +selectNode = async function (node, inspector, reason) { + const onUpdate = waitForUpdate(inspector, true); + await _selectNode(node, inspector, reason); + await onUpdate; +}; + +/** + * Wait until the provided element's text content matches the provided text. + * Based on the waitFor helper, see documentation in + * devtools/client/shared/test/shared-head.js + * + * @param {DOMNode} element + * The element to check. + * @param {String} expectedText + * The text that is expected to be set as textContent of the element. + */ +async function waitForElementTextContent(element, expectedText) { + await waitFor( + () => element.textContent === expectedText, + `Couldn't get "${expectedText}" as the text content of the given element` + ); + ok(true, `Found the expected text (${expectedText}) for the given element`); +} diff --git a/devtools/client/inspector/boxmodel/types.js b/devtools/client/inspector/boxmodel/types.js new file mode 100644 index 0000000000..b477974b65 --- /dev/null +++ b/devtools/client/inspector/boxmodel/types.js @@ -0,0 +1,21 @@ +/* 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 PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +/** + * The box model data for the current selected node. + */ +exports.boxModel = { + // Whether or not the geometry editor is enabled + geometryEditorEnabled: PropTypes.bool, + + // The layout information of the current selected node + layout: PropTypes.object, + + // The offset parent for the selected node + offsetParent: PropTypes.object, +}; diff --git a/devtools/client/inspector/boxmodel/utils/editing-session.js b/devtools/client/inspector/boxmodel/utils/editing-session.js new file mode 100644 index 0000000000..3175375f22 --- /dev/null +++ b/devtools/client/inspector/boxmodel/utils/editing-session.js @@ -0,0 +1,188 @@ +/* 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"; + +/** + * An instance of EditingSession tracks changes that have been made during the + * modification of box model values. All of these changes can be reverted by + * calling revert. + * + * @param {InspectorPanel} inspector + * The inspector panel. + * @param {Document} doc + * A DOM document that can be used to test style rules. + * @param {Array} rules + * An array of the style rules defined for the node being + * edited. These should be in order of priority, least + * important first. + */ +function EditingSession({ inspector, doc, elementRules }) { + this._doc = doc; + this._inspector = inspector; + this._rules = elementRules; + this._modifications = new Map(); +} + +EditingSession.prototype = { + /** + * Gets the value of a single property from the CSS rule. + * + * @param {StyleRuleFront} rule + * The CSS rule. + * @param {String} property + * The name of the property. + * @return {String} the value. + */ + getPropertyFromRule(rule, property) { + // Use the parsed declarations in the StyleRuleFront object if available. + const index = this.getPropertyIndex(property, rule); + if (index !== -1) { + return rule.declarations[index].value; + } + + // Fallback to parsing the cssText locally otherwise. + const dummyStyle = this._element.style; + dummyStyle.cssText = rule.cssText; + return dummyStyle.getPropertyValue(property); + }, + + /** + * Returns the current value for a property as a string or the empty string if + * no style rules affect the property. + * + * @param {String} property + * The name of the property as a string + */ + getProperty(property) { + // Create a hidden element for getPropertyFromRule to use + const div = this._doc.createElement("div"); + div.setAttribute("style", "display: none"); + this._doc.getElementById("inspector-main-content").appendChild(div); + this._element = this._doc.createElement("p"); + div.appendChild(this._element); + + // As the rules are in order of priority we can just iterate until we find + // the first that defines a value for the property and return that. + for (const rule of this._rules) { + const value = this.getPropertyFromRule(rule, property); + if (value !== "") { + div.remove(); + return value; + } + } + div.remove(); + return ""; + }, + + /** + * Get the index of a given css property name in a CSS rule. + * Or -1, if there are no properties in the rule yet. + * + * @param {String} name + * The property name. + * @param {StyleRuleFront} rule + * Optional, defaults to the element style rule. + * @return {Number} The property index in the rule. + */ + getPropertyIndex(name, rule = this._rules[0]) { + if (!rule.declarations.length) { + return -1; + } + + return rule.declarations.findIndex(p => p.name === name); + }, + + /** + * Sets a number of properties on the node. + * + * @param {Array} properties + * An array of properties, each is an object with name and + * value properties. If the value is "" then the property + * is removed. + * @return {Promise} Resolves when the modifications are complete. + */ + async setProperties(properties) { + for (const property of properties) { + // Get a RuleModificationList or RuleRewriter helper object from the + // StyleRuleActor to make changes to CSS properties. + // Note that RuleRewriter doesn't support modifying several properties at + // once, so we do this in a sequence here. + const modifications = this._rules[0].startModifyingProperties( + this._inspector.cssProperties + ); + + // Remember the property so it can be reverted. + if (!this._modifications.has(property.name)) { + this._modifications.set( + property.name, + this.getPropertyFromRule(this._rules[0], property.name) + ); + } + + // Find the index of the property to be changed, or get the next index to + // insert the new property at. + let index = this.getPropertyIndex(property.name); + if (index === -1) { + index = this._rules[0].declarations.length; + } + + if (property.value == "") { + modifications.removeProperty(index, property.name); + } else { + modifications.setProperty(index, property.name, property.value, ""); + } + + await modifications.apply(); + } + }, + + /** + * Reverts all of the property changes made by this instance. + * + * @return {Promise} Resolves when all properties have been reverted. + */ + async revert() { + // Revert each property that we modified previously, one by one. See + // setProperties for information about why. + for (const [property, value] of this._modifications) { + const modifications = this._rules[0].startModifyingProperties( + this._inspector.cssProperties + ); + + // Find the index of the property to be reverted. + let index = this.getPropertyIndex(property); + + if (value != "") { + // If the property doesn't exist anymore, insert at the beginning of the + // rule. + if (index === -1) { + index = 0; + } + modifications.setProperty(index, property, value, ""); + } else { + // If the property doesn't exist anymore, no need to remove it. It had + // not been added after all. + if (index === -1) { + continue; + } + modifications.removeProperty(index, property); + } + + await modifications.apply(); + } + }, + + destroy() { + this._modifications.clear(); + + this._cssProperties = null; + this._doc = null; + this._inspector = null; + this._modifications = null; + this._rules = null; + }, +}; + +module.exports = EditingSession; diff --git a/devtools/client/inspector/boxmodel/utils/moz.build b/devtools/client/inspector/boxmodel/utils/moz.build new file mode 100644 index 0000000000..76a5656294 --- /dev/null +++ b/devtools/client/inspector/boxmodel/utils/moz.build @@ -0,0 +1,9 @@ +# -*- 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( + "editing-session.js", +) |