fix(animations): only use the WA-polyfill alongside AnimationBuilder (#22143)

This patch removes the need to include the Web Animations API Polyfill
(web-animations-js) as a dependency. Angular will now fallback to using
CSS Keyframes in the event that `element.animate` is no longer supported
by the browser.

In the event that an application does use `AnimationBuilder` then the
web-animations-js polyfill is required to enable programmatic,
position-based access to an animation.

Closes #17496

PR Close #22143
This commit is contained in:
Matias Niemelä 2018-02-08 15:01:43 -08:00 committed by Victor Berchet
parent 9eecb0b27f
commit b2f366b3b7
23 changed files with 1680 additions and 81 deletions

View File

@ -16,8 +16,10 @@ animation logic with the rest of your application code, for ease of control.
Angular animations are built on top of the standard [Web Animations API](https://w3c.github.io/web-animations/) Angular animations are built on top of the standard [Web Animations API](https://w3c.github.io/web-animations/)
and run natively on [browsers that support it](http://caniuse.com/#feat=web-animation). and run natively on [browsers that support it](http://caniuse.com/#feat=web-animation).
For other browsers, a polyfill is required. Uncomment the `web-animations-js` polyfill from the `polyfills.ts` file. As of Angular 6, If the Web Animations API is not supported natively by the browser, then Angular will use CSS
keyframes as a fallback instead (automatically). This means that the polyfill is no longer required unless any
code uses [AnimationBuilder](/api/animations/AnimationBuilder). If your code does use AnimationBuilder, then
uncomment the `web-animations-js` polyfill from the `polyfills.ts` file generated by Angular CLI.
</div> </div>
<div class="l-sub-section"> <div class="l-sub-section">

View File

@ -133,6 +133,8 @@ But if you need an optional polyfill, you'll have to install its npm package.
For example, [if you need the web animations polyfill](http://caniuse.com/#feat=web-animation), you could install it with `npm`, using the following command (or the `yarn` equivalent): For example, [if you need the web animations polyfill](http://caniuse.com/#feat=web-animation), you could install it with `npm`, using the following command (or the `yarn` equivalent):
<code-example language="sh" class="code-shell"> <code-example language="sh" class="code-shell">
# note that the web-animations-js polyfill is only here as an example
# it isn't a strict requirement of Angular anymore (more below)
npm install --save web-animations-js npm install --save web-animations-js
</code-example> </code-example>
@ -226,7 +228,8 @@ These are the polyfills required to run an Angular application on each supported
Some features of Angular may require additional polyfills. Some features of Angular may require additional polyfills.
For example, the animations library relies on the standard web animation API, which is only available in Chrome and Firefox today. You'll need a polyfill to use animations in other browsers. For example, the animations library relies on the standard web animation API, which is only available in Chrome and Firefox today.
(note that the dependency of web-animations-js in Angular is only necessary if `AnimationBuilder` is used.)
Here are the features which may require additional polyfills: Here are the features which may require additional polyfills:
@ -276,6 +279,8 @@ Here are the features which may require additional polyfills:
<td> <td>
[Animations](guide/animations) [Animations](guide/animations)
<br>Only if `Animation Builder` is used within the application--standard
animation support in Angular doesn't require any polyfills (as of NG6).
</td> </td>
@ -286,7 +291,8 @@ Here are the features which may require additional polyfills:
</td> </td>
<td> <td>
All but Chrome and Firefox<br>Not supported in IE9 <p>If AnimationBuilder is used then the polyfill will enable scrubbing
support for IE/Edge and Safari (Chrome and Firefox support this natively).</p>
</td> </td>
</tr> </tr>

View File

@ -10,5 +10,7 @@ export {AnimationStyleNormalizer as ɵAnimationStyleNormalizer, NoopAnimationSty
export {WebAnimationsStyleNormalizer as ɵWebAnimationsStyleNormalizer} from './dsl/style_normalization/web_animations_style_normalizer'; export {WebAnimationsStyleNormalizer as ɵWebAnimationsStyleNormalizer} from './dsl/style_normalization/web_animations_style_normalizer';
export {NoopAnimationDriver as ɵNoopAnimationDriver} from './render/animation_driver'; export {NoopAnimationDriver as ɵNoopAnimationDriver} from './render/animation_driver';
export {AnimationEngine as ɵAnimationEngine} from './render/animation_engine_next'; export {AnimationEngine as ɵAnimationEngine} from './render/animation_engine_next';
export {CssKeyframesDriver as ɵCssKeyframesDriver} from './render/css_keyframes/css_keyframes_driver';
export {CssKeyframesPlayer as ɵCssKeyframesPlayer} from './render/css_keyframes/css_keyframes_player';
export {WebAnimationsDriver as ɵWebAnimationsDriver, supportsWebAnimations as ɵsupportsWebAnimations} from './render/web_animations/web_animations_driver'; export {WebAnimationsDriver as ɵWebAnimationsDriver, supportsWebAnimations as ɵsupportsWebAnimations} from './render/web_animations/web_animations_driver';
export {WebAnimationsPlayer as ɵWebAnimationsPlayer} from './render/web_animations/web_animations_player'; export {WebAnimationsPlayer as ɵWebAnimationsPlayer} from './render/web_animations/web_animations_player';

View File

@ -33,7 +33,8 @@ export class NoopAnimationDriver implements AnimationDriver {
animate( animate(
element: any, keyframes: {[key: string]: string | number}[], duration: number, delay: number, element: any, keyframes: {[key: string]: string | number}[], duration: number, delay: number,
easing: string, previousPlayers: any[] = []): AnimationPlayer { easing: string, previousPlayers: any[] = [],
scrubberAccessRequested?: boolean): AnimationPlayer {
return new NoopAnimationPlayer(duration, delay); return new NoopAnimationPlayer(duration, delay);
} }
} }
@ -56,5 +57,5 @@ export abstract class AnimationDriver {
abstract animate( abstract animate(
element: any, keyframes: {[key: string]: string | number}[], duration: number, delay: number, element: any, keyframes: {[key: string]: string | number}[], duration: number, delay: number,
easing?: string|null, previousPlayers?: any[]): any; easing?: string|null, previousPlayers?: any[], scrubberAccessRequested?: boolean): any;
} }

View File

@ -0,0 +1,151 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AnimationPlayer, ɵStyleData} from '@angular/animations';
import {allowPreviousPlayerStylesMerge, balancePreviousStylesIntoKeyframes, computeStyle} from '../../util';
import {AnimationDriver} from '../animation_driver';
import {containsElement, invokeQuery, matchesElement, validateStyleProperty} from '../shared';
import {CssKeyframesPlayer} from './css_keyframes_player';
import {DirectStylePlayer} from './direct_style_player';
const KEYFRAMES_NAME_PREFIX = 'gen_css_kf_';
const TAB_SPACE = ' ';
export class CssKeyframesDriver implements AnimationDriver {
private _count = 0;
private readonly _head: any = document.querySelector('head');
private _warningIssued = false;
validateStyleProperty(prop: string): boolean { return validateStyleProperty(prop); }
matchesElement(element: any, selector: string): boolean {
return matchesElement(element, selector);
}
containsElement(elm1: any, elm2: any): boolean { return containsElement(elm1, elm2); }
query(element: any, selector: string, multi: boolean): any[] {
return invokeQuery(element, selector, multi);
}
computeStyle(element: any, prop: string, defaultValue?: string): string {
return (window.getComputedStyle(element) as any)[prop] as string;
}
buildKeyframeElement(element: any, name: string, keyframes: {[key: string]: any}[]): any {
keyframes = keyframes.map(kf => hypenatePropsObject(kf));
let keyframeStr = `@keyframes ${name} {\n`;
let tab = '';
keyframes.forEach(kf => {
tab = TAB_SPACE;
const offset = parseFloat(kf.offset);
keyframeStr += `${tab}${offset * 100}% {\n`;
tab += TAB_SPACE;
Object.keys(kf).forEach(prop => {
const value = kf[prop];
switch (prop) {
case 'offset':
return;
case 'easing':
if (value) {
keyframeStr += `${tab}animation-timing-function: ${value};\n`;
}
return;
default:
keyframeStr += `${tab}${prop}: ${value};\n`;
return;
}
});
keyframeStr += `${tab}}\n`;
});
keyframeStr += `}\n`;
const kfElm = document.createElement('style');
kfElm.innerHTML = keyframeStr;
return kfElm;
}
animate(
element: any, keyframes: ɵStyleData[], duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = [], scrubberAccessRequested?: boolean): AnimationPlayer {
if (scrubberAccessRequested) {
this._notifyFaultyScrubber();
}
const previousCssKeyframePlayers = <CssKeyframesPlayer[]>previousPlayers.filter(
player => player instanceof CssKeyframesPlayer);
const previousStyles: {[key: string]: any} = {};
if (allowPreviousPlayerStylesMerge(duration, delay)) {
previousCssKeyframePlayers.forEach(player => {
let styles = player.currentSnapshot;
Object.keys(styles).forEach(prop => previousStyles[prop] = styles[prop]);
});
}
keyframes = balancePreviousStylesIntoKeyframes(element, keyframes, previousStyles);
const finalStyles = flattenKeyframesIntoStyles(keyframes);
// if there is no animation then there is no point in applying
// styles and waiting for an event to get fired. This causes lag.
// It's better to just directly apply the styles to the element
// via the direct styling animation player.
if (duration == 0) {
return new DirectStylePlayer(element, finalStyles);
}
const animationName = `${KEYFRAMES_NAME_PREFIX}${this._count++}`;
const kfElm = this.buildKeyframeElement(element, animationName, keyframes);
document.querySelector('head') !.appendChild(kfElm);
const player = new CssKeyframesPlayer(
element, keyframes, animationName, duration, delay, easing, finalStyles);
player.onDestroy(() => removeElement(kfElm));
return player;
}
private _notifyFaultyScrubber() {
if (!this._warningIssued) {
console.warn(
'@angular/animations: please load the web-animations.js polyfill to allow programmatic access...\n',
' visit http://bit.ly/IWukam to learn more about using the web-animation-js polyfill.');
this._warningIssued = true;
}
}
}
function flattenKeyframesIntoStyles(
keyframes: null | {[key: string]: any} | {[key: string]: any}[]): {[key: string]: any} {
let flatKeyframes: {[key: string]: any} = {};
if (keyframes) {
const kfs = Array.isArray(keyframes) ? keyframes : [keyframes];
kfs.forEach(kf => {
Object.keys(kf).forEach(prop => {
if (prop == 'offset' || prop == 'easing') return;
flatKeyframes[prop] = kf[prop];
});
});
}
return flatKeyframes;
}
function hypenatePropsObject(object: {[key: string]: any}): {[key: string]: any} {
const newObj: {[key: string]: any} = {};
Object.keys(object).forEach(prop => {
const newProp = prop.replace(/([a-z])([A-Z])/g, '$1-$2');
newObj[newProp] = object[prop];
});
return newObj;
}
function removeElement(node: any) {
node.parentNode.removeChild(node);
}

View File

@ -0,0 +1,149 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AnimationPlayer} from '@angular/animations';
import {computeStyle} from '../../util';
import {ElementAnimationStyleHandler} from './element_animation_style_handler';
const DEFAULT_FILL_MODE = 'forwards';
const DEFAULT_EASING = 'linear';
const ANIMATION_END_EVENT = 'animationend';
export enum AnimatorControlState {
INITIALIZED = 1,
STARTED = 2,
FINISHED = 3,
DESTROYED = 4
}
export class CssKeyframesPlayer implements AnimationPlayer {
private _onDoneFns: Function[] = [];
private _onStartFns: Function[] = [];
private _onDestroyFns: Function[] = [];
private _started = false;
private _styler: ElementAnimationStyleHandler;
public parentPlayer: AnimationPlayer;
public readonly totalTime: number;
public readonly easing: string;
public currentSnapshot: {[key: string]: string} = {};
public state = 0;
constructor(
public readonly element: any, public readonly keyframes: {[key: string]: string | number}[],
public readonly animationName: string, private readonly _duration: number,
private readonly _delay: number, easing: string,
private readonly _finalStyles: {[key: string]: any}) {
this.easing = easing || DEFAULT_EASING;
this.totalTime = _duration + _delay;
this._buildStyler();
}
onStart(fn: () => void): void { this._onStartFns.push(fn); }
onDone(fn: () => void): void { this._onDoneFns.push(fn); }
onDestroy(fn: () => void): void { this._onDestroyFns.push(fn); }
destroy() {
this.init();
if (this.state >= AnimatorControlState.DESTROYED) return;
this.state = AnimatorControlState.DESTROYED;
this._styler.destroy();
this._flushStartFns();
this._flushDoneFns();
this._onDestroyFns.forEach(fn => fn());
this._onDestroyFns = [];
}
private _flushDoneFns() {
this._onDoneFns.forEach(fn => fn());
this._onDoneFns = [];
}
private _flushStartFns() {
this._onStartFns.forEach(fn => fn());
this._onStartFns = [];
}
finish() {
this.init();
if (this.state >= AnimatorControlState.FINISHED) return;
this.state = AnimatorControlState.FINISHED;
this._styler.finish();
this._flushStartFns();
this._flushDoneFns();
}
setPosition(value: number) { this._styler.setPosition(value); }
getPosition(): number { return this._styler.getPosition(); }
hasStarted(): boolean { return this.state >= AnimatorControlState.STARTED; }
init(): void {
if (this.state >= AnimatorControlState.INITIALIZED) return;
this.state = AnimatorControlState.INITIALIZED;
const elm = this.element;
this._styler.apply();
if (this._delay) {
this._styler.pause();
}
}
play(): void {
this.init();
if (!this.hasStarted()) {
this._flushStartFns();
this.state = AnimatorControlState.STARTED;
}
this._styler.resume();
}
pause(): void {
this.init();
this._styler.pause();
}
restart(): void {
this.reset();
this.play();
}
reset(): void {
this._styler.destroy();
this._buildStyler();
this._styler.apply();
}
private _buildStyler() {
this._styler = new ElementAnimationStyleHandler(
this.element, this.animationName, this._duration, this._delay, this.easing,
DEFAULT_FILL_MODE, () => this.finish());
}
/* @internal */
triggerCallback(phaseName: string): void {
const methods = phaseName == 'start' ? this._onStartFns : this._onDoneFns;
methods.forEach(fn => fn());
methods.length = 0;
}
beforeDestroy() {
this.init();
const styles: {[key: string]: string} = {};
if (this.hasStarted()) {
const finished = this.state >= AnimatorControlState.FINISHED;
Object.keys(this._finalStyles).forEach(prop => {
if (prop != 'offset') {
styles[prop] = finished ? this._finalStyles[prop] : computeStyle(this.element, prop);
}
});
}
this.currentSnapshot = styles;
}
}

View File

@ -0,0 +1,45 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {NoopAnimationPlayer} from '@angular/animations';
export class DirectStylePlayer extends NoopAnimationPlayer {
private _startingStyles: {[key: string]: any}|null = {};
private __initialized = false;
constructor(public element: any, private _styles: {[key: string]: any}) { super(); }
init() {
if (this.__initialized || !this._startingStyles) return;
this.__initialized = true;
Object.keys(this._styles).forEach(prop => {
this._startingStyles ![prop] = this.element.style[prop];
});
super.init();
}
play() {
if (!this._startingStyles) return;
this.init();
Object.keys(this._styles).forEach(prop => { this.element.style[prop] = this._styles[prop]; });
super.play();
}
destroy() {
if (!this._startingStyles) return;
Object.keys(this._startingStyles).forEach(prop => {
const value = this._startingStyles ![prop];
if (value) {
this.element.style[prop] = value;
} else {
this.element.style.removeProperty(prop);
}
});
this._startingStyles = null;
super.destroy();
}
}

View File

@ -0,0 +1,147 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
const ELAPSED_TIME_MAX_DECIMAL_PLACES = 3;
const ANIMATION_PROP = 'animation';
const ANIMATIONEND_EVENT = 'animationend';
const ONE_SECOND = 1000;
export class ElementAnimationStyleHandler {
private readonly _eventFn: (e: any) => any;
private _finished = false;
private _destroyed = false;
private _startTime = 0;
private _position = 0;
constructor(
private readonly _element: any, private readonly _name: string,
private readonly _duration: number, private readonly _delay: number,
private readonly _easing: string, private readonly _fillMode: ''|'both'|'forwards',
private readonly _onDoneFn: () => any) {
this._eventFn = (e) => this._handleCallback(e);
}
apply() {
applyKeyframeAnimation(
this._element,
`${this._duration}ms ${this._easing} ${this._delay}ms 1 normal ${this._fillMode} ${this._name}`);
addRemoveAnimationEvent(this._element, this._eventFn, false);
this._startTime = Date.now();
}
pause() { playPauseAnimation(this._element, this._name, 'paused'); }
resume() { playPauseAnimation(this._element, this._name, 'running'); }
setPosition(position: number) {
const index = findIndexForAnimation(this._element, this._name);
this._position = position * this._duration;
setAnimationStyle(this._element, 'Delay', `-${this._position}ms`, index);
}
getPosition() { return this._position; }
private _handleCallback(event: any) {
const timestamp = event._ngTestManualTimestamp || Date.now();
const elapsedTime =
parseFloat(event.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES)) * ONE_SECOND;
if (event.animationName == this._name &&
Math.max(timestamp - this._startTime, 0) >= this._delay && elapsedTime >= this._duration) {
this.finish();
}
}
finish() {
if (this._finished) return;
this._finished = true;
this._onDoneFn();
addRemoveAnimationEvent(this._element, this._eventFn, true);
}
destroy() {
if (this._destroyed) return;
this._destroyed = true;
this.finish();
removeKeyframeAnimation(this._element, this._name);
}
}
function playPauseAnimation(element: any, name: string, status: 'running' | 'paused') {
const index = findIndexForAnimation(element, name);
setAnimationStyle(element, 'PlayState', status, index);
}
function applyKeyframeAnimation(element: any, value: string): number {
const anim = getAnimationStyle(element, '').trim();
let index = 0;
if (anim.length) {
index = countChars(anim, ',') + 1;
value = `${anim}, ${value}`;
}
setAnimationStyle(element, '', value);
return index;
}
function removeKeyframeAnimation(element: any, name: string) {
const anim = getAnimationStyle(element, '');
const tokens = anim.split(',');
const index = findMatchingTokenIndex(tokens, name);
if (index >= 0) {
tokens.splice(index, 1);
const newValue = tokens.join(',');
setAnimationStyle(element, '', newValue);
}
}
function findIndexForAnimation(element: any, value: string) {
const anim = getAnimationStyle(element, '');
if (anim.indexOf(',') > 0) {
const tokens = anim.split(',');
return findMatchingTokenIndex(tokens, value);
}
return findMatchingTokenIndex([anim], value);
}
function findMatchingTokenIndex(tokens: string[], searchToken: string): number {
for (let i = 0; i < tokens.length; i++) {
if (tokens[i].indexOf(searchToken) >= 0) {
return i;
}
}
return -1;
}
function addRemoveAnimationEvent(element: any, fn: (e: any) => any, doRemove: boolean) {
doRemove ? element.removeEventListener(ANIMATIONEND_EVENT, fn) :
element.addEventListener(ANIMATIONEND_EVENT, fn);
}
function setAnimationStyle(element: any, name: string, value: string, index?: number) {
const prop = ANIMATION_PROP + name;
if (index != null) {
const oldValue = element.style[prop];
if (oldValue.length) {
const tokens = oldValue.split(',');
tokens[index] = value;
value = tokens.join(',');
}
}
element.style[prop] = value;
}
function getAnimationStyle(element: any, name: string) {
return element.style[ANIMATION_PROP + name];
}
function countChars(value: string, char: string): number {
let count = 0;
for (let i = 0; i < value.length; i++) {
const c = value.charAt(i);
if (c === char) count++;
}
return count;
}

