--- a/devtools/client/inspector/animation/animation.js
+++ b/devtools/client/inspector/animation/animation.js
@@ -3,50 +3,76 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { AnimationsFront } = require("devtools/shared/fronts/animation");
const { createElement, createFactory } = require("devtools/client/shared/vendor/react");
const { Provider } = require("devtools/client/shared/vendor/react-redux");
+const EventEmitter = require("devtools/shared/event-emitter");
+
const App = createFactory(require("./components/App"));
-const { isAllTimingEffectEqual } = require("./utils/utils");
const { updateAnimations } = require("./actions/animations");
const { updateElementPickerEnabled } = require("./actions/element-picker");
const { updateSidebarSize } = require("./actions/sidebar");
+const { isAllAnimationEqual } = require("./utils/utils");
class AnimationInspector {
constructor(inspector) {
this.inspector = inspector;
+ this.getNodeFromActor = this.getNodeFromActor.bind(this);
this.toggleElementPicker = this.toggleElementPicker.bind(this);
this.update = this.update.bind(this);
this.onElementPickerStarted = this.onElementPickerStarted.bind(this);
this.onElementPickerStopped = this.onElementPickerStopped.bind(this);
this.onSidebarResized = this.onSidebarResized.bind(this);
this.onSidebarSelect = this.onSidebarSelect.bind(this);
+ EventEmitter.decorate(this);
+ this.emit = this.emit.bind(this);
+
this.init();
}
init() {
+ const {
+ setSelectedNode,
+ onShowBoxModelHighlighterForNode,
+ } = this.inspector.getCommonComponentProps();
+
+ const {
+ onHideBoxModelHighlighter,
+ } = this.inspector.getPanel("boxmodel").getComponentProps();
+
+ const {
+ emit: emitEventForTest,
+ getNodeFromActor,
+ toggleElementPicker,
+ } = this;
+
const target = this.inspector.target;
this.animationsFront = new AnimationsFront(target.client, target.form);
const provider = createElement(Provider,
{
id: "newanimationinspector",
key: "newanimationinspector",
store: this.inspector.store
},
App(
{
- toggleElementPicker: this.toggleElementPicker
+ emitEventForTest,
+ getNodeFromActor,
+ onHideBoxModelHighlighter,
+ onShowBoxModelHighlighterForNode,
+ setSelectedNode,
+ toggleElementPicker,
}
)
);
this.provider = provider;
this.inspector.selection.on("new-node-front", this.update);
this.inspector.sidebar.on("newanimationinspector-selected", this.onSidebarSelect);
this.inspector.toolbox.on("inspector-sidebar-resized", this.onSidebarResized);
@@ -73,30 +99,34 @@ class AnimationInspector {
const done = this.inspector.updating("newanimationinspector");
const selection = this.inspector.selection;
const animations =
selection.isConnected() && selection.isElementNode()
? await this.animationsFront.getAnimationPlayersForNode(selection.nodeFront)
: [];
- if (!this.animations || !isAllTimingEffectEqual(animations, this.animations)) {
+ if (!this.animations || !isAllAnimationEqual(animations, this.animations)) {
this.inspector.store.dispatch(updateAnimations(animations));
this.animations = animations;
}
done();
}
isPanelVisible() {
return this.inspector && this.inspector.toolbox && this.inspector.sidebar &&
this.inspector.toolbox.currentToolId === "inspector" &&
this.inspector.sidebar.getCurrentTabID() === "newanimationinspector";
}
+ getNodeFromActor(actorID) {
+ return this.inspector.walker.getNodeFromActor(actorID, ["node"]);
+ }
+
toggleElementPicker() {
this.inspector.toolbox.highlighterUtils.togglePicker();
}
onElementPickerStarted() {
this.inspector.store.dispatch(updateElementPickerEnabled(true));
}
--- a/devtools/client/inspector/animation/components/AnimationItem.js
+++ b/devtools/client/inspector/animation/components/AnimationItem.js
@@ -1,27 +1,53 @@
/* 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 { PureComponent } = require("devtools/client/shared/vendor/react");
+const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const AnimationTarget = createFactory(require("./AnimationTarget"));
+
class AnimationItem extends PureComponent {
static get propTypes() {
return {
animation: PropTypes.object.isRequired,
+ emitEventForTest: PropTypes.func.isRequired,
+ getNodeFromActor: PropTypes.func.isRequired,
+ onHideBoxModelHighlighter: PropTypes.func.isRequired,
+ onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
};
}
render() {
+ const {
+ animation,
+ emitEventForTest,
+ getNodeFromActor,
+ onHideBoxModelHighlighter,
+ onShowBoxModelHighlighterForNode,
+ setSelectedNode,
+ } = this.props;
+
return dom.li(
{
className: "animation-item"
- }
+ },
+ AnimationTarget(
+ {
+ animation,
+ emitEventForTest,
+ getNodeFromActor,
+ onHideBoxModelHighlighter,
+ onShowBoxModelHighlighterForNode,
+ setSelectedNode,
+ }
+ )
);
}
}
module.exports = AnimationItem;
--- a/devtools/client/inspector/animation/components/AnimationList.js
+++ b/devtools/client/inspector/animation/components/AnimationList.js
@@ -9,22 +9,47 @@ const dom = require("devtools/client/sha
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const AnimationItem = createFactory(require("./AnimationItem"));
class AnimationList extends PureComponent {
static get propTypes() {
return {
animations: PropTypes.arrayOf(PropTypes.object).isRequired,
+ emitEventForTest: PropTypes.func.isRequired,
+ getNodeFromActor: PropTypes.func.isRequired,
+ onHideBoxModelHighlighter: PropTypes.func.isRequired,
+ onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
};
}
render() {
+ const {
+ animations,
+ emitEventForTest,
+ getNodeFromActor,
+ onHideBoxModelHighlighter,
+ onShowBoxModelHighlighterForNode,
+ setSelectedNode,
+ } = this.props;
+
return dom.ul(
{
className: "animation-list"
},
- this.props.animations.map(animation => AnimationItem({ animation }))
+ animations.map(animation =>
+ AnimationItem(
+ {
+ animation,
+ emitEventForTest,
+ getNodeFromActor,
+ onHideBoxModelHighlighter,
+ onShowBoxModelHighlighterForNode,
+ setSelectedNode,
+ }
+ )
+ )
);
}
}
module.exports = AnimationList;
--- a/devtools/client/inspector/animation/components/AnimationListContainer.js
+++ b/devtools/client/inspector/animation/components/AnimationListContainer.js
@@ -11,33 +11,50 @@ const dom = require("devtools/client/sha
const AnimationList = createFactory(require("./AnimationList"));
const AnimationListHeader = createFactory(require("./AnimationListHeader"));
class AnimationListContainer extends PureComponent {
static get propTypes() {
return {
animations: PropTypes.arrayOf(PropTypes.object).isRequired,
+ emitEventForTest: PropTypes.func.isRequired,
+ getNodeFromActor: PropTypes.func.isRequired,
+ onHideBoxModelHighlighter: PropTypes.func.isRequired,
+ onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
};
}
render() {
- const { animations } = this.props;
+ const {
+ animations,
+ emitEventForTest,
+ getNodeFromActor,
+ onHideBoxModelHighlighter,
+ onShowBoxModelHighlighterForNode,
+ setSelectedNode,
+ } = this.props;
return dom.div(
{
className: "animation-list-container"
},
AnimationListHeader(
{
animations
}
),
AnimationList(
{
- animations
+ animations,
+ emitEventForTest,
+ getNodeFromActor,
+ onHideBoxModelHighlighter,
+ onShowBoxModelHighlighterForNode,
+ setSelectedNode,
}
)
);
}
}
module.exports = AnimationListContainer;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/AnimationTarget.js
@@ -0,0 +1,137 @@
+/* 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 { PureComponent } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+const { REPS, MODE } = require("devtools/client/shared/components/reps/reps");
+const { Rep } = REPS;
+const ElementNode = REPS.ElementNode;
+
+class AnimationTarget extends PureComponent {
+ static get propTypes() {
+ return {
+ animation: PropTypes.object.isRequired,
+ emitEventForTest: PropTypes.func.isRequired,
+ getNodeFromActor: PropTypes.func.isRequired,
+ onHideBoxModelHighlighter: PropTypes.func.isRequired,
+ onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ nodeFront: null,
+ };
+ }
+
+ componentWillMount() {
+ this.updateNodeFront(this.props.animation);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (this.props.animation.actorID !== nextProps.animation.actorID) {
+ this.updateNodeFront(nextProps.animation);
+ }
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return this.state.nodeFront !== nextState.nodeFront;
+ }
+
+ /**
+ * While waiting for a reps fix in https://github.com/devtools-html/reps/issues/92,
+ * translate nodeFront to a grip-like object that can be used with an ElementNode rep.
+ *
+ * @params {NodeFront} nodeFront
+ * The NodeFront for which we want to create a grip-like object.
+ * @returns {Object} a grip-like object that can be used with Reps.
+ */
+ translateNodeFrontToGrip(nodeFront) {
+ let { attributes } = nodeFront;
+
+ // The main difference between NodeFront and grips is that attributes are treated as
+ // a map in grips and as an array in NodeFronts.
+ let attributesMap = {};
+ for (let {name, value} of attributes) {
+ attributesMap[name] = value;
+ }
+
+ return {
+ actor: nodeFront.actorID,
+ preview: {
+ attributes: attributesMap,
+ attributesLength: attributes.length,
+ isConnected: true,
+ nodeName: nodeFront.nodeName.toLowerCase(),
+ nodeType: nodeFront.nodeType,
+ }
+ };
+ }
+
+ async updateNodeFront(animation) {
+ const { emitEventForTest, getNodeFromActor } = this.props;
+
+ // Try and get it from the playerFront directly.
+ let nodeFront = animation.animationTargetNodeFront;
+
+ // Next, get it from the walkerActor if it wasn't found.
+ if (!nodeFront) {
+ try {
+ nodeFront = await getNodeFromActor(animation.actorID);
+ } 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.
+ console.error(e);
+ return;
+ }
+ }
+
+ this.setState({ nodeFront });
+ emitEventForTest("animation-target-rendered");
+ }
+
+ render() {
+ const {
+ onHideBoxModelHighlighter,
+ onShowBoxModelHighlighterForNode,
+ setSelectedNode,
+ } = this.props;
+
+ const { nodeFront } = this.state;
+
+ if (!nodeFront) {
+ return dom.div(
+ {
+ className: "animation-target"
+ }
+ );
+ }
+
+ return dom.div(
+ {
+ className: "animation-target"
+ },
+ Rep(
+ {
+ defaultRep: ElementNode,
+ mode: MODE.TINY,
+ object: this.translateNodeFrontToGrip(nodeFront),
+ onDOMNodeMouseOut: () => onHideBoxModelHighlighter(),
+ onDOMNodeMouseOver: () => onShowBoxModelHighlighterForNode(nodeFront),
+ onInspectIconClick: () => setSelectedNode(nodeFront, "animation-panel"),
+ }
+ )
+ );
+ }
+}
+
+module.exports = AnimationTarget;
--- a/devtools/client/inspector/animation/components/App.js
+++ b/devtools/client/inspector/animation/components/App.js
@@ -11,35 +11,53 @@ const { connect } = require("devtools/cl
const AnimationListContainer = createFactory(require("./AnimationListContainer"));
const NoAnimationPanel = createFactory(require("./NoAnimationPanel"));
class App extends PureComponent {
static get propTypes() {
return {
animations: PropTypes.arrayOf(PropTypes.object).isRequired,
+ emitEventForTest: PropTypes.func.isRequired,
+ getNodeFromActor: PropTypes.func.isRequired,
+ onHideBoxModelHighlighter: PropTypes.func.isRequired,
+ onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
toggleElementPicker: PropTypes.func.isRequired,
};
}
shouldComponentUpdate(nextProps, nextState) {
return this.props.animations.length !== 0 || nextProps.animations.length !== 0;
}
render() {
- const { animations, toggleElementPicker } = this.props;
+ const {
+ animations,
+ emitEventForTest,
+ getNodeFromActor,
+ onHideBoxModelHighlighter,
+ onShowBoxModelHighlighterForNode,
+ setSelectedNode,
+ toggleElementPicker,
+ } = this.props;
return dom.div(
{
id: "animation-container"
},
animations.length ?
AnimationListContainer(
{
- animations
+ animations,
+ emitEventForTest,
+ getNodeFromActor,
+ onHideBoxModelHighlighter,
+ onShowBoxModelHighlighterForNode,
+ setSelectedNode,
}
)
:
NoAnimationPanel(
{
toggleElementPicker
}
)
--- a/devtools/client/inspector/animation/components/moz.build
+++ b/devtools/client/inspector/animation/components/moz.build
@@ -2,13 +2,14 @@
# 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(
'AnimationItem.js',
'AnimationList.js',
'AnimationListContainer.js',
'AnimationListHeader.js',
+ 'AnimationTarget.js',
'AnimationTimelineTickItem.js',
'AnimationTimelineTickList.js',
'App.js',
- 'NoAnimationPanel.js'
+ 'NoAnimationPanel.js',
)
--- a/devtools/client/inspector/animation/test/head.js
+++ b/devtools/client/inspector/animation/test/head.js
@@ -31,16 +31,17 @@ registerCleanupFunction(() => {
* Open the toolbox, with the inspector tool visible and the animationinspector
* sidebar selected.
*
* @return {Promise} that resolves when the inspector is ready.
*/
const openAnimationInspector = async function () {
const { inspector, toolbox } = await openInspectorSidebarTab(TAB_NAME);
await inspector.once("inspector-updated");
+ await waitForAllAnimationTargets(inspector);
const { animationinspector: animationInspector } = inspector;
const panel = inspector.panelWin.document.getElementById("animation-container");
return { animationInspector, toolbox, inspector, panel };
};
/**
* Close the toolbox.
*
@@ -99,24 +100,39 @@ addTab = async function (url) {
* and animations of its subtree are properly displayed.
*/
const selectNodeAndWaitForAnimations = async function (data, inspector, reason = "test") {
// We want to make sure the rest of the test waits for the animations to
// be properly displayed (wait for all target DOM nodes to be previewed).
const onUpdated = inspector.once("inspector-updated");
await selectNode(data, inspector, reason);
await onUpdated;
+ await waitForAllAnimationTargets(inspector);
};
/**
* Set the sidebar width by given parameter.
*
* @param {String} width
* Change sidebar width by given parameter.
* @param {InspectorPanel} inspector
* The instance of InspectorPanel currently loaded in the toolbox
* @return {Promise} Resolves when the sidebar size changed.
*/
const setSidebarWidth = async function (width, inspector) {
const onUpdated = inspector.toolbox.once("inspector-sidebar-resized");
inspector.splitBox.setState({ width });
await onUpdated;
};
+
+/**
+ * Wait for all AnimationTarget components to be fully loaded
+ * (fetched their related actor and rendered).
+ *
+ * @param {Inspector} inspector
+ */
+const waitForAllAnimationTargets = async function (inspector) {
+ const { animationinspector: animationInspector } = inspector;
+
+ for (let i = 0; i < animationInspector.animations.length; i++) {
+ await animationInspector.once("animation-target-rendered");
+ }
+};
--- a/devtools/client/inspector/animation/utils/utils.js
+++ b/devtools/client/inspector/animation/utils/utils.js
@@ -40,29 +40,33 @@ function findOptimalTimeInterval(minTime
return interval;
}
multiplier *= 10;
}
}
/**
- * Check the equality timing effects from given animations.
+ * Check the equality of the given animations.
*
* @param {Array} animations.
- * @param {Array} same to avobe.
- * @return {Boolean} true: same timing effects
+ * @param {Array} same to above.
+ * @return {Boolean} true: same animations
*/
-function isAllTimingEffectEqual(animationsA, animationsB) {
+function isAllAnimationEqual(animationsA, animationsB) {
if (animationsA.length !== animationsB.length) {
return false;
}
for (let i = 0; i < animationsA.length; i++) {
- if (!isTimingEffectEqual(animationsA[i].state, animationsB[i].state)) {
+ const animationA = animationsA[i];
+ const animationB = animationsB[i];
+
+ if (animationA.actorID !== animationB.actorID ||
+ !isTimingEffectEqual(animationsA[i].state, animationsB[i].state)) {
return false;
}
}
return true;
}
/**
@@ -79,10 +83,10 @@ function isTimingEffectEqual(stateA, sta
stateA.easing === stateB.easing &&
stateA.endDelay === stateB.endDelay &&
stateA.fill === stateB.fill &&
stateA.iterationCount === stateB.iterationCount &&
stateA.iterationStart === stateB.iterationStart;
}
exports.findOptimalTimeInterval = findOptimalTimeInterval;
-exports.isAllTimingEffectEqual = isAllTimingEffectEqual;
+exports.isAllAnimationEqual = isAllAnimationEqual;
exports.isTimingEffectEqual = isTimingEffectEqual;
--- a/devtools/client/themes/animation.css
+++ b/devtools/client/themes/animation.css
@@ -2,16 +2,17 @@
* 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/. */
/* Animation-inspector specific theme variables */
:root {
--animation-even-background-color: rgba(0, 0, 0, 0.05);
--command-pick-image: url(chrome://devtools/skin/images/command-pick.svg);
+ --sidebar-width: 200px;
}
:root.theme-dark {
--animation-even-background-color: rgba(255, 255, 255, 0.05);
}
:root.theme-firebug {
--command-pick-image: url(chrome://devtools/skin/images/firebug/command-pick.svg);
@@ -23,17 +24,17 @@
justify-content: flex-end;
padding: 0;
}
/* Animation Timeline Tick List */
.animation-timeline-tick-list {
margin-right: 10px;
position: relative;
- width: calc(100% - 210px);
+ width: calc(100% - var(--sidebar-width) - 10px);
}
.animation-timeline-tick-item {
border-left: 0.5px solid rgba(128, 136, 144, .5);
height: 100vh;
position: absolute;
}
@@ -48,16 +49,29 @@
.animation-item {
height: 30px;
}
.animation-item:nth-child(2n+1) {
background-color: var(--animation-even-background-color);
}
+/* Animation Target */
+.animation-target {
+ align-items: center;
+ display: flex;
+ height: 100%;
+ padding-left: 4px;
+ width: var(--sidebar-width);
+}
+
+.animation-target .tag-name {
+ cursor: default;
+}
+
/* No Animation Panel */
.animation-error-message {
overflow: auto;
}
.animation-error-message > p {
white-space: pre;
}