Bug 1213651 - Avoid server round-trips when displaying animated dom nodes; r=tromey

The main change here is that nodeFronts that have already been displayed
in the timeline are stored in a WeakMap so they can be retrieved from it
next time they're displayed and avoid a server-side round trip which, in
turn, causes the UI to flicker.

The other change is that now, it is possible to tell the animations actor
what is the current walker actor, which allows animation player actors to
directly send the NodeActor ID as part of their forms. Which, in most cases,
completely eliminates the server round-trip, because the corresponding
NodeFronts are already known on the client, so we get them from there.

The last change done here is that AnimationTargetNode now becomes a thin
wrapper on top of the new DomNodePreview component that was extracted so
it can be reused in other places.
This commit is contained in:
Patrick Brosset 2016-01-21 13:19:58 +01:00
parent 0cfb6c0b29
commit 8fbd12a804
17 changed files with 564 additions and 404 deletions

View File

@ -4,9 +4,6 @@
# Exclude expected objdirs.
obj*/**
# Temporarily ignore HTML files that still need to be fixed.
devtools/**/*.html
# We ignore all these directories by default, until we get them enabled.
# If you are enabling a directory, please add directory specific exclusions
# below.
@ -17,7 +14,6 @@ caps/**
chrome/**
config/**
db/**
devtools/**
docshell/**
dom/**
editor/**
@ -91,24 +87,52 @@ browser/locales/**
browser/extensions/loop/**
# devtools/ exclusions
# Ignore d3
devtools/client/shared/d3.js
devtools/client/webaudioeditor/lib/dagre-d3.js
devtools/*.js
devtools/client/*.js
devtools/client/aboutdebugging/**
devtools/client/animationinspector/**
devtools/client/canvasdebugger/**
devtools/client/commandline/**
devtools/client/debugger/**
devtools/client/eyedropper/**
devtools/client/framework/**
# devtools/client/inspector/shared/*.js files are eslint-clean, so they aren't
# included in the ignore list.
devtools/client/inspector/computed/**
devtools/client/inspector/fonts/**
devtools/client/inspector/layout/**
devtools/client/inspector/markup/**
devtools/client/inspector/rules/**
devtools/client/inspector/shared/test/**
devtools/client/inspector/test/**
devtools/client/inspector/*.js
devtools/client/jsonview/**
devtools/client/memory/**
devtools/client/netmonitor/**
devtools/client/performance/**
devtools/client/projecteditor/**
devtools/client/promisedebugger/**
devtools/client/responsivedesign/**
devtools/client/scratchpad/**
devtools/client/shadereditor/**
devtools/client/shared/**
devtools/client/sourceeditor/**
devtools/client/storage/**
devtools/client/styleeditor/**
devtools/client/tilt/**
devtools/client/webaudioeditor/**
devtools/client/webconsole/**
devtools/client/webide/**
devtools/server/**
devtools/shared/**
# Ignore codemirror
devtools/client/sourceeditor/codemirror/*.js
devtools/client/sourceeditor/codemirror/**/*.js
devtools/client/sourceeditor/test/codemirror/*
# Ignore jquery test libs
devtools/client/markupview/test/lib_*
# Ignore pre-processed files
# Ignore devtools pre-processed files
devtools/client/framework/toolbox-process-window.js
devtools/client/performance/system.js
devtools/client/webide/webide-prefs.js
devtools/client/preferences/**
# Ignore various libs
# Ignore devtools third-party libs
devtools/shared/jsbeautify/*
devtools/shared/acorn/*
devtools/client/sourceeditor/tern/*
@ -117,6 +141,12 @@ devtools/shared/sourcemap/*
devtools/shared/qrcode/decoder/*
devtools/shared/qrcode/encoder/*
devtools/client/shared/vendor/*
devtools/client/shared/d3.js
devtools/client/webaudioeditor/lib/dagre-d3.js
devtools/client/sourceeditor/codemirror/*.js
devtools/client/sourceeditor/codemirror/**/*.js
devtools/client/sourceeditor/test/codemirror/*
devtools/client/markupview/test/lib_*
# mobile/android/ exclusions
mobile/android/chrome/content

View File

@ -93,7 +93,9 @@ var getServerTraits = Task.async(function*(target) {
{ name: "hasSetCurrentTimes", actor: "animations",
method: "setCurrentTimes" },
{ name: "hasGetFrames", actor: "animationplayer",
method: "getFrames" }
method: "getFrames" },
{ name: "hasSetWalkerActor", actor: "animations",
method: "setWalkerActor" },
];
let traits = {};
@ -147,6 +149,12 @@ var AnimationsController = {
return;
}
// Let the AnimationsActor know what WalkerActor we're using. This will
// come in handy later to return references to DOM Nodes.
if (this.traits.hasSetWalkerActor) {
yield this.animationsFront.setWalkerActor(gInspector.walker);
}
this.startListeners();
yield this.onNewNodeFront();

View File

@ -3,35 +3,22 @@
const {Cu} = require("chrome");
Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
const {
createNode,
TargetNodeHighlighter
} = require("devtools/client/animationinspector/utils");
const {DomNodePreview} = require(
"devtools/client/inspector/shared/dom-node-preview");
const STRINGS_URI = "chrome://devtools/locale/animationinspector.properties";
const L10N = new ViewHelpers.L10N(STRINGS_URI);
// Map dom node fronts by animation fronts so we don't have to get them from the
// walker every time the timeline is refreshed.
var nodeFronts = new WeakMap();
/**
* UI component responsible for displaying a preview of the target dom node of
* a given animation.
* @param {InspectorPanel} inspector Requires a reference to the inspector-panel
* to highlight and select the node, as well as refresh it when there are
* mutations.
* @param {Object} options Supported properties are:
* - compact {Boolean} Defaults to false. If true, nodes will be previewed like
* tag#id.class instead of <tag id="id" class="class">
* Accepts the same parameters as the DomNodePreview component. See
* devtools/client/inspector/shared/dom-node-preview.js for documentation.
*/
function AnimationTargetNode(inspector, options = {}) {
function AnimationTargetNode(inspector, options) {
this.inspector = inspector;
this.options = options;
this.onPreviewMouseOver = this.onPreviewMouseOver.bind(this);
this.onPreviewMouseOut = this.onPreviewMouseOut.bind(this);
this.onSelectNodeClick = this.onSelectNodeClick.bind(this);
this.onMarkupMutations = this.onMarkupMutations.bind(this);
this.onHighlightNodeClick = this.onHighlightNodeClick.bind(this);
this.onTargetHighlighterLocked = this.onTargetHighlighterLocked.bind(this);
this.previewer = new DomNodePreview(inspector, options);
EventEmitter.decorate(this);
}
@ -39,282 +26,51 @@ exports.AnimationTargetNode = AnimationTargetNode;
AnimationTargetNode.prototype = {
init: function(containerEl) {
let document = containerEl.ownerDocument;
// Init the markup for displaying the target node.
this.el = createNode({
parent: containerEl,
attributes: {
"class": "animation-target"
}
});
// Icon to select the node in the inspector.
this.highlightNodeEl = createNode({
parent: this.el,
nodeType: "span",
attributes: {
"class": "node-highlighter",
"title": L10N.getStr("node.highlightNodeLabel")
}
});
// Wrapper used for mouseover/out event handling.
this.previewEl = createNode({
parent: this.el,
nodeType: "span",
attributes: {
"title": L10N.getStr("node.selectNodeLabel")
}
});
if (!this.options.compact) {
this.previewEl.appendChild(document.createTextNode("<"));
}
// Tag name.
this.tagNameEl = createNode({
parent: this.previewEl,
nodeType: "span",
attributes: {
"class": "tag-name theme-fg-color3"
}
});
// Id attribute container.
this.idEl = createNode({
parent: this.previewEl,
nodeType: "span"
});
if (!this.options.compact) {
createNode({
parent: this.idEl,
nodeType: "span",
attributes: {
"class": "attribute-name theme-fg-color2"
},
textContent: "id"
});
this.idEl.appendChild(document.createTextNode("=\""));
} else {
createNode({
parent: this.idEl,
nodeType: "span",
attributes: {
"class": "theme-fg-color2"
},
textContent: "#"
});
}
createNode({
parent: this.idEl,
nodeType: "span",
attributes: {
"class": "attribute-value theme-fg-color6"
}
});
if (!this.options.compact) {
this.idEl.appendChild(document.createTextNode("\""));
}
// Class attribute container.
this.classEl = createNode({
parent: this.previewEl,
nodeType: "span"
});
if (!this.options.compact) {
createNode({
parent: this.classEl,
nodeType: "span",
attributes: {
"class": "attribute-name theme-fg-color2"
},
textContent: "class"
});
this.classEl.appendChild(document.createTextNode("=\""));
} else {
createNode({
parent: this.classEl,
nodeType: "span",
attributes: {
"class": "theme-fg-color6"
},
textContent: "."
});
}
createNode({
parent: this.classEl,
nodeType: "span",
attributes: {
"class": "attribute-value theme-fg-color6"
}
});
if (!this.options.compact) {
this.classEl.appendChild(document.createTextNode("\""));
this.previewEl.appendChild(document.createTextNode(">"));
}
this.startListeners();
},
startListeners: function() {
// Init events for highlighting and selecting the node.
this.previewEl.addEventListener("mouseover", this.onPreviewMouseOver);
this.previewEl.addEventListener("mouseout", this.onPreviewMouseOut);
this.previewEl.addEventListener("click", this.onSelectNodeClick);
this.highlightNodeEl.addEventListener("click", this.onHighlightNodeClick);
// Start to listen for markupmutation events.
this.inspector.on("markupmutation", this.onMarkupMutations);
// Listen to the target node highlighter.
TargetNodeHighlighter.on("highlighted", this.onTargetHighlighterLocked);
},
stopListeners: function() {
TargetNodeHighlighter.off("highlighted", this.onTargetHighlighterLocked);
this.inspector.off("markupmutation", this.onMarkupMutations);
this.previewEl.removeEventListener("mouseover", this.onPreviewMouseOver);
this.previewEl.removeEventListener("mouseout", this.onPreviewMouseOut);
this.previewEl.removeEventListener("click", this.onSelectNodeClick);
this.highlightNodeEl.removeEventListener("click", this.onHighlightNodeClick);
this.previewer.init(containerEl);
this.isDestroyed = false;
},
destroy: function() {
TargetNodeHighlighter.unhighlight().catch(e => console.error(e));
this.stopListeners();
this.el.remove();
this.el = this.tagNameEl = this.idEl = this.classEl = null;
this.highlightNodeEl = this.previewEl = null;
this.nodeFront = this.inspector = this.playerFront = null;
},
get highlighterUtils() {
if (this.inspector && this.inspector.toolbox) {
return this.inspector.toolbox.highlighterUtils;
}
return null;
},
onPreviewMouseOver: function() {
if (!this.nodeFront || !this.highlighterUtils) {
return;
}
this.highlighterUtils.highlightNodeFront(this.nodeFront)
.catch(e => console.error(e));
},
onPreviewMouseOut: function() {
if (!this.nodeFront || !this.highlighterUtils) {
return;
}
this.highlighterUtils.unhighlight()
.catch(e => console.error(e));
},
onSelectNodeClick: function() {
if (!this.nodeFront) {
return;
}
this.inspector.selection.setNodeFront(this.nodeFront, "animationinspector");
},
onHighlightNodeClick: function(e) {
e.stopPropagation();
let classList = this.highlightNodeEl.classList;
let isHighlighted = classList.contains("selected");
if (isHighlighted) {
classList.remove("selected");
TargetNodeHighlighter.unhighlight().then(() => {
this.emit("target-highlighter-unlocked");
}, e => console.error(e));
} else {
classList.add("selected");
TargetNodeHighlighter.highlight(this).then(() => {
this.emit("target-highlighter-locked");
}, e => console.error(e));
}
},
onTargetHighlighterLocked: function(e, animationTargetNode) {
if (animationTargetNode !== this) {
this.highlightNodeEl.classList.remove("selected");
}
},
onMarkupMutations: function(e, mutations) {
if (!this.nodeFront || !this.playerFront) {
return;
}
for (let {target} of mutations) {
if (target === this.nodeFront) {
// Re-render with the same nodeFront to update the output.
this.render(this.playerFront);
break;
}
}
this.previewer.destroy();
this.inspector = null;
this.isDestroyed = true;
},
render: Task.async(function*(playerFront) {
this.playerFront = playerFront;
this.nodeFront = undefined;
// Get the nodeFront from the cache if it was stored previously.
let nodeFront = nodeFronts.get(playerFront);
try {
this.nodeFront = yield this.inspector.walker.getNodeFromActor(
playerFront.actorID, ["node"]);
} catch (e) {
if (!this.el) {
// The panel was destroyed in the meantime. Just log a warning.
console.warn("Cound't retrieve the animation target node, widget " +
"destroyed");
} else {
// This was an unexpected error, log it.
console.error(e);
}
return;
// Try and get it from the playerFront directly next.
if (!nodeFront) {
nodeFront = playerFront.animationTargetNodeFront;
}
if (!this.nodeFront || !this.el) {
return;
}
let {tagName, attributes} = this.nodeFront;
this.tagNameEl.textContent = tagName.toLowerCase();
let idIndex = attributes.findIndex(({name}) => name === "id");
if (idIndex > -1 && attributes[idIndex].value) {
this.idEl.querySelector(".attribute-value").textContent =
attributes[idIndex].value;
this.idEl.style.display = "inline";
} else {
this.idEl.style.display = "none";
}
let classIndex = attributes.findIndex(({name}) => name === "class");
if (classIndex > -1 && attributes[classIndex].value) {
let value = attributes[classIndex].value;
if (this.options.compact) {
value = value.split(" ").join(".");
// Finally, get it from the walkerActor if it wasn't found.
if (!nodeFront) {
try {
nodeFront = yield this.inspector.walker.getNodeFromActor(
playerFront.actorID, ["node"]);
} catch (e) {
// If an error occured while getting the nodeFront and if it can't be
// attributed to the panel having been destroyed in the meantime, this
// error needs to be logged and render needs to stop.
if (!this.isDestroyed) {
console.error(e);
}
return;
}
this.classEl.querySelector(".attribute-value").textContent = value;
this.classEl.style.display = "inline";
} else {
this.classEl.style.display = "none";
// In all cases, if by now the panel doesn't exist anymore, we need to
// stop rendering too.
if (this.isDestroyed) {
return;
}
}
// Add the nodeFront to the cache.
nodeFronts.set(playerFront, nodeFront);
this.previewer.render(nodeFront);
this.emit("target-retrieved");
})
};

View File

@ -25,7 +25,7 @@ add_task(function*() {
// Reduce the known nodeFronts to a set to make them unique.
let nodeFronts = new Set(panel.animationsTimelineComponent
.targetNodes.map(n => n.nodeFront));
.targetNodes.map(n => n.previewer.nodeFront));
is(nodeFronts.size, 3,
"The animations are applied to 3 different node fronts");
});

View File

@ -16,16 +16,18 @@ add_task(function*() {
yield selectNode(".animated", inspector);
let targetNodeComponent = panel.animationsTimelineComponent.targetNodes[0];
let {previewer} = targetNodeComponent;
// Make sure to wait for the target-retrieved event if the nodeFront hasn't
// yet been retrieved by the TargetNodeComponent.
if (!targetNodeComponent.nodeFront) {
if (!previewer.nodeFront) {
yield targetNodeComponent.once("target-retrieved");
}
is(targetNodeComponent.el.textContent, "div#.ball.animated",
is(previewer.el.textContent, "div#.ball.animated",
"The target element's content is correct");
let highlighterEl = targetNodeComponent.el.querySelector(".node-highlighter");
let highlighterEl = previewer.el.querySelector(".node-highlighter");
ok(highlighterEl,
"The icon to highlight the target element in the page exists");
});

View File

@ -24,7 +24,7 @@ add_task(function*() {
let targetNodeComponent = targets[0];
info("Retrieve the part of the widget that highlights the node on hover");
let highlightingEl = targetNodeComponent.previewEl;
let highlightingEl = targetNodeComponent.previewer.previewEl;
info("Listen to node-highlight event and mouse over the widget");
let onHighlight = toolbox.once("node-highlight");
@ -39,7 +39,7 @@ add_task(function*() {
highlightingEl.ownerDocument.defaultView);
ok(true, "The node-highlight event was fired");
is(targetNodeComponent.nodeFront, nodeFront,
is(targetNodeComponent.previewer.nodeFront, nodeFront,
"The highlighted node is the one stored on the animation widget");
is(nodeFront.tagName, "DIV",
"The highlighted node has the correct tagName");
@ -60,12 +60,12 @@ add_task(function*() {
"selection to change");
let onSelection = inspector.selection.once("new-node-front");
onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
let nodeEl = targetNodeComponent.previewEl;
let nodeEl = targetNodeComponent.previewer.previewEl;
EventUtils.sendMouseEvent({type: "click"}, nodeEl,
nodeEl.ownerDocument.defaultView);
yield onSelection;
is(inspector.selection.nodeFront, targetNodeComponent.nodeFront,
is(inspector.selection.nodeFront, targetNodeComponent.previewer.nodeFront,
"The selected node is the one stored on the animation widget");
yield onPanelUpdated;

View File

@ -11,42 +11,44 @@ requestLongerTimeout(2);
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {toolbox, inspector, panel} = yield openAnimationInspector();
let {panel} = yield openAnimationInspector();
let targets = panel.animationsTimelineComponent.targetNodes;
info("Click on the highlighter icon for the first animated node");
yield lockHighlighterOn(targets[0]);
ok(targets[0].highlightNodeEl.classList.contains("selected"),
let domNodePreview1 = targets[0].previewer;
yield lockHighlighterOn(domNodePreview1);
ok(domNodePreview1.highlightNodeEl.classList.contains("selected"),
"The highlighter icon is selected");
info("Click on the highlighter icon for the second animated node");
yield lockHighlighterOn(targets[1]);
ok(targets[1].highlightNodeEl.classList.contains("selected"),
let domNodePreview2 = targets[1].previewer;
yield lockHighlighterOn(domNodePreview2);
ok(domNodePreview2.highlightNodeEl.classList.contains("selected"),
"The highlighter icon is selected");
ok(!targets[0].highlightNodeEl.classList.contains("selected"),
ok(!domNodePreview1.highlightNodeEl.classList.contains("selected"),
"The highlighter icon for the first node is unselected");
info("Click again to unhighlight");
yield unlockHighlighterOn(targets[1]);
ok(!targets[1].highlightNodeEl.classList.contains("selected"),
yield unlockHighlighterOn(domNodePreview2);
ok(!domNodePreview2.highlightNodeEl.classList.contains("selected"),
"The highlighter icon for the second node is unselected");
});
function* lockHighlighterOn(targetComponent) {
let onLocked = targetComponent.once("target-highlighter-locked");
clickOnHighlighterIcon(targetComponent);
function* lockHighlighterOn(domNodePreview) {
let onLocked = domNodePreview.once("target-highlighter-locked");
clickOnHighlighterIcon(domNodePreview);
yield onLocked;
}
function* unlockHighlighterOn(targetComponent) {
let onUnlocked = targetComponent.once("target-highlighter-unlocked");
clickOnHighlighterIcon(targetComponent);
function* unlockHighlighterOn(domNodePreview) {
let onUnlocked = domNodePreview.once("target-highlighter-unlocked");
clickOnHighlighterIcon(domNodePreview);
yield onUnlocked;
}
function clickOnHighlighterIcon(targetComponent) {
let lockEl = targetComponent.highlightNodeEl;
function clickOnHighlighterIcon(domNodePreview) {
let lockEl = domNodePreview.highlightNodeEl;
EventUtils.sendMouseEvent({type: "click"}, lockEl,
lockEl.ownerDocument.defaultView);
}

View File

@ -351,7 +351,7 @@ function isNodeVisible(node) {
var waitForAllAnimationTargets = Task.async(function*(panel) {
let targets = panel.animationsTimelineComponent.targetNodes;
yield promise.all(targets.map(t => {
if (!t.nodeFront) {
if (!t.previewer.nodeFront) {
return t.once("target-retrieved");
}
return false;

View File

@ -146,43 +146,6 @@ function findOptimalTimeInterval(timeScale,
exports.findOptimalTimeInterval = findOptimalTimeInterval;
/**
* The TargetNodeHighlighter util is a helper for AnimationTargetNode components
* that is used to lock the highlighter on animated nodes in the page.
* It instantiates a new highlighter that is then shared amongst all instances
* of AnimationTargetNode. This is useful because that means showing the
* highlighter on one animated node will unhighlight the previously highlighted
* one, but will not interfere with the default inspector highlighter.
*/
var TargetNodeHighlighter = {
highlighter: null,
isShown: false,
highlight: Task.async(function*(animationTargetNode) {
if (!this.highlighter) {
let hUtils = animationTargetNode.inspector.toolbox.highlighterUtils;
this.highlighter = yield hUtils.getHighlighterByType("BoxModelHighlighter");
}
yield this.highlighter.show(animationTargetNode.nodeFront);
this.isShown = true;
this.emit("highlighted", animationTargetNode);
}),
unhighlight: Task.async(function*() {
if (!this.highlighter || !this.isShown) {
return;
}
yield this.highlighter.hide();
this.isShown = false;
this.emit("unhighlighted");
})
};
EventEmitter.decorate(TargetNodeHighlighter);
exports.TargetNodeHighlighter = TargetNodeHighlighter;
/**
* Format a timestamp (in ms) as a mm:ss.mmm string.
* @param {Number} time

View File

@ -0,0 +1,333 @@
/* 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 {Cu} = require("chrome");
Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
const {createNode} = require("devtools/client/animationinspector/utils");
const STRINGS_URI = "chrome://devtools/locale/inspector.properties";
const L10N = new ViewHelpers.L10N(STRINGS_URI);
/**
* UI component responsible for displaying a preview of a dom node.
* @param {InspectorPanel} inspector Requires a reference to the inspector-panel
* to highlight and select the node, as well as refresh it when there are
* mutations.
* @param {Object} options Supported properties are:
* - compact {Boolean} Defaults to false.
* By default, nodes are previewed like <tag id="id" class="class">
* If true, nodes will be previewed like tag#id.class instead.
*/
function DomNodePreview(inspector, options = {}) {
this.inspector = inspector;
this.options = options;
this.onPreviewMouseOver = this.onPreviewMouseOver.bind(this);
this.onPreviewMouseOut = this.onPreviewMouseOut.bind(this);
this.onSelectElClick = this.onSelectElClick.bind(this);
this.onMarkupMutations = this.onMarkupMutations.bind(this);
this.onHighlightElClick = this.onHighlightElClick.bind(this);
this.onHighlighterLocked = this.onHighlighterLocked.bind(this);
EventEmitter.decorate(this);
}
exports.DomNodePreview = DomNodePreview;
DomNodePreview.prototype = {
init: function(containerEl) {
let document = containerEl.ownerDocument;
// Init the markup for displaying the target node.
this.el = createNode({
parent: containerEl,
attributes: {
"class": "animation-target"
}
});
// Icon to select the node in the inspector.
this.highlightNodeEl = createNode({
parent: this.el,
nodeType: "span",
attributes: {
"class": "node-highlighter",
"title": L10N.getStr("inspector.nodePreview.highlightNodeLabel")
}
});
// Wrapper used for mouseover/out event handling.
this.previewEl = createNode({
parent: this.el,
nodeType: "span",
attributes: {
"title": L10N.getStr("inspector.nodePreview.selectNodeLabel")
}
});
if (!this.options.compact) {
this.previewEl.appendChild(document.createTextNode("<"));
}
// Tag name.
this.tagNameEl = createNode({
parent: this.previewEl,
nodeType: "span",
attributes: {
"class": "tag-name theme-fg-color3"
}
});
// Id attribute container.
this.idEl = createNode({
parent: this.previewEl,
nodeType: "span"
});
if (!this.options.compact) {
createNode({
parent: this.idEl,
nodeType: "span",
attributes: {
"class": "attribute-name theme-fg-color2"
},
textContent: "id"
});
this.idEl.appendChild(document.createTextNode("=\""));
} else {
createNode({
parent: this.idEl,
nodeType: "span",
attributes: {
"class": "theme-fg-color6"
},
textContent: "#"
});
}
createNode({
parent: this.idEl,
nodeType: "span",
attributes: {
"class": "attribute-value theme-fg-color6"
}
});
if (!this.options.compact) {
this.idEl.appendChild(document.createTextNode("\""));
}
// Class attribute container.
this.classEl = createNode({
parent: this.previewEl,
nodeType: "span"
});
if (!this.options.compact) {
createNode({
parent: this.classEl,
nodeType: "span",
attributes: {
"class": "attribute-name theme-fg-color2"
},
textContent: "class"
});
this.classEl.appendChild(document.createTextNode("=\""));
} else {
createNode({
parent: this.classEl,
nodeType: "span",
attributes: {
"class": "theme-fg-color6"
},
textContent: "."
});
}
createNode({
parent: this.classEl,
nodeType: "span",
attributes: {
"class": "attribute-value theme-fg-color6"
}
});
if (!this.options.compact) {
this.classEl.appendChild(document.createTextNode("\""));
this.previewEl.appendChild(document.createTextNode(">"));
}
this.startListeners();
},
startListeners: function() {
// Init events for highlighting and selecting the node.
this.previewEl.addEventListener("mouseover", this.onPreviewMouseOver);
this.previewEl.addEventListener("mouseout", this.onPreviewMouseOut);
this.previewEl.addEventListener("click", this.onSelectElClick);
this.highlightNodeEl.addEventListener("click", this.onHighlightElClick);
// Start to listen for markupmutation events.
this.inspector.on("markupmutation", this.onMarkupMutations);
// Listen to the target node highlighter.
HighlighterLock.on("highlighted", this.onHighlighterLocked);
},
stopListeners: function() {
HighlighterLock.off("highlighted", this.onHighlighterLocked);
this.inspector.off("markupmutation", this.onMarkupMutations);
this.previewEl.removeEventListener("mouseover", this.onPreviewMouseOver);
this.previewEl.removeEventListener("mouseout", this.onPreviewMouseOut);
this.previewEl.removeEventListener("click", this.onSelectElClick);
this.highlightNodeEl.removeEventListener("click", this.onHighlightElClick);
},
destroy: function() {
HighlighterLock.unhighlight().catch(e => console.error(e));
this.stopListeners();
this.el.remove();
this.el = this.tagNameEl = this.idEl = this.classEl = null;
this.highlightNodeEl = this.previewEl = null;
this.nodeFront = this.inspector = null;
},
get highlighterUtils() {
if (this.inspector && this.inspector.toolbox) {
return this.inspector.toolbox.highlighterUtils;
}
return null;
},
onPreviewMouseOver: function() {
if (!this.nodeFront || !this.highlighterUtils) {
return;
}
this.highlighterUtils.highlightNodeFront(this.nodeFront)
.catch(e => console.error(e));
},
onPreviewMouseOut: function() {
if (!this.nodeFront || !this.highlighterUtils) {
return;
}
this.highlighterUtils.unhighlight()
.catch(e => console.error(e));
},
onSelectElClick: function() {
if (!this.nodeFront) {
return;
}
this.inspector.selection.setNodeFront(this.nodeFront, "dom-node-preview");
},
onHighlightElClick: function(e) {
e.stopPropagation();
let classList = this.highlightNodeEl.classList;
let isHighlighted = classList.contains("selected");
if (isHighlighted) {
classList.remove("selected");
HighlighterLock.unhighlight().then(() => {
this.emit("target-highlighter-unlocked");
}, error => console.error(error));
} else {
classList.add("selected");
HighlighterLock.highlight(this).then(() => {
this.emit("target-highlighter-locked");
}, error => console.error(error));
}
},
onHighlighterLocked: function(e, domNodePreview) {
if (domNodePreview !== this) {
this.highlightNodeEl.classList.remove("selected");
}
},
onMarkupMutations: function(e, mutations) {
if (!this.nodeFront) {
return;
}
for (let {target} of mutations) {
if (target === this.nodeFront) {
// Re-render with the same nodeFront to update the output.
this.render(this.nodeFront);
break;
}
}
},
render: function(nodeFront) {
this.nodeFront = nodeFront;
let {tagName, attributes} = nodeFront;
this.tagNameEl.textContent = tagName.toLowerCase();
let idIndex = attributes.findIndex(({name}) => name === "id");
if (idIndex > -1 && attributes[idIndex].value) {
this.idEl.querySelector(".attribute-value").textContent =
attributes[idIndex].value;
this.idEl.style.display = "inline";
} else {
this.idEl.style.display = "none";
}
let classIndex = attributes.findIndex(({name}) => name === "class");
if (classIndex > -1 && attributes[classIndex].value) {
let value = attributes[classIndex].value;
if (this.options.compact) {
value = value.split(" ").join(".");
}
this.classEl.querySelector(".attribute-value").textContent = value;
this.classEl.style.display = "inline";
} else {
this.classEl.style.display = "none";
}
}
};
/**
* HighlighterLock is a helper used to lock the highlighter on DOM nodes in the
* page.
* It instantiates a new highlighter that is then shared amongst all instances
* of DomNodePreview. This is useful because that means showing the highlighter
* on one node will unhighlight the previously highlighted one, but will not
* interfere with the default inspector highlighter.
*/
var HighlighterLock = {
highlighter: null,
isShown: false,
highlight: Task.async(function*(animationTargetNode) {
if (!this.highlighter) {
let util = animationTargetNode.inspector.toolbox.highlighterUtils;
this.highlighter = yield util.getHighlighterByType("BoxModelHighlighter");
}
yield this.highlighter.show(animationTargetNode.nodeFront);
this.isShown = true;
this.emit("highlighted", animationTargetNode);
}),
unhighlight: Task.async(function*() {
if (!this.highlighter || !this.isShown) {
return;
}
yield this.highlighter.hide();
this.isShown = false;
this.emit("unhighlighted");
})
};
EventEmitter.decorate(HighlighterLock);

View File

@ -5,6 +5,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
'dom-node-preview.js',
'style-inspector-menu.js',
'style-inspector-overlays.js',
'utils.js'

View File

@ -32,11 +32,16 @@ const TOOLTIP_IMAGE_TYPE = "image";
const TOOLTIP_FONTFAMILY_TYPE = "font-family";
// Types of nodes in the rule/computed-view
const VIEW_NODE_SELECTOR_TYPE = exports.VIEW_NODE_SELECTOR_TYPE = 1;
const VIEW_NODE_PROPERTY_TYPE = exports.VIEW_NODE_PROPERTY_TYPE = 2;
const VIEW_NODE_VALUE_TYPE = exports.VIEW_NODE_VALUE_TYPE = 3;
const VIEW_NODE_IMAGE_URL_TYPE = exports.VIEW_NODE_IMAGE_URL_TYPE = 4;
const VIEW_NODE_LOCATION_TYPE = exports.VIEW_NODE_LOCATION_TYPE = 5;
const VIEW_NODE_SELECTOR_TYPE = 1;
exports.VIEW_NODE_SELECTOR_TYPE = VIEW_NODE_SELECTOR_TYPE;
const VIEW_NODE_PROPERTY_TYPE = 2;
exports.VIEW_NODE_PROPERTY_TYPE = VIEW_NODE_PROPERTY_TYPE;
const VIEW_NODE_VALUE_TYPE = 3;
exports.VIEW_NODE_VALUE_TYPE = VIEW_NODE_VALUE_TYPE;
const VIEW_NODE_IMAGE_URL_TYPE = 4;
exports.VIEW_NODE_IMAGE_URL_TYPE = VIEW_NODE_IMAGE_URL_TYPE;
const VIEW_NODE_LOCATION_TYPE = 5;
exports.VIEW_NODE_LOCATION_TYPE = VIEW_NODE_LOCATION_TYPE;
/**
* Manages all highlighters in the style-inspector.
@ -183,9 +188,9 @@ HighlightersOverlay.prototype = {
// promise. This causes some tests to fail when trying to install a
// rejection handler on the result of the call. To avoid this, check
// whether the result is truthy before installing the handler.
let promise = this.highlighters[this.highlighterShown].hide();
if (promise) {
promise.then(null, e => console.error(e));
let onHidden = this.highlighters[this.highlighterShown].hide();
if (onHidden) {
onHidden.then(null, e => console.error(e));
}
this.highlighterShown = null;

View File

@ -6,7 +6,7 @@
"use strict";
const {Cc, Ci, Cu} = require("chrome");
const {Ci, Cu} = require("chrome");
const {setTimeout, clearTimeout} =
Cu.import("resource://gre/modules/Timer.jsm", {});
const {parseDeclarations} =
@ -28,7 +28,7 @@ const HTML_NS = "http://www.w3.org/1999/xhtml";
* @param {object} attributes
* A set of attributes to set on the node.
*/
function createChild(parent, tagName, attributes={}) {
function createChild(parent, tagName, attributes = {}) {
let elt = parent.ownerDocument.createElementNS(HTML_NS, tagName);
for (let attr in attributes) {
if (attributes.hasOwnProperty(attr)) {

View File

@ -93,17 +93,3 @@ timeline.csstransition.nameLabel=%S - CSS Transition
# This can happen if devtools couldn't figure out the type of the animation.
# %S will be replaced by the name of the transition at run-time.
timeline.unknown.nameLabel=%S
# LOCALIZATION NOTE (node.selectNodeLabel):
# This string is displayed in a tooltip of the animation panel that is shown
# when hovering over an animated node (e.g. something like div.animated).
# The tooltip invites the user to click on the node in order to select it in the
# inspector panel.
node.selectNodeLabel=Click to select this node in the Inspector
# LOCALIZATION NOTE (node.highlightNodeLabel):
# This string is displayed in a tooltip of the animation panel that is shown
# when hovering over the inspector icon displayed next to animated nodes.
# The tooltip invites the user to click on the icon in order to show the node
# highlighter.
node.highlightNodeLabel=Click to highlight this node in the page

View File

@ -116,3 +116,22 @@ inspector.menu.editAttribute.label=Edit Attribute %S
# when the user right-clicks on the attribute of a node in the inspector,
# and that allows to remove this attribute.
inspector.menu.removeAttribute.label=Remove Attribute %S
# LOCALIZATION NOTE (inspector.nodePreview.selectNodeLabel):
# This string is displayed in a tooltip that is shown when hovering over a DOM
# node preview (e.g. something like "div#foo.bar").
# DOM node previews can be displayed in places like the animation-inspector, the
# console or the object inspector.
# The tooltip invites the user to click on the node in order to select it in the
# inspector panel.
inspector.nodePreview.selectNodeLabel=Click to select this node in the Inspector
# LOCALIZATION NOTE (inspector.nodePreview.highlightNodeLabel):
# This string is displayed in a tooltip that is shown when hovering over a the
# inspector icon displayed next to a DOM node preview (e.g. next to something
# like "div#foo.bar").
# DOM node previews can be displayed in places like the animation-inspector, the
# console or the object inspector.
# The tooltip invites the user to click on the icon in order to highlight the
# node in the page.
inspector.nodePreview.highlightNodeLabel=Click to highlight this node in the page

View File

@ -70,6 +70,7 @@ var AnimationPlayerActor = ActorClass({
this.onAnimationMutation = this.onAnimationMutation.bind(this);
this.walker = animationsActor.walker;
this.tabActor = animationsActor.tabActor;
this.player = player;
this.node = player.effect.target;
@ -89,7 +90,9 @@ var AnimationPlayerActor = ActorClass({
if (this.observer && !Cu.isDeadWrapper(this.observer)) {
this.observer.disconnect();
}
this.tabActor = this.player = this.node = this.styles = this.observer = null;
this.tabActor = this.player = this.node = this.styles = null;
this.observer = this.walker = null;
Actor.prototype.destroy.call(this);
},
@ -107,6 +110,12 @@ var AnimationPlayerActor = ActorClass({
let data = this.getCurrentState();
data.actor = this.actorID;
// If we know the WalkerActor, and if the animated node is known by it, then
// return its corresponding NodeActor ID too.
if (this.walker && this.walker.hasNode(this.node)) {
data.animationTargetNodeActorID = this.walker.getNode(this.node).actorID;
}
return data;
},
@ -381,6 +390,18 @@ var AnimationPlayerFront = FrontClass(AnimationPlayerActor, {
Front.prototype.destroy.call(this);
},
/**
* If the AnimationsActor was given a reference to the WalkerActor previously
* then calling this getter will return the animation target NodeFront.
*/
get animationTargetNodeFront() {
if (!this._form.animationTargetNodeActorID) {
return null;
}
return this.conn.getActor(this._form.animationTargetNodeActorID);
},
/**
* Getter for the initial state of the player. Up to date states can be
* retrieved by calling the getCurrentState method.
@ -495,7 +516,7 @@ var AnimationsActor = exports.AnimationsActor = ActorClass({
events.off(this.tabActor, "navigate", this.onNavigate);
this.stopAnimationPlayerUpdates();
this.tabActor = this.observer = this.actors = null;
this.tabActor = this.observer = this.actors = this.walker = null;
},
/**
@ -506,6 +527,23 @@ var AnimationsActor = exports.AnimationsActor = ActorClass({
this.destroy();
},
/**
* Clients can optionally call this with a reference to their WalkerActor.
* If they do, then AnimationPlayerActor's forms are going to also include
* NodeActor IDs when the corresponding NodeActors do exist.
* This, in turns, is helpful for clients to avoid having to go back once more
* to the server to get a NodeActor for a particular animation.
* @param {WalkerActor} walker
*/
setWalkerActor: method(function(walker) {
this.walker = walker;
}, {
request: {
walker: Arg(0, "domwalker")
},
response: {}
}),
/**
* Retrieve the list of AnimationPlayerActor actors for currently running
* animations on a node and its descendants.

View File

@ -1367,7 +1367,7 @@ var WalkerActor = protocol.ActorClass({
let target = current.target;
if (this._refMap.has(target)) {
let actor = this._refMap.get(target);
let actor = this.getNode(target);
let mutation = {
type: "events",
target: actor.actorID,
@ -1469,13 +1469,30 @@ var WalkerActor = protocol.ActorClass({
protocol.Actor.prototype.unmanage.call(this, actor);
},
hasNode: function(node) {
return this._refMap.has(node);
/**
* Determine if the walker has come across this DOM node before.
* @param {DOMNode} rawNode
* @return {Boolean}
*/
hasNode: function(rawNode) {
return this._refMap.has(rawNode);
},
/**
* If the walker has come across this DOM node before, then get the
* corresponding node actor.
* @param {DOMNode} rawNode
* @return {NodeActor}
*/
getNode: function(rawNode) {
return this._refMap.get(rawNode);
},
_ref: function(node) {
let actor = this._refMap.get(node);
if (actor) return actor;
let actor = this.getNode(node);
if (actor) {
return actor;
}
actor = new NodeActor(this, node);
@ -1763,7 +1780,7 @@ var WalkerActor = protocol.ActorClass({
let child = walker.firstChild();
while (child) {
let childActor = this._refMap.get(child);
let childActor = this.getNode(child);
if (childActor) {
this.releaseNode(childActor, options);
}
@ -1789,7 +1806,7 @@ var WalkerActor = protocol.ActorClass({
let walker = this.getDocumentWalker(node.rawNode);
let cur;
while ((cur = walker.parentNode())) {
let parent = this._refMap.get(cur);
let parent = this.getNode(cur);
if (!parent) {
// This parent didn't exist, so hasn't been seen by the client yet.
newParents.add(this._ref(cur));
@ -2942,7 +2959,7 @@ var WalkerActor = protocol.ActorClass({
events.emit(this, "any-mutation");
for (let change of mutations) {
let targetActor = this._refMap.get(change.target);
let targetActor = this.getNode(change.target);
if (!targetActor) {
continue;
}
@ -2972,7 +2989,7 @@ var WalkerActor = protocol.ActorClass({
let removedActors = [];
let addedActors = [];
for (let removed of change.removedNodes) {
let removedActor = this._refMap.get(removed);
let removedActor = this.getNode(removed);
if (!removedActor) {
// If the client never encountered this actor we don't need to
// mention that it was removed.
@ -2983,7 +3000,7 @@ var WalkerActor = protocol.ActorClass({
removedActors.push(removedActor.actorID);
}
for (let added of change.addedNodes) {
let addedActor = this._refMap.get(added);
let addedActor = this.getNode(added);
if (!addedActor) {
// If the client never encounted this actor we don't need to tell
// it about its addition for ownership tree purposes - if the
@ -3021,7 +3038,7 @@ var WalkerActor = protocol.ActorClass({
return;
}
let frame = getFrameElement(window);
let frameActor = this._refMap.get(frame);
let frameActor = this.getNode(frame);
if (!frameActor) {
return;
}
@ -3076,7 +3093,7 @@ var WalkerActor = protocol.ActorClass({
}
let doc = window.document;
let documentActor = this._refMap.get(doc);
let documentActor = this.getNode(doc);
if (!documentActor) {
return;
}
@ -3098,7 +3115,7 @@ var WalkerActor = protocol.ActorClass({
// they should reread the children list.
this.queueMutation({
type: "childList",
target: this._refMap.get(parentNode).actorID,
target: this.getNode(parentNode).actorID,
added: [],
removed: []
});