View File

@ -44,7 +44,7 @@ export class TimelineAnimationEngine {
const element = i.element; const element = i.element;
const keyframes = normalizeKeyframes( const keyframes = normalizeKeyframes(
this._driver, this._normalizer, element, i.keyframes, preStyles, postStyles); this._driver, this._normalizer, element, i.keyframes, preStyles, postStyles);
return this._driver.animate(element, keyframes, i.duration, i.delay, i.easing, []); return this._driver.animate(element, keyframes, i.duration, i.delay, i.easing, [], true);
} }
create(id: string, element: any, options: AnimationOptions = {}): AnimationPlayer { create(id: string, element: any, options: AnimationOptions = {}): AnimationPlayer {

View File

@ -7,12 +7,17 @@
*/ */
import {AnimationPlayer, ɵStyleData} from '@angular/animations'; import {AnimationPlayer, ɵStyleData} from '@angular/animations';
import {allowPreviousPlayerStylesMerge, balancePreviousStylesIntoKeyframes, copyStyles} from '../../util';
import {AnimationDriver} from '../animation_driver'; import {AnimationDriver} from '../animation_driver';
import {CssKeyframesDriver} from '../css_keyframes/css_keyframes_driver';
import {containsElement, invokeQuery, matchesElement, validateStyleProperty} from '../shared'; import {containsElement, invokeQuery, matchesElement, validateStyleProperty} from '../shared';
import {WebAnimationsPlayer} from './web_animations_player'; import {WebAnimationsPlayer} from './web_animations_player';
export class WebAnimationsDriver implements AnimationDriver { export class WebAnimationsDriver implements AnimationDriver {
private _isNativeImpl = /\{\s*\[native\s+code\]\s*\}/.test(getElementAnimateFn().toString());
private _cssKeyframesDriver = new CssKeyframesDriver();
validateStyleProperty(prop: string): boolean { return validateStyleProperty(prop); } validateStyleProperty(prop: string): boolean { return validateStyleProperty(prop); }
matchesElement(element: any, selector: string): boolean { matchesElement(element: any, selector: string): boolean {
@ -29,24 +34,46 @@ export class WebAnimationsDriver implements AnimationDriver {
return (window.getComputedStyle(element) as any)[prop] as string; return (window.getComputedStyle(element) as any)[prop] as string;
} }
overrideWebAnimationsSupport(supported: boolean) { this._isNativeImpl = supported; }
animate( animate(
element: any, keyframes: ɵStyleData[], duration: number, delay: number, easing: string, element: any, keyframes: ɵStyleData[], duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): WebAnimationsPlayer { previousPlayers: AnimationPlayer[] = [], scrubberAccessRequested?: boolean): AnimationPlayer {
const useKeyframes = !scrubberAccessRequested && !this._isNativeImpl;
if (useKeyframes) {
return this._cssKeyframesDriver.animate(
element, keyframes, duration, delay, easing, previousPlayers);
}
const fill = delay == 0 ? 'both' : 'forwards'; const fill = delay == 0 ? 'both' : 'forwards';
const playerOptions: {[key: string]: string | number} = {duration, delay, fill}; const playerOptions: {[key: string]: string | number} = {duration, delay, fill};
// we check for this to avoid having a null|undefined value be present // we check for this to avoid having a null|undefined value be present
// for the easing (which results in an error for certain browsers #9752) // for the easing (which results in an error for certain browsers #9752)
if (easing) { if (easing) {
playerOptions['easing'] = easing; playerOptions['easing'] = easing;
} }
const previousStyles: {[key: string]: any} = {};
const previousWebAnimationPlayers = <WebAnimationsPlayer[]>previousPlayers.filter( const previousWebAnimationPlayers = <WebAnimationsPlayer[]>previousPlayers.filter(
player => { return player instanceof WebAnimationsPlayer; }); player => player instanceof WebAnimationsPlayer);
return new WebAnimationsPlayer(element, keyframes, playerOptions, previousWebAnimationPlayers);
if (allowPreviousPlayerStylesMerge(duration, delay)) {
previousWebAnimationPlayers.forEach(player => {
let styles = player.currentSnapshot;
Object.keys(styles).forEach(prop => previousStyles[prop] = styles[prop]);
});
}
keyframes = keyframes.map(styles => copyStyles(styles, false));
keyframes = balancePreviousStylesIntoKeyframes(element, keyframes, previousStyles);
return new WebAnimationsPlayer(element, keyframes, playerOptions);
} }
} }
export function supportsWebAnimations() { export function supportsWebAnimations() {
return typeof Element !== 'undefined' && typeof(<any>Element).prototype['animate'] === 'function'; return typeof getElementAnimateFn() === 'function';
}
function getElementAnimateFn(): any {
return (typeof Element !== 'undefined' && (<any>Element).prototype['animate']) || {};
} }

View File

@ -7,7 +7,7 @@
*/ */
import {AnimationPlayer} from '@angular/animations'; import {AnimationPlayer} from '@angular/animations';
import {allowPreviousPlayerStylesMerge, copyStyles} from '../../util'; import {allowPreviousPlayerStylesMerge, balancePreviousStylesIntoKeyframes, computeStyle, copyStyles} from '../../util';
import {DOMAnimation} from './dom_animation'; import {DOMAnimation} from './dom_animation';
@ -27,23 +27,14 @@ export class WebAnimationsPlayer implements AnimationPlayer {
public time = 0; public time = 0;
public parentPlayer: AnimationPlayer|null = null; public parentPlayer: AnimationPlayer|null = null;
public previousStyles: {[styleName: string]: string | number} = {};
public currentSnapshot: {[styleName: string]: string | number} = {}; public currentSnapshot: {[styleName: string]: string | number} = {};
constructor( constructor(
public element: any, public keyframes: {[key: string]: string | number}[], public element: any, public keyframes: {[key: string]: string | number}[],
public options: {[key: string]: string | number}, public options: {[key: string]: string | number}) {
private previousPlayers: WebAnimationsPlayer[] = []) {
this._duration = <number>options['duration']; this._duration = <number>options['duration'];
this._delay = <number>options['delay'] || 0; this._delay = <number>options['delay'] || 0;
this.time = this._duration + this._delay; this.time = this._duration + this._delay;
if (allowPreviousPlayerStylesMerge(this._duration, this._delay)) {
previousPlayers.forEach(player => {
let styles = player.currentSnapshot;
Object.keys(styles).forEach(prop => this.previousStyles[prop] = styles[prop]);
});
}
} }
private _onFinish() { private _onFinish() {
@ -63,30 +54,7 @@ export class WebAnimationsPlayer implements AnimationPlayer {
if (this._initialized) return; if (this._initialized) return;
this._initialized = true; this._initialized = true;
const keyframes = this.keyframes.map(styles => copyStyles(styles, false)); const keyframes = this.keyframes;
const previousStyleProps = Object.keys(this.previousStyles);
if (previousStyleProps.length && keyframes.length) {
let startingKeyframe = keyframes[0];
let missingStyleProps: string[] = [];
previousStyleProps.forEach(prop => {
if (!startingKeyframe.hasOwnProperty(prop)) {
missingStyleProps.push(prop);
}
startingKeyframe[prop] = this.previousStyles[prop];
});
if (missingStyleProps.length) {
const self = this;
// tslint:disable-next-line
for (var i = 1; i < keyframes.length; i++) {
let kf = keyframes[i];
missingStyleProps.forEach(function(prop) {
kf[prop] = _computeStyle(self.element, prop);
});
}
}
}
(this as{domPlayer: DOMAnimation}).domPlayer = (this as{domPlayer: DOMAnimation}).domPlayer =
this._triggerWebAnimation(this.element, keyframes, this.options); this._triggerWebAnimation(this.element, keyframes, this.options);
this._finalKeyframe = keyframes.length ? keyframes[keyframes.length - 1] : {}; this._finalKeyframe = keyframes.length ? keyframes[keyframes.length - 1] : {};
@ -178,7 +146,7 @@ export class WebAnimationsPlayer implements AnimationPlayer {
Object.keys(this._finalKeyframe).forEach(prop => { Object.keys(this._finalKeyframe).forEach(prop => {
if (prop != 'offset') { if (prop != 'offset') {
styles[prop] = styles[prop] =
this._finished ? this._finalKeyframe[prop] : _computeStyle(this.element, prop); this._finished ? this._finalKeyframe[prop] : computeStyle(this.element, prop);
} }
}); });
} }
@ -192,7 +160,3 @@ export class WebAnimationsPlayer implements AnimationPlayer {
methods.length = 0; methods.length = 0;
} }
} }
function _computeStyle(element: any, prop: string): string {
return (<any>window.getComputedStyle(element))[prop];
}

View File

@ -235,6 +235,30 @@ export function allowPreviousPlayerStylesMerge(duration: number, delay: number)
return duration === 0 || delay === 0; return duration === 0 || delay === 0;
} }
export function balancePreviousStylesIntoKeyframes(
element: any, keyframes: {[key: string]: any}[], previousStyles: {[key: string]: any}) {
const previousStyleProps = Object.keys(previousStyles);
if (previousStyleProps.length && keyframes.length) {
let startingKeyframe = keyframes[0];
let missingStyleProps: string[] = [];
previousStyleProps.forEach(prop => {
if (!startingKeyframe.hasOwnProperty(prop)) {
missingStyleProps.push(prop);
}
startingKeyframe[prop] = previousStyles[prop];
});
if (missingStyleProps.length) {
// tslint:disable-next-line
for (var i = 1; i < keyframes.length; i++) {
let kf = keyframes[i];
missingStyleProps.forEach(function(prop) { kf[prop] = computeStyle(element, prop); });
}
}
}
return keyframes;
}
export function visitDslNode( export function visitDslNode(
visitor: AnimationDslVisitor, node: AnimationMetadata, context: any): any; visitor: AnimationDslVisitor, node: AnimationMetadata, context: any): any;
export function visitDslNode( export function visitDslNode(
@ -271,3 +295,7 @@ export function visitDslNode(visitor: any, node: any, context: any): any {
throw new Error(`Unable to resolve animation metadata node #${node.type}`); throw new Error(`Unable to resolve animation metadata node #${node.type}`);
} }
} }
export function computeStyle(element: any, prop: string): string {
return (<any>window.getComputedStyle(element))[prop];
}

