Bug 1155653 - Preview animation target nodes in animationinspector panel; r=bgrins
authorPatrick Brosset <pbrosset@mozilla.com>
Mon, 04 May 2015 13:55:51 +0200
changeset 273640 f88d5fb92a29339ae00e569b4058ccd646ddcdf2
parent 273639 45a614729d09f06077735dfe2edca13d84707f0b
child 273641 77f5a6d23ffc9684b5c59ed66b02649588c02698
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1155653
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1155653 - Preview animation target nodes in animationinspector panel; r=bgrins This adds a generic walker method that cna be used to retrieve AnimationPlayerActor's target DOM nodes in a way that they can be highlighted/selected later in the inspector. Making the AnimationPlayerActor return the nodeActor directly wouldn't fully work, as the walker would have missed the missing parent nodeFronts updates. On the UI, this adds a new component in each PlayerWidget that displays a preview of the target DOM nodes, highlight them on hover, and select them on click.
browser/devtools/animationinspector/animation-controller.js
browser/devtools/animationinspector/animation-panel.js
browser/devtools/animationinspector/components.js
browser/devtools/animationinspector/moz.build
browser/devtools/animationinspector/test/browser.ini
browser/devtools/animationinspector/test/browser_animation_playerWidgets_target_nodes.js
browser/devtools/animationinspector/test/browser_animation_target_highlight_select.js
browser/themes/shared/devtools/animationinspector.css
toolkit/devtools/server/actors/inspector.js
toolkit/devtools/server/tests/mochitest/chrome.ini
toolkit/devtools/server/tests/mochitest/test_inspector_getNodeFromActor.html
--- a/browser/devtools/animationinspector/animation-controller.js
+++ b/browser/devtools/animationinspector/animation-controller.js
@@ -107,16 +107,18 @@ let AnimationsController = {
     // Expose actor capabilities.
     this.hasToggleAll = yield target.actorHasMethod("animations", "toggleAll");
     this.hasSetCurrentTime = yield target.actorHasMethod("animationplayer",
                                                          "setCurrentTime");
     this.hasMutationEvents = yield target.actorHasMethod("animations",
                                                          "stopAnimationPlayerUpdates");
     this.hasSetPlaybackRate = yield target.actorHasMethod("animationplayer",
                                                           "setPlaybackRate");
+    this.hasTargetNode = yield target.actorHasMethod("domwalker",
+                                                     "getNodeFromActor");
 
     if (this.destroyed) {
       console.warn("Could not fully initialize the AnimationsController");
       return;
     }
 
     this.startListeners();
     yield this.onNewNodeFront();
--- a/browser/devtools/animationinspector/animation-panel.js
+++ b/browser/devtools/animationinspector/animation-panel.js
@@ -1,16 +1,23 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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 {
+  PlayerMetaDataHeader,
+  PlaybackRateSelector,
+  AnimationTargetNode,
+  createNode
+} = require("devtools/animationinspector/components");
+
 /**
  * The main animations panel UI.
  */
 let AnimationsPanel = {
   UI_UPDATED_EVENT: "ui-updated",
   PANEL_INITIALIZED: "panel-initialized",
 
   initialize: Task.async(function*() {
@@ -194,16 +201,19 @@ function PlayerWidget(player, containerE
   this.onFastForwardBtnClick = this.onFastForwardBtnClick.bind(this);
   this.onCurrentTimeChanged = this.onCurrentTimeChanged.bind(this);
   this.onPlaybackRateChanged = this.onPlaybackRateChanged.bind(this);
 
   this.metaDataComponent = new PlayerMetaDataHeader();
   if (AnimationsController.hasSetPlaybackRate) {
     this.rateComponent = new PlaybackRateSelector();
   }
+  if (AnimationsController.hasTargetNode) {
+    this.targetNodeComponent = new AnimationTargetNode(gInspector);
+  }
 }
 
 PlayerWidget.prototype = {
   initialize: Task.async(function*() {
     if (this.initialized) {
       return;
     }
     this.initialized = true;
@@ -219,16 +229,19 @@ PlayerWidget.prototype = {
     this.destroyed = true;
 
     this.stopTimelineAnimation();
     this.stopListeners();
     this.metaDataComponent.destroy();
     if (this.rateComponent) {
       this.rateComponent.destroy();
     }
+    if (this.targetNodeComponent) {
+      this.targetNodeComponent.destroy();
+    }
 
     this.el.remove();
     this.playPauseBtnEl = this.rewindBtnEl = this.fastForwardBtnEl = null;
     this.currentTimeEl = this.timeDisplayEl = null;
     this.containerEl = this.el = this.player = null;
   }),
 
   startListeners: function() {
@@ -256,21 +269,27 @@ PlayerWidget.prototype = {
       this.rateComponent.off("rate-changed", this.onPlaybackRateChanged);
     }
   },
 
   createMarkup: function() {
     let state = this.player.state;
 
     this.el = createNode({
+      parent: this.containerEl,
       attributes: {
         "class": "player-widget " + state.playState
       }
     });
 
+    if (this.targetNodeComponent) {
+      this.targetNodeComponent.init(this.el);
+      this.targetNodeComponent.render(this.player);
+    }
+
     this.metaDataComponent.init(this.el);
     this.metaDataComponent.render(state);
 
     // Timeline widget.
     let timelineEl = createNode({
       parent: this.el,
       attributes: {
         "class": "timeline"
@@ -354,18 +373,16 @@ PlayerWidget.prototype = {
     // Time display
     this.timeDisplayEl = createNode({
       parent: timelineEl,
       attributes: {
         "class": "time-display"
       }
     });
 
-    this.containerEl.appendChild(this.el);
-
     // Show the initial time.
     this.displayTime(state.currentTime);
   },
 
   /**
    * Executed when the playPause button is clicked.
    * Note that tests may want to call this callback directly rather than
    * simulating a click on the button since it returns the promise returned by
@@ -566,266 +583,8 @@ PlayerWidget.prototype = {
    */
   stopTimelineAnimation: function() {
     if (this.rafID) {
       cancelAnimationFrame(this.rafID);
       this.rafID = null;
     }
   }
 };
-
-/**
- * UI component responsible for displaying and updating the player meta-data:
- * name, duration, iterations, delay.
- * The parent UI component for this should drive its updates by calling
- * render(state) whenever it wants the component to update.
- */
-function PlayerMetaDataHeader() {
-  // Store the various state pieces we need to only refresh the UI when things
-  // change.
-  this.state = {};
-}
-
-PlayerMetaDataHeader.prototype = {
-  init: function(containerEl) {
-    // The main title element.
-    this.el = createNode({
-      parent: containerEl,
-      attributes: {
-        "class": "animation-title"
-      }
-    });
-
-    // Animation name.
-    this.nameLabel = createNode({
-      parent: this.el,
-      nodeType: "span"
-    });
-
-    this.nameValue = createNode({
-      parent: this.el,
-      nodeType: "strong",
-      attributes: {
-        "style": "display:none;"
-      }
-    });
-
-    // Animation duration, delay and iteration container.
-    let metaData = createNode({
-      parent: this.el,
-      nodeType: "span",
-      attributes: {
-        "class": "meta-data"
-      }
-    });
-
-    // Animation duration.
-    this.durationLabel = createNode({
-      parent: metaData,
-      nodeType: "span"
-    });
-    this.durationLabel.textContent = L10N.getStr("player.animationDurationLabel");
-
-    this.durationValue = createNode({
-      parent: metaData,
-      nodeType: "strong"
-    });
-
-    // Animation delay (hidden by default since there may not be a delay).
-    this.delayLabel = createNode({
-      parent: metaData,
-      nodeType: "span",
-      attributes: {
-        "style": "display:none;"
-      }
-    });
-    this.delayLabel.textContent = L10N.getStr("player.animationDelayLabel");
-
-    this.delayValue = createNode({
-      parent: metaData,
-      nodeType: "strong"
-    });
-
-    // Animation iteration count (also hidden by default since we don't display
-    // single iterations).
-    this.iterationLabel = createNode({
-      parent: metaData,
-      nodeType: "span",
-      attributes: {
-        "style": "display:none;"
-      }
-    });
-    this.iterationLabel.textContent = L10N.getStr("player.animationIterationCountLabel");
-
-    this.iterationValue = createNode({
-      parent: metaData,
-      nodeType: "strong",
-      attributes: {
-        "style": "display:none;"
-      }
-    });
-  },
-
-  destroy: function() {
-    this.state = null;
-    this.el.remove();
-    this.el = null;
-    this.nameLabel = this.nameValue = null;
-    this.durationLabel = this.durationValue = null;
-    this.delayLabel = this.delayValue = null;
-    this.iterationLabel = this.iterationValue = null;
-  },
-
-  render: function(state) {
-    // Update the name if needed.
-    if (state.name !== this.state.name) {
-      if (state.name) {
-        // Animations (and transitions since bug 1122414) have names.
-        this.nameLabel.textContent = L10N.getStr("player.animationNameLabel");
-        this.nameValue.style.display = "inline";
-        this.nameValue.textContent = state.name;
-      } else {
-        // With older actors, Css transitions don't have names.
-        this.nameLabel.textContent = L10N.getStr("player.transitionNameLabel");
-        this.nameValue.style.display = "none";
-      }
-    }
-
-    // update the duration value if needed.
-    if (state.duration !== this.state.duration) {
-      this.durationValue.textContent = L10N.getFormatStr("player.timeLabel",
-        L10N.numberWithDecimals(state.duration / 1000, 2));
-    }
-
-    // Update the delay if needed.
-    if (state.delay !== this.state.delay) {
-      if (state.delay) {
-        this.delayLabel.style.display = "inline";
-        this.delayValue.style.display = "inline";
-        this.delayValue.textContent = L10N.getFormatStr("player.timeLabel",
-          L10N.numberWithDecimals(state.delay / 1000, 2));
-      } else {
-        // Hide the delay elements if there is no delay defined.
-        this.delayLabel.style.display = "none";
-        this.delayValue.style.display = "none";
-      }
-    }
-
-    // Update the iterationCount if needed.
-    if (state.iterationCount !== this.state.iterationCount) {
-      if (state.iterationCount !== 1) {
-        this.iterationLabel.style.display = "inline";
-        this.iterationValue.style.display = "inline";
-        let count = state.iterationCount ||
-                    L10N.getStr("player.infiniteIterationCount");
-        this.iterationValue.innerHTML = count;
-      } else {
-        // Hide the iteration elements if iteration is 1.
-        this.iterationLabel.style.display = "none";
-        this.iterationValue.style.display = "none";
-      }
-    }
-
-    this.state = state;
-  }
-};
-
-/**
- * UI component responsible for displaying the playback rate drop-down in each
- * player widget, updating it when the state changes, and emitting events when
- * the user selects a new value.
- * The parent UI component for this should drive its updates by calling
- * render(state) whenever it wants the component to update.
- */
-function PlaybackRateSelector() {
-  this.currentRate = null;
-  this.onSelectionChanged = this.onSelectionChanged.bind(this);
-  EventEmitter.decorate(this);
-}
-
-PlaybackRateSelector.prototype = {
-  PRESETS: [.1, .5, 1, 2, 5, 10],
-
-  init: function(containerEl) {
-    // This component is simple enough that we can re-create the markup every
-    // time it's rendered. So here we only store the parentEl.
-    this.parentEl = containerEl;
-  },
-
-  destroy: function() {
-    this.removeSelect();
-    this.parentEl = this.el = null;
-  },
-
-  removeSelect: function() {
-    if (this.el) {
-      this.el.removeEventListener("change", this.onSelectionChanged);
-      this.el.remove();
-    }
-  },
-
-  /**
-   * Get the ordered list of presets, including the current playbackRate if
-   * different from the existing presets.
-   */
-  getCurrentPresets: function({playbackRate}) {
-    return [...new Set([...this.PRESETS, playbackRate])].sort((a,b) => a > b);
-  },
-
-  render: function(state) {
-    if (state.playbackRate === this.currentRate) {
-      return;
-    }
-
-    this.removeSelect();
-
-    this.el = createNode({
-      parent: this.parentEl,
-      nodeType: "select",
-      attributes: {
-        "class": "rate devtools-button"
-      }
-    });
-
-    for (let preset of this.getCurrentPresets(state)) {
-      let option = createNode({
-        parent: this.el,
-        nodeType: "option",
-        attributes: {
-          value: preset,
-        }
-      });
-      option.textContent = L10N.getFormatStr("player.playbackRateLabel", preset);
-      if (preset === state.playbackRate) {
-        option.setAttribute("selected", "");
-      }
-    }
-
-    this.el.addEventListener("change", this.onSelectionChanged);
-
-    this.currentRate = state.playbackRate;
-  },
-
-  onSelectionChanged: function(e) {
-    this.emit("rate-changed", parseFloat(this.el.value));
-  }
-};
-
-/**
- * DOM node creation helper function.
- * @param {Object} Options to customize the node to be created.
- * @return {DOMNode} The newly created node.
- */
-function createNode(options) {
-  let type = options.nodeType || "div";
-  let node = document.createElement(type);
-
-  for (let name in options.attributes || {}) {
-    let value = options.attributes[name];
-    node.setAttribute(name, value);
-  }
-
-  if (options.parent) {
-    options.parent.appendChild(node);
-  }
-
-  return node;
-}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/components.js
@@ -0,0 +1,502 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Set of reusable UI components for the animation-inspector UI.
+// All components in this module share a common API:
+// 1. construct the component:
+//    let c = new ComponentName();
+// 2. initialize the markup of the component in a given parent node:
+//    c.init(containerElement);
+// 3. render the component, passing in some sort of state:
+//    This may be called over and over again when the state changes, to update
+//    the component output.
+//    c.render(state);
+// 4. destroy the component:
+//    c.destroy();
+
+const {Cu} = require('chrome');
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+
+const STRINGS_URI = "chrome://browser/locale/devtools/animationinspector.properties";
+const L10N = new ViewHelpers.L10N(STRINGS_URI);
+
+/**
+ * UI component responsible for displaying and updating the player meta-data:
+ * name, duration, iterations, delay.
+ * The parent UI component for this should drive its updates by calling
+ * render(state) whenever it wants the component to update.
+ */
+function PlayerMetaDataHeader() {
+  // Store the various state pieces we need to only refresh the UI when things
+  // change.
+  this.state = {};
+}
+
+exports.PlayerMetaDataHeader = PlayerMetaDataHeader;
+
+PlayerMetaDataHeader.prototype = {
+  init: function(containerEl) {
+    // The main title element.
+    this.el = createNode({
+      parent: containerEl,
+      attributes: {
+        "class": "animation-title"
+      }
+    });
+
+    // Animation name.
+    this.nameLabel = createNode({
+      parent: this.el,
+      nodeType: "span"
+    });
+
+    this.nameValue = createNode({
+      parent: this.el,
+      nodeType: "strong",
+      attributes: {
+        "style": "display:none;"
+      }
+    });
+
+    // Animation duration, delay and iteration container.
+    let metaData = createNode({
+      parent: this.el,
+      nodeType: "span",
+      attributes: {
+        "class": "meta-data"
+      }
+    });
+
+    // Animation duration.
+    this.durationLabel = createNode({
+      parent: metaData,
+      nodeType: "span"
+    });
+    this.durationLabel.textContent = L10N.getStr("player.animationDurationLabel");
+
+    this.durationValue = createNode({
+      parent: metaData,
+      nodeType: "strong"
+    });
+
+    // Animation delay (hidden by default since there may not be a delay).
+    this.delayLabel = createNode({
+      parent: metaData,
+      nodeType: "span",
+      attributes: {
+        "style": "display:none;"
+      }
+    });
+    this.delayLabel.textContent = L10N.getStr("player.animationDelayLabel");
+
+    this.delayValue = createNode({
+      parent: metaData,
+      nodeType: "strong"
+    });
+
+    // Animation iteration count (also hidden by default since we don't display
+    // single iterations).
+    this.iterationLabel = createNode({
+      parent: metaData,
+      nodeType: "span",
+      attributes: {
+        "style": "display:none;"
+      }
+    });
+    this.iterationLabel.textContent = L10N.getStr("player.animationIterationCountLabel");
+
+    this.iterationValue = createNode({
+      parent: metaData,
+      nodeType: "strong",
+      attributes: {
+        "style": "display:none;"
+      }
+    });
+  },
+
+  destroy: function() {
+    this.state = null;
+    this.el.remove();
+    this.el = null;
+    this.nameLabel = this.nameValue = null;
+    this.durationLabel = this.durationValue = null;
+    this.delayLabel = this.delayValue = null;
+    this.iterationLabel = this.iterationValue = null;
+  },
+
+  render: function(state) {
+    // Update the name if needed.
+    if (state.name !== this.state.name) {
+      if (state.name) {
+        // Animations (and transitions since bug 1122414) have names.
+        this.nameLabel.textContent = L10N.getStr("player.animationNameLabel");
+        this.nameValue.style.display = "inline";
+        this.nameValue.textContent = state.name;
+      } else {
+        // With older actors, Css transitions don't have names.
+        this.nameLabel.textContent = L10N.getStr("player.transitionNameLabel");
+        this.nameValue.style.display = "none";
+      }
+    }
+
+    // update the duration value if needed.
+    if (state.duration !== this.state.duration) {
+      this.durationValue.textContent = L10N.getFormatStr("player.timeLabel",
+        L10N.numberWithDecimals(state.duration / 1000, 2));
+    }
+
+    // Update the delay if needed.
+    if (state.delay !== this.state.delay) {
+      if (state.delay) {
+        this.delayLabel.style.display = "inline";
+        this.delayValue.style.display = "inline";
+        this.delayValue.textContent = L10N.getFormatStr("player.timeLabel",
+          L10N.numberWithDecimals(state.delay / 1000, 2));
+      } else {
+        // Hide the delay elements if there is no delay defined.
+        this.delayLabel.style.display = "none";
+        this.delayValue.style.display = "none";
+      }
+    }
+
+    // Update the iterationCount if needed.
+    if (state.iterationCount !== this.state.iterationCount) {
+      if (state.iterationCount !== 1) {
+        this.iterationLabel.style.display = "inline";
+        this.iterationValue.style.display = "inline";
+        let count = state.iterationCount ||
+                    L10N.getStr("player.infiniteIterationCount");
+        this.iterationValue.innerHTML = count;
+      } else {
+        // Hide the iteration elements if iteration is 1.
+        this.iterationLabel.style.display = "none";
+        this.iterationValue.style.display = "none";
+      }
+    }
+
+    this.state = state;
+  }
+};
+
+/**
+ * UI component responsible for displaying the playback rate drop-down in each
+ * player widget, updating it when the state changes, and emitting events when
+ * the user selects a new value.
+ * The parent UI component for this should drive its updates by calling
+ * render(state) whenever it wants the component to update.
+ */
+function PlaybackRateSelector() {
+  this.currentRate = null;
+  this.onSelectionChanged = this.onSelectionChanged.bind(this);
+  EventEmitter.decorate(this);
+}
+
+exports.PlaybackRateSelector = PlaybackRateSelector;
+
+PlaybackRateSelector.prototype = {
+  PRESETS: [.1, .5, 1, 2, 5, 10],
+
+  init: function(containerEl) {
+    // This component is simple enough that we can re-create the markup every
+    // time it's rendered. So here we only store the parentEl.
+    this.parentEl = containerEl;
+  },
+
+  destroy: function() {
+    this.removeSelect();
+    this.parentEl = this.el = null;
+  },
+
+  removeSelect: function() {
+    if (this.el) {
+      this.el.removeEventListener("change", this.onSelectionChanged);
+      this.el.remove();
+    }
+  },
+
+  /**
+   * Get the ordered list of presets, including the current playbackRate if
+   * different from the existing presets.
+   */
+  getCurrentPresets: function({playbackRate}) {
+    return [...new Set([...this.PRESETS, playbackRate])].sort((a,b) => a > b);
+  },
+
+  render: function(state) {
+    if (state.playbackRate === this.currentRate) {
+      return;
+    }
+
+    this.removeSelect();
+
+    this.el = createNode({
+      parent: this.parentEl,
+      nodeType: "select",
+      attributes: {
+        "class": "rate devtools-button"
+      }
+    });
+
+    for (let preset of this.getCurrentPresets(state)) {
+      let option = createNode({
+        parent: this.el,
+        nodeType: "option",
+        attributes: {
+          value: preset,
+        }
+      });
+      option.textContent = L10N.getFormatStr("player.playbackRateLabel", preset);
+      if (preset === state.playbackRate) {
+        option.setAttribute("selected", "");
+      }
+    }
+
+    this.el.addEventListener("change", this.onSelectionChanged);
+
+    this.currentRate = state.playbackRate;
+  },
+
+  onSelectionChanged: function(e) {
+    this.emit("rate-changed", parseFloat(this.el.value));
+  }
+};
+
+/**
+ * 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.
+ */
+function AnimationTargetNode(inspector) {
+  this.inspector = inspector;
+
+  this.onPreviewMouseOver = this.onPreviewMouseOver.bind(this);
+  this.onPreviewMouseOut = this.onPreviewMouseOut.bind(this);
+  this.onSelectNodeClick = this.onSelectNodeClick.bind(this);
+  this.onMarkupMutations = this.onMarkupMutations.bind(this);
+
+  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.selectNodeEl = createNode({
+      parent: this.el,
+      nodeType: "span",
+      attributes: {
+        "class": "node-selector"
+      }
+    });
+
+    // Wrapper used for mouseover/out event handling.
+    this.previewEl = createNode({
+      parent: this.el,
+      nodeType: "span"
+    });
+
+    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"
+    });
+
+    createNode({
+      parent: this.idEl,
+      nodeType: "span",
+      attributes: {
+        "class": "attribute-name theme-fg-color2"
+      }
+    }).textContent = "id";
+
+    this.idEl.appendChild(document.createTextNode("=\""));
+
+    createNode({
+      parent: this.idEl,
+      nodeType: "span",
+      attributes: {
+        "class": "attribute-value theme-fg-color6"
+      }
+    });
+
+    this.idEl.appendChild(document.createTextNode("\""));
+
+    // Class attribute container.
+    this.classEl = createNode({
+      parent: this.previewEl,
+      nodeType: "span"
+    });
+
+    createNode({
+      parent: this.classEl,
+      nodeType: "span",
+      attributes: {
+        "class": "attribute-name theme-fg-color2"
+      }
+    }).textContent = "class";
+
+    this.classEl.appendChild(document.createTextNode("=\""));
+
+    createNode({
+      parent: this.classEl,
+      nodeType: "span",
+      attributes: {
+        "class": "attribute-value theme-fg-color6"
+      }
+    });
+
+    this.classEl.appendChild(document.createTextNode("\""));
+
+    this.previewEl.appendChild(document.createTextNode(">"));
+
+    // Init events for highlighting and selecting the node.
+    this.previewEl.addEventListener("mouseover", this.onPreviewMouseOver);
+    this.previewEl.addEventListener("mouseout", this.onPreviewMouseOut);
+    this.selectNodeEl.addEventListener("click", this.onSelectNodeClick);
+
+    // Start to listen for markupmutation events.
+    this.inspector.on("markupmutation", this.onMarkupMutations);
+  },
+
+  destroy: function() {
+    this.inspector.off("markupmutation", this.onMarkupMutations);
+    this.previewEl.removeEventListener("mouseover", this.onPreviewMouseOver);
+    this.previewEl.removeEventListener("mouseout", this.onPreviewMouseOut);
+    this.selectNodeEl.removeEventListener("click", this.onSelectNodeClick);
+    this.el.remove();
+    this.el = this.tagNameEl = this.idEl = this.classEl = null;
+    this.selectNodeEl = this.previewEl = null;
+    this.nodeFront = this.inspector = this.playerFront = null;
+  },
+
+  onPreviewMouseOver: function() {
+    if (!this.nodeFront) {
+      return;
+    }
+    this.inspector.toolbox.highlighterUtils.highlightNodeFront(this.nodeFront);
+  },
+
+  onPreviewMouseOut: function() {
+    this.inspector.toolbox.highlighterUtils.unhighlight();
+  },
+
+  onSelectNodeClick: function() {
+    if (!this.nodeFront) {
+      return;
+    }
+    this.inspector.selection.setNodeFront(this.nodeFront, "animationinspector");
+  },
+
+  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;
+      }
+    }
+  },
+
+  render: function(playerFront) {
+    this.playerFront = playerFront;
+    this.inspector.walker.getNodeFromActor(playerFront.actorID, ["node"]).then(nodeFront => {
+      // We might have been destroyed in the meantime, or the node might not be found.
+      if (!this.el || !nodeFront) {
+        return;
+      }
+
+      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) {
+        this.classEl.querySelector(".attribute-value").textContent =
+          attributes[classIndex].value;
+        this.classEl.style.display = "inline";
+      } else {
+        this.classEl.style.display = "none";
+      }
+
+      this.emit("target-retrieved");
+    }, e => {
+      this.nodeFront = null;
+      if (!this.el) {
+        console.warn("Cound't retrieve the animation target node, widget destroyed");
+      } else {
+        console.error(e);
+      }
+    });
+  }
+};
+
+/**
+ * DOM node creation helper function.
+ * @param {Object} Options to customize the node to be created.
+ * - nodeType {String} Optional, defaults to "div",
+ * - attributes {Object} Optional attributes object like
+ *   {attrName1:value1, attrName2: value2, ...}
+ * - parent {DOMNode} Mandatory node to append the newly created node to.
+ * @return {DOMNode} The newly created node.
+ */
+function createNode(options) {
+  if (!options.parent) {
+    throw new Error("Missing parent DOMNode to create new node");
+  }
+
+  let type = options.nodeType || "div";
+  let node = options.parent.ownerDocument.createElement(type);
+
+  for (let name in options.attributes || {}) {
+    let value = options.attributes[name];
+    node.setAttribute(name, value);
+  }
+
+  options.parent.appendChild(node);
+  return node;
+}
+
+exports.createNode = createNode;
--- a/browser/devtools/animationinspector/moz.build
+++ b/browser/devtools/animationinspector/moz.build
@@ -1,7 +1,11 @@
 # -*- 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/.
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+
+EXTRA_JS_MODULES.devtools.animationinspector += [
+    'components.js',
+]
--- a/browser/devtools/animationinspector/test/browser.ini
+++ b/browser/devtools/animationinspector/test/browser.ini
@@ -15,24 +15,26 @@ support-files =
 [browser_animation_playerFronts_are_refreshed.js]
 [browser_animation_playerWidgets_appear_on_panel_init.js]
 [browser_animation_playerWidgets_destroy.js]
 [browser_animation_playerWidgets_disables_on_finished.js]
 [browser_animation_playerWidgets_dont_show_time_after_duration.js]
 [browser_animation_playerWidgets_have_control_buttons.js]
 [browser_animation_playerWidgets_meta_data.js]
 [browser_animation_playerWidgets_state_after_pause.js]
+[browser_animation_playerWidgets_target_nodes.js]
 [browser_animation_rate_select_shows_presets.js]
 [browser_animation_refresh_on_added_animation.js]
 [browser_animation_refresh_on_removed_animation.js]
 [browser_animation_refresh_when_active.js]
 [browser_animation_same_nb_of_playerWidgets_and_playerFronts.js]
 [browser_animation_setting_currentTime_works_and_pauses.js]
 [browser_animation_setting_playbackRate_works.js]
 [browser_animation_shows_player_on_valid_node.js]
+[browser_animation_target_highlight_select.js]
 [browser_animation_timeline_animates.js]
 [browser_animation_timeline_is_enabled.js]
 [browser_animation_timeline_waits_for_delay.js]
 [browser_animation_toggle_button_resets_on_navigate.js]
 [browser_animation_toggle_button_toggles_animations.js]
 [browser_animation_toggle_button_updates_playerWidgets.js]
 [browser_animation_toolbar_exists.js]
 [browser_animation_ui_updates_when_animation_changes.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_playerWidgets_target_nodes.js
@@ -0,0 +1,31 @@
+/* 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 player widgets display information about target nodes
+
+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 widget = panel.playerWidgets[0];
+
+  // Make sure to wait for the target-retrieved event if the nodeFront hasn't
+  // yet been retrieved by the TargetNodeComponent.
+  if (!widget.targetNodeComponent.nodeFront) {
+    yield widget.targetNodeComponent.once("target-retrieved");
+  }
+
+  let targetEl = widget.el.querySelector(".animation-target");
+  ok(targetEl, "The player widget has a target element");
+  is(targetEl.textContent, "<divid=\"\"class=\"ball animated\">",
+    "The target element's content is correct");
+
+  let selectorEl = targetEl.querySelector(".node-selector");
+  ok(selectorEl, "The icon to select the target element in the inspector exists");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_target_highlight_select.js
@@ -0,0 +1,62 @@
+/* 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 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();
+
+  info("Select the simple animated node");
+  yield selectNode(".animated", inspector);
+
+  // Make sure to wait for the target-retrieved event if the nodeFront hasn't
+  // yet been retrieved by the TargetNodeComponent.
+  let targetNodeComponent = panel.playerWidgets[0].targetNodeComponent;
+  if (!targetNodeComponent.nodeFront) {
+    yield targetNodeComponent.once("target-retrieved");
+  }
+
+  info("Retrieve the part of the widget that highlights the node on hover");
+  let highlightingEl = targetNodeComponent.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;
+
+  ok(true, "The node-highlight event was fired");
+  is(targetNodeComponent.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");
+
+  info("Select the body node in order to have the list of all animations");
+  yield selectNode("body", inspector);
+
+  // Make sure to wait for the target-retrieved event if the nodeFront hasn't
+  // yet been retrieved by the TargetNodeComponent.
+  targetNodeComponent = panel.playerWidgets[0].targetNodeComponent;
+  if (!targetNodeComponent.nodeFront) {
+    yield targetNodeComponent.once("target-retrieved");
+  }
+
+  info("Click on the first animation widget's selector icon and wait for the selection to change");
+  let onSelection = inspector.selection.once("new-node-front");
+  let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
+  let selectIconEl = targetNodeComponent.selectNodeEl;
+  EventUtils.sendMouseEvent({type: "click"}, selectIconEl,
+                            selectIconEl.ownerDocument.defaultView);
+  yield onSelection;
+
+  is(inspector.selection.nodeFront, targetNodeComponent.nodeFront,
+    "The selected node is the one stored on the animation widget");
+
+  yield onPanelUpdated;
+});
--- a/browser/themes/shared/devtools/animationinspector.css
+++ b/browser/themes/shared/devtools/animationinspector.css
@@ -94,16 +94,46 @@ body {
     background-image: url("debugger-pause@2x.png");
   }
 
   #toggle-all.paused::before {
     background-image: url("debugger-play@2x.png");
   }
 }
 
+/* Animation target node gutter, contains a preview of the dom node */
+
+.animation-target {
+  background-color: var(--theme-toolbar-background);
+  padding: 1px 4px;
+  box-sizing: border-box;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.animation-target .attribute-name {
+  padding-left: 4px;
+}
+
+.animation-target .node-selector {
+  background: url("chrome://browser/skin/devtools/vview-open-inspector.png") no-repeat 0 0;
+  padding-left: 16px;
+  margin-right: 5px;
+  cursor: pointer;
+}
+
+.animation-target .node-selector:hover {
+  background-position: -32px 0;
+}
+
+.animation-target .node-selector:active {
+  background-position: -16px 0;
+}
+
 /* Animation title gutter, contains the name, duration, iteration */
 
 .animation-title {
   background-color: var(--theme-toolbar-background);
   border-bottom: 1px solid var(--theme-splitter-color);
   padding: 1px 4px;
   word-wrap: break-word;
   overflow: auto;
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -2880,37 +2880,85 @@ var WalkerActor = protocol.ActorClass({
       objectActorID: Arg(0, "string")
     },
     response: {
       nodeFront: RetVal("nullable:disconnectedNode")
     }
   }),
 
   /**
-   * Given an StyleSheetActor (identified by its ID), commonly used in the
+   * Given a StyleSheetActor (identified by its ID), commonly used in the
    * style-editor, get its ownerNode and return the corresponding walker's
-   * NodeActor
+   * NodeActor.
+   * Note that getNodeFromActor was added later and can now be used instead.
    */
   getStyleSheetOwnerNode: method(function(styleSheetActorID) {
-    let styleSheetActor = this.conn.getActor(styleSheetActorID);
-    let ownerNode = styleSheetActor.ownerNode;
-
-    if (!styleSheetActor || !ownerNode) {
-      return null;
-    }
-
-    return this.attachElement(ownerNode);
+    return this.getNodeFromActor(styleSheetActorID, ["ownerNode"]);
   }, {
     request: {
       styleSheetActorID: Arg(0, "string")
     },
     response: {
       ownerNode: RetVal("nullable:disconnectedNode")
     }
   }),
+
+  /**
+   * This method can be used to retrieve NodeActor for DOM nodes from other
+   * actors in a way that they can later be highlighted in the page, or
+   * selected in the inspector.
+   * If an actor has a reference to a DOM node, and the UI needs to know about
+   * this DOM node (and possibly select it in the inspector), the UI should
+   * first retrieve a reference to the walkerFront:
+   *
+   * // Make sure the inspector/walker have been initialized first.
+   * toolbox.initInspector().then(() => {
+   *  // Retrieve the walker.
+   *  let walker = toolbox.walker;
+   * });
+   *
+   * And then call this method:
+   *
+   * // Get the nodeFront from my actor, passing the ID and properties path.
+   * walker.getNodeFromActor(myActorID, ["element"]).then(nodeFront => {
+   *   // Use the nodeFront, e.g. select the node in the inspector.
+   *   toolbox.getPanel("inspector").selection.setNodeFront(nodeFront);
+   * });
+   *
+   * @param {String} actorID The ID for the actor that has a reference to the
+   * DOM node.
+   * @param {Array} path Where, on the actor, is the DOM node stored. If in the
+   * scope of the actor, the node is available as `this.data.node`, then this
+   * should be ["data", "node"].
+   * @return {NodeActor} The attached NodeActor, or null if it couldn't be found.
+   */
+  getNodeFromActor: method(function(actorID, path) {
+    let actor = this.conn.getActor(actorID);
+    if (!actor) {
+      return null;
+    }
+
+    let obj = actor;
+    for (let name of path) {
+      if (!(name in obj)) {
+        return null;
+      }
+      obj = obj[name];
+    }
+
+    return this.attachElement(obj);
+  }, {
+    request: {
+      actorID: Arg(0, "string"),
+      path: Arg(1, "array:string")
+    },
+    response: {
+      node: RetVal("nullable:disconnectedNode")
+    }
+  })
 });
 
 /**
  * Client side of the DOM walker.
  */
 var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, {
   // Set to true if cleanup should be requested after every mutation list.
   autoCleanup: true,
@@ -3058,16 +3106,24 @@ var WalkerFront = exports.WalkerFront = 
   getStyleSheetOwnerNode: protocol.custom(function(styleSheetActorID) {
     return this._getStyleSheetOwnerNode(styleSheetActorID).then(response => {
       return response ? response.node : null;
     });
   }, {
     impl: "_getStyleSheetOwnerNode"
   }),
 
+  getNodeFromActor: protocol.custom(function(actorID, path) {
+    return this._getNodeFromActor(actorID, path).then(response => {
+      return response ? response.node : null;
+    });
+  }, {
+    impl: "_getNodeFromActor"
+  }),
+
   _releaseFront: function(node, force) {
     if (node.retained && !force) {
       node.reparent(null);
       this._retainedOrphans.add(node);
       return;
     }
 
     if (node.retained) {
--- a/toolkit/devtools/server/tests/mochitest/chrome.ini
+++ b/toolkit/devtools/server/tests/mochitest/chrome.ini
@@ -48,16 +48,17 @@ skip-if = buildapp == 'mulet'
 [test_getProcess.html]
 skip-if = buildapp == 'mulet'
 [test_inspector-anonymous.html]
 [test_inspector-changeattrs.html]
 [test_inspector-changevalue.html]
 [test_inspector-dead-nodes.html]
 [test_inspector_getImageData.html]
 skip-if = buildapp == 'mulet'
+[test_inspector_getNodeFromActor.html]
 [test_inspector-hide.html]
 [test_inspector-insert.html]
 [test_inspector-mutations-attr.html]
 [test_inspector-mutations-childlist.html]
 [test_inspector-mutations-frameload.html]
 [test_inspector-mutations-value.html]
 [test_inspector-pseudoclass-lock.html]
 [test_inspector-release.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_inspector_getNodeFromActor.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1155653
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1155653</title>
+
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+  <script type="application/javascript;version=1.8" src="inspector-helpers.js"></script>
+  <script type="application/javascript;version=1.8">
+Components.utils.import("resource://gre/modules/devtools/Loader.jsm");
+const inspector = devtools.require("devtools/server/actors/inspector");
+
+window.onload = function() {
+  SimpleTest.waitForExplicitFinish();
+  runNextTest();
+}
+
+var gWalker;
+
+addTest(function() {
+  let url = document.getElementById("inspectorContent").href;
+  attachURL(url, function(err, client, tab, doc) {
+    let {InspectorFront} = devtools.require("devtools/server/actors/inspector");
+    let inspector = InspectorFront(client, tab);
+
+    promiseDone(inspector.getWalker().then(walker => {
+      gWalker = walker;
+    }).then(runNextTest));
+  });
+});
+
+addTest(function() {
+  info("Try to get a NodeFront from an invalid actorID");
+  gWalker.getNodeFromActor("invalid", ["node"]).then(node => {
+    ok(!node, "The node returned is null");
+    runNextTest();
+  });
+});
+
+addTest(function() {
+  info("Try to get a NodeFront from a valid actorID but invalid path");
+  gWalker.getNodeFromActor(gWalker.actorID, ["invalid", "path"]).then(node => {
+    ok(!node, "The node returned is null");
+    runNextTest();
+  });
+});
+
+addTest(function() {
+  info("Try to get a NodeFront from a valid actorID and valid path");
+  gWalker.getNodeFromActor(gWalker.actorID, ["rootDoc"]).then(rootDocNode => {
+    ok(rootDocNode, "A node was returned");
+    is(rootDocNode, gWalker.rootNode, "The right node was returned");
+    runNextTest();
+  });
+});
+
+addTest(function() {
+  info("Try to get a NodeFront from a valid actorID and valid complex path");
+  gWalker.getNodeFromActor(gWalker.actorID,
+    ["tabActor", "window", "document", "body"]).then(bodyNode => {
+    ok(bodyNode, "A node was returned");
+    gWalker.querySelector(gWalker.rootNode, "body").then(node => {
+      is(bodyNode, node, "The body node was returned");
+      runNextTest();
+    });
+  });
+});
+
+addTest(function() {
+  gWalker = null;
+  runNextTest();
+});
+  </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1155653">Mozilla Bug 1155653</a>
+<a id="inspectorContent" target="_blank" href="inspector_getImageData.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>