Bug 1120899 - Extract the animationplayer meta-data UI to a refreshable component; r=bgrins

This commit is contained in:
Patrick Brosset 2015-02-04 14:03:29 +01:00
parent c159c9c250
commit 97e5971148
11 changed files with 289 additions and 75 deletions

View File

@ -138,6 +138,8 @@ function PlayerWidget(player, containerEl) {
this.onStateChanged = this.onStateChanged.bind(this); this.onStateChanged = this.onStateChanged.bind(this);
this.onPlayPauseBtnClick = this.onPlayPauseBtnClick.bind(this); this.onPlayPauseBtnClick = this.onPlayPauseBtnClick.bind(this);
this.metaDataComponent = new PlayerMetaDataHeader();
} }
PlayerWidget.prototype = { PlayerWidget.prototype = {
@ -159,6 +161,7 @@ PlayerWidget.prototype = {
this.stopTimelineAnimation(); this.stopTimelineAnimation();
this.stopListeners(); this.stopListeners();
this.metaDataComponent.destroy();
this.el.remove(); this.el.remove();
this.playPauseBtnEl = this.currentTimeEl = this.timeDisplayEl = null; this.playPauseBtnEl = this.currentTimeEl = this.timeDisplayEl = null;
@ -184,45 +187,8 @@ PlayerWidget.prototype = {
} }
}); });
// Animation header this.metaDataComponent.createMarkup(this.el);
let titleEl = createNode({ this.metaDataComponent.render(state);
parent: this.el,
attributes: {
"class": "animation-title"
}
});
let titleHTML = "";
// Name.
if (state.name) {
// Css animations have names.
titleHTML += L10N.getStr("player.animationNameLabel");
titleHTML += "<strong>" + state.name + "</strong>";
} else {
// Css transitions don't.
titleHTML += L10N.getStr("player.transitionNameLabel");
}
// Duration, delay and iteration count.
titleHTML += "<span class='meta-data'>";
titleHTML += L10N.getStr("player.animationDurationLabel");
titleHTML += "<strong>" + L10N.getFormatStr("player.timeLabel",
this.getFormattedTime(state.duration)) + "</strong>";
if (state.delay) {
titleHTML += L10N.getStr("player.animationDelayLabel");
titleHTML += "<strong>" + L10N.getFormatStr("player.timeLabel",
this.getFormattedTime(state.delay)) + "</strong>";
}
if (state.iterationCount !== 1) {
titleHTML += L10N.getStr("player.animationIterationCountLabel");
let count = state.iterationCount || L10N.getStr("player.infiniteIterationCount");
titleHTML += "<strong>" + count + "</strong>";
}
titleHTML += "</span>";
titleEl.innerHTML = titleHTML;
// Timeline widget. // Timeline widget.
let timelineEl = createNode({ let timelineEl = createNode({
@ -296,18 +262,6 @@ PlayerWidget.prototype = {
this.displayTime(state.currentTime); this.displayTime(state.currentTime);
}, },
/**
* Format time as a string.
* @param {Number} time Defaults to the player's currentTime.
* @return {String} The formatted time, e.g. "10.55"
*/
getFormattedTime: function(time) {
return (time/1000).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
},
/** /**
* Executed when the playPause button is clicked. * Executed when the playPause button is clicked.
* Note that tests may want to call this callback directly rather than * Note that tests may want to call this callback directly rather than
@ -328,7 +282,8 @@ PlayerWidget.prototype = {
*/ */
onStateChanged: function() { onStateChanged: function() {
let state = this.player.state; let state = this.player.state;
this.updateWidgetState(state.playState); this.updateWidgetState(state);
this.metaDataComponent.render(state);
switch (state.playState) { switch (state.playState) {
case "finished": case "finished":
@ -354,7 +309,7 @@ PlayerWidget.prototype = {
pause: function() { pause: function() {
// Switch to the right className on the element right away to avoid waiting // Switch to the right className on the element right away to avoid waiting
// for the next state update to change the playPause icon. // for the next state update to change the playPause icon.
this.updateWidgetState("paused"); this.updateWidgetState({playState: "paused"});
return this.player.pause().then(() => { return this.player.pause().then(() => {
this.stopTimelineAnimation(); this.stopTimelineAnimation();
}); });
@ -368,12 +323,12 @@ PlayerWidget.prototype = {
play: function() { play: function() {
// Switch to the right className on the element right away to avoid waiting // Switch to the right className on the element right away to avoid waiting
// for the next state update to change the playPause icon. // for the next state update to change the playPause icon.
this.updateWidgetState("running"); this.updateWidgetState({playState: "running"});
this.startTimelineAnimation(); this.startTimelineAnimation();
return this.player.play(); return this.player.play();
}, },
updateWidgetState: function(playState) { updateWidgetState: function({playState}) {
this.el.className = "player-widget " + playState; this.el.className = "player-widget " + playState;
}, },
@ -417,7 +372,7 @@ PlayerWidget.prototype = {
// Set the time label value. // Set the time label value.
this.timeDisplayEl.textContent = L10N.getFormatStr("player.timeLabel", this.timeDisplayEl.textContent = L10N.getFormatStr("player.timeLabel",
this.getFormattedTime(time)); L10N.numberWithDecimals(time / 1000, 2));
// Set the timeline slider value. // Set the timeline slider value.
if (!state.iterationCount && time !== state.duration) { if (!state.iterationCount && time !== state.duration) {
@ -437,6 +392,162 @@ PlayerWidget.prototype = {
} }
}; };
/**
* 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 = {
createMarkup: function(containerEl) {
// The main title element.
this.el = createNode({
parent: containerEl,
attributes: {
"class": "animation-title"
}
});
// Animation name (value hidden by default since transitions don't have names).
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) {
// Css animations have names.
this.nameLabel.textContent = L10N.getStr("player.animationNameLabel");
this.nameValue.style.display = "inline";
this.nameValue.textContent = state.name;
} else {
// Css transitions don't.
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;
}
};
/** /**
* DOM node creation helper function. * DOM node creation helper function.
* @param {Object} Options to customize the node to be created. * @param {Object} Options to customize the node to be created.

View File

@ -22,3 +22,4 @@ support-files =
[browser_animation_timeline_animates.js] [browser_animation_timeline_animates.js]
[browser_animation_timeline_waits_for_delay.js] [browser_animation_timeline_waits_for_delay.js]
[browser_animation_ui_updates_when_animation_changes.js] [browser_animation_ui_updates_when_animation_changes.js]
[browser_animation_ui_updates_when_animation_data_changes.js]

View File

@ -13,12 +13,22 @@ add_task(function*() {
info("Selecting a node with an animation that doesn't repeat"); info("Selecting a node with an animation that doesn't repeat");
yield selectNode(".long", inspector); yield selectNode(".long", inspector);
let widget = panel.playerWidgets[0]; let widget = panel.playerWidgets[0];
let metaDataLabels = widget.el.querySelectorAll(".animation-title .meta-data strong");
is(metaDataLabels.length, 1, "Only the duration is shown"); ok(isNodeVisible(widget.metaDataComponent.durationValue),
"The duration value is shown");
ok(!isNodeVisible(widget.metaDataComponent.delayValue),
"The delay value is hidden");
ok(!isNodeVisible(widget.metaDataComponent.iterationValue),
"The iteration count is hidden");
info("Selecting a node with an animation that repeats several times"); info("Selecting a node with an animation that repeats several times");
yield selectNode(".delayed", inspector); yield selectNode(".delayed", inspector);
widget = panel.playerWidgets[0]; widget = panel.playerWidgets[0];
let iterationLabel = widget.el.querySelectorAll(".animation-title .meta-data strong")[2];
is(iterationLabel.textContent, "10", "The iteration is shown"); ok(isNodeVisible(widget.metaDataComponent.durationValue),
"The duration value is shown");
ok(isNodeVisible(widget.metaDataComponent.delayValue),
"The delay value is shown");
ok(isNodeVisible(widget.metaDataComponent.iterationValue),
"The iteration count is shown");
}); });

View File

@ -8,6 +8,8 @@
// slider don't show values bigger than the animation duration (which would // slider don't show values bigger than the animation duration (which would
// happen if the local requestAnimationFrame loop didn't stop correctly). // happen if the local requestAnimationFrame loop didn't stop correctly).
let L10N = new ViewHelpers.L10N();
add_task(function*() { add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {inspector, panel} = yield openAnimationInspector(); let {inspector, panel} = yield openAnimationInspector();
@ -35,6 +37,6 @@ add_task(function*() {
is(widget.currentTimeEl.value, front.state.duration, is(widget.currentTimeEl.value, front.state.duration,
"The timeline slider has the right value"); "The timeline slider has the right value");
is(widget.timeDisplayEl.textContent, is(widget.timeDisplayEl.textContent,
widget.getFormattedTime(front.state.duration) + "s", L10N.numberWithDecimals(front.state.duration / 1000, 2) + "s",
"The timeline slider has the right value"); "The timeline slider has the right value");
}); });

View File

@ -26,9 +26,11 @@ add_task(function*() {
ok(metaDataEl, "The meta-data element exists"); ok(metaDataEl, "The meta-data element exists");
let metaDataEls = metaDataEl.querySelectorAll("strong"); let metaDataEls = metaDataEl.querySelectorAll("strong");
is(metaDataEls.length, 2, "2 meta-data elements were found"); is(metaDataEls.length, 3, "3 meta-data elements were found");
is(metaDataEls[0].textContent, "2.00s", is(metaDataEls[0].textContent, "2s",
"The first meta-data is the duration, and is correct"); "The first meta-data is the duration, and is correct");
ok(!isNodeVisible(metaDataEls[1]),
"The second meta-data is hidden, since there's no delay on the animation");
info("Select the node with the delayed animation"); info("Select the node with the delayed animation");
yield selectNode(".delayed", inspector); yield selectNode(".delayed", inspector);
@ -40,10 +42,13 @@ add_task(function*() {
metaDataEls = titleEl.querySelectorAll(".meta-data strong"); metaDataEls = titleEl.querySelectorAll(".meta-data strong");
is(metaDataEls.length, 3, is(metaDataEls.length, 3,
"3 meta-data elements were found for the delayed animation"); "3 meta-data elements were found for the delayed animation");
is(metaDataEls[0].textContent, "3.00s", is(metaDataEls[0].textContent, "3s",
"The first meta-data is the duration, and is correct"); "The first meta-data is the duration, and is correct");
is(metaDataEls[1].textContent, "60.00s", ok(isNodeVisible(metaDataEls[0]), "The duration is shown");
is(metaDataEls[1].textContent, "60s",
"The second meta-data is the delay, and is correct"); "The second meta-data is the delay, and is correct");
ok(isNodeVisible(metaDataEls[1]), "The delay is shown");
is(metaDataEls[2].textContent, "10", is(metaDataEls[2].textContent, "10",
"The third meta-data is the iteration count, and is correct"); "The third meta-data is the iteration count, and is correct");
ok(isNodeVisible(metaDataEls[2]), "The iteration count is shown");
}); });

View File

@ -7,6 +7,8 @@
// Test that once an animation is paused and its widget is refreshed, the right // Test that once an animation is paused and its widget is refreshed, the right
// initial time is displayed. // initial time is displayed.
let L10N = new ViewHelpers.L10N();
add_task(function*() { add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {inspector, panel} = yield openAnimationInspector(); let {inspector, panel} = yield openAnimationInspector();
@ -25,6 +27,7 @@ add_task(function*() {
widget = panel.playerWidgets[0]; widget = panel.playerWidgets[0];
ok(widget.el.classList.contains("paused"), "The widget is still in paused mode"); ok(widget.el.classList.contains("paused"), "The widget is still in paused mode");
is(widget.timeDisplayEl.textContent, is(widget.timeDisplayEl.textContent,
widget.getFormattedTime(widget.player.state.currentTime) + "s", L10N.numberWithDecimals(widget.player.state.currentTime / 1000, 2) + "s",
"The initial time has been set to the player's"); "The initial time has been set to the player's");
}); });

View File

@ -20,5 +20,5 @@ add_task(function*() {
is(timeline.value, 0, "The timeline is at 0 since the animation hasn't started"); is(timeline.value, 0, "The timeline is at 0 since the animation hasn't started");
let timeLabel = widget.timeDisplayEl; let timeLabel = widget.timeDisplayEl;
is(timeLabel.textContent, "0.00s", "The current time is 0"); is(timeLabel.textContent, "0s", "The current time is 0");
}); });

View File

@ -0,0 +1,45 @@
/* 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";
// Verify that if the animation's duration, iterations or delay change in
// content, then the widget reflects the changes.
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {panel, inspector} = yield openAnimationInspector();
info("Select the test node");
yield selectNode(".animated", inspector);
info("Get the player widget");
let widget = panel.playerWidgets[0];
yield setStyle(widget, "animationDuration", "5.5s");
is(widget.metaDataComponent.durationValue.textContent, "5.50s",
"The widget shows the new duration");
yield setStyle(widget, "animationIterationCount", "300");
is(widget.metaDataComponent.iterationValue.textContent, "300",
"The widget shows the new iteration count");
yield setStyle(widget, "animationDelay", "45s");
is(widget.metaDataComponent.delayValue.textContent, "45s",
"The widget shows the new delay");
});
function* setStyle(widget, name, value) {
info("Change the animation style via the content DOM. Setting " +
name + " to " + value);
yield executeInContent("Test:SetNodeStyle", {
propertyName: name,
propertyValue: value
}, {
node: getNode(".animated")
});
info("Wait for the next state update");
yield widget.player.once(widget.player.AUTO_REFRESH_EVENT);
}

View File

@ -26,3 +26,21 @@ addMessageListener("Test:ToggleAnimationPlayer", function(msg) {
sendAsyncMessage("Test:ToggleAnimationPlayer"); sendAsyncMessage("Test:ToggleAnimationPlayer");
}); });
/**
* Set a given style property value on a node. This is useful to dynamically
* change an animation's duration or delay for instance.
* @param {Object} data
* - {String} propertyName The name of the property to set.
* - {String} propertyValue The value for the property.
* @param {Object} objects
* - {DOMNode} node The node to use
*/
addMessageListener("Test:SetNodeStyle", function(msg) {
let {propertyName, propertyValue} = msg.data;
let {node} = msg.objects;
node.style[propertyName] = propertyValue;
sendAsyncMessage("Test:SetNodeStyle");
});

View File

@ -54,7 +54,7 @@
left: 10px; left: 10px;
background: red; background: red;
animation: simple-animation 2s animation: simple-animation 2s;
} }
.long { .long {
@ -62,7 +62,7 @@
left: 10px; left: 10px;
background: blue; background: blue;
animation: simple-animation 120s animation: simple-animation 120s;
} }
@keyframes simple-animation { @keyframes simple-animation {

View File

@ -5,11 +5,12 @@
"use strict"; "use strict";
const Cu = Components.utils; const Cu = Components.utils;
let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
let TargetFactory = devtools.TargetFactory; const TargetFactory = devtools.TargetFactory;
let {console} = Components.utils.import("resource://gre/modules/devtools/Console.jsm", {}); const {console} = Components.utils.import("resource://gre/modules/devtools/Console.jsm", {});
const {ViewHelpers} = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {});
// All tests are asynchronous // All tests are asynchronous
waitForExplicitFinish(); waitForExplicitFinish();
@ -263,12 +264,30 @@ function executeInContent(name, data={}, objects={}, expectResponse=true) {
* Simulate a click on the playPause button of a playerWidget. * Simulate a click on the playPause button of a playerWidget.
*/ */
let togglePlayPauseButton = Task.async(function*(widget) { let togglePlayPauseButton = Task.async(function*(widget) {
let nextState = widget.player.state.playState === "running" ? "paused" : "running";
// Note that instead of simulating a real event here, the callback is just // Note that instead of simulating a real event here, the callback is just
// called. This is better because the callback returns a promise, so we know // called. This is better because the callback returns a promise, so we know
// when the player is paused, and we don't really care to test that simulating // when the player is paused, and we don't really care to test that simulating
// a DOM event actually works. // a DOM event actually works.
yield widget.onPlayPauseBtnClick(); let onClicked = widget.onPlayPauseBtnClick();
// Verify that the button's state is changed immediately, even if it will be
// changed anyway with the next auto-refresh.
ok(widget.el.classList.contains(nextState),
"The button's state was changed in the UI before the request was sent");
yield onClicked;
// Wait for the next sate change event to make sure the state is updated // Wait for the next sate change event to make sure the state is updated
yield widget.player.once(widget.player.AUTO_REFRESH_EVENT); yield widget.player.once(widget.player.AUTO_REFRESH_EVENT);
}); });
/**
* Is the given node visible in the page (rendered in the frame tree).
* @param {DOMNode}
* @return {Boolean}
*/
function isNodeVisible(node) {
return !!node.getClientRects().length;
}