View File

@ -10,5 +10,6 @@ ts_library(
"//packages/animations/browser", "//packages/animations/browser",
"//packages/animations/browser/testing", "//packages/animations/browser/testing",
"//packages/core", "//packages/core",
"//packages/core/testing",
], ],
) )

View File

@ -0,0 +1,400 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing';
import {CssKeyframesDriver} from '../../../src/render/css_keyframes/css_keyframes_driver';
import {CssKeyframesPlayer} from '../../../src/render/css_keyframes/css_keyframes_player';
import {DirectStylePlayer} from '../../../src/render/css_keyframes/direct_style_player';
import {assertElementExistsInDom, createElement, findKeyframeDefinition, forceReflow, makeAnimationEvent, supportsAnimationEventCreation} from './shared';
const CSS_KEYFRAME_RULE_TYPE = 7;
describe('CssKeyframesDriver tests', () => {
if (typeof Element == 'undefined' || typeof document == 'undefined' ||
typeof(window as any)['AnimationEvent'] == 'undefined')
return;
describe('building keyframes', () => {
it('should build CSS keyframe style object containing the keyframe styles', () => {
const elm = createElement();
const animator = new CssKeyframesDriver();
const kfElm = animator.buildKeyframeElement(elm, 'myKfAnim', [
{opacity: 0, width: '0px', offset: 0},
{opacity: 0.5, width: '100px', offset: 0.5},
{opacity: 1, width: '200px', offset: 1},
]);
const head = document.querySelector('head') !;
head.appendChild(kfElm);
forceReflow();
const sheet = kfElm.sheet;
const kfRule = findKeyframeDefinition(sheet);
expect(kfRule.name).toEqual('myKfAnim');
expect(kfRule.type).toEqual(CSS_KEYFRAME_RULE_TYPE);
const keyframeCssRules = kfRule.cssRules;
expect(keyframeCssRules.length).toEqual(3);
const [from, mid, to] = keyframeCssRules;
expect(from.keyText).toEqual('0%');
expect(mid.keyText).toEqual('50%');
expect(to.keyText).toEqual('100%');
const fromStyles = from.style;
expect(fromStyles.opacity).toEqual('0');
expect(fromStyles.width).toEqual('0px');
const midStyles = mid.style;
expect(midStyles.opacity).toEqual('0.5');
expect(midStyles.width).toEqual('100px');
const toStyles = to.style;
expect(toStyles.opacity).toEqual('1');
expect(toStyles.width).toEqual('200px');
});
it('should convert numeric values into px-suffixed data', () => {
const elm = createElement();
const animator = new CssKeyframesDriver();
const kfElm = animator.buildKeyframeElement(elm, 'myKfAnim', [
{width: '0px', offset: 0},
{width: '100px', offset: 0.5},
{width: '200px', offset: 1},
]);
const head = document.querySelector('head') !;
head.appendChild(kfElm);
forceReflow();
const sheet = kfElm.sheet;
const kfRule = findKeyframeDefinition(sheet);
const keyframeCssRules = kfRule.cssRules;
const [from, mid, to] = keyframeCssRules;
expect(from.style.width).toEqual('0px');
expect(mid.style.width).toEqual('100px');
expect(to.style.width).toEqual('200px');
});
});
describe('when animating', () => {
it('should set an animation on the element that matches the generated animation', () => {
const elm = createElement();
const animator = new CssKeyframesDriver();
const player = animator.animate(
elm,
[
{width: '0px', offset: 0},
{width: '200px', offset: 1},
],
1234, 0, 'ease-out');
const sheet: any = document.styleSheets[document.styleSheets.length - 1];
const kfRule = findKeyframeDefinition(sheet);
player.init();
const {animationName, duration, easing} = parseElementAnimationStyle(elm);
expect(animationName).toEqual(kfRule.name);
expect(duration).toEqual(1234);
expect(easing).toEqual('ease-out');
});
it('should animate until the `animationend` method is emitted, but stil retain the <style> method and the element animation details',
fakeAsync(() => {
// IE10 and IE11 cannot create an instanceof AnimationEvent
if (!supportsAnimationEventCreation()) return;
const elm = createElement();
const animator = new CssKeyframesDriver();
assertExistingAnimationDuration(elm, 0);
const player = <CssKeyframesPlayer>animator.animate(
elm,
[
{width: '0px', offset: 0},
{width: '200px', offset: 1},
],
1234, 0, 'ease-out');
const matchingStyleElm = findStyleObjectWithKeyframes();
player.play();
assertExistingAnimationDuration(elm, 1234);
assertElementExistsInDom(matchingStyleElm, true);
let completed = false;
player.onDone(() => completed = true);
expect(completed).toBeFalsy();
flushMicrotasks();
expect(completed).toBeFalsy();
const event = makeAnimationEvent('end', player.animationName, 1234);
elm.dispatchEvent(event);
flushMicrotasks();
expect(completed).toBeTruthy();
assertExistingAnimationDuration(elm, 1234);
assertElementExistsInDom(matchingStyleElm, true);
}));
it('should animate until finish() is called, but stil retain the <style> method and the element animation details',
fakeAsync(() => {
const elm = createElement();
const animator = new CssKeyframesDriver();
assertExistingAnimationDuration(elm, 0);
const player = animator.animate(
elm,
[
{width: '0px', offset: 0},
{width: '200px', offset: 1},
],
1234, 0, 'ease-out');
const matchingStyleElm = findStyleObjectWithKeyframes();
player.play();
assertExistingAnimationDuration(elm, 1234);
assertElementExistsInDom(matchingStyleElm, true);
let completed = false;
player.onDone(() => completed = true);
expect(completed).toBeFalsy();
flushMicrotasks();
expect(completed).toBeFalsy();
player.finish();
flushMicrotasks();
expect(completed).toBeTruthy();
assertExistingAnimationDuration(elm, 1234);
assertElementExistsInDom(matchingStyleElm, true);
}));
it('should animate until the destroy method is called and cleanup the element animation details',
fakeAsync(() => {
const elm = createElement();
const animator = new CssKeyframesDriver();
assertExistingAnimationDuration(elm, 0);
const player = animator.animate(
elm,
[
{width: '0px', offset: 0},
{width: '200px', offset: 1},
],
1234, 0, 'ease-out');
player.play();
assertExistingAnimationDuration(elm, 1234);
let completed = false;
player.onDone(() => completed = true);
flushMicrotasks();
expect(completed).toBeFalsy();
player.destroy();
flushMicrotasks();
expect(completed).toBeTruthy();
assertExistingAnimationDuration(elm, 0);
}));
it('should return an instance of a direct style player if an animation has a duration of 0',
() => {
const elm = createElement();
const animator = new CssKeyframesDriver();
assertExistingAnimationDuration(elm, 0);
const player = animator.animate(
elm,
[
{width: '0px', offset: 0},
{width: '200px', offset: 1},
],
0, 0, 'ease-out');
expect(player instanceof DirectStylePlayer).toBeTruthy();
});
it('should cleanup the associated <style> object when the animation is destroyed',
fakeAsync(() => {
const elm = createElement();
const animator = new CssKeyframesDriver();
const player = animator.animate(
elm,
[
{width: '0px', offset: 0},
{width: '200px', offset: 1},
],
1234, 0, 'ease-out');
player.play();
const matchingStyleElm = findStyleObjectWithKeyframes();
assertElementExistsInDom(matchingStyleElm, true);
player.destroy();
flushMicrotasks();
assertElementExistsInDom(matchingStyleElm, false);
}));
it('should return the final styles when capture() is called', () => {
const elm = createElement();
const animator = new CssKeyframesDriver();
const player = <CssKeyframesPlayer>animator.animate(
elm,
[
{color: 'red', width: '111px', height: '111px', offset: 0},
{color: 'blue', height: '999px', width: '999px', offset: 1},
],
2000, 0, 'ease-out');
player.play();
player.finish();
player.beforeDestroy !();
expect(player.currentSnapshot).toEqual({
width: '999px',
height: '999px',
color: 'blue',
});
});
it('should return the intermediate styles when capture() is called in the middle of the animation',
() => {
const elm = createElement();
document.body.appendChild(elm); // this is required so GCS works
const animator = new CssKeyframesDriver();
const player = <CssKeyframesPlayer>animator.animate(
elm,
[
{width: '0px', height: '0px', offset: 0},
{height: '100px', width: '100px', offset: 1},
],
2000, 0, 'ease-out');
player.play();
player.setPosition(0.5);
player.beforeDestroy();
const result = player.currentSnapshot;
expect(parseFloat(result['width'])).toBeGreaterThan(0);
expect(parseFloat(result['height'])).toBeGreaterThan(0);
});
it('should capture existing keyframe player styles in and merge in the styles into the follow up player\'s keyframes',
() => {
// IE cannot modify the position of an animation...
// note that this feature is only for testing purposes
if (isIE()) return;
const elm = createElement();
elm.style.border = '1px solid black';
document.body.appendChild(elm); // this is required so GCS works
const animator = new CssKeyframesDriver();
const p1 = <CssKeyframesPlayer>animator.animate(
elm,
[
{width: '0px', lineHeight: '20px', offset: 0},
{width: '100px', lineHeight: '50px', offset: 1},
],
2000, 0, 'ease-out');
const p2 = <CssKeyframesPlayer>animator.animate(
elm,
[
{height: '100px', offset: 0},
{height: '300px', offset: 1},
],
2000, 0, 'ease-out');
p1.play();
p1.setPosition(0.5);
p1.beforeDestroy();
p2.play();
p2.setPosition(0.5);
p2.beforeDestroy();
const p3 = <CssKeyframesPlayer>animator.animate(
elm,
[
{height: '0px', width: '0px', offset: 0},
{height: '400px', width: '400px', offset: 0.5},
{height: '500px', width: '500px', offset: 1},
],
2000, 0, 'ease-out', [p1, p2]);
p3.init();
const [k1, k2, k3] = p3.keyframes;
const offset = k1.offset;
expect(offset).toEqual(0);
const width = parseInt(k1['width'] as string);
expect(width).toBeGreaterThan(0);
expect(width).toBeLessThan(100);
const bWidth = parseInt(k1['lineHeight'] as string);
expect(bWidth).toBeGreaterThan(20);
expect(bWidth).toBeLessThan(50);
const height = parseFloat(k1['height'] as string);
expect(height).toBeGreaterThan(100);
expect(height).toBeLessThan(300);
// since the lineHeight wasn't apart of the follow-up animation,
// it's values were copied over into all the keyframes
const b1 = bWidth;
const b2 = parseInt(k2['lineHeight'] as string);
const b3 = parseInt(k3['lineHeight'] as string);
expect(b1).toEqual(b2);
expect(b2).toEqual(b3);
// we delete the lineHeight values because they are float-based
// and each browser has a different value based on precision...
// therefore we can't assert it directly below (asserting it above
// on the first keyframe was all that was needed since they are the same)
delete k2['lineHeight'];
delete k3['lineHeight'];
expect(k2).toEqual({width: '400px', height: '400px', offset: 0.5});
expect(k3).toEqual({width: '500px', height: '500px', offset: 1});
});
});
});
function assertExistingAnimationDuration(element: any, duration: number) {
expect(parseElementAnimationStyle(element).duration).toEqual(duration);
}
function findStyleObjectWithKeyframes(): any|null {
const sheetWithKeyframes = document.styleSheets[document.styleSheets.length - 1];
const styleElms = Array.from(document.querySelectorAll('head style') as any as any[]);
return styleElms.find(elm => elm.sheet == sheetWithKeyframes) || null;
}
function parseElementAnimationStyle(element: any):
{duration: number, delay: number, easing: string, animationName: string} {
const style = element.style;
const duration = parseInt(style.animationDuration || 0);
const delay = style.animationDelay;
const easing = style.animationTimingFunction;
const animationName = style.animationName;
return {duration, delay, easing, animationName};
}
function isIE() {
// note that this only applies to older IEs (not edge)
return (window as any).document['documentMode'] ? true : false;
}

