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