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:

committed by
Victor Berchet

parent
9eecb0b27f
commit
b2f366b3b7
@ -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];
|
||||
}
|
||||
|
Reference in New Issue
Block a user