Bug 1206420 - Display animated pseudo-elements in the animation-inspector; r=tromey
MozReview-Commit-ID: 5bMOxKG6pMm
--- a/devtools/client/animationinspector/animation-controller.js
+++ b/devtools/client/animationinspector/animation-controller.js
@@ -225,18 +225,17 @@ var AnimationsController = {
this.nodeFront === gInspector.selection.nodeFront) {
return;
}
this.nodeFront = gInspector.selection.nodeFront;
let done = gInspector.updating("animationscontroller");
if (!gInspector.selection.isConnected() ||
- !gInspector.selection.isElementNode() ||
- gInspector.selection.isPseudoElementNode()) {
+ !gInspector.selection.isElementNode()) {
this.destroyAnimationPlayers();
this.emit(this.PLAYERS_UPDATED_EVENT);
done();
return;
}
yield this.refreshAnimationPlayers(this.nodeFront);
this.emit(this.PLAYERS_UPDATED_EVENT, this.animationPlayers);
--- a/devtools/client/animationinspector/animation-panel.js
+++ b/devtools/client/animationinspector/animation-panel.js
@@ -178,19 +178,18 @@ var AnimationsPanel = {
togglePlayers: function(isVisible) {
if (isVisible) {
document.body.removeAttribute("empty");
document.body.setAttribute("timeline", "true");
} else {
document.body.setAttribute("empty", "true");
document.body.removeAttribute("timeline");
- $("#error-type").textContent = gInspector.selection.isPseudoElementNode()
- ? L10N.getStr("panel.pseudoElementSelected")
- : L10N.getStr("panel.invalidElementSelected");
+ $("#error-type").textContent =
+ L10N.getStr("panel.invalidElementSelected");
}
},
onPickerStarted: function() {
this.pickerButtonEl.setAttribute("checked", "true");
},
onPickerStopped: function() {
--- a/devtools/client/animationinspector/test/browser.ini
+++ b/devtools/client/animationinspector/test/browser.ini
@@ -3,16 +3,17 @@ tags = devtools
subsuite = devtools
skip-if = e10s && debug # bug 1252283
support-files =
doc_body_animation.html
doc_frame_script.js
doc_keyframes.html
doc_modify_playbackRate.html
doc_negative_animation.html
+ doc_pseudo_elements.html
doc_simple_animation.html
doc_multiple_animation_types.html
head.js
[browser_animation_animated_properties_displayed.js]
[browser_animation_click_selects_animation.js]
[browser_animation_controller_exposes_document_currentTime.js]
skip-if = os == "linux" && !debug # Bug 1234567
@@ -20,16 +21,17 @@ skip-if = os == "linux" && !debug # Bug
[browser_animation_keyframe_click_to_set_time.js]
[browser_animation_keyframe_markers.js]
[browser_animation_mutations_with_same_names.js]
[browser_animation_panel_exists.js]
[browser_animation_participate_in_inspector_update.js]
[browser_animation_playerFronts_are_refreshed.js]
[browser_animation_playerWidgets_appear_on_panel_init.js]
[browser_animation_playerWidgets_target_nodes.js]
+[browser_animation_pseudo_elements.js]
[browser_animation_refresh_on_added_animation.js]
[browser_animation_refresh_on_removed_animation.js]
skip-if = os == "linux" && !debug # Bug 1227792
[browser_animation_refresh_when_active.js]
[browser_animation_running_on_compositor.js]
[browser_animation_same_nb_of_playerWidgets_and_playerFronts.js]
[browser_animation_shows_player_on_valid_node.js]
[browser_animation_spacebar_toggles_animations.js]
--- a/devtools/client/animationinspector/test/browser_animation_empty_on_invalid_nodes.js
+++ b/devtools/client/animationinspector/test/browser_animation_empty_on_invalid_nodes.js
@@ -39,26 +39,9 @@ add_task(function*() {
is(panel.animationsTimelineComponent.animations.length, 0,
"No animation players stored in the timeline component for a text node");
is(panel.animationsTimelineComponent.animationsEl.childNodes.length, 0,
"No animation displayed in the timeline component for a text node");
is(document.querySelector("#error-type").textContent,
L10N.getStr("panel.invalidElementSelected"),
"The correct error message is displayed");
-
- info("Select the pseudo element node and check that the panel is empty " +
- "and contains the special animated pseudo-element message");
- let pseudoElParent = yield getNodeFront(".pseudo", inspector);
- let {nodes} = yield inspector.walker.children(pseudoElParent);
- let pseudoEl = nodes[0];
- onUpdated = panel.once(panel.UI_UPDATED_EVENT);
- yield selectNode(pseudoEl, inspector);
- yield onUpdated;
-
- is(panel.animationsTimelineComponent.animations.length, 0,
- "No animation players stored in the timeline component for a pseudo-node");
- is(panel.animationsTimelineComponent.animationsEl.childNodes.length, 0,
- "No animation displayed in the timeline component for a pseudo-node");
- is(document.querySelector("#error-type").textContent,
- L10N.getStr("panel.pseudoElementSelected"),
- "The correct error message is displayed");
});
new file mode 100644
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_pseudo_elements.js
@@ -0,0 +1,49 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that animated pseudo-elements do show in the timeline.
+
+add_task(function*() {
+ yield addTab(URL_ROOT + "doc_pseudo_elements.html");
+ let {inspector, panel} = yield openAnimationInspector();
+ let timeline = panel.animationsTimelineComponent;
+
+ info("With <body> selected by default check the content of the timeline");
+ is(timeline.timeBlocks.length, 3, "There are 3 animations in the timeline");
+
+ let getTargetNodeText = index => {
+ let el = timeline.targetNodes[index].previewer.previewEl;
+ return [...el.childNodes]
+ .map(n => n.style.display === "none" ? "" : n.textContent)
+ .join("");
+ };
+
+ is(getTargetNodeText(0), "body", "The first animated node is <body>");
+ is(getTargetNodeText(1), "::before", "The second animated node is ::before");
+ is(getTargetNodeText(2), "::after", "The third animated node is ::after");
+
+ info("Getting the before and after nodeFronts");
+ let bodyContainer = yield getContainerForSelector("body", inspector);
+ let getBodyChildNodeFront = index => {
+ return bodyContainer.elt.children[1].childNodes[index].container.node;
+ };
+ let beforeNode = getBodyChildNodeFront(0);
+ let afterNode = getBodyChildNodeFront(1);
+
+ info("Select the ::before pseudo-element in the inspector");
+ yield selectNode(beforeNode, inspector);
+ is(timeline.timeBlocks.length, 1, "There is 1 animation in the timeline");
+ is(timeline.targetNodes[0].previewer.nodeFront,
+ inspector.selection.nodeFront,
+ "The right node front is displayed in the timeline");
+
+ info("Select the ::after pseudo-element in the inspector");
+ yield selectNode(afterNode, inspector);
+ is(timeline.timeBlocks.length, 1, "There is 1 animation in the timeline");
+ is(timeline.targetNodes[0].previewer.nodeFront,
+ inspector.selection.nodeFront,
+ "The right node front is displayed in the timeline");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/animationinspector/test/doc_pseudo_elements.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Animated pseudo elements</title>
+ <style>
+ html, body {
+ margin: 0;
+ height: 100%;
+ width: 100%;
+ overflow: hidden;
+ display: flex;
+ justify-content: center;
+ align-items: flex-end;
+ }
+
+ body {
+ animation: color 2s linear infinite;
+ background: #333;
+ }
+
+ @keyframes color {
+ to {
+ filter: hue-rotate(360deg);
+ }
+ }
+
+ body::before,
+ body::after {
+ content: "";
+ flex-grow: 1;
+ height: 100%;
+ animation: grow 1s linear infinite alternate;
+ }
+
+ body::before {
+ background: hsl(120, 80%, 80%);
+ }
+ body::after {
+ background: hsl(240, 80%, 80%);
+ animation-delay: -.5s;
+ }
+
+ @keyframes grow {
+ 0% {height: 100%; animation-timing-function: ease-in-out;}
+ 10% {height: 80%; animation-timing-function: ease-in-out;}
+ 20% {height: 60%; animation-timing-function: ease-in-out;}
+ 30% {height: 70%; animation-timing-function: ease-in-out;}
+ 40% {height: 50%; animation-timing-function: ease-in-out;}
+ 50% {height: 30%; animation-timing-function: ease-in-out;}
+ 60% {height: 80%; animation-timing-function: ease-in-out;}
+ 70% {height: 90%; animation-timing-function: ease-in-out;}
+ 80% {height: 70%; animation-timing-function: ease-in-out;}
+ 90% {height: 60%; animation-timing-function: ease-in-out;}
+ 100% {height: 100%; animation-timing-function: ease-in-out;}
+ }
+ </style>
+ </head>
+ <body>
+ </body>
+</html>
\ No newline at end of file
--- a/devtools/client/animationinspector/test/doc_simple_animation.html
+++ b/devtools/client/animationinspector/test/doc_simple_animation.html
@@ -77,31 +77,16 @@
.no-compositor {
top: 0;
right: 10px;
background: gold;
animation: no-compositor 10s cubic-bezier(.57,-0.02,1,.31) forwards;
}
- .pseudo {
- top: 800px;
- left: 10px;
- }
-
- .pseudo::before {
- content: "";
- width: 50%;
- height: 50%;
- border-radius: 50%;
- background: black;
- position: absolute;
- animation: simple-animation 1s infinite alternate;
- }
-
@keyframes simple-animation {
100% {
transform: translateX(300px);
}
}
@keyframes other-animation {
100% {
@@ -122,11 +107,10 @@
<div class="ball animated"></div>
<div class="ball multi"></div>
<div class="ball delayed"></div>
<div class="ball multi-finite"></div>
<div class="ball short"></div>
<div class="ball long"></div>
<div class="ball negative-delay"></div>
<div class="ball no-compositor"></div>
- <div class="ball pseudo"></div>
</body>
</html>
--- a/devtools/client/inspector/shared/dom-node-preview.js
+++ b/devtools/client/inspector/shared/dom-node-preview.js
@@ -68,16 +68,25 @@ DomNodePreview.prototype = {
"title": L10N.getStr("inspector.nodePreview.selectNodeLabel")
}
});
if (!this.options.compact) {
this.previewEl.appendChild(document.createTextNode("<"));
}
+ // Only used for ::before and ::after pseudo-elements.
+ this.pseudoEl = createNode({
+ parent: this.previewEl,
+ nodeType: "span",
+ attributes: {
+ "class": "pseudo-element theme-fg-color5"
+ }
+ });
+
// Tag name.
this.tagNameEl = createNode({
parent: this.previewEl,
nodeType: "span",
attributes: {
"class": "tag-name theme-fg-color3"
}
});
@@ -188,17 +197,17 @@ DomNodePreview.prototype = {
},
destroy: function() {
HighlighterLock.unhighlight().catch(e => console.error(e));
this.stopListeners();
this.el.remove();
- this.el = this.tagNameEl = this.idEl = this.classEl = null;
+ this.el = this.tagNameEl = this.idEl = this.classEl = this.pseudoEl = null;
this.highlightNodeEl = this.previewEl = null;
this.nodeFront = this.inspector = null;
},
get highlighterUtils() {
if (this.inspector && this.inspector.toolbox) {
return this.inspector.toolbox.highlighterUtils;
}
@@ -266,17 +275,27 @@ DomNodePreview.prototype = {
}
}
},
render: function(nodeFront) {
this.nodeFront = nodeFront;
let {tagName, attributes} = nodeFront;
- this.tagNameEl.textContent = tagName.toLowerCase();
+ if (nodeFront.isPseudoElement) {
+ this.pseudoEl.textContent = nodeFront.isBeforePseudoElement
+ ? "::before"
+ : "::after";
+ this.pseudoEl.style.display = "inline";
+ this.tagNameEl.style.display = "none";
+ } else {
+ this.tagNameEl.textContent = tagName.toLowerCase();
+ this.pseudoEl.style.display = "none";
+ this.tagNameEl.style.display = "inline";
+ }
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";
--- a/devtools/client/locales/en-US/animationinspector.properties
+++ b/devtools/client/locales/en-US/animationinspector.properties
@@ -11,22 +11,16 @@
# documentation on web development on the web.
# LOCALIZATION NOTE (panel.invalidElementSelected):
# This is the label shown in the panel when an invalid node is currently
# selected in the inspector (i.e. a non-element node or a node that is not
# animated).
panel.invalidElementSelected=No animations were found for the current element.
-# LOCALIZATION NOTE (panel.pseudoElementSelected):
-# This is the label shown in the panel when a pseudo-element is currently
-# selected in the inspector (pseudo-elements can be animated, but the tool
-# doesn't yet support them).
-panel.pseudoElementSelected=Animated pseudo-elements are not supported yet.
-
# LOCALIZATION NOTE (player.animationNameLabel):
# This string is displayed in each animation player widget. It is the label
# displayed before the animation name.
player.animationNameLabel=Animation:
# LOCALIZATION NOTE (player.transitionNameLabel):
# This string is displayed in each animation player widget. It is the label
# displayed in the header, when the element is animated by mean of a css
--- a/devtools/server/actors/animation.js
+++ b/devtools/server/actors/animation.js
@@ -69,35 +69,75 @@ var AnimationPlayerActor = ActorClass({
*/
initialize: function(animationsActor, player) {
Actor.prototype.initialize.call(this, animationsActor.conn);
this.onAnimationMutation = this.onAnimationMutation.bind(this);
this.walker = animationsActor.walker;
this.player = player;
- this.node = player.effect.target;
// Listen to animation mutations on the node to alert the front when the
// current animation changes.
+ // If the node is a pseudo-element, then we listen on its parent with
+ // subtree:true (there's no risk of getting too many notifications in
+ // onAnimationMutation since we filter out events that aren't for the
+ // current animation).
this.observer = new this.window.MutationObserver(this.onAnimationMutation);
- this.observer.observe(this.node, {animations: true});
+ if (this.isPseudoElement) {
+ this.observer.observe(this.node.parentElement,
+ {animations: true, subtree: true});
+ } else {
+ this.observer.observe(this.node, {animations: true});
+ }
},
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.player = this.node = this.observer = this.walker = null;
+ this.player = this.observer = this.walker = null;
Actor.prototype.destroy.call(this);
},
+ get isPseudoElement() {
+ return !this.player.effect.target.ownerDocument;
+ },
+
+ get node() {
+ if (this._node) {
+ return this._node;
+ }
+
+ let node = this.player.effect.target;
+
+ if (this.isPseudoElement) {
+ // The target is a CSSPseudoElement object which just has a property that
+ // points to its parent element and a string type (::before or ::after).
+ let treeWalker = this.walker.getDocumentWalker(node.parentElement);
+ while (treeWalker.nextNode()) {
+ let currentNode = treeWalker.currentNode;
+ if ((currentNode.nodeName === "_moz_generated_content_before" &&
+ node.type === "::before") ||
+ (currentNode.nodeName === "_moz_generated_content_after" &&
+ node.type === "::after")) {
+ this._node = currentNode;
+ }
+ }
+ } else {
+ // The target is a DOM node.
+ this._node = node;
+ }
+
+ return this._node;
+ },
+
get window() {
return this.node.ownerDocument.defaultView;
},
/**
* Release the actor, when it isn't needed anymore.
* Protocol.js uses this release method to call the destroy method.
*/
@@ -564,20 +604,17 @@ var AnimationsActor = exports.Animations
* Note that calling this method a second time will destroy all previously
* retrieved AnimationPlayerActors. Indeed, the lifecycle of these actors
* is managed here on the server and tied to getAnimationPlayersForNode
* being called.
* @param {NodeActor} nodeActor The NodeActor as defined in
* /devtools/server/actors/inspector
*/
getAnimationPlayersForNode: method(function(nodeActor) {
- let animations = [
- ...nodeActor.rawNode.getAnimations(),
- ...this.getAllAnimations(nodeActor.rawNode)
- ];
+ let animations = nodeActor.rawNode.getAnimations({subtree: true});
// Destroy previously stored actors
if (this.actors) {
this.actors.forEach(actor => actor.destroy());
}
this.actors = [];
for (let i = 0; i < animations.length; i++) {
@@ -617,50 +654,34 @@ var AnimationsActor = exports.Animations
// actually removed from the node (e.g. css class removed) or when they
// are finished and don't have forwards animation-fill-mode.
// In the latter case, we don't send an event, because the corresponding
// animation can still be seeked/resumed, so we want the client to keep
// its reference to the AnimationPlayerActor.
if (player.playState !== "idle") {
continue;
}
- // FIXME: In bug 1249219, we support the animation mutation for pseudo
- // elements. However, the timeline may not be ready yet to
- // display those correctly. Therefore, we add this check to bails out if
- // the mutation target is a pseudo-element.
- // Note. Only CSSPseudoElement object has |type| attribute, so if type
- // exists, it is a CSSPseudoElement object.
- if (player.effect.target.type) {
- continue;
- }
+
let index = this.actors.findIndex(a => a.player === player);
if (index !== -1) {
eventData.push({
type: "removed",
player: this.actors[index]
});
this.actors.splice(index, 1);
}
}
for (let player of addedAnimations) {
// If the added player already exists, it means we previously filtered
// it out when it was reported as removed. So filter it out here too.
if (this.actors.find(a => a.player === player)) {
continue;
}
- // FIXME: In bug 1249219, we support the animation mutation for pseudo
- // elements. However, the timeline may not be ready yet to
- // display those correctly. Therefore, we add this check to bails out if
- // the mutation target is a pseudo-element.
- // Note. Only CSSPseudoElement object has |type| attribute, so if type
- // exists, it is a CSSPseudoElement object.
- if (player.effect.target.type) {
- continue;
- }
+
// If the added player has the same name and target node as a player we
// already have, it means it's a transition that's re-starting. So send
// a "removed" event for the one we already have.
let index = this.actors.findIndex(a => {
let isSameType = a.player.constructor === player.constructor;
let isSameName = (a.isCssAnimation() &&
a.player.animationName === player.animationName) ||
(a.isCssTransition() &&
@@ -715,35 +736,24 @@ var AnimationsActor = exports.Animations
* nested frames) and finds all existing animation players.
* @param {DOMNode} rootNode The root node to start iterating at. Animation
* players will *not* be reported for this node.
* @param {Boolean} traverseFrames Whether we should iterate through nested
* frames too.
* @return {Array} An array of AnimationPlayer objects.
*/
getAllAnimations: function(rootNode, traverseFrames) {
- let animations = [];
-
- // These loops shouldn't be as bad as they look.
- // Typically, there will be very few nested frames, and getElementsByTagName
- // is really fast even on large DOM trees.
- for (let element of rootNode.getElementsByTagNameNS("*", "*")) {
- if (traverseFrames && element.contentWindow) {
- animations = [
- ...animations,
- ...this.getAllAnimations(element.contentWindow.document, traverseFrames)
- ];
- } else {
- animations = [
- ...animations,
- ...element.getAnimations()
- ];
- }
+ if (!traverseFrames) {
+ return rootNode.getAnimations({subtree: true});
}
+ let animations = [];
+ for (let {document} of this.tabActor.windows) {
+ animations = [...animations, ...document.getAnimations({subtree: true})];
+ }
return animations;
},
onWillNavigate: function({isTopLevel}) {
if (isTopLevel) {
this.stopAnimationPlayerUpdates();
}
},