View File

@ -0,0 +1,78 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing';
import {DirectStylePlayer} from '../../../src/render/css_keyframes/direct_style_player';
import {assertStyle, createElement} from './shared';
const CSS_KEYFRAME_RULE_TYPE = 7;
describe('DirectStylePlayer tests', () => {
if (typeof Element == 'undefined' || typeof document == 'undefined') return;
it('should apply the styling to the given element when the animation starts and remove when destroyed',
() => {
const element = createElement();
const player = new DirectStylePlayer(element, {opacity: 0.5});
assertStyle(element, 'opacity', '');
player.play();
assertStyle(element, 'opacity', '0.5');
player.finish();
assertStyle(element, 'opacity', '0.5');
player.destroy();
assertStyle(element, 'opacity', '');
});
it('should finish the animation after one tick', fakeAsync(() => {
const element = createElement();
const player = new DirectStylePlayer(element, {opacity: 0.5});
let done = false;
player.onDone(() => done = true);
expect(done).toBeFalsy();
player.play();
expect(done).toBeFalsy();
flushMicrotasks();
expect(done).toBeTruthy();
}));
it('should restore existing element styles once the animation is destroyed', fakeAsync(() => {
const element = createElement();
element.style['width'] = '100px';
element.style['height'] = '200px';
const player = new DirectStylePlayer(element, {width: '500px', opacity: 0.5});
assertStyle(element, 'width', '100px');
assertStyle(element, 'height', '200px');
assertStyle(element, 'opacity', '');
player.init();
assertStyle(element, 'width', '100px');
assertStyle(element, 'height', '200px');
assertStyle(element, 'opacity', '');
player.play();
assertStyle(element, 'width', '500px');
assertStyle(element, 'height', '200px');
assertStyle(element, 'opacity', '0.5');
player.destroy();
assertStyle(element, 'width', '100px');
assertStyle(element, 'height', '200px');
assertStyle(element, 'opacity', '');
}));
});

