diff --git a/devtools/client/inspector/grids/grid-inspector.js b/devtools/client/inspector/grids/grid-inspector.js index 8efa464c1695..9117b4ce04a0 100644 --- a/devtools/client/inspector/grids/grid-inspector.js +++ b/devtools/client/inspector/grids/grid-inspector.js @@ -8,6 +8,8 @@ const Services = require("Services"); const { Task } = require("devtools/shared/task"); const SwatchColorPickerTooltip = require("devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip"); +const { throttle } = require("devtools/client/inspector/shared/utils"); +const { compareFragmentsGeometry } = require("devtools/client/inspector/grids/utils/utils"); const { updateGridColor, @@ -51,9 +53,9 @@ function GridInspector(inspector, window) { this.getSwatchColorPickerTooltip = this.getSwatchColorPickerTooltip.bind(this); this.updateGridPanel = this.updateGridPanel.bind(this); - this.onGridLayoutChange = this.onGridLayoutChange.bind(this); + this.onNavigate = this.onNavigate.bind(this); this.onHighlighterChange = this.onHighlighterChange.bind(this); - this.onReflow = this.onReflow.bind(this); + this.onReflow = throttle(this.onReflow, 500, this); this.onSetGridOverlayColor = this.onSetGridOverlayColor.bind(this); this.onShowGridAreaHighlight = this.onShowGridAreaHighlight.bind(this); this.onShowGridCellHighlight = this.onShowGridCellHighlight.bind(this); @@ -94,7 +96,7 @@ GridInspector.prototype = { this.highlighters.on("grid-highlighter-hidden", this.onHighlighterChange); this.highlighters.on("grid-highlighter-shown", this.onHighlighterChange); this.inspector.sidebar.on("select", this.onSidebarSelect); - this.inspector.target.on("navigate", this.onGridLayoutChange); + this.inspector.on("new-root", this.onNavigate); this.onSidebarSelect(); }), @@ -107,7 +109,7 @@ GridInspector.prototype = { this.highlighters.off("grid-highlighter-hidden", this.onHighlighterChange); this.highlighters.off("grid-highlighter-shown", this.onHighlighterChange); this.inspector.sidebar.off("select", this.onSidebarSelect); - this.inspector.target.off("navigate", this.onGridLayoutChange); + this.inspector.off("new-root", this.onNavigate); this.inspector.reflowTracker.untrackReflows(this, this.onReflow); @@ -211,7 +213,7 @@ GridInspector.prototype = { * Returns true if the layout panel is visible, and false otherwise. */ isPanelVisible() { - return this.inspector.toolbox && this.inspector.sidebar && + return this.inspector && this.inspector.toolbox && this.inspector.sidebar && this.inspector.toolbox.currentToolId === "inspector" && this.inspector.sidebar.getCurrentTabID() === "layoutview"; }, @@ -278,13 +280,19 @@ GridInspector.prototype = { for (let i = 0; i < gridFronts.length; i++) { let grid = gridFronts[i]; - let nodeFront; - try { - nodeFront = yield this.walker.getNodeFromActor(grid.actorID, ["containerEl"]); - } catch (e) { - // This call might fail if called asynchrously after the toolbox is finished - // closing. - return; + let nodeFront = grid.containerNodeFront; + + // If the GridFront didn't yet have access to the NodeFront for its container, then + // get it from the walker. This happens when the walker hasn't yet seen this + // particular DOM Node in the tree yet, or when we are connected to an older server. + if (!nodeFront) { + try { + nodeFront = yield this.walker.getNodeFromActor(grid.actorID, ["containerEl"]); + } catch (e) { + // This call might fail if called asynchrously after the toolbox is finished + // closing. + return; + } } let fallbackColor = GRID_COLORS[i % GRID_COLORS.length]; @@ -303,9 +311,10 @@ GridInspector.prototype = { }), /** - * Handler for "navigate" event fired by the tab target. Updates grid panel contents. + * Handler for "new-root" event fired by the inspector, which indicates a page + * navigation. Updates grid panel contents. */ - onGridLayoutChange() { + onNavigate() { if (this.isPanelVisible()) { this.updateGridPanel(); } @@ -345,15 +354,81 @@ GridInspector.prototype = { }, /** - * Handler for the "reflow" event fired by the inspector's reflow tracker. On reflows, - * update the grid panel content. + * Given a list of new grid fronts, and if we have a currently highlighted grid, check + * if its fragments have changed. + * + * @param {Array} newGridFronts + * A list of GridFront objects. + * @return {Boolean} */ - onReflow() { - if (this.isPanelVisible()) { - this.updateGridPanel(); + haveCurrentFragmentsChanged(newGridFronts) { + const currentNode = this.highlighters.gridHighlighterShown; + if (!currentNode) { + return false; } + + const newGridFront = newGridFronts.find(g => g.containerNodeFront === currentNode); + if (!newGridFront) { + return false; + } + + const { grids } = this.store.getState(); + const oldFragments = grids.find(g => g.nodeFront === currentNode).gridFragments; + const newFragments = newGridFront.gridFragments; + + return !compareFragmentsGeometry(oldFragments, newFragments); }, + /** + * Handler for the "reflow" event fired by the inspector's reflow tracker. On reflows, + * update the grid panel content, because the shape or number of grids on the page may + * have changed. + * + * Note that there may be frequent reflows on the page and that not all of them actually + * cause the grids to change. So, we want to limit how many times we update the grid + * panel to only reflows that actually either change the list of grids, or those that + * change the current outlined grid. + * To achieve this, this function compares the list of grid containers from before and + * after the reflow, as well as the grid fragment data on the currently highlighted + * grid. + */ + onReflow: Task.async(function* () { + if (!this.isPanelVisible()) { + return; + } + + // The list of grids currently displayed. + const { grids } = this.store.getState(); + + // The new list of grids from the server. + let newGridFronts; + try { + newGridFronts = yield this.layoutInspector.getAllGrids(this.walker.rootNode); + } catch (e) { + // This call might fail if called asynchrously after the toolbox is finished + // closing. + return; + } + + // Compare the list of DOM nodes which define these grids. + const oldNodeFronts = grids.map(grid => grid.nodeFront.actorID); + const newNodeFronts = newGridFronts.filter(grid => grid.containerNodeFront) + .map(grid => grid.containerNodeFront.actorID); + if (grids.length === newGridFronts.length && + oldNodeFronts.sort().join(",") == newNodeFronts.sort().join(",")) { + // Same list of containers, but let's check if the geometry of the current grid has + // changed, if it hasn't we can safely abort. + if (!this.highlighters.gridHighlighterShown || + (this.highlighters.gridHighlighterShown && + !this.haveCurrentFragmentsChanged(newGridFronts))) { + return; + } + } + + // Either the list of containers or the current fragments have changed, do update. + this.updateGridPanel(newGridFronts); + }), + /** * Handler for a change in the grid overlay color picker for a grid container. * diff --git a/devtools/client/inspector/grids/moz.build b/devtools/client/inspector/grids/moz.build index 74ce577196a7..55d2d8c97f33 100644 --- a/devtools/client/inspector/grids/moz.build +++ b/devtools/client/inspector/grids/moz.build @@ -17,3 +17,4 @@ DevToolsModules( ) BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] +XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini'] diff --git a/devtools/client/inspector/grids/test/browser.ini b/devtools/client/inspector/grids/test/browser.ini index fc23ef1e5ca7..3abe7983ece5 100644 --- a/devtools/client/inspector/grids/test/browser.ini +++ b/devtools/client/inspector/grids/test/browser.ini @@ -28,5 +28,6 @@ support-files = [browser_grids_grid-outline-highlight-area.js] [browser_grids_grid-outline-highlight-cell.js] [browser_grids_grid-outline-selected-grid.js] +[browser_grids_grid-outline-updates-on-grid-change.js] [browser_grids_highlighter-setting-rules-grid-toggle.js] [browser_grids_number-of-css-grids-telemetry.js] diff --git a/devtools/client/inspector/grids/test/browser_grids_grid-outline-updates-on-grid-change.js b/devtools/client/inspector/grids/test/browser_grids_grid-outline-updates-on-grid-change.js new file mode 100644 index 000000000000..6dc0896265f4 --- /dev/null +++ b/devtools/client/inspector/grids/test/browser_grids_grid-outline-updates-on-grid-change.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the grid outline does reflect the grid in the page even after the grid has +// changed. + +const TEST_URI = ` + +
+
item 1
+
item 2
+
+`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + + let { inspector, gridInspector, testActor } = yield openLayoutView(); + let { document: doc } = gridInspector; + let { highlighters, store } = inspector; + + info("Clicking on the first checkbox to highlight the grid"); + let checkbox = doc.querySelector("#grid-list input"); + + let onHighlighterShown = highlighters.once("grid-highlighter-shown"); + let onCheckboxChange = waitUntilState(store, state => + state.grids.length == 1 && state.grids[0].highlighted); + let onGridOutlineRendered = waitForDOM(doc, ".grid-outline-cell", 2); + + checkbox.click(); + + yield onHighlighterShown; + yield onCheckboxChange; + let elements = yield onGridOutlineRendered; + + info("Checking the grid outline is shown."); + is(elements.length, 2, "Grid outline is shown."); + + info("Changing the grid in the page"); + let onReflow = new Promise(resolve => { + let listener = { + callback: () => { + inspector.reflowTracker.untrackReflows(listener, listener.callback); + resolve(); + } + }; + inspector.reflowTracker.trackReflows(listener, listener.callback); + }); + let onGridOutlineChanged = waitForDOM(doc, ".grid-outline-cell", 4); + + testActor.eval(` + const div = content.document.createElement("div"); + div.textContent = "item 3"; + content.document.querySelector(".container").appendChild(div); + `); + + yield onReflow; + elements = yield onGridOutlineChanged; + + info("Checking the grid outline is correct."); + is(elements.length, 4, "Grid outline was changed."); +}); diff --git a/devtools/client/inspector/grids/test/unit/.eslintrc.js b/devtools/client/inspector/grids/test/unit/.eslintrc.js new file mode 100644 index 000000000000..54a9a6361b5c --- /dev/null +++ b/devtools/client/inspector/grids/test/unit/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the common devtools xpcshell eslintrc config. + "extends": "../../../../../.eslintrc.xpcshell.js" +}; diff --git a/devtools/client/inspector/grids/test/unit/head.js b/devtools/client/inspector/grids/test/unit/head.js new file mode 100644 index 000000000000..eb95bd6b90df --- /dev/null +++ b/devtools/client/inspector/grids/test/unit/head.js @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* eslint no-unused-vars: [2, {"vars": "local"}] */ + +const { utils: Cu } = Components; +const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); diff --git a/devtools/client/inspector/grids/test/unit/test_compare_fragments_geometry.js b/devtools/client/inspector/grids/test/unit/test_compare_fragments_geometry.js new file mode 100644 index 000000000000..546eb9a3b3ea --- /dev/null +++ b/devtools/client/inspector/grids/test/unit/test_compare_fragments_geometry.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { compareFragmentsGeometry } = require("devtools/client/inspector/grids/utils/utils"); + +const TESTS = [{ + desc: "No fragments", + grids: [[], []], + expected: true +}, { + desc: "Different number of fragments", + grids: [ + [{}, {}, {}], + [{}, {}] + ], + expected: false +}, { + desc: "Different number of columns", + grids: [ + [{cols: {lines: [{}, {}]}, rows: {lines: []}}], + [{cols: {lines: [{}]}, rows: {lines: []}}] + ], + expected: false +}, { + desc: "Different number of rows", + grids: [ + [{cols: {lines: [{}, {}]}, rows: {lines: [{}]}}], + [{cols: {lines: [{}, {}]}, rows: {lines: [{}, {}]}}] + ], + expected: false +}, { + desc: "Different number of rows and columns", + grids: [ + [{cols: {lines: [{}]}, rows: {lines: [{}]}}], + [{cols: {lines: [{}, {}]}, rows: {lines: [{}, {}]}}] + ], + expected: false +}, { + desc: "Different column sizes", + grids: [ + [{cols: {lines: [{start: 0}, {start: 500}]}, rows: {lines: []}}], + [{cols: {lines: [{start: 0}, {start: 1000}]}, rows: {lines: []}}] + ], + expected: false +}, { + desc: "Different row sizes", + grids: [ + [{cols: {lines: [{start: 0}, {start: 500}]}, rows: {lines: [{start: -100}]}}], + [{cols: {lines: [{start: 0}, {start: 500}]}, rows: {lines: [{start: 0}]}}] + ], + expected: false +}, { + desc: "Different row and column sizes", + grids: [ + [{cols: {lines: [{start: 0}, {start: 500}]}, rows: {lines: [{start: -100}]}}], + [{cols: {lines: [{start: 0}, {start: 505}]}, rows: {lines: [{start: 0}]}}] + ], + expected: false +}, { + desc: "Complete structure, same fragments", + grids: [ + [{cols: {lines: [{start: 0}, {start: 100.3}, {start: 200.6}]}, + rows: {lines: [{start: 0}, {start: 1000}, {start: 2000}]}}], + [{cols: {lines: [{start: 0}, {start: 100.3}, {start: 200.6}]}, + rows: {lines: [{start: 0}, {start: 1000}, {start: 2000}]}}] + ], + expected: true +}]; + +function run_test() { + for (let { desc, grids, expected } of TESTS) { + if (desc) { + do_print(desc); + } + equal(compareFragmentsGeometry(grids[0], grids[1]), expected); + } +} diff --git a/devtools/client/inspector/grids/test/unit/xpcshell.ini b/devtools/client/inspector/grids/test/unit/xpcshell.ini new file mode 100644 index 000000000000..c52a930c717f --- /dev/null +++ b/devtools/client/inspector/grids/test/unit/xpcshell.ini @@ -0,0 +1,6 @@ +[DEFAULT] +tags = devtools +firefox-appdir = browser +head = head.js + +[test_compare_fragments_geometry.js] diff --git a/devtools/client/inspector/grids/utils/moz.build b/devtools/client/inspector/grids/utils/moz.build index e3053b63fab6..f6a6af241ddd 100644 --- a/devtools/client/inspector/grids/utils/moz.build +++ b/devtools/client/inspector/grids/utils/moz.build @@ -6,4 +6,5 @@ DevToolsModules( 'l10n.js', + 'utils.js', ) diff --git a/devtools/client/inspector/grids/utils/utils.js b/devtools/client/inspector/grids/utils/utils.js new file mode 100644 index 000000000000..6a6c341fe5ff --- /dev/null +++ b/devtools/client/inspector/grids/utils/utils.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"; + +/** + * Compares 2 sets of grid fragments to each other and checks if they have the same + * general geometry. + * This means that things like areas, area names or line names are ignored. + * This only checks if the 2 sets of fragments have as many fragments, as many lines, and + * that those lines are at the same distance. + * + * @param {Array} fragments1 + * A list of gridFragment objects. + * @param {Array} fragments2 + * Another list of gridFragment objects to compare to the first list. + * @return {Boolean} + * True if the fragments are the same, false otherwise. + */ +function compareFragmentsGeometry(fragments1, fragments2) { + // Compare the number of fragments. + if (fragments1.length !== fragments2.length) { + return false; + } + + // Compare the number of areas, rows and columns. + for (let i = 0; i < fragments1.length; i++) { + if (fragments1[i].cols.lines.length !== fragments2[i].cols.lines.length || + fragments1[i].rows.lines.length !== fragments2[i].rows.lines.length) { + return false; + } + } + + // Compare the offset of lines. + for (let i = 0; i < fragments1.length; i++) { + for (let j = 0; j < fragments1[i].cols.lines.length; j++) { + if (fragments1[i].cols.lines[j].start !== fragments2[i].cols.lines[j].start) { + return false; + } + } + for (let j = 0; j < fragments1[i].rows.lines.length; j++) { + if (fragments1[i].rows.lines[j].start !== fragments2[i].rows.lines[j].start) { + return false; + } + } + } + + return true; +} + +module.exports.compareFragmentsGeometry = compareFragmentsGeometry; diff --git a/devtools/client/inspector/rules/rules.js b/devtools/client/inspector/rules/rules.js index bfc063d4d959..9a65157f53b2 100644 --- a/devtools/client/inspector/rules/rules.js +++ b/devtools/client/inspector/rules/rules.js @@ -29,7 +29,7 @@ const { } = require("devtools/client/inspector/shared/node-types"); const StyleInspectorMenu = require("devtools/client/inspector/shared/style-inspector-menu"); const TooltipsOverlay = require("devtools/client/inspector/shared/tooltips-overlay"); -const {createChild, promiseWarn, throttle} = require("devtools/client/inspector/shared/utils"); +const {createChild, promiseWarn, debounce} = require("devtools/client/inspector/shared/utils"); const EventEmitter = require("devtools/shared/event-emitter"); const KeyShortcuts = require("devtools/client/shared/key-shortcuts"); const clipboardHelper = require("devtools/shared/platform/clipboard"); @@ -107,8 +107,8 @@ function CssRuleView(inspector, document, store, pageStyle) { this.store = store || {}; this.pageStyle = pageStyle; - // Allow tests to override throttling behavior, as this can cause intermittents. - this.throttle = throttle; + // Allow tests to override debouncing behavior, as this can cause intermittents. + this.debounce = debounce; this.cssProperties = getCssProperties(inspector.toolbox); diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js index 566bae25925f..e00a11f0a4cd 100644 --- a/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js +++ b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js @@ -120,8 +120,8 @@ function* testCompletion([key, completion, open, selected], info("Synthesizing key " + key); EventUtils.synthesizeKey(key, {}, view.styleWindow); - // Flush the throttle for the preview text. - view.throttle.flush(); + // Flush the debounce for the preview text. + view.debounce.flush(); yield onSuggest; yield onPopupEvent; diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.js b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.js index fde8f5d1248e..957990a502b4 100644 --- a/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.js +++ b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.js @@ -99,8 +99,8 @@ function* testCompletion([key, modifiers, completion, open, selected, change], EventUtils.synthesizeKey(key, modifiers, view.styleWindow); - // Flush the throttle for the preview text. - view.throttle.flush(); + // Flush the debounce for the preview text. + view.debounce.flush(); yield onDone; yield onPopupEvent; diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js index d89e5129d9ea..504b85a958a8 100644 --- a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js +++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js @@ -107,8 +107,8 @@ function* testCompletion([key, modifiers, completion, open, selected, change], info("Synthesizing key " + key + ", modifiers: " + Object.keys(modifiers)); EventUtils.synthesizeKey(key, modifiers, view.styleWindow); - // Flush the throttle for the preview text. - view.throttle.flush(); + // Flush the debounce for the preview text. + view.debounce.flush(); yield onDone; yield onPopupEvent; diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js index ec939eafc844..529e19c7a598 100644 --- a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js +++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js @@ -99,7 +99,7 @@ add_task(function* () { let node = editor.popup._list.childNodes[editor.popup.selectedIndex]; EventUtils.synthesizeMouseAtCenter(node, {}, editor.popup._window); - view.throttle.flush(); + view.debounce.flush(); yield onSuggest; yield onRuleviewChanged; diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js index 8e16601c732b..e9bdfcc8d210 100644 --- a/devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js @@ -72,7 +72,7 @@ function* runTestData(view, {value, commitKey, modifiers, expected}) { info("Entering test data " + value); let onRuleViewChanged = view.once("ruleview-changed"); EventUtils.sendString(value, view.styleWindow); - view.throttle.flush(); + view.debounce.flush(); yield onRuleViewChanged; info("Entering the commit key " + commitKey + " " + modifiers); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js index ee0a1fa744ca..d9a4a8728782 100644 --- a/devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js @@ -41,10 +41,10 @@ function* editAndCheck(view) { info("Entering a new value"); EventUtils.sendString(newPaddingValue, view.styleWindow); - info("Waiting for the throttled previewValue to apply the " + + info("Waiting for the debounced previewValue to apply the " + "changes to document"); - view.throttle.flush(); + view.debounce.flush(); yield onPropertyChange; info("Waiting for ruleview-refreshed after previewValue was applied."); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js index ca63cedccfc4..2c88ef02a92c 100644 --- a/devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js @@ -238,7 +238,7 @@ function* runIncrementTest(propertyEditor, view, tests) { // requests when the test ends). let onRuleViewChanged = view.once("ruleview-changed"); EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); - view.throttle.flush(); + view.debounce.flush(); yield onRuleViewChanged; } @@ -272,7 +272,7 @@ function* testIncrement(editor, options, view) { // Only expect a change if the value actually changed! if (options.start !== options.end) { - view.throttle.flush(); + view.debounce.flush(); yield onRuleViewChanged; } diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_02.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_02.js index 7e6315236cbf..9e9fbc62ef11 100644 --- a/devtools/client/inspector/rules/test/browser_rules_edit-property_02.js +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_02.js @@ -69,7 +69,7 @@ function* testEditProperty(inspector, ruleView) { for (let ch of "red;") { let onPreviewDone = ruleView.once("ruleview-changed"); EventUtils.sendChar(ch, ruleView.styleWindow); - ruleView.throttle.flush(); + ruleView.debounce.flush(); yield onPreviewDone; is(prop.editor.warning.hidden, true, "warning triangle is hidden or shown as appropriate"); diff --git a/devtools/client/inspector/rules/test/browser_rules_livepreview.js b/devtools/client/inspector/rules/test/browser_rules_livepreview.js index 1f1302a70b8a..a811cdb204ba 100644 --- a/devtools/client/inspector/rules/test/browser_rules_livepreview.js +++ b/devtools/client/inspector/rules/test/browser_rules_livepreview.js @@ -53,7 +53,7 @@ function* testLivePreviewData(data, ruleView, selector) { info("Entering value in the editor: " + data.value); let onPreviewDone = ruleView.once("ruleview-changed"); EventUtils.sendString(data.value, ruleView.styleWindow); - ruleView.throttle.flush(); + ruleView.debounce.flush(); yield onPreviewDone; let onValueDone = ruleView.once("ruleview-changed"); diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js index dd1360b96cdb..2534a93977fb 100644 --- a/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js +++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js @@ -15,7 +15,7 @@ add_task(function* () { // Turn off throttling, which can cause intermittents. Throttling is used by // the TextPropertyEditor. - view.throttle = () => {}; + view.debounce = () => {}; yield selectNode("div", inspector); diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_09.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_09.js index 620e5d336e81..b52f6f4167c2 100644 --- a/devtools/client/inspector/rules/test/browser_rules_search-filter_09.js +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_09.js @@ -61,7 +61,7 @@ add_task(function* () { info("Entering a value and bluring the field to expect a rule change"); onRuleViewChanged = view.once("ruleview-changed"); editor.input.value = "100%"; - view.throttle.flush(); + view.debounce.flush(); yield onRuleViewChanged; onRuleViewChanged = view.once("ruleview-changed"); diff --git a/devtools/client/inspector/rules/test/head.js b/devtools/client/inspector/rules/test/head.js index 0d8f9f88a937..4fdd9ffa41ee 100644 --- a/devtools/client/inspector/rules/test/head.js +++ b/devtools/client/inspector/rules/test/head.js @@ -289,7 +289,7 @@ var addProperty = Task.async(function* (view, ruleIndex, name, value, // triggers a ruleview-changed event (see bug 1209295). let onPreview = view.once("ruleview-changed"); editor.input.value = value; - view.throttle.flush(); + view.debounce.flush(); yield onPreview; let onValueAdded = view.once("ruleview-changed"); @@ -328,7 +328,7 @@ var setProperty = Task.async(function* (view, textProp, value, } else { EventUtils.sendString(value, view.styleWindow); } - view.throttle.flush(); + view.debounce.flush(); yield onPreview; let onValueDone = view.once("ruleview-changed"); diff --git a/devtools/client/inspector/rules/views/text-property-editor.js b/devtools/client/inspector/rules/views/text-property-editor.js index 1f1406173622..0c30b8d13add 100644 --- a/devtools/client/inspector/rules/views/text-property-editor.js +++ b/devtools/client/inspector/rules/views/text-property-editor.js @@ -75,7 +75,7 @@ function TextPropertyEditor(ruleEditor, property) { this._onSwatchCommit = this._onSwatchCommit.bind(this); this._onSwatchPreview = this._onSwatchPreview.bind(this); this._onSwatchRevert = this._onSwatchRevert.bind(this); - this._onValidate = this.ruleView.throttle(this._previewValue, 10, this); + this._onValidate = this.ruleView.debounce(this._previewValue, 10, this); this.update = this.update.bind(this); this.updatePropertyState = this.updatePropertyState.bind(this); @@ -899,7 +899,7 @@ TextPropertyEditor.prototype = { * True if we're reverting the previously previewed value */ _previewValue: function (value, reverting = false) { - // Since function call is throttled, we need to make sure we are still + // Since function call is debounced, we need to make sure we are still // editing, and any selector modifications have been completed if (!reverting && (!this.editing || this.ruleEditor.isEditing)) { return; diff --git a/devtools/client/inspector/shared/utils.js b/devtools/client/inspector/shared/utils.js index 60dda914c742..465f1669fd03 100644 --- a/devtools/client/inspector/shared/utils.js +++ b/devtools/client/inspector/shared/utils.js @@ -100,17 +100,18 @@ function advanceValidate(keyCode, value, insertionPoint) { exports.advanceValidate = advanceValidate; /** - * Create a throttling function wrapper to regulate its frequency. + * Create a debouncing function wrapper to only call the target function after a certain + * amount of time has passed without it being called. * * @param {Function} func - * The function to throttle + * The function to debounce * @param {number} wait - * The throttling period + * The wait period * @param {Object} scope * The scope to use for func - * @return {Function} The throttled function + * @return {Function} The debounced function */ -function throttle(func, wait, scope) { +function debounce(func, wait, scope) { let timer = null; return function () { @@ -126,6 +127,55 @@ function throttle(func, wait, scope) { }; } +exports.debounce = debounce; + +/** + * From underscore's `_.throttle` + * http://underscorejs.org + * (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + * Underscore may be freely distributed under the MIT license. + * + * Returns a function, that, when invoked, will only be triggered at most once during a + * given window of time. The throttled function will run as much as it can, without ever + * going more than once per wait duration. + * + * @param {Function} func + * The function to throttle + * @param {number} wait + * The wait period + * @param {Object} scope + * The scope to use for func + * @return {Function} The throttled function + */ +function throttle(func, wait, scope) { + let args, result; + let timeout = null; + let previous = 0; + + let later = function () { + previous = Date.now(); + timeout = null; + result = func.apply(scope, args); + args = null; + }; + + return function () { + let now = Date.now(); + let remaining = wait - (now - previous); + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(scope, args); + args = null; + } else if (!timeout) { + timeout = setTimeout(later, remaining); + } + return result; + }; +} + exports.throttle = throttle; /** diff --git a/devtools/client/inspector/test/shared-head.js b/devtools/client/inspector/test/shared-head.js index 6404aa38c7e8..bbab92965f58 100644 --- a/devtools/client/inspector/test/shared-head.js +++ b/devtools/client/inspector/test/shared-head.js @@ -78,9 +78,9 @@ var openInspectorSidebarTab = Task.async(function* (id) { */ function openRuleView() { return openInspectorSidebarTab("ruleview").then(data => { - // Replace the view to use a custom throttle function that can be triggered manually + // Replace the view to use a custom debounce function that can be triggered manually // through an additional ".flush()" property. - data.inspector.getPanel("ruleview").view.throttle = manualThrottle(); + data.inspector.getPanel("ruleview").view.debounce = manualDebounce(); return { toolbox: data.toolbox, @@ -199,16 +199,16 @@ var selectNode = Task.async(function* (selector, inspector, reason = "test") { /** * Create a throttling function that can be manually "flushed". This is to replace the - * use of the `throttle` function from `devtools/client/inspector/shared/utils.js`, which + * use of the `debounce` function from `devtools/client/inspector/shared/utils.js`, which * has a setTimeout that can cause intermittents. - * @return {Function} This function has the same function signature as throttle, but + * @return {Function} This function has the same function signature as debounce, but * the property `.flush()` has been added for flushing out any - * throttled calls. + * debounced calls. */ -function manualThrottle() { +function manualDebounce() { let calls = []; - function throttle(func, wait, scope) { + function debounce(func, wait, scope) { return function () { let existingCall = calls.find(call => call.func === func); if (existingCall) { @@ -219,12 +219,12 @@ function manualThrottle() { }; } - throttle.flush = function () { + debounce.flush = function () { calls.forEach(({func, scope, args}) => func.apply(scope, args)); calls = []; }; - return throttle; + return debounce; } /** diff --git a/devtools/server/actors/layout.js b/devtools/server/actors/layout.js index 1650eef0bc30..8de70f81f026 100644 --- a/devtools/server/actors/layout.js +++ b/devtools/server/actors/layout.js @@ -58,6 +58,13 @@ var GridActor = ActorClassWithSpec(gridSpec, { gridFragments: this.gridFragments }; + // If the WalkerActor already knows the container element, then also return its + // ActorID so we avoid the client from doing another round trip to get it in many + // cases. + if (this.walker.hasNode(this.containerEl)) { + form.containerNodeActorID = this.walker.getNode(this.containerEl).actorID; + } + return form; }, }); diff --git a/devtools/shared/fronts/layout.js b/devtools/shared/fronts/layout.js index 5a1a6185d369..e55098356b47 100644 --- a/devtools/shared/fronts/layout.js +++ b/devtools/shared/fronts/layout.js @@ -16,6 +16,18 @@ const GridFront = FrontClassWithSpec(gridSpec, { this._form = form; }, + /** + * In some cases, the GridActor already knows the NodeActor ID of the node where the + * grid is located. In such cases, this getter returns the NodeFront for it. + */ + get containerNodeFront() { + if (!this._form.containerNodeActorID) { + return null; + } + + return this.conn.getActor(this._form.containerNodeActorID); + }, + /** * Getter for the grid fragments data. */