--- a/devtools/client/animationinspector/animation-controller.js
+++ b/devtools/client/animationinspector/animation-controller.js
@@ -88,17 +88,19 @@ var getServerTraits = Task.async(functio
method: "setPlaybackRate" },
{ name: "hasSetPlaybackRates", actor: "animations",
method: "setPlaybackRates" },
{ name: "hasTargetNode", actor: "domwalker",
method: "getNodeFromActor" },
{ name: "hasSetCurrentTimes", actor: "animations",
method: "setCurrentTimes" },
{ name: "hasGetFrames", actor: "animationplayer",
- method: "getFrames" }
+ method: "getFrames" },
+ { name: "hasSetWalkerActor", actor: "animations",
+ method: "setWalkerActor" }
];
let traits = {};
for (let {name, actor, method} of config) {
traits[name] = yield target.actorHasMethod(actor, method);
}
return traits;
@@ -142,16 +144,22 @@ var AnimationsController = {
// Expose actor capabilities.
this.traits = yield getServerTraits(target);
if (this.destroyed) {
console.warn("Could not fully initialize the 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();
this.initialized.resolve();
}),
destroy: Task.async(function*() {
if (!this.initialized) {
--- a/devtools/client/animationinspector/components/animation-target-node.js
+++ b/devtools/client/animationinspector/components/animation-target-node.js
@@ -1,320 +1,76 @@
"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,
- 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);
}
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;
- }
-
- if (!this.nodeFront || !this.el) {
- return;
+ // Try and get it from the playerFront directly next.
+ if (!nodeFront) {
+ nodeFront = playerFront.animationTargetNodeFront;
}
- 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";
+ // 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;
+ } finally {
+ // In all cases, if by now the panel doesn't exist anymore, we need to
+ // stop rendering.
+ if (this.isDestroyed) {
+ return;
+ }
+ }
}
- 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(".");
- }
+ // Add the nodeFront to the cache.
+ nodeFronts.set(playerFront, nodeFront);
- this.classEl.querySelector(".attribute-value").textContent = value;
- this.classEl.style.display = "inline";
- } else {
- this.classEl.style.display = "none";
- }
-
+ this.previewer.render(nodeFront);
this.emit("target-retrieved");
})
};
--- a/devtools/client/animationinspector/test/browser_animation_mutations_with_same_names.js
+++ b/devtools/client/animationinspector/test/browser_animation_mutations_with_same_names.js
@@ -20,12 +20,12 @@ add_task(function*() {
}
yield waitForAllAnimationTargets(panel);
is(panel.animationsTimelineComponent.animations.length, 3,
"The timeline shows 3 animations too");
// 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");
});
--- a/devtools/client/animationinspector/test/browser_animation_playerWidgets_target_nodes.js
+++ b/devtools/client/animationinspector/test/browser_animation_playerWidgets_target_nodes.js
@@ -11,21 +11,23 @@ requestLongerTimeout(2);
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {inspector, panel} = yield openAnimationInspector();
info("Select the simple animated node");
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");
});
--- a/devtools/client/animationinspector/test/browser_animation_target_highlight_select.js
+++ b/devtools/client/animationinspector/test/browser_animation_target_highlight_select.js
@@ -19,32 +19,32 @@ add_task(function*() {
yield selectNode(".animated", inspector);
yield onPanelUpdated;
let targets = yield waitForAllAnimationTargets(panel);
// Arbitrary select the first one
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");
EventUtils.synthesizeMouse(highlightingEl, 10, 5, {type: "mouseover"},
highlightingEl.ownerDocument.defaultView);
let nodeFront = yield onHighlight;
// Do not forget to mouseout, otherwise we get random mouseover event
// when selecting another node, which triggers some requests in animation
// inspector.
EventUtils.synthesizeMouse(highlightingEl, 10, 5, {type: "mouseout"},
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");
is(nodeFront.attributes[0].name, "class",
"The highlighted node has the correct attributes");
is(nodeFront.attributes[0].value, "ball animated",
"The highlighted node has the correct class");
@@ -55,19 +55,19 @@ add_task(function*() {
targets = yield waitForAllAnimationTargets(panel);
targetNodeComponent = targets[0];
info("Click on the first animated node component and wait for the " +
"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;
yield waitForAllAnimationTargets(panel);
});
--- a/devtools/client/animationinspector/test/browser_animation_target_highlighter_lock.js
+++ b/devtools/client/animationinspector/test/browser_animation_target_highlighter_lock.js
@@ -6,47 +6,49 @@
requestLongerTimeout(2);
// Test that the DOM element targets displayed in animation player widgets can
// be used to highlight elements in the DOM and select them in the inspector.
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);
}
--- a/devtools/client/animationinspector/test/head.js
+++ b/devtools/client/animationinspector/test/head.js
@@ -346,17 +346,17 @@ function isNodeVisible(node) {
* Wait for all AnimationTargetNode instances to be fully loaded
* (fetched their related actor and rendered), and return them.
* @param {AnimationsPanel} panel
* @return {Array} all AnimationTargetNode instances
*/
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;
}));
return targets;
});
/**
--- a/devtools/client/animationinspector/utils.js
+++ b/devtools/client/animationinspector/utils.js
@@ -142,53 +142,16 @@ function findOptimalTimeInterval(timeSca
}
return scaledStep;
}
}
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
* @return {String}
*/
function formatStopwatchTime(time) {
// Format falsy values as 0
if (!time) {
return "00:00.000";
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/shared/dom-node-preview.js
@@ -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.onSelectNodeClick = this.onSelectNodeClick.bind(this);
+ this.onMarkupMutations = this.onMarkupMutations.bind(this);
+ this.onHighlightNodeClick = this.onHighlightNodeClick.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.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.
+ 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.onSelectNodeClick);
+ this.highlightNodeEl.removeEventListener("click", this.onHighlightNodeClick);
+ },
+
+ 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));
+ },
+
+ onSelectNodeClick: function() {
+ if (!this.nodeFront) {
+ return;
+ }
+ this.inspector.selection.setNodeFront(this.nodeFront, "dom-node-preview");
+ },
+
+ onHighlightNodeClick: 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 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(HighlighterLock);
--- a/devtools/client/inspector/shared/moz.build
+++ b/devtools/client/inspector/shared/moz.build
@@ -1,13 +1,14 @@
# -*- Mode: python; c-basic-offset: 4; 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(
+ 'dom-node-preview.js',
'style-inspector-menu.js',
'style-inspector-overlays.js',
'utils.js'
)
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/devtools/client/locales/en-US/animationinspector.properties
+++ b/devtools/client/locales/en-US/animationinspector.properties
@@ -88,22 +88,8 @@ timeline.cssanimation.nameLabel=%S - CSS
timeline.csstransition.nameLabel=%S - CSS Transition
# LOCALIZATION NOTE (timeline.unknown.nameLabel):
# This string is displayed in a tooltip of the animation panel that is shown
# when hovering over the name of an unknown animation type in the timeline UI.
# 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
--- a/devtools/client/locales/en-US/inspector.properties
+++ b/devtools/client/locales/en-US/inspector.properties
@@ -111,8 +111,27 @@ inspector.menu.selectElement.label=Selec
# to edit an attribute on this node.
inspector.menu.editAttribute.label=Edit Attribute %S
# LOCALIZATION NOTE (inspector.menu.removeAttribute.label): This is the label of a
# sub-menu "Attribute" in the inspector contextual-menu that appears
# 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
--- a/devtools/server/actors/animation.js
+++ b/devtools/server/actors/animation.js
@@ -65,16 +65,17 @@ var AnimationPlayerActor = ActorClass({
* @param {AnimationsActor} The main AnimationsActor instance
* @param {AnimationPlayer} The player object returned by getAnimationPlayers
*/
initialize: function(animationsActor, player) {
Actor.prototype.initialize.call(this, animationsActor.conn);
this.onAnimationMutation = this.onAnimationMutation.bind(this);
+ this.walker = animationsActor.walker;
this.tabActor = animationsActor.tabActor;
this.player = player;
this.node = player.effect.target;
let win = this.node.ownerDocument.defaultView;
this.styles = win.getComputedStyle(this.node);
// Listen to animation mutations on the node to alert the front when the
@@ -84,17 +85,19 @@ var AnimationPlayerActor = ActorClass({
},
destroy: function() {
// Only try to disconnect the observer if it's not already dead (i.e. if the
// container view hasn't navigated since).
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);
},
/**
* Release the actor, when it isn't needed anymore.
* Protocol.js uses this release method to call the destroy method.
*/
release: method(function() {}, {release: true}),
@@ -102,16 +105,22 @@ var AnimationPlayerActor = ActorClass({
form: function(detail) {
if (detail === "actorid") {
return this.actorID;
}
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;
},
isAnimation: function(player=this.player) {
return player instanceof this.tabActor.window.CSSAnimation;
},
isTransition: function(player=this.player) {
@@ -377,16 +386,28 @@ var AnimationPlayerFront = FrontClass(An
this.state = this.initialState;
},
destroy: function() {
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.
*/
get initialState() {
return {
type: this._form.type,
startTime: this._form.startTime,
previousStartTime: this._form.previousStartTime,
@@ -490,28 +511,45 @@ var AnimationsActor = exports.Animations
},
destroy: function() {
Actor.prototype.destroy.call(this);
events.off(this.tabActor, "will-navigate", this.onWillNavigate);
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;
},
/**
* Since AnimationsActor doesn't have a protocol.js parent actor that takes
* care of its lifetime, implementing disconnect is required to cleanup.
*/
disconnect: function() {
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.
* @param {NodeActor} nodeActor The NodeActor as defined in
* /devtools/server/actors/inspector
*/
getAnimationPlayersForNode: method(function(nodeActor) {
let animations = [
...nodeActor.rawNode.getAnimations(),
--- a/devtools/server/actors/inspector.js
+++ b/devtools/server/actors/inspector.js
@@ -1362,17 +1362,17 @@ var WalkerActor = protocol.ActorClass({
*/
_onEventListenerChange: function(changesEnum) {
let changes = changesEnum.enumerate();
while (changes.hasMoreElements()) {
let current = changes.getNext().QueryInterface(Ci.nsIEventListenerChange);
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,
hasEventListeners: actor._hasEventListeners
};
this.queueMutation(mutation);
}
}
@@ -1464,23 +1464,40 @@ var WalkerActor = protocol.ActorClass({
this._activePseudoClassLocks.has(actor)) {
this.clearPseudoClassLocks(actor);
}
this._refMap.delete(actor.rawNode);
}
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);
// Add the node actor as a child of this walker actor, assigning
// it an actorID.
this.manage(actor);
this._refMap.set(node, actor);
@@ -1758,17 +1775,17 @@ var WalkerActor = protocol.ActorClass({
// Forcing a retained node to go away.
this._retainedOrphans.delete(node);
}
let walker = this.getDocumentWalker(node.rawNode);
let child = walker.firstChild();
while (child) {
- let childActor = this._refMap.get(child);
+ let childActor = this.getNode(child);
if (childActor) {
this.releaseNode(childActor, options);
}
child = walker.nextSibling();
}
node.destroy();
}, {
@@ -1784,17 +1801,17 @@ var WalkerActor = protocol.ActorClass({
*/
ensurePathToRoot: function(node, newParents=new Set()) {
if (!node) {
return newParents;
}
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));
} else {
// This parent did exist, so the client knows about it.
return newParents;
}
}
@@ -2937,17 +2954,17 @@ var WalkerActor = protocol.ActorClass({
*/
onMutations: function(mutations) {
// Notify any observers that want *all* mutations (even on nodes that aren't
// referenced). This is not sent over the protocol so can only be used by
// scripts running in the server process.
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;
}
let targetNode = change.target;
let type = change.type;
let mutation = {
type: type,
target: targetActor.actorID,
@@ -2967,28 +2984,28 @@ var WalkerActor = protocol.ActorClass({
mutation.newValue = targetNode.nodeValue;
}
} else if (type === "childList" || type === "nativeAnonymousChildList") {
// Get the list of removed and added actors that the client has seen
// so that it can keep its ownership tree up to date.
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.
continue;
}
// While removed from the tree, nodes are saved as orphaned.
this._orphaned.add(removedActor);
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
// client wants to see the new nodes it can ask for children.
continue;
}
// The actor is reconnected to the ownership tree, unorphan
// it and let the client know so that its ownership tree is up
@@ -3016,17 +3033,17 @@ var WalkerActor = protocol.ActorClass({
this.rootNode = this.document();
this.queueMutation({
type: "newRoot",
target: this.rootNode.form()
});
return;
}
let frame = getFrameElement(window);
- let frameActor = this._refMap.get(frame);
+ let frameActor = this.getNode(frame);
if (!frameActor) {
return;
}
this.queueMutation({
type: "frameLoad",
target: frameActor.actorID,
});
@@ -3071,17 +3088,17 @@ var WalkerActor = protocol.ActorClass({
this.queueMutation({
target: this.rootNode.actorID,
type: "unretained",
nodes: releasedOrphans
});
}
let doc = window.document;
- let documentActor = this._refMap.get(doc);
+ let documentActor = this.getNode(doc);
if (!documentActor) {
return;
}
if (this.rootDoc === doc) {
this.rootDoc = null;
this.rootNode = null;
}
@@ -3093,17 +3110,17 @@ var WalkerActor = protocol.ActorClass({
let walker = this.getDocumentWalker(doc);
let parentNode = walker.parentNode();
if (parentNode) {
// Send a childList mutation on the frame so that clients know
// they should reread the children list.
this.queueMutation({
type: "childList",
- target: this._refMap.get(parentNode).actorID,
+ target: this.getNode(parentNode).actorID,
added: [],
removed: []
});
}
// Need to force a release of this node, because those nodes can't
// be accessed anymore.
this.releaseNode(documentActor, { force: true });