From f1d7bb9abaf2b512f368f3d3a4c849fdbe049240 Mon Sep 17 00:00:00 2001 From: Gabriel Luong Date: Sat, 28 Jan 2017 12:50:26 -0500 Subject: [PATCH] Bug 1333561 - Part 3: Implements the box model panel in the layout view. r=jdescottes --- devtools/client/inspector/inspector.xhtml | 1 + .../inspector/layout/actions/box-model.js | 23 ++ .../client/inspector/layout/actions/index.js | 3 + .../client/inspector/layout/actions/moz.build | 1 + .../client/inspector/layout/components/App.js | 20 +- .../inspector/layout/components/BoxModel.js | 52 ++++ .../layout/components/BoxModelEditable.js | 65 +++++ .../layout/components/BoxModelInfo.js | 62 +++++ .../layout/components/BoxModelMain.js | 261 ++++++++++++++++++ .../inspector/layout/components/moz.build | 4 + devtools/client/inspector/layout/layout.js | 217 ++++++++++++++- .../inspector/layout/reducers/box-model.js | 31 +++ .../client/inspector/layout/reducers/index.js | 1 + .../inspector/layout/reducers/moz.build | 1 + devtools/client/inspector/layout/types.js | 10 + devtools/client/themes/boxmodel.css | 69 +++-- 16 files changed, 774 insertions(+), 47 deletions(-) create mode 100644 devtools/client/inspector/layout/actions/box-model.js create mode 100644 devtools/client/inspector/layout/components/BoxModel.js create mode 100644 devtools/client/inspector/layout/components/BoxModelEditable.js create mode 100644 devtools/client/inspector/layout/components/BoxModelInfo.js create mode 100644 devtools/client/inspector/layout/components/BoxModelMain.js create mode 100644 devtools/client/inspector/layout/reducers/box-model.js diff --git a/devtools/client/inspector/inspector.xhtml b/devtools/client/inspector/inspector.xhtml index 37c21930adda..d3e13fb7b781 100644 --- a/devtools/client/inspector/inspector.xhtml +++ b/devtools/client/inspector/inspector.xhtml @@ -13,6 +13,7 @@ + diff --git a/devtools/client/inspector/layout/actions/box-model.js b/devtools/client/inspector/layout/actions/box-model.js new file mode 100644 index 000000000000..6c9d24c5a63f --- /dev/null +++ b/devtools/client/inspector/layout/actions/box-model.js @@ -0,0 +1,23 @@ +/* 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_LAYOUT, +} = require("./index"); + +module.exports = { + + /** + * Update the layout state with the new layout properties. + */ + updateLayout(layout) { + return { + type: UPDATE_LAYOUT, + layout, + }; + }, + +}; diff --git a/devtools/client/inspector/layout/actions/index.js b/devtools/client/inspector/layout/actions/index.js index bd7c56f4f37a..d2b3383877db 100644 --- a/devtools/client/inspector/layout/actions/index.js +++ b/devtools/client/inspector/layout/actions/index.js @@ -14,6 +14,9 @@ createEnum([ // Update the entire grids state with the new list of grids. "UPDATE_GRIDS", + // Update the layout state with the latest layout properties. + "UPDATE_LAYOUT", + // Update the grid highlighter's show grid line numbers state. "UPDATE_SHOW_GRID_LINE_NUMBERS", diff --git a/devtools/client/inspector/layout/actions/moz.build b/devtools/client/inspector/layout/actions/moz.build index 09b7039d19de..5e157a4bd728 100644 --- a/devtools/client/inspector/layout/actions/moz.build +++ b/devtools/client/inspector/layout/actions/moz.build @@ -5,6 +5,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. DevToolsModules( + 'box-model.js', 'grids.js', 'highlighter-settings.js', 'index.js', diff --git a/devtools/client/inspector/layout/components/App.js b/devtools/client/inspector/layout/components/App.js index c6d79b4a2df1..3deb1be85c88 100644 --- a/devtools/client/inspector/layout/components/App.js +++ b/devtools/client/inspector/layout/components/App.js @@ -8,19 +8,29 @@ const { addons, createClass, createFactory, DOM: dom, PropTypes } = require("devtools/client/shared/vendor/react"); const { connect } = require("devtools/client/shared/vendor/react-redux"); +const { LocalizationHelper } = require("devtools/shared/l10n"); + const Accordion = createFactory(require("./Accordion")); +const BoxModel = createFactory(require("./BoxModel")); const Grid = createFactory(require("./Grid")); const Types = require("../types"); const { getStr } = require("../utils/l10n"); +const BOXMODEL_STRINGS_URI = "devtools/client/locales/boxmodel.properties"; +const BOXMODEL_L10N = new LocalizationHelper(BOXMODEL_STRINGS_URI); + const App = createClass({ displayName: "App", propTypes: { + boxModel: PropTypes.shape(Types.boxModel).isRequired, grids: PropTypes.arrayOf(PropTypes.shape(Types.grid)).isRequired, highlighterSettings: PropTypes.shape(Types.highlighterSettings).isRequired, + onShowBoxModelEditor: PropTypes.func.isRequired, + onHideBoxModelHighlighter: PropTypes.func.isRequired, + onShowBoxModelHighlighter: PropTypes.func.isRequired, onToggleGridHighlighter: PropTypes.func.isRequired, onToggleShowGridLineNumbers: PropTypes.func.isRequired, onToggleShowInfiniteLines: PropTypes.func.isRequired, @@ -35,12 +45,18 @@ const App = createClass({ }, Accordion({ items: [ + { + header: BOXMODEL_L10N.getStr("boxmodel.title"), + component: BoxModel, + componentProps: this.props, + opened: true, + }, { header: getStr("layout.header"), component: Grid, componentProps: this.props, - opened: true - } + opened: true, + }, ] }) ); diff --git a/devtools/client/inspector/layout/components/BoxModel.js b/devtools/client/inspector/layout/components/BoxModel.js new file mode 100644 index 000000000000..c6492d54830c --- /dev/null +++ b/devtools/client/inspector/layout/components/BoxModel.js @@ -0,0 +1,52 @@ +/* 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 { addons, createClass, createFactory, DOM: dom, PropTypes } = + require("devtools/client/shared/vendor/react"); + +const BoxModelInfo = createFactory(require("./BoxModelInfo")); +const BoxModelMain = createFactory(require("./BoxModelMain")); + +const Types = require("../types"); + +module.exports = createClass({ + + displayName: "BoxModel", + + propTypes: { + boxModel: PropTypes.shape(Types.boxModel).isRequired, + onHideBoxModelHighlighter: PropTypes.func.isRequired, + onShowBoxModelEditor: PropTypes.func.isRequired, + onShowBoxModelHighlighter: PropTypes.func.isRequired, + }, + + mixins: [ addons.PureRenderMixin ], + + render() { + let { + boxModel, + onHideBoxModelHighlighter, + onShowBoxModelEditor, + onShowBoxModelHighlighter, + } = this.props; + + return dom.div( + { + className: "boxmodel-container", + }, + BoxModelMain({ + boxModel, + onHideBoxModelHighlighter, + onShowBoxModelEditor, + onShowBoxModelHighlighter, + }), + BoxModelInfo({ + boxModel, + }) + ); + }, + +}); diff --git a/devtools/client/inspector/layout/components/BoxModelEditable.js b/devtools/client/inspector/layout/components/BoxModelEditable.js new file mode 100644 index 000000000000..f90954d5ffe5 --- /dev/null +++ b/devtools/client/inspector/layout/components/BoxModelEditable.js @@ -0,0 +1,65 @@ +/* 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 { addons, createClass, DOM: dom, PropTypes } = + require("devtools/client/shared/vendor/react"); +const { editableItem } = require("devtools/client/shared/inplace-editor"); + +const LONG_TEXT_ROTATE_LIMIT = 3; + +module.exports = createClass({ + + displayName: "BoxModelEditable", + + propTypes: { + box: PropTypes.string.isRequired, + direction: PropTypes.string.isRequired, + property: PropTypes.string.isRequired, + textContent: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + onShowBoxModelEditor: PropTypes.func.isRequired, + }, + + mixins: [ addons.PureRenderMixin ], + + componentDidMount() { + let { property, onShowBoxModelEditor } = this.props; + + editableItem({ + element: this.refs.span, + }, (element, event) => { + onShowBoxModelEditor(element, event, property); + }); + }, + + render() { + let { + box, + direction, + property, + textContent, + } = this.props; + + let rotate = (direction == "left" || direction == "right") && + textContent.toString().length > LONG_TEXT_ROTATE_LIMIT; + + return dom.p( + { + className: `boxmodel-${box} boxmodel-${direction} + ${rotate ? "boxmodel-rotate" : ""}`, + }, + dom.span( + { + className: "boxmodel-editable", + "data-box": box, + title: property, + ref: "span", + }, + textContent + ) + ); + }, + +}); diff --git a/devtools/client/inspector/layout/components/BoxModelInfo.js b/devtools/client/inspector/layout/components/BoxModelInfo.js new file mode 100644 index 000000000000..c2337562e204 --- /dev/null +++ b/devtools/client/inspector/layout/components/BoxModelInfo.js @@ -0,0 +1,62 @@ +/* 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 { LocalizationHelper } = require("devtools/shared/l10n"); +const { addons, createClass, DOM: dom, PropTypes } = + require("devtools/client/shared/vendor/react"); + +const Types = require("../types"); + +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); + +module.exports = createClass({ + + displayName: "BoxModelInfo", + + propTypes: { + boxModel: PropTypes.shape(Types.boxModel).isRequired, + }, + + mixins: [ addons.PureRenderMixin ], + + render() { + let { boxModel } = this.props; + let { layout } = boxModel; + let { width, height, position } = layout; + + return dom.div( + { + className: "boxmodel-info", + }, + dom.span( + { + className: "boxmodel-element-size", + }, + SHARED_L10N.getFormatStr("dimensions", width, height) + ), + dom.section( + { + className: "boxmodel-position-group", + }, + dom.button({ + className: "layout-geometry-editor devtools-button", + title: BOXMODEL_L10N.getStr("boxmodel.geometryButton.tooltip"), + }), + dom.span( + { + className: "boxmodel-element-position", + }, + position + ) + ) + ); + }, + +}); diff --git a/devtools/client/inspector/layout/components/BoxModelMain.js b/devtools/client/inspector/layout/components/BoxModelMain.js new file mode 100644 index 000000000000..34319bcd0711 --- /dev/null +++ b/devtools/client/inspector/layout/components/BoxModelMain.js @@ -0,0 +1,261 @@ +/* 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 { addons, createClass, createFactory, DOM: dom, PropTypes } = + require("devtools/client/shared/vendor/react"); + +const { LocalizationHelper } = require("devtools/shared/l10n"); + +const BoxModelEditable = createFactory(require("./BoxModelEditable")); + +const Types = require("../types"); + +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); + +module.exports = createClass({ + + displayName: "BoxModelMain", + + propTypes: { + boxModel: PropTypes.shape(Types.boxModel).isRequired, + onHideBoxModelHighlighter: PropTypes.func.isRequired, + onShowBoxModelEditor: PropTypes.func.isRequired, + onShowBoxModelHighlighter: PropTypes.func.isRequired, + }, + + mixins: [ addons.PureRenderMixin ], + + getBorderOrPaddingValue(property) { + let { layout } = this.props.boxModel; + return layout[property] ? parseFloat(layout[property]) : "-"; + }, + + getHeightOrWidthValue(property) { + let { layout } = this.props.boxModel; + + if (property == undefined) { + return "-"; + } + + 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; + }, + + getMarginValue(property, direction) { + let { layout } = this.props.boxModel; + let autoMargins = layout.autoMargins || {}; + let value = "-"; + + if (direction in autoMargins) { + value = "auto"; + } else if (layout[property]) { + value = parseFloat(layout[property]); + } + + return value; + }, + + onHighlightMouseOver(event) { + let region = event.target.getAttribute("data-box"); + if (!region) { + this.props.onHideBoxModelHighlighter(); + } + + this.props.onShowBoxModelHighlighter({ + region, + showOnly: region, + onlyRegionArea: true, + }); + }, + + render() { + let { boxModel, onShowBoxModelEditor } = this.props; + let { layout } = boxModel; + let { width, height } = layout; + + let borderTop = this.getBorderOrPaddingValue("border-top-width"); + let borderRight = this.getBorderOrPaddingValue("border-right-width"); + let borderBottom = this.getBorderOrPaddingValue("border-bottom-width"); + let borderLeft = this.getBorderOrPaddingValue("border-left-width"); + + let paddingTop = this.getBorderOrPaddingValue("padding-top"); + let paddingRight = this.getBorderOrPaddingValue("padding-right"); + let paddingBottom = this.getBorderOrPaddingValue("padding-bottom"); + let paddingLeft = this.getBorderOrPaddingValue("padding-left"); + + let marginTop = this.getMarginValue("margin-top", "top"); + let marginRight = this.getMarginValue("margin-right", "right"); + let marginBottom = this.getMarginValue("margin-bottom", "bottom"); + let marginLeft = this.getMarginValue("margin-left", "left"); + + width = this.getHeightOrWidthValue(width); + height = this.getHeightOrWidthValue(height); + + return dom.div( + { + className: "boxmodel-main", + onMouseOver: this.onHighlightMouseOver, + onMouseOut: this.props.onHideBoxModelHighlighter, + }, + dom.span( + { + className: "boxmodel-legend", + "data-box": "margin", + title: BOXMODEL_L10N.getStr("boxmodel.margin"), + }, + BOXMODEL_L10N.getStr("boxmodel.margin") + ), + dom.div( + { + className: "boxmodel-margins", + "data-box": "margin", + title: BOXMODEL_L10N.getStr("boxmodel.margin"), + }, + dom.span( + { + className: "boxmodel-legend", + "data-box": "border", + title: BOXMODEL_L10N.getStr("boxmodel.border"), + }, + BOXMODEL_L10N.getStr("boxmodel.border") + ), + dom.div( + { + className: "boxmodel-borders", + "data-box": "border", + title: BOXMODEL_L10N.getStr("boxmodel.border"), + }, + dom.span( + { + className: "boxmodel-legend", + "data-box": "padding", + title: BOXMODEL_L10N.getStr("boxmodel.padding"), + }, + BOXMODEL_L10N.getStr("boxmodel.padding") + ), + dom.div( + { + className: "boxmodel-paddings", + "data-box": "padding", + title: BOXMODEL_L10N.getStr("boxmodel.padding"), + }, + dom.div({ + className: "boxmodel-content", + "data-box": "content", + title: BOXMODEL_L10N.getStr("boxmodel.content"), + }) + ) + ), + ), + BoxModelEditable({ + box: "margin", + direction: "top", + property: "margin-top", + textContent: marginTop, + onShowBoxModelEditor, + }), + BoxModelEditable({ + box: "margin", + direction: "right", + property: "margin-right", + textContent: marginRight, + onShowBoxModelEditor, + }), + BoxModelEditable({ + box: "margin", + direction: "bottom", + property: "margin-bottom", + textContent: marginBottom, + onShowBoxModelEditor, + }), + BoxModelEditable({ + box: "margin", + direction: "left", + property: "margin-left", + textContent: marginLeft, + onShowBoxModelEditor, + }), + BoxModelEditable({ + box: "border", + direction: "top", + property: "border-top-width", + textContent: borderTop, + onShowBoxModelEditor, + }), + BoxModelEditable({ + box: "border", + direction: "right", + property: "border-right-width", + textContent: borderRight, + onShowBoxModelEditor, + }), + BoxModelEditable({ + box: "border", + direction: "bottom", + property: "border-bottom-width", + textContent: borderBottom, + onShowBoxModelEditor, + }), + BoxModelEditable({ + box: "border", + direction: "left", + property: "border-left-width", + textContent: borderLeft, + onShowBoxModelEditor, + }), + BoxModelEditable({ + box: "padding", + direction: "top", + property: "padding-top", + textContent: paddingTop, + onShowBoxModelEditor, + }), + BoxModelEditable({ + box: "padding", + direction: "right", + property: "padding-right", + textContent: paddingRight, + onShowBoxModelEditor, + }), + BoxModelEditable({ + box: "padding", + direction: "bottom", + property: "padding-bottom", + textContent: paddingBottom, + onShowBoxModelEditor, + }), + BoxModelEditable({ + box: "padding", + direction: "left", + property: "padding-left", + textContent: paddingLeft, + onShowBoxModelEditor, + }), + dom.p( + { + className: "boxmodel-size", + }, + dom.span( + { + "data-box": "content", + title: BOXMODEL_L10N.getStr("boxmodel.content"), + }, + SHARED_L10N.getFormatStr("dimensions", width, height) + ) + ) + ); + }, + +}); diff --git a/devtools/client/inspector/layout/components/moz.build b/devtools/client/inspector/layout/components/moz.build index 8f4f2ca18663..a61fd3009eb7 100644 --- a/devtools/client/inspector/layout/components/moz.build +++ b/devtools/client/inspector/layout/components/moz.build @@ -8,6 +8,10 @@ DevToolsModules( 'Accordion.css', 'Accordion.js', 'App.js', + 'BoxModel.js', + 'BoxModelEditable.js', + 'BoxModelInfo.js', + 'BoxModelMain.js', 'Grid.js', 'GridDisplaySettings.js', 'GridList.js', diff --git a/devtools/client/inspector/layout/layout.js b/devtools/client/inspector/layout/layout.js index 13545d7f0572..fe67dbc50715 100644 --- a/devtools/client/inspector/layout/layout.js +++ b/devtools/client/inspector/layout/layout.js @@ -6,9 +6,16 @@ const Services = require("Services"); const { Task } = require("devtools/shared/task"); +const { getCssProperties } = require("devtools/shared/fronts/css-properties"); +const { ReflowFront } = require("devtools/shared/fronts/reflow"); + +const { InplaceEditor } = require("devtools/client/shared/inplace-editor"); const { createFactory, createElement } = require("devtools/client/shared/vendor/react"); const { Provider } = require("devtools/client/shared/vendor/react-redux"); +const { + updateLayout, +} = require("./actions/box-model"); const { updateGridHighlighted, updateGrids, @@ -21,10 +28,13 @@ const { const App = createFactory(require("./components/App")); const Store = require("./store"); +const EditingSession = require("./utils/editing-session"); + const { LocalizationHelper } = require("devtools/shared/l10n"); const INSPECTOR_L10N = new LocalizationHelper("devtools/client/locales/inspector.properties"); +const NUMERIC = /^-?[\d\.]+$/; const SHOW_GRID_LINE_NUMBERS = "devtools.gridinspector.showGridLineNumbers"; const SHOW_INFINITE_LINES_PREF = "devtools.gridinspector.showInfiniteLines"; @@ -35,15 +45,19 @@ function LayoutView(inspector, window) { this.store = null; this.walker = this.inspector.walker; + this.updateBoxModel = this.updateBoxModel.bind(this); + this.onGridLayoutChange = this.onGridLayoutChange.bind(this); this.onHighlighterChange = this.onHighlighterChange.bind(this); + this.onNewSelection = this.onNewSelection.bind(this); this.onSidebarSelect = this.onSidebarSelect.bind(this); + this.init(); + this.highlighters.on("grid-highlighter-hidden", this.onHighlighterChange); this.highlighters.on("grid-highlighter-shown", this.onHighlighterChange); + this.inspector.selection.on("new-node-front", this.onNewSelection); this.inspector.sidebar.on("select", this.onSidebarSelect); - - this.init(); } LayoutView.prototype = { @@ -63,6 +77,95 @@ LayoutView.prototype = { this.loadHighlighterSettings(); let app = App({ + /** + * Hides the box-model highlighter on the currently selected element. + */ + onHideBoxModelHighlighter: () => { + let toolbox = this.inspector.toolbox; + toolbox.highlighterUtils.unhighlight(); + }, + + /** + * 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) => { + let session = new EditingSession({ + inspector: this.inspector, + doc: this.document, + elementRules: this.elementRules, + }); + let initialValue = session.getProperty(property); + + let editor = new InplaceEditor({ + element: 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"; + } + + let properties = [ + { name: property, value: value } + ]; + + if (property.substring(0, 7) == "border-") { + let bprop = property.substring(0, property.length - 5) + "style"; + let style = session.getProperty(bprop); + if (!style || style == "none" || style == "hidden") { + properties.push({ name: bprop, value: "solid" }); + } + } + + session.setProperties(properties).catch(e => console.error(e)); + }, + done: (value, commit) => { + editor.elt.parentNode.classList.remove("boxmodel-editing"); + if (!commit) { + session.revert().then(() => { + session.destroy(); + }, e => console.error(e)); + return; + } + + let node = this.inspector.selection.nodeFront; + this.inspector.pageStyle.getLayout(node, { + autoMargins: true, + }).then(layout => { + this.store.dispatch(updateLayout(layout)); + }, e => console.error(e)); + }, + contextMenu: this.inspector.onTextBoxContextMenu, + cssProperties: getCssProperties(this.inspector.toolbox) + }, event); + }, + + /** + * Shows the box-model highlighter on the currently selected element. + * + * @param {Object} options + * Options passed to the highlighter actor. + */ + onShowBoxModelHighlighter: (options = {}) => { + let toolbox = this.inspector.toolbox; + let nodeFront = this.inspector.selection.nodeFront; + + toolbox.highlighterUtils.highlightNodeFront(nodeFront, options); + }, /** * Handler for a change in the input checkboxes in the GridList component. @@ -120,7 +223,6 @@ LayoutView.prototype = { } } }, - }); let provider = createElement(Provider, { @@ -147,9 +249,16 @@ LayoutView.prototype = { destroy() { this.highlighters.off("grid-highlighter-hidden", this.onHighlighterChange); this.highlighters.off("grid-highlighter-shown", this.onHighlighterChange); + this.inspector.selection.off("new-node-front", this.onNewSelection); this.inspector.sidebar.off("select", this.onSidebarSelect); this.layoutInspector.off("grid-layout-changed", this.onGridLayoutChange); + if (this.reflowFront) { + this.untrackReflows(); + this.reflowFront.destroy(); + this.reflowFront = null; + } + this.document = null; this.inspector = null; this.layoutInspector = null; @@ -166,6 +275,16 @@ LayoutView.prototype = { this.inspector.sidebar.getCurrentTabID() === "layoutview"; }, + /** + * Returns true if the layout panel is visible and the current node is valid to + * be displayed in the view. + */ + isPanelVisibleAndNodeValid() { + return this.isPanelVisible() && + this.inspector.selection.isConnected() && + this.inspector.selection.isElementNode(); + }, + /** * Load the grid highligher display settings into the store from the stored preferences. */ @@ -180,13 +299,79 @@ LayoutView.prototype = { }, /** - * Refreshes the layout view by dispatching the new grid data. This is called when the + * Starts listening to reflows in the current tab. + */ + trackReflows() { + if (!this.reflowFront) { + let { target } = this.inspector; + if (target.form.reflowActor) { + this.reflowFront = ReflowFront(target.client, + target.form); + } else { + return; + } + } + + this.reflowFront.on("reflows", this.updateBoxModel); + this.reflowFront.start(); + }, + + /** + * Stops listening to reflows in the current tab. + */ + untrackReflows() { + if (!this.reflowFront) { + return; + } + + this.reflowFront.off("reflows", this.updateBoxModel); + this.reflowFront.stop(); + }, + + /** + * Updates the box model panel by dispatching the new layout data. + */ + updateBoxModel() { + let lastRequest = Task.spawn((function* () { + if (!(this.isPanelVisible() && + this.inspector.selection.isConnected() && + this.inspector.selection.isElementNode())) { + return null; + } + + let node = this.inspector.selection.nodeFront; + let layout = yield this.inspector.pageStyle.getLayout(node, { + autoMargins: true, + }); + let styleEntries = yield this.inspector.pageStyle.getApplied(node, {}); + this.elementRules = styleEntries.map(e => e.rule); + + // 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._lastRequest = null; + + this.inspector.emit("boxmodel-view-updated"); + return null; + }).bind(this)).catch(console.error); + + this._lastRequest = lastRequest; + }, + + /** + * Updates the grid panel by dispatching the new grid data. This is called when the * layout view becomes visible or the view needs to be updated with new grid data. * * @param {Array|null} gridFronts * Optional array of all GridFront in the current page. */ - refresh: Task.async(function* (gridFronts) { + updateGridPanel: Task.async(function* (gridFronts) { // Stop refreshing if the inspector or store is already destroyed. if (!this.inspector || !this.store) { return; @@ -221,7 +406,7 @@ LayoutView.prototype = { */ onGridLayoutChange(grids) { if (this.isPanelVisible()) { - this.refresh(grids); + this.updateGridPanel(grids); } }, @@ -240,6 +425,17 @@ LayoutView.prototype = { this.store.dispatch(updateGridHighlighted(nodeFront, highlighted)); }, + /** + * Selection 'new-node-front' event handler. + */ + onNewSelection: function () { + if (!this.isPanelVisibleAndNodeValid()) { + return; + } + + this.updateBoxModel(); + }, + /** * Handler for the inspector sidebar select event. Starts listening for * "grid-layout-changed" if the layout panel is visible. Otherwise, stop @@ -249,11 +445,18 @@ LayoutView.prototype = { onSidebarSelect() { if (!this.isPanelVisible()) { this.layoutInspector.off("grid-layout-changed", this.onGridLayoutChange); + this.untrackReflows(); return; } + if (this.inspector.selection.isConnected() && + this.inspector.selection.isElementNode()) { + this.trackReflows(); + } + this.layoutInspector.on("grid-layout-changed", this.onGridLayoutChange); - this.refresh(); + this.updateBoxModel(); + this.updateGridPanel(); }, }; diff --git a/devtools/client/inspector/layout/reducers/box-model.js b/devtools/client/inspector/layout/reducers/box-model.js new file mode 100644 index 000000000000..cfe512cc0b87 --- /dev/null +++ b/devtools/client/inspector/layout/reducers/box-model.js @@ -0,0 +1,31 @@ +/* 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_LAYOUT, +} = require("../actions/index"); + +const INITIAL_BOX_MODEL = { + layout: {}, +}; + +let reducers = { + + [UPDATE_LAYOUT](boxModel, { layout }) { + return Object.assign({}, boxModel, { + layout, + }); + }, + +}; + +module.exports = function (boxModel = INITIAL_BOX_MODEL, action) { + let reducer = reducers[action.type]; + if (!reducer) { + return boxModel; + } + return reducer(boxModel, action); +}; diff --git a/devtools/client/inspector/layout/reducers/index.js b/devtools/client/inspector/layout/reducers/index.js index 56d27202a0fb..4ab7d2eb949d 100644 --- a/devtools/client/inspector/layout/reducers/index.js +++ b/devtools/client/inspector/layout/reducers/index.js @@ -4,5 +4,6 @@ "use strict"; +exports.boxModel = require("./box-model"); exports.grids = require("./grids"); exports.highlighterSettings = require("./highlighter-settings"); diff --git a/devtools/client/inspector/layout/reducers/moz.build b/devtools/client/inspector/layout/reducers/moz.build index 09b7039d19de..5e157a4bd728 100644 --- a/devtools/client/inspector/layout/reducers/moz.build +++ b/devtools/client/inspector/layout/reducers/moz.build @@ -5,6 +5,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. DevToolsModules( + 'box-model.js', 'grids.js', 'highlighter-settings.js', 'index.js', diff --git a/devtools/client/inspector/layout/types.js b/devtools/client/inspector/layout/types.js index 60c49efa2ad1..478f145b1cae 100644 --- a/devtools/client/inspector/layout/types.js +++ b/devtools/client/inspector/layout/types.js @@ -6,6 +6,16 @@ const { PropTypes } = require("devtools/client/shared/vendor/react"); +/** + * The box model data for the current selected node. + */ +exports.boxModel = { + + // The layout information of the current selected node + layout: PropTypes.object, + +}; + /** * A single grid container in the document. */ diff --git a/devtools/client/themes/boxmodel.css b/devtools/client/themes/boxmodel.css index 5a3289faeda6..30ea728e8017 100644 --- a/devtools/client/themes/boxmodel.css +++ b/devtools/client/themes/boxmodel.css @@ -2,13 +2,11 @@ * 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/ */ -#boxmodel-wrapper { - border-bottom-style: solid; - border-bottom-width: 1px; - border-color: var(--theme-splitter-color); -} +/** + * This is the stylesheet of the Box Model view implemented in the layout panel. + */ -#boxmodel-container { +.boxmodel-container { /* The view will grow bigger as the window gets resized, until 400px */ max-width: 400px; margin: 0px auto; @@ -17,24 +15,24 @@ /* Header */ -#boxmodel-header, -#boxmodel-info { +.boxmodel-header, +.boxmodel-info { display: flex; align-items: center; padding: 4px 17px; } -#layout-geometry-editor { +.layout-geometry-editor { visibility: hidden; } -#layout-geometry-editor::before { +.layout-geometry-editor::before { background: url(images/geometry-editor.svg) no-repeat center center / 16px 16px; } /* Main: contains the box-model regions */ -#boxmodel-main { +.boxmodel-main { position: relative; box-sizing: border-box; /* The regions are semi-transparent, so the white background is partly @@ -42,7 +40,7 @@ background-color: white; color: var(--theme-selection-color); /* Make sure there is some space between the window's edges and the regions */ - margin: 0 14px 4px 14px; + margin: 14px 14px 4px 14px; width: calc(100% - 2 * 14px); } @@ -53,73 +51,73 @@ /* Regions are 3 nested elements with wide borders and outlines */ -#boxmodel-content { +.boxmodel-content { height: 18px; } -#boxmodel-margins, -#boxmodel-borders, -#boxmodel-padding { +.boxmodel-margins, +.boxmodel-borders, +.boxmodel-paddings { border-color: hsla(210,100%,85%,0.2); border-width: 18px; border-style: solid; outline: dotted 1px hsl(210,100%,85%); } -#boxmodel-margins { +.boxmodel-margins { /* This opacity applies to all of the regions, since they are nested */ opacity: .8; } /* Regions colors */ -#boxmodel-margins { +.boxmodel-margins { border-color: #edff64; } -#boxmodel-borders { +.boxmodel-borders { border-color: #444444; } -#boxmodel-padding { +.boxmodel-paddings { border-color: #6a5acd; } -#boxmodel-content { +.boxmodel-content { background-color: #87ceeb; } -.theme-firebug #boxmodel-main, -.theme-firebug #boxmodel-borders, -.theme-firebug #boxmodel-content { +.theme-firebug .boxmodel-main, +.theme-firebug .boxmodel-borders, +.theme-firebug .boxmodel-content { border-style: solid; } -.theme-firebug #boxmodel-main, -.theme-firebug #boxmodel-header { +.theme-firebug .boxmodel-main, +.theme-firebug .boxmodel-header { font-family: var(--proportional-font-family); } -.theme-firebug #boxmodel-main { +.theme-firebug .boxmodel-main { color: var(--theme-body-color); font-size: var(--theme-toolbar-font-size); } -.theme-firebug #boxmodel-header { +.theme-firebug .boxmodel-header { font-size: var(--theme-toolbar-font-size); } /* Editable region sizes are contained in absolutely positioned

*/ -#boxmodel-main > p { +.boxmodel-main > p { position: absolute; pointer-events: none; margin: 0; text-align: center; } -#boxmodel-main > p > span, -#boxmodel-main > p > input { +.boxmodel-main > p > span, +.boxmodel-main > p > input { vertical-align: middle; pointer-events: auto; } @@ -235,11 +233,6 @@ border-bottom-color: hsl(0, 0%, 50%); } -.styleinspector-propertyeditor { - border: 1px solid #ccc; - padding: 0; -} - /* Make sure the content size doesn't appear as editable like the other sizes */ .boxmodel-size > span { @@ -248,11 +241,11 @@ /* Box Model Info: contains the position and size of the element */ -#boxmodel-element-size { +.boxmodel-element-size { flex: 1; } -#boxmodel-position-group { +.boxmodel-position-group { display: flex; align-items: center; }