mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1228005 - 1 - Display animated property list with keyframes when an animation is selected; r=tromey r=bgrins
This commit is contained in:
parent
8f92eca884
commit
6da9f4b579
@ -257,7 +257,9 @@ AnimationTargetNode.prototype = {
|
||||
this.inspector.selection.setNodeFront(this.nodeFront, "animationinspector");
|
||||
},
|
||||
|
||||
onHighlightNodeClick: function() {
|
||||
onHighlightNodeClick: function(e) {
|
||||
e.stopPropagation();
|
||||
|
||||
let classList = this.highlightNodeEl.classList;
|
||||
|
||||
let isHighlighted = classList.contains("selected");
|
||||
@ -466,7 +468,8 @@ var TimeScale = {
|
||||
let length = (delay / playbackRate) +
|
||||
((duration / playbackRate) *
|
||||
(!iterationCount ? 1 : iterationCount));
|
||||
this.maxEndTime = Math.max(this.maxEndTime, previousStartTime + length);
|
||||
let endTime = previousStartTime + length;
|
||||
this.maxEndTime = Math.max(this.maxEndTime, endTime);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -493,7 +496,7 @@ var TimeScale = {
|
||||
* @return {Number}
|
||||
*/
|
||||
durationToDistance: function(duration) {
|
||||
return duration * 100 / (this.maxEndTime - this.minStartTime);
|
||||
return duration * 100 / this.getDuration();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -502,8 +505,7 @@ var TimeScale = {
|
||||
* @return {Number}
|
||||
*/
|
||||
distanceToTime: function(distance) {
|
||||
return this.minStartTime +
|
||||
((this.maxEndTime - this.minStartTime) * distance / 100);
|
||||
return this.minStartTime + (this.getDuration() * distance / 100);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -524,15 +526,44 @@ var TimeScale = {
|
||||
* @return {String} The formatted time string.
|
||||
*/
|
||||
formatTime: function(time) {
|
||||
let duration = this.maxEndTime - this.minStartTime;
|
||||
|
||||
// Format in milliseconds if the total duration is short enough.
|
||||
if (duration <= MILLIS_TIME_FORMAT_MAX_DURATION) {
|
||||
if (this.getDuration() <= MILLIS_TIME_FORMAT_MAX_DURATION) {
|
||||
return L10N.getFormatStr("timeline.timeGraduationLabel", time.toFixed(0));
|
||||
}
|
||||
|
||||
// Otherwise format in seconds.
|
||||
return L10N.getFormatStr("player.timeLabel", (time / 1000).toFixed(1));
|
||||
},
|
||||
|
||||
getDuration: function() {
|
||||
return this.maxEndTime - this.minStartTime;
|
||||
},
|
||||
|
||||
/**
|
||||
* Given an animation, get the various dimensions (in %) useful to draw the
|
||||
* animation in the timeline.
|
||||
*/
|
||||
getAnimationDimensions: function({state}) {
|
||||
let start = state.previousStartTime || 0;
|
||||
let duration = state.duration;
|
||||
let rate = state.playbackRate;
|
||||
let count = state.iterationCount;
|
||||
let delay = state.delay || 0;
|
||||
|
||||
// The start position.
|
||||
let x = this.startTimeToDistance(start + (delay / rate));
|
||||
// The width for a single iteration.
|
||||
let w = this.durationToDistance(duration / rate);
|
||||
// The width for all iterations.
|
||||
let iterationW = w * (count || 1);
|
||||
// The start position of the delay.
|
||||
let delayX = this.durationToDistance((delay < 0 ? 0 : delay) / rate);
|
||||
// The width of the delay.
|
||||
let delayW = this.durationToDistance(Math.abs(delay) / rate);
|
||||
// The width of the delay if it is positive, 0 otherwise.
|
||||
let negativeDelayW = delay < 0 ? delayW : 0;
|
||||
|
||||
return {x, w, iterationW, delayX, delayW, negativeDelayW};
|
||||
}
|
||||
};
|
||||
|
||||
@ -556,6 +587,7 @@ function AnimationsTimeline(inspector) {
|
||||
this.animations = [];
|
||||
this.targetNodes = [];
|
||||
this.timeBlocks = [];
|
||||
this.details = [];
|
||||
this.inspector = inspector;
|
||||
|
||||
this.onAnimationStateChanged = this.onAnimationStateChanged.bind(this);
|
||||
@ -565,6 +597,7 @@ function AnimationsTimeline(inspector) {
|
||||
this.onScrubberMouseMove = this.onScrubberMouseMove.bind(this);
|
||||
this.onAnimationSelected = this.onAnimationSelected.bind(this);
|
||||
this.onWindowResize = this.onWindowResize.bind(this);
|
||||
this.onFrameSelected = this.onFrameSelected.bind(this);
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
@ -584,7 +617,7 @@ AnimationsTimeline.prototype = {
|
||||
|
||||
let scrubberContainer = createNode({
|
||||
parent: this.rootWrapperEl,
|
||||
attributes: {"class": "scrubber-wrapper"}
|
||||
attributes: {"class": "scrubber-wrapper track-container"}
|
||||
});
|
||||
|
||||
this.scrubberEl = createNode({
|
||||
@ -605,7 +638,7 @@ AnimationsTimeline.prototype = {
|
||||
this.timeHeaderEl = createNode({
|
||||
parent: this.rootWrapperEl,
|
||||
attributes: {
|
||||
"class": "time-header"
|
||||
"class": "time-header track-container"
|
||||
}
|
||||
});
|
||||
this.timeHeaderEl.addEventListener("mousedown", this.onScrubberMouseDown);
|
||||
@ -643,19 +676,20 @@ AnimationsTimeline.prototype = {
|
||||
this.inspector = null;
|
||||
},
|
||||
|
||||
destroyTargetNodes: function() {
|
||||
for (let targetNode of this.targetNodes) {
|
||||
targetNode.destroy();
|
||||
/**
|
||||
* Destroy sub-components that have been created and stored on this instance.
|
||||
* @param {String} name An array of components will be expected in this[name]
|
||||
* @param {Array} handlers An option list of event handlers information that
|
||||
* should be used to remove these handlers.
|
||||
*/
|
||||
destroySubComponents: function(name, handlers = []) {
|
||||
for (let component of this[name]) {
|
||||
for (let {event, fn} of handlers) {
|
||||
component.off(event, fn);
|
||||
}
|
||||
component.destroy();
|
||||
}
|
||||
this.targetNodes = [];
|
||||
},
|
||||
|
||||
destroyTimeBlocks: function() {
|
||||
for (let timeBlock of this.timeBlocks) {
|
||||
timeBlock.off("selected", this.onAnimationSelected);
|
||||
timeBlock.destroy();
|
||||
}
|
||||
this.timeBlocks = [];
|
||||
this[name] = [];
|
||||
},
|
||||
|
||||
unrender: function() {
|
||||
@ -663,8 +697,12 @@ AnimationsTimeline.prototype = {
|
||||
animation.off("changed", this.onAnimationStateChanged);
|
||||
}
|
||||
TimeScale.reset();
|
||||
this.destroyTargetNodes();
|
||||
this.destroyTimeBlocks();
|
||||
this.destroySubComponents("targetNodes");
|
||||
this.destroySubComponents("timeBlocks");
|
||||
this.destroySubComponents("details", [{
|
||||
event: "frame-selected",
|
||||
fn: this.onFrameSelected
|
||||
}]);
|
||||
this.animationsEl.innerHTML = "";
|
||||
},
|
||||
|
||||
@ -679,21 +717,33 @@ AnimationsTimeline.prototype = {
|
||||
},
|
||||
|
||||
onAnimationSelected: function(e, animation) {
|
||||
// Unselect the previously selected animation if any.
|
||||
[...this.rootWrapperEl.querySelectorAll(".animation.selected")].forEach(el => {
|
||||
el.classList.remove("selected");
|
||||
});
|
||||
|
||||
// Select the new animation.
|
||||
let index = this.animations.indexOf(animation);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
this.rootWrapperEl.querySelectorAll(".animation")[index]
|
||||
.classList.toggle("selected");
|
||||
|
||||
// Relay the event to the parent component.
|
||||
this.emit("selected", animation);
|
||||
let el = this.rootWrapperEl;
|
||||
let animationEl = el.querySelectorAll(".animation")[index];
|
||||
let propsEl = el.querySelectorAll(".animated-properties")[index];
|
||||
|
||||
// Toggle the selected state on this animation.
|
||||
animationEl.classList.toggle("selected");
|
||||
propsEl.classList.toggle("selected");
|
||||
|
||||
// Render the details component for this animation if it was shown.
|
||||
if (animationEl.classList.contains("selected")) {
|
||||
this.details[index].render(animation);
|
||||
this.emit("animation-selected", animation);
|
||||
} else {
|
||||
this.emit("animation-unselected", animation);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* When a frame gets selected, move the scrubber to the corresponding position
|
||||
*/
|
||||
onFrameSelected: function(e, {x}) {
|
||||
this.moveScrubberTo(x, true);
|
||||
},
|
||||
|
||||
onScrubberMouseDown: function(e) {
|
||||
@ -728,14 +778,17 @@ AnimationsTimeline.prototype = {
|
||||
this.moveScrubberTo(e.pageX);
|
||||
},
|
||||
|
||||
moveScrubberTo: function(pageX) {
|
||||
moveScrubberTo: function(pageX, noOffset) {
|
||||
this.stopAnimatingScrubber();
|
||||
|
||||
// The offset needs to be in % and relative to the timeline's area (so we
|
||||
// subtract the scrubber's left offset, which is equal to the sidebar's
|
||||
// width).
|
||||
let offset = (pageX - this.timeHeaderEl.offsetLeft) * 100 /
|
||||
this.timeHeaderEl.offsetWidth;
|
||||
let offset = pageX;
|
||||
if (!noOffset) {
|
||||
offset -= this.timeHeaderEl.offsetLeft;
|
||||
}
|
||||
offset = offset * 100 / this.timeHeaderEl.offsetWidth;
|
||||
if (offset < 0) {
|
||||
offset = 0;
|
||||
}
|
||||
@ -782,6 +835,21 @@ AnimationsTimeline.prototype = {
|
||||
}
|
||||
});
|
||||
|
||||
// Right below the line is a hidden-by-default line for displaying the
|
||||
// inline keyframes.
|
||||
let detailsEl = createNode({
|
||||
parent: this.animationsEl,
|
||||
nodeType: "li",
|
||||
attributes: {
|
||||
"class": "animated-properties"
|
||||
}
|
||||
});
|
||||
|
||||
let details = new AnimationDetails();
|
||||
details.init(detailsEl);
|
||||
details.on("frame-selected", this.onFrameSelected);
|
||||
this.details.push(details);
|
||||
|
||||
// Left sidebar for the animated node.
|
||||
let animatedNodeEl = createNode({
|
||||
parent: animationEl,
|
||||
@ -789,6 +857,7 @@ AnimationsTimeline.prototype = {
|
||||
"class": "target"
|
||||
}
|
||||
});
|
||||
|
||||
// Draw the animated node target.
|
||||
let targetNode = new AnimationTargetNode(this.inspector, {compact: true});
|
||||
targetNode.init(animatedNodeEl);
|
||||
@ -799,9 +868,10 @@ AnimationsTimeline.prototype = {
|
||||
let timeBlockEl = createNode({
|
||||
parent: animationEl,
|
||||
attributes: {
|
||||
"class": "time-block"
|
||||
"class": "time-block track-container"
|
||||
}
|
||||
});
|
||||
|
||||
// Draw the animation time block.
|
||||
let timeBlock = new AnimationTimeBlock();
|
||||
timeBlock.init(timeBlockEl);
|
||||
@ -810,6 +880,7 @@ AnimationsTimeline.prototype = {
|
||||
|
||||
timeBlock.on("selected", this.onAnimationSelected);
|
||||
}
|
||||
|
||||
// Use the document's current time to position the scrubber (if the server
|
||||
// doesn't provide it, hide the scrubber entirely).
|
||||
// Note that because the currentTime was sent via the protocol, some time
|
||||
@ -932,47 +1003,45 @@ AnimationTimeBlock.prototype = {
|
||||
|
||||
destroy: function() {
|
||||
this.containerEl.removeEventListener("click", this.onClick);
|
||||
while (this.containerEl.firstChild) {
|
||||
this.containerEl.firstChild.remove();
|
||||
}
|
||||
this.unrender();
|
||||
this.containerEl = null;
|
||||
this.animation = null;
|
||||
},
|
||||
|
||||
unrender: function() {
|
||||
while (this.containerEl.firstChild) {
|
||||
this.containerEl.firstChild.remove();
|
||||
}
|
||||
},
|
||||
|
||||
render: function(animation) {
|
||||
this.unrender();
|
||||
|
||||
this.animation = animation;
|
||||
let {state} = this.animation;
|
||||
|
||||
// Create a container element to hold the delay and iterations.
|
||||
// It is positioned according to its delay (divided by the playbackrate),
|
||||
// and its width is according to its duration (divided by the playbackrate).
|
||||
let start = state.previousStartTime || 0;
|
||||
let duration = state.duration;
|
||||
let rate = state.playbackRate;
|
||||
let count = state.iterationCount;
|
||||
let delay = state.delay || 0;
|
||||
|
||||
let x = TimeScale.startTimeToDistance(start + (delay / rate));
|
||||
let w = TimeScale.durationToDistance(duration / rate);
|
||||
let iterationW = w * (count || 1);
|
||||
let delayW = TimeScale.durationToDistance(Math.abs(delay) / rate);
|
||||
let {x, iterationW, delayX, delayW, negativeDelayW} =
|
||||
TimeScale.getAnimationDimensions(animation);
|
||||
|
||||
let iterations = createNode({
|
||||
parent: this.containerEl,
|
||||
attributes: {
|
||||
"class": state.type + " iterations" + (count ? "" : " infinite"),
|
||||
"class": state.type + " iterations" +
|
||||
(state.iterationCount ? "" : " infinite"),
|
||||
// Individual iterations are represented by setting the size of the
|
||||
// repeating linear-gradient.
|
||||
"style": `left:${x}%;
|
||||
width:${iterationW}%;
|
||||
background-size:${100 / (count || 1)}% 100%;`
|
||||
background-size:${100 / (state.iterationCount || 1)}% 100%;`
|
||||
}
|
||||
});
|
||||
|
||||
// The animation name is displayed over the iterations.
|
||||
// Note that in case of negative delay, we push the name towards the right
|
||||
// so the delay can be shown.
|
||||
let negativeDelayW = delay < 0 ? delayW : 0;
|
||||
createNode({
|
||||
parent: iterations,
|
||||
attributes: {
|
||||
@ -985,13 +1054,12 @@ AnimationTimeBlock.prototype = {
|
||||
});
|
||||
|
||||
// Delay.
|
||||
if (delay) {
|
||||
if (state.delay) {
|
||||
// Negative delays need to start at 0.
|
||||
let delayX = TimeScale.durationToDistance((delay < 0 ? 0 : delay) / rate);
|
||||
createNode({
|
||||
parent: iterations,
|
||||
attributes: {
|
||||
"class": "delay" + (delay < 0 ? " negative" : ""),
|
||||
"class": "delay" + (state.delay < 0 ? " negative" : ""),
|
||||
"style": `left:-${delayX}%;
|
||||
width:${delayW}%;`
|
||||
}
|
||||
@ -1005,12 +1073,8 @@ AnimationTimeBlock.prototype = {
|
||||
|
||||
let text = "";
|
||||
|
||||
// Adding the name (the type isn't always available, older servers don't
|
||||
// send it).
|
||||
text +=
|
||||
state.type
|
||||
? L10N.getFormatStr("timeline." + state.type + ".nameLabel", state.name)
|
||||
: state.name;
|
||||
// Adding the name.
|
||||
text += getFormattedAnimationTitle({state});
|
||||
text += "\n";
|
||||
|
||||
// Adding the delay.
|
||||
@ -1046,9 +1110,239 @@ AnimationTimeBlock.prototype = {
|
||||
return text;
|
||||
},
|
||||
|
||||
onClick: function() {
|
||||
onClick: function(e) {
|
||||
e.stopPropagation();
|
||||
this.emit("selected", this.animation);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* UI component responsible for displaying detailed information for a given
|
||||
* animation.
|
||||
* This includes information about timing, easing, keyframes, animated
|
||||
* properties.
|
||||
*/
|
||||
function AnimationDetails() {
|
||||
EventEmitter.decorate(this);
|
||||
|
||||
this.onFrameSelected = this.onFrameSelected.bind(this);
|
||||
|
||||
this.keyframeComponents = [];
|
||||
}
|
||||
|
||||
exports.AnimationDetails = AnimationDetails;
|
||||
|
||||
AnimationDetails.prototype = {
|
||||
// These are part of frame objects but are not animated properties. This
|
||||
// array is used to skip them.
|
||||
NON_PROPERTIES: ["easing", "composite", "computedOffset", "offset"],
|
||||
|
||||
init: function(containerEl) {
|
||||
this.containerEl = containerEl;
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.unrender();
|
||||
this.containerEl = null;
|
||||
},
|
||||
|
||||
unrender: function() {
|
||||
for (let component of this.keyframeComponents) {
|
||||
component.off("frame-selected", this.onFrameSelected);
|
||||
component.destroy();
|
||||
}
|
||||
this.keyframeComponents = [];
|
||||
|
||||
while (this.containerEl.firstChild) {
|
||||
this.containerEl.firstChild.remove();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert a list of frames into a list of tracks, one per animated property,
|
||||
* each with a list of frames.
|
||||
*/
|
||||
getTracksFromFrames: function(frames) {
|
||||
let tracks = {};
|
||||
|
||||
for (let frame of frames) {
|
||||
for (let name in frame) {
|
||||
if (this.NON_PROPERTIES.indexOf(name) != -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!tracks[name]) {
|
||||
tracks[name] = [];
|
||||
}
|
||||
|
||||
tracks[name].push({
|
||||
value: frame[name],
|
||||
offset: frame.computedOffset
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return tracks;
|
||||
},
|
||||
|
||||
render: Task.async(function*(animation) {
|
||||
this.unrender();
|
||||
|
||||
if (!animation) {
|
||||
return;
|
||||
}
|
||||
this.animation = animation;
|
||||
|
||||
let frames = yield animation.getFrames();
|
||||
|
||||
// We might have been destroyed in the meantime, or the component might
|
||||
// have been re-rendered.
|
||||
if (!this.containerEl || this.animation !== animation) {
|
||||
return;
|
||||
}
|
||||
// Useful for tests to know when the keyframes have been retrieved.
|
||||
this.emit("keyframes-retrieved");
|
||||
|
||||
// Build an element for each animated property track.
|
||||
this.tracks = this.getTracksFromFrames(frames);
|
||||
for (let propertyName in this.tracks) {
|
||||
let line = createNode({
|
||||
parent: this.containerEl,
|
||||
attributes: {"class": "property"}
|
||||
});
|
||||
|
||||
createNode({
|
||||
// text-overflow doesn't work in flex items, so we need a second level
|
||||
// of container to actually have an ellipsis on the name.
|
||||
// See bug 972664.
|
||||
parent: createNode({
|
||||
parent: line,
|
||||
attributes: {"class": "name"},
|
||||
}),
|
||||
textContent: getCssPropertyName(propertyName)
|
||||
});
|
||||
|
||||
// Add the keyframes diagram for this property.
|
||||
let framesWrapperEl = createNode({
|
||||
parent: line,
|
||||
attributes: {"class": "track-container"}
|
||||
});
|
||||
|
||||
let framesEl = createNode({
|
||||
parent: framesWrapperEl,
|
||||
attributes: {"class": "frames"}
|
||||
});
|
||||
|
||||
// Scale the list of keyframes according to the current time scale.
|
||||
let {x, w} = TimeScale.getAnimationDimensions(animation);
|
||||
framesEl.style.left = `${x}%`;
|
||||
framesEl.style.width = `${w}%`;
|
||||
|
||||
let keyframesComponent = new Keyframes();
|
||||
keyframesComponent.init(framesEl);
|
||||
keyframesComponent.render({
|
||||
keyframes: this.tracks[propertyName],
|
||||
propertyName: propertyName,
|
||||
animation: animation
|
||||
});
|
||||
keyframesComponent.on("frame-selected", this.onFrameSelected);
|
||||
|
||||
this.keyframeComponents.push(keyframesComponent);
|
||||
}
|
||||
}),
|
||||
|
||||
onFrameSelected: function(e, args) {
|
||||
// Relay the event up, it's needed in parents too.
|
||||
this.emit(e, args);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* UI component responsible for displaying a list of keyframes.
|
||||
*/
|
||||
function Keyframes() {
|
||||
EventEmitter.decorate(this);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
}
|
||||
|
||||
exports.Keyframes = Keyframes;
|
||||
|
||||
Keyframes.prototype = {
|
||||
init: function(containerEl) {
|
||||
this.containerEl = containerEl;
|
||||
|
||||
this.keyframesEl = createNode({
|
||||
parent: this.containerEl,
|
||||
attributes: {"class": "keyframes"}
|
||||
});
|
||||
|
||||
this.containerEl.addEventListener("click", this.onClick);
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.containerEl.removeEventListener("click", this.onClick);
|
||||
this.keyframesEl.remove();
|
||||
this.containerEl = this.keyframesEl = this.animation = null;
|
||||
},
|
||||
|
||||
render: function({keyframes, propertyName, animation}) {
|
||||
this.keyframes = keyframes;
|
||||
this.propertyName = propertyName;
|
||||
this.animation = animation;
|
||||
|
||||
this.keyframesEl.classList.add(animation.state.type);
|
||||
for (let frame of this.keyframes) {
|
||||
createNode({
|
||||
parent: this.keyframesEl,
|
||||
attributes: {
|
||||
"class": "frame",
|
||||
"style": `left:${frame.offset * 100}%;`,
|
||||
"data-offset": frame.offset,
|
||||
"data-property": propertyName,
|
||||
"title": frame.value
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
// If the click happened on a frame, tell our parent about it.
|
||||
if (!e.target.classList.contains("frame")) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
this.emit("frame-selected", {
|
||||
animation: this.animation,
|
||||
propertyName: this.propertyName,
|
||||
offset: parseFloat(e.target.dataset.offset),
|
||||
value: e.target.getAttribute("title"),
|
||||
x: e.target.offsetLeft + e.target.closest(".frames").offsetLeft
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let sortedUnique = arr => [...new Set(arr)].sort((a, b) => a > b);
|
||||
|
||||
/**
|
||||
* Get a formatted title for this animation. This will be either:
|
||||
* "some-name", "some-name : CSS Transition", or "some-name : CSS Animation",
|
||||
* depending if the server provides the type, and what type it is.
|
||||
* @param {AnimationPlayerFront} animation
|
||||
*/
|
||||
function getFormattedAnimationTitle({state}) {
|
||||
// Older servers don't send the type.
|
||||
return state.type
|
||||
? L10N.getFormatStr("timeline." + state.type + ".nameLabel", state.name)
|
||||
: state.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn propertyName into property-name.
|
||||
* @param {String} jsPropertyName A camelcased CSS property name. Typically
|
||||
* something that comes out of computed styles. E.g. borderBottomColor
|
||||
* @return {String} The corresponding CSS property name: border-bottom-color
|
||||
*/
|
||||
function getCssPropertyName(jsPropertyName) {
|
||||
return jsPropertyName.replace(/[A-Z]/g, "-$&").toLowerCase();
|
||||
}
|
||||
|
@ -16,32 +16,40 @@ add_task(function*() {
|
||||
ok(!selected.length, "There are no animations selected by default");
|
||||
|
||||
info("Click on the first animation, expect the right event and right class");
|
||||
let animation0 = yield clickToSelect(timeline, 0);
|
||||
let animation0 = yield clickToChangeSelection(timeline, 0);
|
||||
is(animation0, timeline.animations[0],
|
||||
"The selected event was emitted with the right animation");
|
||||
ok(isTimeBlockSelected(timeline, 0),
|
||||
"The time block has the right selected class");
|
||||
|
||||
info("Click on the second animation, expect the first one to be unselected");
|
||||
let animation1 = yield clickToSelect(timeline, 1);
|
||||
info("Click on the second animation, expect it to be selected too");
|
||||
let animation1 = yield clickToChangeSelection(timeline, 1);
|
||||
is(animation1, timeline.animations[1],
|
||||
"The selected event was emitted with the right animation");
|
||||
ok(isTimeBlockSelected(timeline, 1),
|
||||
"The second time block has the right selected class");
|
||||
|
||||
info("Click again on the first animation and check if it unselects");
|
||||
yield clickToChangeSelection(timeline, 0, true);
|
||||
ok(!isTimeBlockSelected(timeline, 0),
|
||||
"The first time block has been unselected");
|
||||
});
|
||||
|
||||
function* clickToSelect(timeline, index) {
|
||||
function* clickToChangeSelection(timeline, index, isUnselect) {
|
||||
info("Click on animation " + index + " in the timeline");
|
||||
let onSelected = timeline.once("selected");
|
||||
let onSelectionChanged = timeline.once(isUnselect
|
||||
? "animation-unselected"
|
||||
: "animation-selected");
|
||||
let timeBlock = timeline.rootWrapperEl.querySelectorAll(".time-block")[index];
|
||||
EventUtils.sendMouseEvent({type: "click"}, timeBlock,
|
||||
timeBlock.ownerDocument.defaultView);
|
||||
return yield onSelected;
|
||||
return yield onSelectionChanged;
|
||||
}
|
||||
|
||||
function isTimeBlockSelected(timeline, index) {
|
||||
let animation = timeline.rootWrapperEl.querySelectorAll(".animation")[index];
|
||||
return animation.classList.contains("selected");
|
||||
let animatedProperties = timeline.rootWrapperEl.querySelectorAll(
|
||||
".animated-properties")[index];
|
||||
return animation.classList.contains("selected") &&
|
||||
animatedProperties.classList.contains("selected");
|
||||
}
|
||||
|
@ -13,6 +13,5 @@ add_task(function*() {
|
||||
let {panel} = yield openAnimationInspector();
|
||||
is(panel.animationsTimelineComponent.animations.length, 1,
|
||||
"One animation is handled by the timeline after init");
|
||||
is(panel.animationsTimelineComponent.animationsEl.childNodes.length, 1,
|
||||
"One animation is displayed after init");
|
||||
assertAnimationsDisplayed(panel, 1, "One animation is displayed after init");
|
||||
});
|
||||
|
@ -20,7 +20,6 @@ const TEST_URL_ROOT = "http://example.com/browser/devtools/client/animationinspe
|
||||
const ROOT_TEST_DIR = getRootDirectory(gTestPath);
|
||||
const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js";
|
||||
const COMMON_FRAME_SCRIPT_URL = "chrome://devtools/content/shared/frame-script-utils.js";
|
||||
const NEW_UI_PREF = "devtools.inspector.animationInspectorV3";
|
||||
const TAB_NAME = "animationinspector";
|
||||
|
||||
// Auto clean-up when a test ends
|
||||
@ -32,9 +31,6 @@ registerCleanupFunction(function*() {
|
||||
}
|
||||
});
|
||||
|
||||
// Make sure the new UI is off by default.
|
||||
Services.prefs.setBoolPref(NEW_UI_PREF, false);
|
||||
|
||||
// Uncomment this pref to dump all devtools emitted events to the console.
|
||||
// Services.prefs.setBoolPref("devtools.dump.emit", true);
|
||||
|
||||
@ -50,7 +46,6 @@ registerCleanupFunction(() => DevToolsUtils.testing = false);
|
||||
registerCleanupFunction(() => {
|
||||
Services.prefs.clearUserPref("devtools.dump.emit");
|
||||
Services.prefs.clearUserPref("devtools.debugger.log");
|
||||
Services.prefs.clearUserPref(NEW_UI_PREF);
|
||||
});
|
||||
|
||||
/**
|
||||
@ -144,10 +139,11 @@ var selectNode = Task.async(function*(data, inspector, reason="test") {
|
||||
* @param {Number} nbAnimations The expected number of animations.
|
||||
* @param {String} msg An optional string to be used as the assertion message.
|
||||
*/
|
||||
function assertAnimationsDisplayed(panel, nbAnimations, msg="") {
|
||||
function assertAnimationsDisplayed(panel, nbAnimations, msg = "") {
|
||||
msg = msg || `There are ${nbAnimations} animations in the panel`;
|
||||
is(panel.animationsTimelineComponent.animationsEl.childNodes.length,
|
||||
nbAnimations, msg);
|
||||
is(panel.animationsTimelineComponent
|
||||
.animationsEl
|
||||
.querySelectorAll(".animation").length, nbAnimations, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -15,10 +15,16 @@
|
||||
:root {
|
||||
/* How high should toolbars be */
|
||||
--toolbar-height: 20px;
|
||||
/* How wide should the sidebar be */
|
||||
--timeline-sidebar-width: 150px;
|
||||
/* How wide should the sidebar be (should be wide enough to contain long
|
||||
property names like 'border-bottom-right-radius' without ellipsis) */
|
||||
--timeline-sidebar-width: 200px;
|
||||
/* How high should animations displayed in the timeline be */
|
||||
--timeline-animation-height: 20px;
|
||||
/* The size of a keyframe marker in the keyframes diagram */
|
||||
--keyframes-marker-size: 10px;
|
||||
/* The color of the time graduation borders. This should match the the color
|
||||
devtools/client/animationinspector/utils.js */
|
||||
--time-graduation-border-color: rgba(128, 136, 144, .5);
|
||||
}
|
||||
|
||||
html {
|
||||
@ -166,21 +172,29 @@ body {
|
||||
background-image: -moz-element(#time-graduations);
|
||||
background-repeat: repeat-y;
|
||||
/* Make the background be 100% of the timeline area so that it resizes with
|
||||
it*/
|
||||
background-size: calc(100% - var(--timeline-sidebar-width)) 100%;
|
||||
it and subtract the width of the sidebar and the buffer at the right of the
|
||||
timeline */
|
||||
background-size: calc(100% - var(--timeline-sidebar-width) - var(--keyframes-marker-size)) 100%;
|
||||
background-position: var(--timeline-sidebar-width) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.animation-timeline .scrubber-wrapper {
|
||||
/* Useful for positioning animations or keyframes in the timeline */
|
||||
.animation-timeline .track-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: var(--timeline-sidebar-width);
|
||||
right: 0;
|
||||
/* Leave the width of a marker right of a track so the 100% markers can be
|
||||
selected easily */
|
||||
right: var(--keyframes-marker-size);
|
||||
height: var(--timeline-animation-height);
|
||||
}
|
||||
|
||||
.animation-timeline .scrubber-wrapper {
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.animation-timeline .scrubber {
|
||||
@ -216,13 +230,7 @@ body {
|
||||
}
|
||||
|
||||
.animation-timeline .time-header {
|
||||
margin-left: var(--timeline-sidebar-width);
|
||||
min-height: var(--toolbar-height);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
/* This is the same color as the time graduations in
|
||||
devtools/client/animationinspector/utils.js */
|
||||
border-bottom: 1px solid rgba(128, 136, 144, .5);
|
||||
cursor: col-resize;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
@ -234,33 +242,37 @@ body {
|
||||
|
||||
.animation-timeline .animations {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin: 0;
|
||||
/* Leave some space for the header */
|
||||
margin-top: var(--timeline-animation-height);
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
border-top: 1px solid var(--time-graduation-border-color);
|
||||
}
|
||||
|
||||
/* Animation block widgets */
|
||||
|
||||
.animation-timeline .animation {
|
||||
padding: 2px 0;
|
||||
margin: 2px 0;
|
||||
height: var(--timeline-animation-height);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.animation-timeline .animation:nth-child(2n) {
|
||||
/* We want animations' background colors to alternate, but each animation has
|
||||
a sibling (hidden by default) that contains the animated properties and
|
||||
keyframes, so we need to alternate every 4 elements. */
|
||||
.animation-timeline .animation:nth-child(4n+1) {
|
||||
background-color: var(--even-animation-timeline-background-color);
|
||||
}
|
||||
|
||||
.animation-timeline .animation.selected {
|
||||
background-color: var(--theme-selection-background-semitransparent);
|
||||
}
|
||||
|
||||
.animation-timeline .animation .target {
|
||||
width: var(--timeline-sidebar-width);
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.animation-timeline .animation-target {
|
||||
@ -268,11 +280,6 @@ body {
|
||||
}
|
||||
|
||||
.animation-timeline .animation .time-block {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: var(--timeline-sidebar-width);
|
||||
right: 0;
|
||||
height: var(--timeline-animation-height);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -384,7 +391,7 @@ body {
|
||||
|
||||
.animation-target {
|
||||
background-color: var(--theme-toolbar-background);
|
||||
padding: 1px 4px;
|
||||
padding: 0 4px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@ -412,31 +419,99 @@ body {
|
||||
filter: url(images/filters.svg#checked-icon-state) brightness(0.9);
|
||||
}
|
||||
|
||||
/* Animation title gutter, contains the name, duration, iteration */
|
||||
/* Inline keyframes info in the timeline */
|
||||
|
||||
.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;
|
||||
.animation-timeline .animated-properties:not(.selected) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.animation-title .meta-data {
|
||||
float: right;
|
||||
.animation-timeline .animated-properties {
|
||||
background-color: var(--theme-selection-background-semitransparent);
|
||||
}
|
||||
|
||||
.animation-title strong {
|
||||
margin: 0 .5em;
|
||||
.animation-timeline .animated-properties ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.animation-title .meta-data .compositor-icon {
|
||||
display: none;
|
||||
background-image: url("images/animation-fast-track.svg");
|
||||
background-repeat: no-repeat;
|
||||
padding-left: 12px;
|
||||
/* Make sure the icon is positioned above the timeline range input so that
|
||||
its tooltip appears on hover */
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
.animation-timeline .animated-properties .property {
|
||||
height: var(--timeline-animation-height);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.animation-timeline .animated-properties .property:nth-child(2n) {
|
||||
background-color: var(--even-animation-timeline-background-color);
|
||||
}
|
||||
|
||||
.animation-timeline .animated-properties .name {
|
||||
width: var(--timeline-sidebar-width);
|
||||
padding-right: var(--keyframes-marker-size);
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
color: var(--theme-body-color-alt);
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.animation-timeline .animated-properties .name div {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.animation-timeline .animated-properties .frames {
|
||||
/* The frames list is absolutely positioned and the left and width properties
|
||||
are dynamically set from javascript to match the animation's startTime and
|
||||
duration */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
/* Using flexbox to vertically center the frames */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Keyframes diagram, displayed below the timeline, inside the animation-details
|
||||
element. */
|
||||
|
||||
.keyframes {
|
||||
/* Actual keyframe markers are positioned absolutely within this container and
|
||||
their position is relative to its size (we know the offset of each frame
|
||||
in percentage) */
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.keyframes.cssanimation {
|
||||
background-color: var(--theme-contrast-background);
|
||||
}
|
||||
|
||||
.keyframes.csstransition {
|
||||
background-color: var(--theme-highlight-blue);
|
||||
}
|
||||
|
||||
.keyframes .frame {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.keyframes .frame::before {
|
||||
content: "";
|
||||
display: block;
|
||||
transform:
|
||||
translateX(calc(var(--keyframes-marker-size) * -.5))
|
||||
/* The extra pixel on the Y axis is so that markers are centered on the
|
||||
horizontal line in the keyframes diagram. */
|
||||
translateY(calc(var(--keyframes-marker-size) * -.5 + 1px));
|
||||
width: var(--keyframes-marker-size);
|
||||
height: var(--keyframes-marker-size);
|
||||
border-radius: 100%;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user