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:
parent
9eecb0b27f
commit
b2f366b3b7
|
@ -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/)
|
||||
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 class="l-sub-section">
|
||||
|
|
|
@ -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):
|
||||
|
||||
<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
|
||||
</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.
|
||||
|
||||
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:
|
||||
|
||||
|
@ -276,6 +279,8 @@ Here are the features which may require additional polyfills:
|
|||
<td>
|
||||
|
||||
[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>
|
||||
|
||||
|
@ -286,7 +291,8 @@ Here are the features which may require additional polyfills:
|
|||
</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>
|
||||
|
||||
</tr>
|
||||
|
|
|
@ -10,5 +10,7 @@ export {AnimationStyleNormalizer as ɵAnimationStyleNormalizer, NoopAnimationSty
|
|||
export {WebAnimationsStyleNormalizer as ɵWebAnimationsStyleNormalizer} from './dsl/style_normalization/web_animations_style_normalizer';
|
||||
export {NoopAnimationDriver as ɵNoopAnimationDriver} from './render/animation_driver';
|
||||
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 {WebAnimationsPlayer as ɵWebAnimationsPlayer} from './render/web_animations/web_animations_player';
|
||||
|
|
|
@ -33,7 +33,8 @@ export class NoopAnimationDriver implements AnimationDriver {
|
|||
|
||||
animate(
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -56,5 +57,5 @@ export abstract class AnimationDriver {
|
|||
|
||||
abstract animate(
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -44,7 +44,7 @@ export class TimelineAnimationEngine {
|
|||
const element = i.element;
|
||||
const keyframes = normalizeKeyframes(
|
||||
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 {
|
||||
|
|
|
@ -7,12 +7,17 @@
|
|||
*/
|
||||
import {AnimationPlayer, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {allowPreviousPlayerStylesMerge, balancePreviousStylesIntoKeyframes, copyStyles} from '../../util';
|
||||
import {AnimationDriver} from '../animation_driver';
|
||||
import {CssKeyframesDriver} from '../css_keyframes/css_keyframes_driver';
|
||||
import {containsElement, invokeQuery, matchesElement, validateStyleProperty} from '../shared';
|
||||
|
||||
import {WebAnimationsPlayer} from './web_animations_player';
|
||||
|
||||
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); }
|
||||
|
||||
matchesElement(element: any, selector: string): boolean {
|
||||
|
@ -29,24 +34,46 @@ export class WebAnimationsDriver implements AnimationDriver {
|
|||
return (window.getComputedStyle(element) as any)[prop] as string;
|
||||
}
|
||||
|
||||
overrideWebAnimationsSupport(supported: boolean) { this._isNativeImpl = supported; }
|
||||
|
||||
animate(
|
||||
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 playerOptions: {[key: string]: string | number} = {duration, delay, fill};
|
||||
|
||||
// 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)
|
||||
if (easing) {
|
||||
playerOptions['easing'] = easing;
|
||||
}
|
||||
|
||||
const previousStyles: {[key: string]: any} = {};
|
||||
const previousWebAnimationPlayers = <WebAnimationsPlayer[]>previousPlayers.filter(
|
||||
player => { return player instanceof WebAnimationsPlayer; });
|
||||
return new WebAnimationsPlayer(element, keyframes, playerOptions, previousWebAnimationPlayers);
|
||||
player => player instanceof WebAnimationsPlayer);
|
||||
|
||||
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() {
|
||||
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']) || {};
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
import {AnimationPlayer} from '@angular/animations';
|
||||
|
||||
import {allowPreviousPlayerStylesMerge, copyStyles} from '../../util';
|
||||
import {allowPreviousPlayerStylesMerge, balancePreviousStylesIntoKeyframes, computeStyle, copyStyles} from '../../util';
|
||||
|
||||
import {DOMAnimation} from './dom_animation';
|
||||
|
||||
|
@ -27,23 +27,14 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
|||
public time = 0;
|
||||
|
||||
public parentPlayer: AnimationPlayer|null = null;
|
||||
public previousStyles: {[styleName: string]: string | number} = {};
|
||||
public currentSnapshot: {[styleName: string]: string | number} = {};
|
||||
|
||||
constructor(
|
||||
public element: any, public keyframes: {[key: string]: string | number}[],
|
||||
public options: {[key: string]: string | number},
|
||||
private previousPlayers: WebAnimationsPlayer[] = []) {
|
||||
public options: {[key: string]: string | number}) {
|
||||
this._duration = <number>options['duration'];
|
||||
this._delay = <number>options['delay'] || 0;
|
||||
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() {
|
||||
|
@ -63,30 +54,7 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
|||
if (this._initialized) return;
|
||||
this._initialized = true;
|
||||
|
||||
const keyframes = this.keyframes.map(styles => copyStyles(styles, false));
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const keyframes = this.keyframes;
|
||||
(this as{domPlayer: DOMAnimation}).domPlayer =
|
||||
this._triggerWebAnimation(this.element, keyframes, this.options);
|
||||
this._finalKeyframe = keyframes.length ? keyframes[keyframes.length - 1] : {};
|
||||
|
@ -178,7 +146,7 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
|||
Object.keys(this._finalKeyframe).forEach(prop => {
|
||||
if (prop != 'offset') {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
function _computeStyle(element: any, prop: string): string {
|
||||
return (<any>window.getComputedStyle(element))[prop];
|
||||
}
|
||||
|
|
|
@ -235,6 +235,30 @@ export function allowPreviousPlayerStylesMerge(duration: number, delay: number)
|
|||
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(
|
||||
visitor: AnimationDslVisitor, node: AnimationMetadata, context: any): any;
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function computeStyle(element: any, prop: string): string {
|
||||
return (<any>window.getComputedStyle(element))[prop];
|
||||
}
|
||||
|
|
|
@ -10,5 +10,6 @@ ts_library(
|
|||
"//packages/animations/browser",
|
||||
"//packages/animations/browser/testing",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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', '');
|
||||
}));
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
|
@ -33,24 +33,6 @@ import {WebAnimationsPlayer} from '../../../src/render/web_animations/web_animat
|
|||
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', () => {
|
||||
const keyframes = [
|
||||
{opacity: 0, offset: 0},
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -60,7 +60,6 @@ import {TestBed} from '../../testing';
|
|||
|
||||
cmp.exp = true;
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
|
||||
expect(engine.players.length).toEqual(1);
|
||||
let webPlayer = engine.players[0].getRealPlayer() as ɵWebAnimationsPlayer;
|
||||
|
@ -69,6 +68,8 @@ import {TestBed} from '../../testing';
|
|||
{height: '0px', offset: 0}, {height: '100px', offset: 1}
|
||||
]);
|
||||
|
||||
webPlayer.finish();
|
||||
|
||||
if (!browserDetection.isOldChrome) {
|
||||
cmp.exp = false;
|
||||
fixture.detectChanges();
|
||||
|
@ -378,9 +379,9 @@ import {TestBed} from '../../testing';
|
|||
|
||||
player = engine.players[0] !;
|
||||
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);
|
||||
expect(approximate(parseFloat(webPlayer.previousStyles['height'] as string), 300))
|
||||
expect(approximate(parseFloat(webPlayer.keyframes[0]['height'] as string), 300))
|
||||
.toBeLessThan(0.05);
|
||||
});
|
||||
|
||||
|
@ -445,9 +446,9 @@ import {TestBed} from '../../testing';
|
|||
expect(players.length).toEqual(5);
|
||||
for (let i = 0; i < players.length; i++) {
|
||||
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);
|
||||
expect(approximate(parseFloat(p.previousStyles['height'] as string), 500))
|
||||
expect(approximate(parseFloat(p.keyframes[0]['height'] as string), 500))
|
||||
.toBeLessThan(0.05);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
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 {ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser';
|
||||
|
||||
|
@ -22,10 +22,7 @@ export class InjectableAnimationEngine extends AnimationEngine {
|
|||
}
|
||||
|
||||
export function instantiateSupportedAnimationDriver() {
|
||||
if (supportsWebAnimations()) {
|
||||
return new WebAnimationsDriver();
|
||||
}
|
||||
return new NoopAnimationDriver();
|
||||
return supportsWebAnimations() ? new WebAnimationsDriver() : new CssKeyframesDriver();
|
||||
}
|
||||
|
||||
export function instantiateDefaultStyleNormalizer() {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
export declare abstract class AnimationDriver {
|
||||
abstract animate(element: any, keyframes: {
|
||||
[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 containsElement(elm1: any, elm2: any): boolean;
|
||||
abstract matchesElement(element: any, selector: string): boolean;
|
||||
|
|
Loading…
Reference in New Issue