View File

@ -0,0 +1,233 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ElementAnimationStyleHandler} from '../../../src/render/css_keyframes/element_animation_style_handler';
import {computeStyle} from '../../../src/util';
import {assertStyle, createElement, makeAnimationEvent, supportsAnimationEventCreation} from './shared';
const EMPTY_FN = () => {};
{
describe('ElementAnimationStyleHandler', () => {
if (typeof Element == 'undefined' || typeof document == 'undefined' ||
typeof(window as any)['AnimationEvent'] == 'undefined')
return;
it('should add and remove an animation on to an element\'s styling', () => {
const element = createElement();
document.body.appendChild(element);
const handler = new ElementAnimationStyleHandler(
element, 'someAnimation', 1234, 999, 'ease-in', 'forwards', EMPTY_FN);
assertStyle(element, 'animation-name', '');
assertStyle(element, 'animation-duration', '');
assertStyle(element, 'animation-delay', '');
assertStyle(element, 'animation-timing-function', '');
assertStyle(element, 'animation-fill-mode', '');
handler.apply();
assertStyle(element, 'animation-name', 'someAnimation');
assertStyle(element, 'animation-duration', '1234ms');
assertStyle(element, 'animation-delay', '999ms');
assertStyle(element, 'animation-timing-function', 'ease-in');
assertStyle(element, 'animation-fill-mode', 'forwards');
handler.finish();
assertStyle(element, 'animation-name', 'someAnimation');
assertStyle(element, 'animation-duration', '1234ms');
assertStyle(element, 'animation-delay', '999ms');
assertStyle(element, 'animation-timing-function', 'ease-in');
assertStyle(element, 'animation-fill-mode', 'forwards');
handler.destroy();
assertStyle(element, 'animation-name', '');
assertStyle(element, 'animation-duration', '');
assertStyle(element, 'animation-delay', '');
assertStyle(element, 'animation-timing-function', '');
assertStyle(element, 'animation-fill-mode', '');
});
it('should respect existing animation styling on an element', () => {
const element = createElement();
document.body.appendChild(element);
element.style.setProperty('animation', 'fooAnimation 1s ease-out forwards');
assertStyle(element, 'animation-name', 'fooAnimation');
const handler = new ElementAnimationStyleHandler(
element, 'barAnimation', 1234, 555, 'ease-out', 'both', EMPTY_FN);
assertStyle(element, 'animation-name', 'fooAnimation');
handler.apply();
assertStyle(element, 'animation-name', 'fooAnimation, barAnimation');
handler.destroy();
assertStyle(element, 'animation-name', 'fooAnimation');
});
it('should respect animation styling that is prefixed after a handler is applied on an element',
() => {
const element = createElement();
document.body.appendChild(element);
const handler = new ElementAnimationStyleHandler(
element, 'barAnimation', 1234, 555, 'ease-out', 'both', EMPTY_FN);
assertStyle(element, 'animation-name', '');
handler.apply();
assertStyle(element, 'animation-name', 'barAnimation');
const anim = element.style.animation;
element.style.setProperty('animation', `${anim}, fooAnimation 1s ease-out forwards`);
assertStyle(element, 'animation-name', 'barAnimation, fooAnimation');
handler.destroy();
assertStyle(element, 'animation-name', 'fooAnimation');
});
it('should respect animation styling that is suffixed after a handler is applied on an element',
() => {
const element = createElement();
document.body.appendChild(element);
const handler = new ElementAnimationStyleHandler(
element, 'barAnimation', 1234, 555, 'ease-out', 'both', EMPTY_FN);
assertStyle(element, 'animation-name', '');
handler.apply();
assertStyle(element, 'animation-name', 'barAnimation');
const anim = element.style.animation;
element.style.setProperty('animation', `fooAnimation 1s ease-out forwards, ${anim}`);
assertStyle(element, 'animation-name', 'fooAnimation, barAnimation');
handler.destroy();
assertStyle(element, 'animation-name', 'fooAnimation');
});
it('should respect existing animation handlers on an element', () => {
const element = createElement();
document.body.appendChild(element);
assertStyle(element, 'animation-name', '');
const h1 = new ElementAnimationStyleHandler(
element, 'fooAnimation', 1234, 333, 'ease-out', 'both', EMPTY_FN);
h1.apply();
assertStyle(element, 'animation-name', 'fooAnimation');
assertStyle(element, 'animation-duration', '1234ms');
assertStyle(element, 'animation-delay', '333ms');
const h2 = new ElementAnimationStyleHandler(
element, 'barAnimation', 5678, 666, 'ease-out', 'both', EMPTY_FN);
h2.apply();
assertStyle(element, 'animation-name', 'fooAnimation, barAnimation');
assertStyle(element, 'animation-duration', '1234ms, 5678ms');
assertStyle(element, 'animation-delay', '333ms, 666ms');
const h3 = new ElementAnimationStyleHandler(
element, 'bazAnimation', 90, 999, 'ease-out', 'both', EMPTY_FN);
h3.apply();
assertStyle(element, 'animation-name', 'fooAnimation, barAnimation, bazAnimation');
assertStyle(element, 'animation-duration', '1234ms, 5678ms, 90ms');
assertStyle(element, 'animation-delay', '333ms, 666ms, 999ms');
h2.destroy();
assertStyle(element, 'animation-name', 'fooAnimation, bazAnimation');
assertStyle(element, 'animation-duration', '1234ms, 90ms');
assertStyle(element, 'animation-delay', '333ms, 999ms');
h1.destroy();
assertStyle(element, 'animation-name', 'bazAnimation');
assertStyle(element, 'animation-duration', '90ms');
assertStyle(element, 'animation-delay', '999ms');
});
it('should fire the onDone method when .finish() is called on the handler', () => {
const element = createElement();
document.body.appendChild(element);
let done = false;
const handler = new ElementAnimationStyleHandler(
element, 'fooAnimation', 1234, 333, 'ease-out', 'both', () => done = true);
expect(done).toBeFalsy();
handler.finish();
expect(done).toBeTruthy();
});
it('should fire the onDone method only once when .finish() is called on the handler', () => {
const element = createElement();
document.body.appendChild(element);
let doneCount = 0;
const handler = new ElementAnimationStyleHandler(
element, 'fooAnimation', 1234, 333, 'ease-out', 'both', () => doneCount++);
expect(doneCount).toEqual(0);
handler.finish();
expect(doneCount).toEqual(1);
handler.finish();
expect(doneCount).toEqual(1);
});
it('should fire the onDone method when .destroy() is called on the handler', () => {
const element = createElement();
document.body.appendChild(element);
let done = false;
const handler = new ElementAnimationStyleHandler(
element, 'fooAnimation', 1234, 333, 'ease-out', 'both', () => done = true);
expect(done).toBeFalsy();
handler.destroy();
expect(done).toBeTruthy();
});
it('should fire the onDone method when the matching animationend event is emitted', () => {
// IE10 and IE11 cannot create an instanceof AnimationEvent
if (!supportsAnimationEventCreation()) return;
const element = createElement();
document.body.appendChild(element);
let done = false;
const handler = new ElementAnimationStyleHandler(
element, 'fooAnimation', 1234, 333, 'ease-out', 'both', () => done = true);
expect(done).toBeFalsy();
handler.apply();
expect(done).toBeFalsy();
let event = makeAnimationEvent('end', 'fooAnimation', 100);
element.dispatchEvent(event);
expect(done).toBeFalsy();
event = makeAnimationEvent('end', 'fooAnimation', 1234);
element.dispatchEvent(event);
expect(done).toBeFalsy();
const timestampAfterDelay = Date.now() + 500;
event = makeAnimationEvent('end', 'fakeAnimation', 1234, timestampAfterDelay);
element.dispatchEvent(event);
expect(done).toBeFalsy();
event = makeAnimationEvent('end', 'fooAnimation', 1234, timestampAfterDelay);
element.dispatchEvent(event);
expect(done).toBeTruthy();
});
});
}

