X Tutup
Skip to content

Commit 39ce9d3

Browse files
author
Robert Messerle
committed
feat(animate): adds basic support for CSS animations on enter and leave
Closes #3876
1 parent effbb54 commit 39ce9d3

26 files changed

+688
-8
lines changed

modules/angular2/angular2.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './core';
22
export * from './profile';
33
export * from './lifecycle_hooks';
4-
export * from './bootstrap';
4+
export * from './bootstrap';
5+
export * from './animate';

modules/angular2/animate.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export {Animation} from './src/animate/animation';
2+
export {AnimationBuilder} from './src/animate/animation_builder';
3+
export {BrowserDetails} from './src/animate/browser_details';
4+
export {CssAnimationBuilder} from './src/animate/css_animation_builder';
5+
export {CssAnimationOptions} from './src/animate/css_animation_options';
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import {
2+
DateWrapper,
3+
StringWrapper,
4+
RegExpWrapper,
5+
NumberWrapper
6+
} from 'angular2/src/core/facade/lang';
7+
import {Math} from 'angular2/src/core/facade/math';
8+
import {camelCaseToDashCase} from 'angular2/src/core/render/dom/util';
9+
import {StringMapWrapper} from 'angular2/src/core/facade/collection';
10+
import {DOM} from 'angular2/src/core/dom/dom_adapter';
11+
12+
import {BrowserDetails} from './browser_details';
13+
import {CssAnimationOptions} from './css_animation_options';
14+
15+
export class Animation {
16+
/** functions to be called upon completion */
17+
callbacks: Function[] = [];
18+
19+
/** the duration (ms) of the animation (whether from CSS or manually set) */
20+
computedDuration: number;
21+
22+
/** the animation delay (ms) (whether from CSS or manually set) */
23+
computedDelay: number;
24+
25+
/** timestamp of when the animation started */
26+
startTime: number;
27+
28+
/** functions for removing event listeners */
29+
eventClearFunctions: Function[] = [];
30+
31+
/** flag used to track whether or not the animation has finished */
32+
completed: boolean = false;
33+
34+
/** total amount of time that the animation should take including delay */
35+
get totalTime(): number {
36+
let delay = this.computedDelay != null ? this.computedDelay : 0;
37+
let duration = this.computedDuration != null ? this.computedDuration : 0;
38+
return delay + duration;
39+
}
40+
41+
/**
42+
* Stores the start time and starts the animation
43+
* @param element
44+
* @param data
45+
* @param browserDetails
46+
*/
47+
constructor(public element: HTMLElement, public data: CssAnimationOptions,
48+
public browserDetails: BrowserDetails) {
49+
this.startTime = DateWrapper.toMillis(DateWrapper.now());
50+
this.setup();
51+
this.wait(timestamp => this.start());
52+
}
53+
54+
wait(callback: Function) {
55+
// Firefox requires 2 frames for some reason
56+
this.browserDetails.raf(callback, 2);
57+
}
58+
59+
/**
60+
* Sets up the initial styles before the animation is started
61+
*/
62+
setup(): void {
63+
if (this.data.fromStyles != null) this.applyStyles(this.data.fromStyles);
64+
if (this.data.duration != null)
65+
this.applyStyles({'transitionDuration': this.data.duration.toString() + 'ms'});
66+
if (this.data.delay != null)
67+
this.applyStyles({'transitionDelay': this.data.delay.toString() + 'ms'});
68+
}
69+
70+
/**
71+
* After the initial setup has occurred, this method adds the animation styles
72+
*/
73+
start(): void {
74+
this.addClasses(this.data.classesToAdd);
75+
this.addClasses(this.data.animationClasses);
76+
this.removeClasses(this.data.classesToRemove);
77+
if (this.data.toStyles != null) this.applyStyles(this.data.toStyles);
78+
var computedStyles = DOM.getComputedStyle(this.element);
79+
this.computedDelay =
80+
Math.max(this.parseDurationString(computedStyles.getPropertyValue('transition-delay')),
81+
this.parseDurationString(this.element.style.getPropertyValue('transition-delay')));
82+
this.computedDuration = Math.max(
83+
this.parseDurationString(computedStyles.getPropertyValue('transition-duration')),
84+
this.parseDurationString(this.element.style.getPropertyValue('transition-duration')));
85+
this.addEvents();
86+
}
87+
88+
/**
89+
* Applies the provided styles to the element
90+
* @param styles
91+
*/
92+
applyStyles(styles: StringMap<string, any>): void {
93+
StringMapWrapper.forEach(styles, (value, key) => {
94+
DOM.setStyle(this.element, camelCaseToDashCase(key), value.toString());
95+
});
96+
}
97+
98+
/**
99+
* Adds the provided classes to the element
100+
* @param classes
101+
*/
102+
addClasses(classes: string[]): void {
103+
for (let i = 0, len = classes.length; i < len; i++) DOM.addClass(this.element, classes[i]);
104+
}
105+
106+
/**
107+
* Removes the provided classes from the element
108+
* @param classes
109+
*/
110+
removeClasses(classes: string[]): void {
111+
for (let i = 0, len = classes.length; i < len; i++) DOM.removeClass(this.element, classes[i]);
112+
}
113+
114+
/**
115+
* Adds events to track when animations have finished
116+
*/
117+
addEvents(): void {
118+
if (this.totalTime > 0) {
119+
this.eventClearFunctions.push(DOM.onAndCancel(
120+
this.element, 'transitionend', (event: any) => this.handleAnimationEvent(event)));
121+
} else {
122+
this.handleAnimationCompleted();
123+
}
124+
}
125+
126+
handleAnimationEvent(event: any): void {
127+
let elapsedTime = Math.round(event.elapsedTime * 1000);
128+
if (!this.browserDetails.elapsedTimeIncludesDelay) elapsedTime += this.computedDelay;
129+
event.stopPropagation();
130+
if (elapsedTime >= this.totalTime) this.handleAnimationCompleted();
131+
}
132+
133+
/**
134+
* Runs all animation callbacks and removes temporary classes
135+
*/
136+
handleAnimationCompleted(): void {
137+
this.removeClasses(this.data.animationClasses);
138+
this.callbacks.forEach(callback => callback());
139+
this.callbacks = [];
140+
this.eventClearFunctions.forEach(fn => fn());
141+
this.eventClearFunctions = [];
142+
this.completed = true;
143+
}
144+
145+
/**
146+
* Adds animation callbacks to be called upon completion
147+
* @param callback
148+
* @returns {Animation}
149+
*/
150+
onComplete(callback: Function): Animation {
151+
if (this.completed) {
152+
callback();
153+
} else {
154+
this.callbacks.push(callback);
155+
}
156+
return this;
157+
}
158+
159+
/**
160+
* Converts the duration string to the number of milliseconds
161+
* @param duration
162+
* @returns {number}
163+
*/
164+
parseDurationString(duration: string): number {
165+
var maxValue = 0;
166+
// duration must have at least 2 characters to be valid. (number + type)
167+
if (duration == null || duration.length < 2) {
168+
return maxValue;
169+
} else if (duration.substring(duration.length - 2) == 'ms') {
170+
let value = NumberWrapper.parseInt(this.stripLetters(duration), 10);
171+
if (value > maxValue) maxValue = value;
172+
} else if (duration.substring(duration.length - 1) == 's') {
173+
let ms = NumberWrapper.parseFloat(this.stripLetters(duration)) * 1000;
174+
let value = Math.floor(ms);
175+
if (value > maxValue) maxValue = value;
176+
}
177+
return maxValue;
178+
}
179+
180+
/**
181+
* Strips the letters from the duration string
182+
* @param str
183+
* @returns {string}
184+
*/
185+
stripLetters(str: string): string {
186+
return StringWrapper.replaceAll(str, RegExpWrapper.create('[^0-9]+$', ''), '');
187+
}
188+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {Injectable} from 'angular2/src/core/di';
2+
3+
import {CssAnimationBuilder} from './css_animation_builder';
4+
import {BrowserDetails} from './browser_details';
5+
6+
@Injectable()
7+
export class AnimationBuilder {
8+
/**
9+
* Used for DI
10+
* @param browserDetails
11+
*/
12+
constructor(public browserDetails: BrowserDetails) {}
13+
14+
/**
15+
* Creates a new CSS Animation
16+
* @returns {CssAnimationBuilder}
17+
*/
18+
css(): CssAnimationBuilder { return new CssAnimationBuilder(this.browserDetails); }
19+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {Injectable} from 'angular2/src/core/di';
2+
import {Math} from 'angular2/src/core/facade/math';
3+
import {DOM} from 'angular2/src/core/dom/dom_adapter';
4+
5+
@Injectable()
6+
export class BrowserDetails {
7+
elapsedTimeIncludesDelay = false;
8+
9+
constructor() { this.doesElapsedTimeIncludesDelay(); }
10+
11+
/**
12+
* Determines if `event.elapsedTime` includes transition delay in the current browser. At this
13+
* time, Chrome and Opera seem to be the only browsers that include this.
14+
*/
15+
doesElapsedTimeIncludesDelay(): void {
16+
var div = DOM.createElement('div');
17+
DOM.setAttribute(div, 'style', `position: absolute; top: -9999px; left: -9999px; width: 1px;
18+
height: 1px; transition: all 1ms linear 1ms;`);
19+
// Firefox requires that we wait for 2 frames for some reason
20+
this.raf(timestamp => {
21+
DOM.on(div, 'transitionend', (event: any) => {
22+
var elapsed = Math.round(event.elapsedTime * 1000);
23+
this.elapsedTimeIncludesDelay = elapsed == 2;
24+
DOM.remove(div);
25+
});
26+
DOM.setStyle(div, 'width', '2px');
27+
}, 2);
28+
}
29+
30+
raf(callback: Function, frames: number = 1): Function {
31+
var queue: RafQueue = new RafQueue(callback, frames);
32+
return () => queue.cancel();
33+
}
34+
}
35+
36+
class RafQueue {
37+
currentFrameId: number;
38+
constructor(public callback: Function, public frames: number) { this._raf(); }
39+
private _raf() {
40+
this.currentFrameId = DOM.requestAnimationFrame(timestamp => this._nextFrame(timestamp));
41+
}
42+
private _nextFrame(timestamp: number) {
43+
this.frames--;
44+
if (this.frames > 0) {
45+
this._raf();
46+
} else {
47+
this.callback(timestamp);
48+
}
49+
}
50+
cancel() {
51+
DOM.cancelAnimationFrame(this.currentFrameId);
52+
this.currentFrameId = null;
53+
}
54+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import {CssAnimationOptions} from './css_animation_options';
2+
import {Animation} from './animation';
3+
import {BrowserDetails} from './browser_details';
4+
5+
export class CssAnimationBuilder {
6+
/** @type {CssAnimationOptions} */
7+
data: CssAnimationOptions = new CssAnimationOptions();
8+
9+
/**
10+
* Accepts public properties for CssAnimationBuilder
11+
*/
12+
constructor(public browserDetails: BrowserDetails) {}
13+
14+
/**
15+
* Adds a temporary class that will be removed at the end of the animation
16+
* @param className
17+
*/
18+
addAnimationClass(className: string): CssAnimationBuilder {
19+
this.data.animationClasses.push(className);
20+
return this;
21+
}
22+
23+
/**
24+
* Adds a class that will remain on the element after the animation has finished
25+
* @param className
26+
*/
27+
addClass(className: string): CssAnimationBuilder {
28+
this.data.classesToAdd.push(className);
29+
return this;
30+
}
31+
32+
/**
33+
* Removes a class from the element
34+
* @param className
35+
*/
36+
removeClass(className: string): CssAnimationBuilder {
37+
this.data.classesToRemove.push(className);
38+
return this;
39+
}
40+
41+
/**
42+
* Sets the animation duration (and overrides any defined through CSS)
43+
* @param duration
44+
*/
45+
setDuration(duration: number): CssAnimationBuilder {
46+
this.data.duration = duration;
47+
return this;
48+
}
49+
50+
/**
51+
* Sets the animation delay (and overrides any defined through CSS)
52+
* @param delay
53+
*/
54+
setDelay(delay: number): CssAnimationBuilder {
55+
this.data.delay = delay;
56+
return this;
57+
}
58+
59+
/**
60+
* Sets styles for both the initial state and the destination state
61+
* @param from
62+
* @param to
63+
*/
64+
setStyles(from: StringMap<string, any>, to: StringMap<string, any>): CssAnimationBuilder {
65+
return this.setFromStyles(from).setToStyles(to);
66+
}
67+
68+
/**
69+
* Sets the initial styles for the animation
70+
* @param from
71+
*/
72+
setFromStyles(from: StringMap<string, any>): CssAnimationBuilder {
73+
this.data.fromStyles = from;
74+
return this;
75+
}
76+
77+
/**
78+
* Sets the destination styles for the animation
79+
* @param to
80+
*/
81+
setToStyles(to: StringMap<string, any>): CssAnimationBuilder {
82+
this.data.toStyles = to;
83+
return this;
84+
}
85+
86+
/**
87+
* Starts the animation and returns a promise
88+
* @param element
89+
*/
90+
start(element: HTMLElement): Animation {
91+
return new Animation(element, this.data, this.browserDetails);
92+
}
93+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export class CssAnimationOptions {
2+
/** initial styles for the element */
3+
fromStyles: StringMap<string, any>;
4+
5+
/** destination styles for the element */
6+
toStyles: StringMap<string, any>;
7+
8+
/** classes to be added to the element */
9+
classesToAdd: string[] = [];
10+
11+
/** classes to be removed from the element */
12+
classesToRemove: string[] = [];
13+
14+
/** classes to be added for the duration of the animation */
15+
animationClasses: string[] = [];
16+
17+
/** override the duration of the animation (in milliseconds) */
18+
duration: number;
19+
20+
/** override the transition delay (in milliseconds) */
21+
delay: number;
22+
}

0 commit comments

Comments
 (0)
X Tutup