--- a/devtools/client/inspector/grids/grid-inspector.js
+++ b/devtools/client/inspector/grids/grid-inspector.js
@@ -3,16 +3,18 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
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,
updateGridHighlighted,
updateGrids,
} = require("./actions/grids");
const {
updateShowGridAreas,
@@ -46,19 +48,19 @@ function GridInspector(inspector, window
this.inspector = inspector;
this.store = inspector.store;
this.telemetry = inspector.telemetry;
this.walker = this.inspector.walker;
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);
this.onShowGridLineNamesHighlight = this.onShowGridLineNamesHighlight.bind(this);
this.onSidebarSelect = this.onSidebarSelect.bind(this);
this.onToggleGridHighlighter = this.onToggleGridHighlighter.bind(this);
this.onToggleShowGridAreas = this.onToggleShowGridAreas.bind(this);
this.onToggleShowGridLineNumbers = this.onToggleShowGridLineNumbers.bind(this);
@@ -89,30 +91,30 @@ GridInspector.prototype = {
{
supportsCssColor4ColorFunction: () => false
}
);
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();
}),
/**
* Destruction function called when the inspector is destroyed. Removes event listeners
* and cleans up references.
*/
destroy() {
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);
this.swatchColorPickerTooltip.destroy();
this.document = null;
this.highlighters = null;
this.inspector = null;
@@ -206,17 +208,17 @@ GridInspector.prototype = {
getSwatchColorPickerTooltip() {
return this.swatchColorPickerTooltip;
},
/**
* 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";
},
/**
* Load the grid highligher display settings into the store from the stored preferences.
*/
loadHighlighterSettings() {
@@ -273,23 +275,29 @@ GridInspector.prototype = {
this.telemetry.log(CSS_GRID_COUNT_HISTOGRAM_ID, gridFronts.length);
this.inspector.previousURL = this.inspector.target.url;
}
let grids = [];
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];
let color = this.getInitialGridColor(nodeFront, fallbackColor);
grids.push({
id: i,
color,
@@ -298,19 +306,20 @@ GridInspector.prototype = {
nodeFront,
});
}
this.store.dispatch(updateGrids(grids));
}),
/**
- * 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();
}
},
/**
* Handler for "grid-highlighter-shown" and "grid-highlighter-hidden" events emitted
* from the HighlightersOverlay. Updates the NodeFront's grid highlighted state.
@@ -340,26 +349,92 @@ GridInspector.prototype = {
}
this.lastHighlighterColor = null;
this.lastHighlighterNode = null;
this.lastHighlighterState = null;
},
/**
- * 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.
*
* @param {NodeFront} node
* The NodeFront of the grid container element for which the grid color is
* being updated.
* @param {String} color
* A hex string representing the color to use.
*/
--- a/devtools/client/inspector/grids/moz.build
+++ b/devtools/client/inspector/grids/moz.build
@@ -12,8 +12,9 @@ DIRS += [
]
DevToolsModules(
'grid-inspector.js',
'types.js',
)
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
--- a/devtools/client/inspector/grids/test/browser.ini
+++ b/devtools/client/inspector/grids/test/browser.ini
@@ -23,10 +23,11 @@ support-files =
[browser_grids_grid-list-on-mutation-element-added.js]
[browser_grids_grid-list-on-mutation-element-removed.js]
[browser_grids_grid-list-toggle-multiple-grids.js]
[browser_grids_grid-list-toggle-single-grid.js]
[browser_grids_grid-outline-cannot-show-outline.js]
[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]
new file mode 100644
--- /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 = `
+ <style>
+ .container {
+ display: grid;
+ grid-template-columns: repeat(2, 20vw);
+ grid-auto-rows: 20px;
+ }
+ </style>
+ <div class="container">
+ <div>item 1</div>
+ <div>item 2</div>
+ </div>
+`;
+
+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.");
+});
new file mode 100644
--- /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"
+};
new file mode 100644
--- /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", {});
new file mode 100644
--- /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);
+ }
+}
new file mode 100644
--- /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]
--- a/devtools/client/inspector/grids/utils/moz.build
+++ b/devtools/client/inspector/grids/utils/moz.build
@@ -1,9 +1,10 @@
# -*- 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(
'l10n.js',
+ 'utils.js',
)
new file mode 100644
--- /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;
--- a/devtools/client/inspector/rules/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -24,17 +24,17 @@ const {
VIEW_NODE_SELECTOR_TYPE,
VIEW_NODE_PROPERTY_TYPE,
VIEW_NODE_VALUE_TYPE,
VIEW_NODE_IMAGE_URL_TYPE,
VIEW_NODE_LOCATION_TYPE,
} = 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");
const AutocompletePopup = require("devtools/client/shared/autocomplete-popup");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
const PREF_DEFAULT_COLOR_UNIT = "devtools.defaultColorUnit";
@@ -102,18 +102,18 @@ const FILTER_STRICT_RE = /\s*`(.*?)`\s*$
function CssRuleView(inspector, document, store, pageStyle) {
this.inspector = inspector;
this.highlighters = inspector.highlighters;
this.styleDocument = document;
this.styleWindow = this.styleDocument.defaultView;
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);
this._outputParser = new OutputParser(document, this.cssProperties);
this._onAddRule = this._onAddRule.bind(this);
this._onContextMenu = this._onContextMenu.bind(this);
this._onCopy = this._onCopy.bind(this);
--- 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
@@ -115,18 +115,18 @@ function* testCompletion([key, completio
// Also listening for popup opened/closed events if needed.
let popupEvent = open ? "popup-opened" : "popup-closed";
let onPopupEvent = editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null;
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;
info("Checking the state");
if (completion !== null) {
is(editor.input.value, completion, "Correct value is autocompleted");
}
--- 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
@@ -94,18 +94,18 @@ function* testCompletion([key, modifiers
info("Synthesizing key " + key + ", modifiers: " + Object.keys(modifiers));
// Also listening for popup opened/closed events if needed.
let popupEvent = open ? "popup-opened" : "popup-closed";
let onPopupEvent = editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null;
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;
// The key might have been a TAB or shift-TAB, in which case the editor will
// be a new one
editor = inplaceEditor(view.styleDocument.activeElement);
--- 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
@@ -102,18 +102,18 @@ function* testCompletion([key, modifiers
// Also listening for popup opened/closed events if needed.
let popupEvent = open ? "popup-opened" : "popup-closed";
let onPopupEvent = editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null;
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;
info("Checking the state");
if (completion !== null) {
// The key might have been a TAB or shift-TAB, in which case the editor will
// be a new one
--- 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
@@ -94,17 +94,17 @@ add_task(function* () {
info("Select the background-color suggestion with a mouse click.");
let onRuleviewChanged = view.once("ruleview-changed");
let onSuggest = editor.once("after-suggest");
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;
is(editor.input.value, EXPECTED_CSS_VALUE,
"Input value correctly autocompleted");
info("Press ESCAPE to leave the input.");
onRuleviewChanged = view.once("ruleview-changed");
--- a/devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js
@@ -67,17 +67,17 @@ function* runTestData(view, {value, comm
let editor = yield focusEditableField(view, propEditor.valueSpan);
is(inplaceEditor(propEditor.valueSpan), editor,
"Focused editor should be the value span.");
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);
onRuleViewChanged = view.once("ruleview-changed");
let onBlur = once(editor.input, "blur");
EventUtils.synthesizeKey(commitKey, modifiers);
yield onBlur;
yield onRuleViewChanged;
--- a/devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js
@@ -36,20 +36,20 @@ function* editAndCheck(view) {
let onPropertyChange = waitForComputedStyleProperty("#testid", null,
"padding-top", newPaddingValue);
let onRefreshAfterPreview = once(view, "ruleview-changed");
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.");
yield onRefreshAfterPreview;
let onBlur = once(editor.input, "blur");
info("Entering the commit key and finishing edit");
--- a/devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js
@@ -233,17 +233,17 @@ function* runIncrementTest(propertyEdito
for (let test in tests) {
yield testIncrement(editor, tests[test], view, propertyEditor);
}
// Blur the field to put back the UI in its initial state (and avoid pending
// 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;
}
function* testIncrement(editor, options, view) {
editor.input.value = options.start;
let input = editor.input;
if (options.selectAll) {
@@ -267,14 +267,14 @@ function* testIncrement(editor, options,
EventUtils.synthesizeKey(key, {altKey: options.alt, shiftKey: options.shift},
view.styleWindow);
yield onKeyUp;
// Only expect a change if the value actually changed!
if (options.start !== options.end) {
- view.throttle.flush();
+ view.debounce.flush();
yield onRuleViewChanged;
}
is(input.value, options.end, "Value changed to " + options.end);
}
--- a/devtools/client/inspector/rules/test/browser_rules_edit-property_02.js
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_02.js
@@ -64,17 +64,17 @@ function* testEditProperty(inspector, ru
info("Entering a value following by a semi-colon to commit it");
let onBlur = once(editor.input, "blur");
// Use sendChar() to pass each character as a string so that we can test
// prop.editor.warning.hidden after each character.
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");
}
yield onBlur;
let newValue = yield executeInContent("Test:GetRulePropertyValue", {
styleSheetIndex: 0,
--- a/devtools/client/inspector/rules/test/browser_rules_livepreview.js
+++ b/devtools/client/inspector/rules/test/browser_rules_livepreview.js
@@ -48,17 +48,17 @@ function* testLivePreviewData(data, rule
info("Focusing the property value inplace-editor");
let editor = yield focusEditableField(ruleView, propEditor.valueSpan);
is(inplaceEditor(propEditor.valueSpan), editor,
"The focused editor is the value");
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");
if (data.escape) {
EventUtils.synthesizeKey("VK_ESCAPE", {});
} else {
EventUtils.synthesizeKey("VK_RETURN", {});
}
--- 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
@@ -10,17 +10,17 @@
const TEST_URI = "<div>Test Element</div>";
add_task(function* () {
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
let {inspector, view} = yield openRuleView();
// Turn off throttling, which can cause intermittents. Throttling is used by
// the TextPropertyEditor.
- view.throttle = () => {};
+ view.debounce = () => {};
yield selectNode("div", inspector);
let ruleEditor = getRuleViewRuleEditor(view, 0);
// Note that we wait for a markup mutation here because this new rule will end
// up creating a style attribute on the node shown in the markup-view.
// (we also wait for the rule-view to refresh).
let onMutation = inspector.once("markupmutation");
--- a/devtools/client/inspector/rules/test/browser_rules_search-filter_09.js
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_09.js
@@ -56,17 +56,17 @@ add_task(function* () {
// Getting the new value editor after focus
editor = inplaceEditor(view.styleDocument.activeElement);
let propEditor = ruleEditor.rule.textProps[2].editor;
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");
editor.input.blur();
yield onRuleViewChanged;
ok(propEditor.container.classList.contains("ruleview-highlight"),
"margin-left text property is correctly highlighted.");
--- a/devtools/client/inspector/rules/test/head.js
+++ b/devtools/client/inspector/rules/test/head.js
@@ -284,17 +284,17 @@ var addProperty = Task.async(function* (
is(editor, inplaceEditor(textProp.editor.valueSpan),
"The inplace editor appeared for the value");
info("Adding value " + value);
// Setting the input value schedules a preview to be shown in 10ms which
// 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");
EventUtils.synthesizeKey(commitValueWith, {}, view.styleWindow);
yield onValueAdded;
if (blurNewProperty) {
view.styleDocument.activeElement.blur();
@@ -323,17 +323,17 @@ var setProperty = Task.async(function* (
yield focusEditableField(view, textProp.editor.valueSpan);
let onPreview = view.once("ruleview-changed");
if (value === null) {
EventUtils.synthesizeKey("VK_DELETE", {}, view.styleWindow);
} else {
EventUtils.sendString(value, view.styleWindow);
}
- view.throttle.flush();
+ view.debounce.flush();
yield onPreview;
let onValueDone = view.once("ruleview-changed");
EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
yield onValueDone;
if (blurNewProperty) {
view.styleDocument.activeElement.blur();
--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -70,17 +70,17 @@ function TextPropertyEditor(ruleEditor,
this._onEnableClicked = this._onEnableClicked.bind(this);
this._onExpandClicked = this._onExpandClicked.bind(this);
this._onStartEditing = this._onStartEditing.bind(this);
this._onNameDone = this._onNameDone.bind(this);
this._onValueDone = this._onValueDone.bind(this);
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);
this._create();
this.update();
}
TextPropertyEditor.prototype = {
@@ -894,17 +894,17 @@ TextPropertyEditor.prototype = {
* Live preview this property, without committing changes.
*
* @param {String} value
* The value to set the current property to.
* @param {Boolean} reverting
* 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;
}
let val = parseSingleValue(this.cssProperties.isKnown, value);
this.ruleEditor.rule.previewPropertyValue(this.prop, val.value,
val.priority);
--- a/devtools/client/inspector/shared/utils.js
+++ b/devtools/client/inspector/shared/utils.js
@@ -95,42 +95,92 @@ function advanceValidate(keyCode, value,
}
}
return false;
}
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 () {
if (timer) {
clearTimeout(timer);
}
let args = arguments;
timer = setTimeout(function () {
timer = null;
func.apply(scope, args);
}, wait);
};
}
+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;
/**
* Event handler that causes a blur on the target if the input has
* multiple CSS properties as the value.
*/
function blurOnMultipleProperties(cssProperties) {
return (e) => {
--- a/devtools/client/inspector/test/shared-head.js
+++ b/devtools/client/inspector/test/shared-head.js
@@ -73,19 +73,19 @@ var openInspectorSidebarTab = Task.async
* Open the toolbox, with the inspector tool visible, and the rule-view
* sidebar tab selected.
*
* @return a promise that resolves when the inspector is ready and the rule view
* is visible and ready
*/
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,
inspector: data.inspector,
testActor: data.testActor,
view: data.inspector.getPanel("ruleview").view
};
});
@@ -194,42 +194,42 @@ var selectNode = Task.async(function* (s
let nodeFront = yield getNodeFront(selector, inspector);
let updated = inspector.once("inspector-updated");
inspector.selection.setNodeFront(nodeFront, reason);
yield updated;
});
/**
* 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) {
existingCall.args = arguments;
} else {
calls.push({ func, wait, scope, args: arguments });
}
};
}
- throttle.flush = function () {
+ debounce.flush = function () {
calls.forEach(({func, scope, args}) => func.apply(scope, args));
calls = [];
};
- return throttle;
+ return debounce;
}
/**
* Wait for a content -> chrome message on the message manager (the window
* messagemanager is used).
*
* @param {String} name
* The message name
--- a/devtools/server/actors/layout.js
+++ b/devtools/server/actors/layout.js
@@ -53,16 +53,23 @@ var GridActor = ActorClassWithSpec(gridS
let gridFragments = this.containerEl.getGridFragments();
this.gridFragments = getStringifiableFragments(gridFragments);
let form = {
actor: this.actorID,
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;
},
});
/**
* The CSS layout actor provides layout information for the given document.
*/
var LayoutActor = ActorClassWithSpec(layoutSpec, {
--- a/devtools/shared/fronts/layout.js
+++ b/devtools/shared/fronts/layout.js
@@ -12,16 +12,28 @@ const GridFront = FrontClassWithSpec(gri
if (detail === "actorid") {
this.actorID = form;
return;
}
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.
*/
get gridFragments() {
return this._form.gridFragments;
}
});
const LayoutFront = FrontClassWithSpec(layoutSpec, {});