View File

@ -0,0 +1,50 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export function forceReflow() {
(document.body as any)['_reflow'] = document.body.clientWidth;
}
export function makeAnimationEvent(
startOrEnd: 'start' | 'end', animationName: string, elapsedTime: number, timestamp?: number) {
const e = new AnimationEvent('animation' + startOrEnd, {animationName, elapsedTime});
if (timestamp) {
(e as any)._ngTestManualTimestamp = timestamp;
}
return e;
}
export function supportsAnimationEventCreation() {
let supported = false;
try {
makeAnimationEvent('end', 'test', 0);
supported = true;
} catch (e) {
}
return supported;
}
export function findKeyframeDefinition(sheet: any): any|null {
return sheet.cssRules[0] || null;
}
export function createElement() {
return document.createElement('div');
}
export function assertStyle(element: any, prop: string, value: string) {
expect(element.style[prop] || '').toEqual(value);
}
export function assertElementExistsInDom(element: any, yes?: boolean) {
const exp = expect(element.parentNode);
if (yes) {
exp.toBeTruthy();
} else {
exp.toBeFalsy();
}
}

View File

@ -0,0 +1,61 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {CssKeyframesPlayer} from '../../../src/render/css_keyframes/css_keyframes_player';
import {DOMAnimation} from '../../../src/render/web_animations/dom_animation';
import {WebAnimationsDriver} from '../../../src/render/web_animations/web_animations_driver';
import {WebAnimationsPlayer} from '../../../src/render/web_animations/web_animations_player';
{
describe('WebAnimationsDriver', () => {
if (typeof Element == 'undefined' || typeof document == 'undefined') return;
describe('when web-animations are not supported natively', () => {
it('should return an instance of a CssKeyframePlayer if scrubbing is not requested', () => {
const element = createElement();
const driver = makeDriver();
driver.overrideWebAnimationsSupport(false);
const player = driver.animate(element, [], 1000, 1000, '', [], false);
expect(player instanceof CssKeyframesPlayer).toBeTruthy();
});
it('should return an instance of a WebAnimationsPlayer if scrubbing is not requested', () => {
const element = createElement();
const driver = makeDriver();
driver.overrideWebAnimationsSupport(false);
const player = driver.animate(element, [], 1000, 1000, '', [], true);
expect(player instanceof WebAnimationsPlayer).toBeTruthy();
});
});
describe('when web-animations are supported natively', () => {
it('should return an instance of a WebAnimationsPlayer if scrubbing is not requested', () => {
const element = createElement();
const driver = makeDriver();
driver.overrideWebAnimationsSupport(true);
const player = driver.animate(element, [], 1000, 1000, '', [], false);
expect(player instanceof WebAnimationsPlayer).toBeTruthy();
});
it('should return an instance of a WebAnimationsPlayer if scrubbing is requested', () => {
const element = createElement();
const driver = makeDriver();
driver.overrideWebAnimationsSupport(true);
const player = driver.animate(element, [], 1000, 1000, '', [], true);
expect(player instanceof WebAnimationsPlayer).toBeTruthy();
});
});
});
}
function makeDriver() {
return new WebAnimationsDriver();
}
function createElement() {
return document.createElement('div');
}

