diff --git a/aio/content/guide/animations.md b/aio/content/guide/animations.md
index 9da7a8229a..e9f86a201d 100644
--- a/aio/content/guide/animations.md
+++ b/aio/content/guide/animations.md
@@ -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.
diff --git a/aio/content/guide/browser-support.md b/aio/content/guide/browser-support.md
index a86c3b5618..9c91a715d8 100644
--- a/aio/content/guide/browser-support.md
+++ b/aio/content/guide/browser-support.md
@@ -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):
+ # 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
@@ -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:
[Animations](guide/animations)
+ Only if `Animation Builder` is used within the application--standard
+ animation support in Angular doesn't require any polyfills (as of NG6).
|
@@ -286,7 +291,8 @@ Here are the features which may require additional polyfills:
- All but Chrome and Firefox Not supported in IE9
+ If AnimationBuilder is used then the polyfill will enable scrubbing
+ support for IE/Edge and Safari (Chrome and Firefox support this natively).
|
diff --git a/packages/animations/browser/src/private_export.ts b/packages/animations/browser/src/private_export.ts
index c0910e2dc8..9c6de25c98 100644
--- a/packages/animations/browser/src/private_export.ts
+++ b/packages/animations/browser/src/private_export.ts
@@ -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';
diff --git a/packages/animations/browser/src/render/animation_driver.ts b/packages/animations/browser/src/render/animation_driver.ts
index 462c6afdcb..9583884507 100644
--- a/packages/animations/browser/src/render/animation_driver.ts
+++ b/packages/animations/browser/src/render/animation_driver.ts
@@ -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;
}
diff --git a/packages/animations/browser/src/render/css_keyframes/css_keyframes_driver.ts b/packages/animations/browser/src/render/css_keyframes/css_keyframes_driver.ts
new file mode 100644
index 0000000000..384b438df4
--- /dev/null
+++ b/packages/animations/browser/src/render/css_keyframes/css_keyframes_driver.ts
@@ -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 =
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);
+}
diff --git a/packages/animations/browser/src/render/css_keyframes/css_keyframes_player.ts b/packages/animations/browser/src/render/css_keyframes/css_keyframes_player.ts
new file mode 100644
index 0000000000..922341513a
--- /dev/null
+++ b/packages/animations/browser/src/render/css_keyframes/css_keyframes_player.ts
@@ -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;
+ }
+}
diff --git a/packages/animations/browser/src/render/css_keyframes/direct_style_player.ts b/packages/animations/browser/src/render/css_keyframes/direct_style_player.ts
new file mode 100644
index 0000000000..e397d5a677
--- /dev/null
+++ b/packages/animations/browser/src/render/css_keyframes/direct_style_player.ts
@@ -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();
+ }
+}
diff --git a/packages/animations/browser/src/render/css_keyframes/element_animation_style_handler.ts b/packages/animations/browser/src/render/css_keyframes/element_animation_style_handler.ts
new file mode 100644
index 0000000000..03e06889d3
--- /dev/null
+++ b/packages/animations/browser/src/render/css_keyframes/element_animation_style_handler.ts
@@ -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;
+}
diff --git a/packages/animations/browser/src/render/timeline_animation_engine.ts b/packages/animations/browser/src/render/timeline_animation_engine.ts
index d90fc1935a..dad8285e01 100644
--- a/packages/animations/browser/src/render/timeline_animation_engine.ts
+++ b/packages/animations/browser/src/render/timeline_animation_engine.ts
@@ -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 {
diff --git a/packages/animations/browser/src/render/web_animations/web_animations_driver.ts b/packages/animations/browser/src/render/web_animations/web_animations_driver.ts
index 56510a1cca..f4063ddf9a 100644
--- a/packages/animations/browser/src/render/web_animations/web_animations_driver.ts
+++ b/packages/animations/browser/src/render/web_animations/web_animations_driver.ts
@@ -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 = 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(Element).prototype['animate'] === 'function';
+ return typeof getElementAnimateFn() === 'function';
+}
+
+function getElementAnimateFn(): any {
+ return (typeof Element !== 'undefined' && (Element).prototype['animate']) || {};
}
diff --git a/packages/animations/browser/src/render/web_animations/web_animations_player.ts b/packages/animations/browser/src/render/web_animations/web_animations_player.ts
index 56e5a2ea0f..99fb96ee03 100644
--- a/packages/animations/browser/src/render/web_animations/web_animations_player.ts
+++ b/packages/animations/browser/src/render/web_animations/web_animations_player.ts
@@ -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 = options['duration'];
this._delay = 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 (window.getComputedStyle(element))[prop];
-}
diff --git a/packages/animations/browser/src/util.ts b/packages/animations/browser/src/util.ts
index 20fd973191..f396f417ad 100644
--- a/packages/animations/browser/src/util.ts
+++ b/packages/animations/browser/src/util.ts
@@ -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 (window.getComputedStyle(element))[prop];
+}
diff --git a/packages/animations/browser/test/BUILD.bazel b/packages/animations/browser/test/BUILD.bazel
index 4c0dc0eee0..29603a85f6 100644
--- a/packages/animations/browser/test/BUILD.bazel
+++ b/packages/animations/browser/test/BUILD.bazel
@@ -10,5 +10,6 @@ ts_library(
"//packages/animations/browser",
"//packages/animations/browser/testing",
"//packages/core",
+ "//packages/core/testing",
],
)
diff --git a/packages/animations/browser/test/render/css_keyframes/css_keyframes_driver_spec.ts b/packages/animations/browser/test/render/css_keyframes/css_keyframes_driver_spec.ts
new file mode 100644
index 0000000000..00adf6e8ee
--- /dev/null
+++ b/packages/animations/browser/test/render/css_keyframes/css_keyframes_driver_spec.ts
@@ -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