diff --git a/devtools/client/inspector/boxmodel/box-model.js b/devtools/client/inspector/boxmodel/box-model.js new file mode 100644 index 000000000000..9edf3164c9c9 --- /dev/null +++ b/devtools/client/inspector/boxmodel/box-model.js @@ -0,0 +1,285 @@ +/* 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 { 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 { updateLayout } = require("./actions/box-model"); + +const EditingSession = require("./utils/editing-session"); + +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.updateBoxModel = this.updateBoxModel.bind(this); + + this.onHideBoxModelHighlighter = this.onHideBoxModelHighlighter.bind(this); + this.onNewSelection = this.onNewSelection.bind(this); + this.onShowBoxModelEditor = this.onShowBoxModelEditor.bind(this); + this.onShowBoxModelHighlighter = this.onShowBoxModelHighlighter.bind(this); + this.onSidebarSelect = this.onSidebarSelect.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.reflowFront) { + this.untrackReflows(); + this.reflowFront.destroy(); + this.reflowFront = null; + } + + this.document = null; + this.inspector = null; + this.walker = null; + }, + + /** + * Returns an object containing the box model's handler functions used in the box + * model's React component props. + */ + getComponentProps() { + return { + onHideBoxModelHighlighter: this.onHideBoxModelHighlighter, + onShowBoxModelEditor: this.onShowBoxModelEditor, + onShowBoxModelHighlighter: this.onShowBoxModelHighlighter, + }; + }, + + /** + * Returns true if the layout panel is visible, and false otherwise. + */ + isPanelVisible() { + return this.inspector.toolbox.currentToolId === "inspector" && + this.inspector.sidebar && + 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(); + }, + + /** + * 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; + }, + + /** + * Selection 'new-node-front' event handler. + */ + onNewSelection: function () { + if (!this.isPanelVisibleAndNodeValid()) { + return; + } + + this.updateBoxModel(); + }, + + /** + * 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 the inspector sidebar select event. Starts listening for + * "grid-layout-changed" if the layout panel is visible. Otherwise, stop + * listening for grid layout changes. Finally, refresh the layout 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(); + }, + +}; + +module.exports = BoxModel; diff --git a/devtools/client/inspector/boxmodel/moz.build b/devtools/client/inspector/boxmodel/moz.build index eeb9697a60cb..07b54256bdd3 100644 --- a/devtools/client/inspector/boxmodel/moz.build +++ b/devtools/client/inspector/boxmodel/moz.build @@ -12,5 +12,6 @@ DIRS += [ ] DevToolsModules( + 'box-model.js', 'types.js', ) diff --git a/devtools/client/inspector/inspector.js b/devtools/client/inspector/inspector.js index 3022b0d8f417..b5933fdecac6 100644 --- a/devtools/client/inspector/inspector.js +++ b/devtools/client/inspector/inspector.js @@ -23,6 +23,7 @@ const Menu = require("devtools/client/framework/menu"); const MenuItem = require("devtools/client/framework/menu-item"); const {HTMLBreadcrumbs} = require("devtools/client/inspector/breadcrumbs"); +const BoxModel = require("devtools/client/inspector/boxmodel/box-model"); const {ComputedViewTool} = require("devtools/client/inspector/computed/computed"); const {FontInspector} = require("devtools/client/inspector/fonts/fonts"); const {InspectorSearch} = require("devtools/client/inspector/inspector-search"); @@ -573,6 +574,7 @@ Inspector.prototype = { this.ruleview = new RuleViewTool(this, this.panelWin); this.computedview = new ComputedViewTool(this, this.panelWin); + this.boxmodel = new BoxModel(this, this.panelWin); if (Services.prefs.getBoolPref("devtools.layoutview.enabled")) { const LayoutView = this.browserRequire("devtools/client/inspector/layout/layout"); diff --git a/devtools/client/inspector/layout/components/App.js b/devtools/client/inspector/layout/components/App.js index 403eccdf9101..0ccc3be23a5d 100644 --- a/devtools/client/inspector/layout/components/App.js +++ b/devtools/client/inspector/layout/components/App.js @@ -11,9 +11,10 @@ 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 BoxModel = createFactory(require("devtools/client/inspector/boxmodel/components/BoxModel")); + const Types = require("../types"); const { getStr } = require("../utils/l10n"); diff --git a/devtools/client/inspector/layout/layout.js b/devtools/client/inspector/layout/layout.js index fa2e96087c24..17df7d05429b 100644 --- a/devtools/client/inspector/layout/layout.js +++ b/devtools/client/inspector/layout/layout.js @@ -6,16 +6,10 @@ 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, @@ -27,13 +21,10 @@ const { const App = createFactory(require("./components/App")); -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"; @@ -44,18 +35,14 @@ function LayoutView(inspector, window) { this.store = inspector.store; 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); } @@ -70,6 +57,12 @@ LayoutView.prototype = { return; } + let { + onHideBoxModelHighlighter, + onShowBoxModelEditor, + onShowBoxModelHighlighter, + } = this.inspector.boxmodel.getComponentProps(); + this.layoutInspector = yield this.inspector.walker.getLayoutInspector(); this.loadHighlighterSettings(); @@ -81,95 +74,9 @@ LayoutView.prototype = { */ showBoxModelProperties: true, - /** - * 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); - }, + onHideBoxModelHighlighter, + onShowBoxModelEditor, + onShowBoxModelHighlighter, /** * Handler for a change in the input checkboxes in the GridList component. @@ -253,16 +160,9 @@ 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; @@ -279,16 +179,6 @@ 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. */ @@ -302,72 +192,6 @@ LayoutView.prototype = { dispatch(updateShowInfiniteLines(showInfinteLines)); }, - /** - * 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. @@ -429,17 +253,6 @@ 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 @@ -449,17 +262,10 @@ 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.updateBoxModel(); this.updateGridPanel(); }, diff --git a/devtools/client/shared/browser-loader.js b/devtools/client/shared/browser-loader.js index ab7505beee9b..e21a899044a3 100644 --- a/devtools/client/shared/browser-loader.js +++ b/devtools/client/shared/browser-loader.js @@ -12,6 +12,7 @@ const Services = devtools.require("Services"); const { AppConstants } = devtools.require("resource://gre/modules/AppConstants.jsm"); const BROWSER_BASED_DIRS = [ + "resource://devtools/client/inspector/boxmodel", "resource://devtools/client/inspector/layout", "resource://devtools/client/jsonview", "resource://devtools/client/shared/vendor",