View File

@ -33,24 +33,6 @@ import {WebAnimationsPlayer} from '../../../src/render/web_animations/web_animat
expect(p.log).toEqual(['pause', 'play']); expect(p.log).toEqual(['pause', 'play']);
}); });
it('should allow an empty set of keyframes with a set of previous styles', () => {
const previousKeyframes = [
{opacity: 0, offset: 0},
{opacity: 1, offset: 1},
];
const previousPlayer = new WebAnimationsPlayer(element, previousKeyframes, {duration: 1000});
previousPlayer.play();
previousPlayer.finish();
previousPlayer.beforeDestroy();
const EMPTY_KEYFRAMES: any[] = [];
const player =
new WebAnimationsPlayer(element, EMPTY_KEYFRAMES, {duration: 1000}, [previousPlayer]);
player.play();
player.destroy();
});
it('should not pause the player if created and started before initialized', () => { it('should not pause the player if created and started before initialized', () => {
const keyframes = [ const keyframes = [
{opacity: 0, offset: 0}, {opacity: 0, offset: 0},

View File

@ -0,0 +1,274 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {animate, group, keyframes, query, state, style, transition, trigger} from '@angular/animations';
import {AnimationDriver, ɵAnimationEngine as AnimationEngine, ɵCssKeyframesDriver as CssKeyframesDriver, ɵCssKeyframesPlayer as CssKeyframesPlayer} from '@angular/animations/browser';
import {AnimationGroupPlayer} from '@angular/animations/src/players/animation_group_player';
import {Component, ViewChild} from '@angular/core';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
import {TestBed} from '../../testing';
(function() {
// these tests are only mean't to be run within the DOM (for now)
// Buggy in Chromium 39, see https://github.com/angular/angular/issues/15793
if (typeof Element == 'undefined') return;
describe('animation integration tests using css keyframe animations', function() {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [{provide: AnimationDriver, useClass: CssKeyframesDriver}],
imports: [BrowserAnimationsModule]
});
});
it('should compute (*) animation styles for a container that is being removed', () => {
@Component({
selector: 'ani-cmp',
template: `
<div @auto *ngIf="exp">
<div style="line-height:20px;">1</div>
<div style="line-height:20px;">2</div>
<div style="line-height:20px;">3</div>
<div style="line-height:20px;">4</div>
<div style="line-height:20px;">5</div>
</div>
`,
animations: [trigger(
'auto',
[
state('void', style({height: '0px'})),
state('*', style({height: '*'})),
transition('* => *', animate(1000)),
])]
})
class Cmp {
public exp: boolean = false;
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(AnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = true;
fixture.detectChanges();
expect(engine.players.length).toEqual(1);
let player = getPlayer(engine) as CssKeyframesPlayer;
expect(player.keyframes).toEqual([{height: '0px', offset: 0}, {height: '100px', offset: 1}]);
player.finish();
if (browserDetection.isOldChrome) return;
cmp.exp = false;
fixture.detectChanges();
player = getPlayer(engine) as CssKeyframesPlayer;
expect(player.keyframes).toEqual([{height: '100px', offset: 0}, {height: '0px', offset: 1}]);
});
it('should cleanup all existing @keyframe <style> objects after the animation has finished',
() => {
@Component({
selector: 'ani-cmp',
template: `
<div [@myAnimation]="myAnimationExp">
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
</div>
`,
animations: [trigger(
'myAnimation',
[
transition(
'* => go',
[
query(
'div',
[
style({opacity: 0}),
animate('1s', style({opacity: 0})),
]),
]),
])]
})
class Cmp {
public myAnimationExp = '';
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(AnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.myAnimationExp = 'go';
fixture.detectChanges();
const webPlayer = <AnimationGroupPlayer>getPlayer(engine);
const players = webPlayer.players as CssKeyframesPlayer[];
expect(players.length).toEqual(5);
const head = document.querySelector('head') !;
const sheets: any[] = [];
for (let i = 0; i < 5; i++) {
const sheet = findStyleObjectWithKeyframes(i);
expect(head.contains(sheet)).toBeTruthy();
sheets.push(sheet);
}
cmp.myAnimationExp = 'go-back';
fixture.detectChanges();
for (let i = 0; i < 5; i++) {
expect(head.contains(sheets[i])).toBeFalsy();
}
});
it('should properly handle easing values that are apart of the sequence', () => {
@Component({
selector: 'ani-cmp',
template: `
<div #elm [@myAnimation]="myAnimationExp"></div>
`,
animations: [
trigger(
'myAnimation',
[
transition(
'* => goSteps',
[
style({opacity: 0}),
animate('1s ease-out', style({opacity: 1})),
]),
transition(
'* => goKeyframes',
[
animate('1s cubic-bezier(0.5, 1, 0.5, 1)', keyframes([
style({opacity: 0}),
style({opacity: 0.5}),
style({opacity: 1}),
])),
]),
]),
]
})
class Cmp {
@ViewChild('elm') public element: any;
public myAnimationExp = '';
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(AnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.myAnimationExp = 'goSteps';
fixture.detectChanges();
let kfElm = findStyleObjectWithKeyframes();
const [r1, r2] = kfElm.sheet.cssRules[0].cssRules;
assertEasing(r1, 'ease-out');
assertEasing(r2, '');
const element = cmp.element.nativeElement;
const webPlayer = getPlayer(engine);
cmp.myAnimationExp = 'goKeyframes';
fixture.detectChanges();
assertEasing(element, 'cubic-bezier(0.5,1,0.5,1)');
});
it('should restore existing style values once the animation completes', () => {
@Component({
selector: 'ani-cmp',
template: `
<div #elm [@myAnimation]="myAnimationExp"></div>
`,
animations: [
trigger(
'myAnimation',
[
state('go', style({width: '200px'})),
transition(
'* => go',
[
style({height: '100px', width: '100px'}), group([
animate('1s', style({height: '200px'})),
animate('1s', style({width: '200px'}))
])
]),
]),
]
})
class Cmp {
@ViewChild('elm') public element: any;
public myAnimationExp = '';
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(AnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
fixture.detectChanges();
const element = cmp.element.nativeElement;
element.style['width'] = '50px';
element.style['height'] = '50px';
assertStyle(element, 'width', '50px');
assertStyle(element, 'height', '50px');
cmp.myAnimationExp = 'go';
fixture.detectChanges();
const player = getPlayer(engine);
assertStyle(element, 'width', '100px');
assertStyle(element, 'height', '100px');
player.finish();
assertStyle(element, 'width', '200px');
assertStyle(element, 'height', '50px');
});
});
})();
function approximate(value: number, target: number) {
return Math.abs(target - value) / value;
}
function getPlayer(engine: AnimationEngine, index = 0) {
return (engine.players[index] as any) !.getRealPlayer();
}
function findStyleObjectWithKeyframes(index?: number): any|null {
const sheetWithKeyframes = document.styleSheets[document.styleSheets.length - (index || 1)];
const styleElms = Array.from(document.querySelectorAll('head style') as any as any[]);
return styleElms.find(elm => elm.sheet == sheetWithKeyframes) || null;
}
function assertEasing(node: any, easing: string) {
expect((node.style.animationTimingFunction || '').replace(/\s+/g, '')).toEqual(easing);
}
function assertStyle(node: any, prop: string, value: string) {
expect(node.style[prop] || '').toEqual(value);
}

View File

@ -60,7 +60,6 @@ import {TestBed} from '../../testing';
cmp.exp = true; cmp.exp = true;
fixture.detectChanges(); fixture.detectChanges();
engine.flush();
expect(engine.players.length).toEqual(1); expect(engine.players.length).toEqual(1);
let webPlayer = engine.players[0].getRealPlayer() as ɵWebAnimationsPlayer; let webPlayer = engine.players[0].getRealPlayer() as ɵWebAnimationsPlayer;
@ -69,6 +68,8 @@ import {TestBed} from '../../testing';
{height: '0px', offset: 0}, {height: '100px', offset: 1} {height: '0px', offset: 0}, {height: '100px', offset: 1}
]); ]);
webPlayer.finish();
if (!browserDetection.isOldChrome) { if (!browserDetection.isOldChrome) {
cmp.exp = false; cmp.exp = false;
fixture.detectChanges(); fixture.detectChanges();
@ -378,9 +379,9 @@ import {TestBed} from '../../testing';
player = engine.players[0] !; player = engine.players[0] !;
webPlayer = player.getRealPlayer() as ɵWebAnimationsPlayer; webPlayer = player.getRealPlayer() as ɵWebAnimationsPlayer;
expect(approximate(parseFloat(webPlayer.previousStyles['width'] as string), 150)) expect(approximate(parseFloat(webPlayer.keyframes[0]['width'] as string), 150))
.toBeLessThan(0.05); .toBeLessThan(0.05);
expect(approximate(parseFloat(webPlayer.previousStyles['height'] as string), 300)) expect(approximate(parseFloat(webPlayer.keyframes[0]['height'] as string), 300))
.toBeLessThan(0.05); .toBeLessThan(0.05);
}); });
@ -445,9 +446,9 @@ import {TestBed} from '../../testing';
expect(players.length).toEqual(5); expect(players.length).toEqual(5);
for (let i = 0; i < players.length; i++) { for (let i = 0; i < players.length; i++) {
const p = players[i] as ɵWebAnimationsPlayer; const p = players[i] as ɵWebAnimationsPlayer;
expect(approximate(parseFloat(p.previousStyles['width'] as string), 250)) expect(approximate(parseFloat(p.keyframes[0]['width'] as string), 250))
.toBeLessThan(0.05); .toBeLessThan(0.05);
expect(approximate(parseFloat(p.previousStyles['height'] as string), 500)) expect(approximate(parseFloat(p.keyframes[0]['height'] as string), 500))
.toBeLessThan(0.05); .toBeLessThan(0.05);
} }
}); });

View File

@ -7,7 +7,7 @@
*/ */
import {AnimationBuilder} from '@angular/animations'; import {AnimationBuilder} from '@angular/animations';
import {AnimationDriver, ɵAnimationEngine as AnimationEngine, ɵAnimationStyleNormalizer as AnimationStyleNormalizer, ɵNoopAnimationDriver as NoopAnimationDriver, ɵWebAnimationsDriver as WebAnimationsDriver, ɵWebAnimationsStyleNormalizer as WebAnimationsStyleNormalizer, ɵsupportsWebAnimations as supportsWebAnimations} from '@angular/animations/browser'; import {AnimationDriver, ɵAnimationEngine as AnimationEngine, ɵAnimationStyleNormalizer as AnimationStyleNormalizer, ɵCssKeyframesDriver as CssKeyframesDriver, ɵNoopAnimationDriver as NoopAnimationDriver, ɵWebAnimationsDriver as WebAnimationsDriver, ɵWebAnimationsStyleNormalizer as WebAnimationsStyleNormalizer, ɵsupportsWebAnimations as supportsWebAnimations} from '@angular/animations/browser';
import {Injectable, NgZone, Provider, RendererFactory2} from '@angular/core'; import {Injectable, NgZone, Provider, RendererFactory2} from '@angular/core';
import {ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser'; import {ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser';
@ -22,10 +22,7 @@ export class InjectableAnimationEngine extends AnimationEngine {
} }
export function instantiateSupportedAnimationDriver() { export function instantiateSupportedAnimationDriver() {
if (supportsWebAnimations()) { return supportsWebAnimations() ? new WebAnimationsDriver() : new CssKeyframesDriver();
return new WebAnimationsDriver();
}
return new NoopAnimationDriver();
} }
export function instantiateDefaultStyleNormalizer() { export function instantiateDefaultStyleNormalizer() {

View File

@ -2,7 +2,7 @@
export declare abstract class AnimationDriver { export declare abstract class AnimationDriver {
abstract animate(element: any, keyframes: { abstract animate(element: any, keyframes: {
[key: string]: string | number; [key: string]: string | number;
}[], duration: number, delay: number, easing?: string | null, previousPlayers?: any[]): any; }[], duration: number, delay: number, easing?: string | null, previousPlayers?: any[], scrubberAccessRequested?: boolean): any;
abstract computeStyle(element: any, prop: string, defaultValue?: string): string; abstract computeStyle(element: any, prop: string, defaultValue?: string): string;
abstract containsElement(elm1: any, elm2: any): boolean; abstract containsElement(elm1: any, elm2: any): boolean;
abstract matchesElement(element: any, selector: string): boolean; abstract matchesElement(element: any, selector: string): boolean;