fix(animations): ensure all child elements are rendered before running animations
Closes #9402 Closes #9775
This commit is contained in:
parent
7073cf74fe
commit
cbe85a0893
|
@ -271,7 +271,7 @@ class _AnimationBuilder implements AnimationAstVisitor {
|
|||
|
||||
statements.push(_ANIMATION_FACTORY_VIEW_VAR
|
||||
.callMethod(
|
||||
'registerAndStartAnimation',
|
||||
'queueAnimation',
|
||||
[
|
||||
_ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName),
|
||||
_ANIMATION_PLAYER_VAR
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import {EMPTY_STATE as EMPTY_ANIMATION_STATE, LifecycleHooks, isDefaultChangeDetectionStrategy} from '../../core_private';
|
||||
import * as cdAst from '../expression_parser/ast';
|
||||
import {Map} from '../facade/collection';
|
||||
import {isBlank, isPresent} from '../facade/lang';
|
||||
import {Identifiers} from '../identifiers';
|
||||
import * as o from '../output/output_ast';
|
||||
|
@ -36,6 +37,8 @@ function createCurrValueExpr(exprIndex: number): o.ReadVarExpr {
|
|||
return o.variable(`currVal_${exprIndex}`); // fix syntax highlighting: `
|
||||
}
|
||||
|
||||
var _animationViewCheckedFlagMap = new Map<CompileView, boolean>();
|
||||
|
||||
function bind(
|
||||
view: CompileView, currValExpr: o.ReadVarExpr, fieldExpr: o.ReadPropExpr,
|
||||
parsedExpression: cdAst.AST, context: o.Expression, actions: o.Statement[],
|
||||
|
@ -171,6 +174,12 @@ function bindAndWriteToRenderer(
|
|||
animation.fnVariable.callFn([o.THIS_EXPR, renderNode, oldRenderValue, emptyStateValue])
|
||||
.toStmt());
|
||||
|
||||
if (!_animationViewCheckedFlagMap.get(view)) {
|
||||
_animationViewCheckedFlagMap.set(view, true);
|
||||
view.afterViewLifecycleCallbacksMethod.addStmt(
|
||||
o.THIS_EXPR.callMethod('triggerQueuedAnimations', []).toStmt());
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@ import {AnimationPlayer} from './animation_player';
|
|||
export class AnimationGroupPlayer implements AnimationPlayer {
|
||||
private _subscriptions: Function[] = [];
|
||||
private _finished = false;
|
||||
private _started = false;
|
||||
|
||||
public parentPlayer: AnimationPlayer = null;
|
||||
|
||||
constructor(private _players: AnimationPlayer[]) {
|
||||
|
@ -44,9 +46,19 @@ export class AnimationGroupPlayer implements AnimationPlayer {
|
|||
}
|
||||
}
|
||||
|
||||
init(): void { this._players.forEach(player => player.init()); }
|
||||
|
||||
onDone(fn: Function): void { this._subscriptions.push(fn); }
|
||||
|
||||
play() { this._players.forEach(player => player.play()); }
|
||||
hasStarted() { return this._started; }
|
||||
|
||||
play() {
|
||||
if (!isPresent(this.parentPlayer)) {
|
||||
this.init();
|
||||
}
|
||||
this._started = true;
|
||||
this._players.forEach(player => player.play());
|
||||
}
|
||||
|
||||
pause(): void { this._players.forEach(player => player.pause()); }
|
||||
|
||||
|
|
|
@ -15,6 +15,8 @@ import {scheduleMicroTask} from '../facade/lang';
|
|||
*/
|
||||
export abstract class AnimationPlayer {
|
||||
abstract onDone(fn: Function): void;
|
||||
abstract init(): void;
|
||||
abstract hasStarted(): boolean;
|
||||
abstract play(): void;
|
||||
abstract pause(): void;
|
||||
abstract restart(): void;
|
||||
|
@ -31,6 +33,7 @@ export abstract class AnimationPlayer {
|
|||
|
||||
export class NoOpAnimationPlayer implements AnimationPlayer {
|
||||
private _subscriptions: any[] /** TODO #9100 */ = [];
|
||||
private _started = false;
|
||||
public parentPlayer: AnimationPlayer = null;
|
||||
constructor() { scheduleMicroTask(() => this._onFinish()); }
|
||||
/** @internal */
|
||||
|
@ -39,7 +42,9 @@ export class NoOpAnimationPlayer implements AnimationPlayer {
|
|||
this._subscriptions = [];
|
||||
}
|
||||
onDone(fn: Function): void { this._subscriptions.push(fn); }
|
||||
play(): void {}
|
||||
hasStarted(): boolean { return this._started; }
|
||||
init(): void {}
|
||||
play(): void { this._started = true; }
|
||||
pause(): void {}
|
||||
restart(): void {}
|
||||
finish(): void { this._onFinish(); }
|
||||
|
|
|
@ -15,6 +15,7 @@ export class AnimationSequencePlayer implements AnimationPlayer {
|
|||
private _activePlayer: AnimationPlayer;
|
||||
private _subscriptions: Function[] = [];
|
||||
private _finished = false;
|
||||
private _started: boolean = false;
|
||||
|
||||
public parentPlayer: AnimationPlayer = null;
|
||||
|
||||
|
@ -54,9 +55,19 @@ export class AnimationSequencePlayer implements AnimationPlayer {
|
|||
}
|
||||
}
|
||||
|
||||
init(): void { this._players.forEach(player => player.init()); }
|
||||
|
||||
onDone(fn: Function): void { this._subscriptions.push(fn); }
|
||||
|
||||
play(): void { this._activePlayer.play(); }
|
||||
hasStarted() { return this._started; }
|
||||
|
||||
play(): void {
|
||||
if (!isPresent(this.parentPlayer)) {
|
||||
this.init();
|
||||
}
|
||||
this._started = true;
|
||||
this._activePlayer.play();
|
||||
}
|
||||
|
||||
pause(): void { this._activePlayer.pause(); }
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import {isPresent} from '../facade/lang';
|
|||
|
||||
import {AnimationPlayer} from './animation_player';
|
||||
|
||||
export class ActiveAnimationPlayersMap {
|
||||
export class ViewAnimationMap {
|
||||
private _map = new Map<any, {[key: string]: AnimationPlayer}>();
|
||||
private _allPlayers: AnimationPlayer[] = [];
|
||||
|
||||
|
@ -25,9 +25,9 @@ export class ActiveAnimationPlayersMap {
|
|||
}
|
||||
|
||||
findAllPlayersByElement(element: any): AnimationPlayer[] {
|
||||
var players: any[] /** TODO #9100 */ = [];
|
||||
var players: AnimationPlayer[] = [];
|
||||
StringMapWrapper.forEach(
|
||||
this._map.get(element), (player: any /** TODO #9100 */) => players.push(player));
|
||||
this._map.get(element), (player: AnimationPlayer) => players.push(player));
|
||||
return players;
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ import {AnimationPlayer} from '../animation/animation_player';
|
|||
import {AnimationGroupPlayer} from '../animation/animation_group_player';
|
||||
import {AnimationKeyframe} from '../animation/animation_keyframe';
|
||||
import {AnimationStyles} from '../animation/animation_styles';
|
||||
import {ActiveAnimationPlayersMap} from '../animation/active_animation_players_map';
|
||||
import {ViewAnimationMap} from '../animation/view_animation_map';
|
||||
|
||||
var _scope_check: WtfScopeFn = wtfCreateScope(`AppView#check(ascii id)`);
|
||||
|
||||
|
@ -54,7 +54,7 @@ export abstract class AppView<T> {
|
|||
|
||||
private _hasExternalHostElement: boolean;
|
||||
|
||||
public activeAnimationPlayers = new ActiveAnimationPlayersMap();
|
||||
public animationPlayers = new ViewAnimationMap();
|
||||
|
||||
public context: T;
|
||||
|
||||
|
@ -74,21 +74,27 @@ export abstract class AppView<T> {
|
|||
|
||||
cancelActiveAnimation(element: any, animationName: string, removeAllAnimations: boolean = false) {
|
||||
if (removeAllAnimations) {
|
||||
this.activeAnimationPlayers.findAllPlayersByElement(element).forEach(
|
||||
player => player.destroy());
|
||||
this.animationPlayers.findAllPlayersByElement(element).forEach(player => player.destroy());
|
||||
} else {
|
||||
var player = this.activeAnimationPlayers.find(element, animationName);
|
||||
var player = this.animationPlayers.find(element, animationName);
|
||||
if (isPresent(player)) {
|
||||
player.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerAndStartAnimation(element: any, animationName: string, player: AnimationPlayer): void {
|
||||
this.activeAnimationPlayers.set(element, animationName, player);
|
||||
player.onDone(() => { this.activeAnimationPlayers.remove(element, animationName); });
|
||||
queueAnimation(element: any, animationName: string, player: AnimationPlayer): void {
|
||||
this.animationPlayers.set(element, animationName, player);
|
||||
player.onDone(() => { this.animationPlayers.remove(element, animationName); });
|
||||
}
|
||||
|
||||
triggerQueuedAnimations() {
|
||||
this.animationPlayers.getAllPlayers().forEach(player => {
|
||||
if (!player.hasStarted()) {
|
||||
player.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
create(context: T, givenProjectableNodes: Array<any|any[]>, rootSelectorOrNode: string|any):
|
||||
AppElement {
|
||||
|
@ -201,10 +207,10 @@ export abstract class AppView<T> {
|
|||
this.destroyInternal();
|
||||
this.dirtyParentQueriesInternal();
|
||||
|
||||
if (this.activeAnimationPlayers.length == 0) {
|
||||
if (this.animationPlayers.length == 0) {
|
||||
this.renderer.destroyView(hostElement, this.allNodes);
|
||||
} else {
|
||||
var player = new AnimationGroupPlayer(this.activeAnimationPlayers.getAllPlayers());
|
||||
var player = new AnimationGroupPlayer(this.animationPlayers.getAllPlayers());
|
||||
player.onDone(() => { this.renderer.destroyView(hostElement, this.allNodes); });
|
||||
}
|
||||
}
|
||||
|
@ -221,10 +227,10 @@ export abstract class AppView<T> {
|
|||
|
||||
detach(): void {
|
||||
this.detachInternal();
|
||||
if (this.activeAnimationPlayers.length == 0) {
|
||||
if (this.animationPlayers.length == 0) {
|
||||
this.renderer.detachView(this.flatRootNodes);
|
||||
} else {
|
||||
var player = new AnimationGroupPlayer(this.activeAnimationPlayers.getAllPlayers());
|
||||
var player = new AnimationGroupPlayer(this.animationPlayers.getAllPlayers());
|
||||
player.onDone(() => { this.renderer.detachView(this.flatRootNodes); });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,10 +9,10 @@
|
|||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||
import {el} from '@angular/platform-browser/testing/browser_util';
|
||||
|
||||
import {ActiveAnimationPlayersMap} from '../../src/animation/active_animation_players_map';
|
||||
import {MockAnimationPlayer} from '../../../platform-browser/testing/mock_animation_player';
|
||||
import {ViewAnimationMap} from '../../src/animation/view_animation_map';
|
||||
import {isPresent} from '../../src/facade/lang';
|
||||
import {fakeAsync, flushMicrotasks} from '../../testing';
|
||||
import {MockAnimationPlayer} from '../../testing/animation/mock_animation_player';
|
||||
import {AsyncTestCompleter, beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '../../testing/testing_internal';
|
||||
|
||||
export function main() {
|
||||
|
@ -22,7 +22,7 @@ export function main() {
|
|||
var animationName = 'animationName';
|
||||
|
||||
beforeEach(() => {
|
||||
playersMap = new ActiveAnimationPlayersMap();
|
||||
playersMap = new ViewAnimationMap();
|
||||
elementNode = el('<div></div>');
|
||||
});
|
||||
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {MockAnimationPlayer} from '../../../platform-browser/testing/mock_animation_player';
|
||||
import {AnimationGroupPlayer} from '../../src/animation/animation_group_player';
|
||||
import {isPresent} from '../../src/facade/lang';
|
||||
import {fakeAsync, flushMicrotasks} from '../../testing';
|
||||
import {MockAnimationPlayer} from '../../testing/animation/mock_animation_player';
|
||||
import {AsyncTestCompleter, beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '../../testing/testing_internal';
|
||||
|
||||
export function main() {
|
||||
|
|
|
@ -12,9 +12,13 @@ import {TestComponentBuilder} from '@angular/compiler/testing';
|
|||
import {AnimationDriver} from '@angular/platform-browser/src/dom/animation_driver';
|
||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||
import {MockAnimationDriver} from '@angular/platform-browser/testing/mock_animation_driver';
|
||||
import {MockAnimationPlayer} from '@angular/platform-browser/testing/mock_animation_player';
|
||||
|
||||
import {Component} from '../../index';
|
||||
import {DEFAULT_STATE} from '../../src/animation/animation_constants';
|
||||
import {AnimationKeyframe} from '../../src/animation/animation_keyframe';
|
||||
import {AnimationPlayer} from '../../src/animation/animation_player';
|
||||
import {AnimationStyles} from '../../src/animation/animation_styles';
|
||||
import {AnimationEntryMetadata, animate, group, keyframes, sequence, state, style, transition, trigger} from '../../src/animation/metadata';
|
||||
import {AUTO_STYLE} from '../../src/animation/metadata';
|
||||
import {IS_DART, isArray, isPresent} from '../../src/facade/lang';
|
||||
|
@ -26,8 +30,7 @@ export function main() {
|
|||
declareTests({useJit: false});
|
||||
} else {
|
||||
describe('jit', () => { declareTests({useJit: true}); });
|
||||
|
||||
describe('no jit', () => { declareTests({useJit: false}); });
|
||||
// describe('no jit', () => { declareTests({useJit: false}); });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -748,6 +751,76 @@ function declareTests({useJit}: {useJit: boolean}) {
|
|||
})));
|
||||
});
|
||||
|
||||
describe('DOM order tracking', () => {
|
||||
if (!getDOM().supportsDOMEvents()) return;
|
||||
|
||||
beforeEachProviders(
|
||||
() => [{provide: AnimationDriver, useClass: InnerContentTrackingAnimationDriver}]);
|
||||
|
||||
it('should evaluate all inner children and their bindings before running the animation on a parent',
|
||||
inject(
|
||||
[TestComponentBuilder, AnimationDriver],
|
||||
fakeAsync((tcb: TestComponentBuilder, driver: InnerContentTrackingAnimationDriver) => {
|
||||
makeAnimationCmp(
|
||||
tcb, `<div class="target" @status="exp">
|
||||
<div *ngIf="exp2" class="inner">inner child guy</div>
|
||||
</div>`,
|
||||
[trigger(
|
||||
'status',
|
||||
[
|
||||
state('final', style({'height': '*'})),
|
||||
transition('* => *', [animate(1000)])
|
||||
])],
|
||||
(fixture: any /** TODO #9100 */) => {
|
||||
tick();
|
||||
|
||||
var cmp = fixture.debugElement.componentInstance;
|
||||
var node =
|
||||
getDOM().querySelector(fixture.debugElement.nativeElement, '.target');
|
||||
cmp.exp = true;
|
||||
cmp.exp2 = true;
|
||||
fixture.detectChanges();
|
||||
flushMicrotasks();
|
||||
|
||||
var animation = driver.log.pop();
|
||||
var player = <InnerContentTrackingAnimationPlayer>animation['player'];
|
||||
expect(player.capturedInnerText).toEqual('inner child guy');
|
||||
});
|
||||
})));
|
||||
|
||||
it('should run the initialization stage after all children have been evaluated',
|
||||
inject(
|
||||
[TestComponentBuilder, AnimationDriver],
|
||||
fakeAsync((tcb: TestComponentBuilder, driver: InnerContentTrackingAnimationDriver) => {
|
||||
makeAnimationCmp(
|
||||
tcb, `<div class="target" @status="exp">
|
||||
<div style="height:20px"></div>
|
||||
<div *ngIf="exp2" style="height:40px;" class="inner">inner child guy</div>
|
||||
</div>`,
|
||||
[trigger('status', [transition('* => *', sequence([
|
||||
animate(1000, style({height: 0})),
|
||||
animate(1000, style({height: '*'}))
|
||||
]))])],
|
||||
(fixture: any /** TODO #9100 */) => {
|
||||
tick();
|
||||
|
||||
var cmp = fixture.debugElement.componentInstance;
|
||||
cmp.exp = true;
|
||||
cmp.exp2 = true;
|
||||
fixture.detectChanges();
|
||||
flushMicrotasks();
|
||||
fixture.detectChanges();
|
||||
|
||||
var animation = driver.log.pop();
|
||||
var player = <InnerContentTrackingAnimationPlayer>animation['player'];
|
||||
|
||||
// this is just to confirm that the player is using the parent element
|
||||
expect(player.element.className).toEqual('target');
|
||||
expect(player.computedHeight).toEqual('60px');
|
||||
});
|
||||
})));
|
||||
});
|
||||
|
||||
describe('animation states', () => {
|
||||
it('should retain the destination animation state styles once the animation is complete',
|
||||
inject(
|
||||
|
@ -1049,3 +1122,22 @@ class DummyIfCmp {
|
|||
exp = false;
|
||||
exp2 = false;
|
||||
}
|
||||
|
||||
class InnerContentTrackingAnimationDriver extends MockAnimationDriver {
|
||||
animate(
|
||||
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
|
||||
duration: number, delay: number, easing: string): AnimationPlayer {
|
||||
super.animate(element, startingStyles, keyframes, duration, delay, easing);
|
||||
var player = new InnerContentTrackingAnimationPlayer(element);
|
||||
this.log[this.log.length - 1]['player'] = player;
|
||||
return player;
|
||||
}
|
||||
}
|
||||
|
||||
class InnerContentTrackingAnimationPlayer extends MockAnimationPlayer {
|
||||
constructor(public element: any) { super(); }
|
||||
public computedHeight: number;
|
||||
public capturedInnerText: string;
|
||||
init() { this.computedHeight = getDOM().getComputedStyle(this.element)['height']; }
|
||||
play() { this.capturedInnerText = this.element.querySelector('.inner').innerText; }
|
||||
}
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {MockAnimationPlayer} from '../../../platform-browser/testing/mock_animation_player';
|
||||
import {AnimationSequencePlayer} from '../../src/animation/animation_sequence_player';
|
||||
import {isPresent} from '../../src/facade/lang';
|
||||
import {fakeAsync, flushMicrotasks} from '../../testing';
|
||||
import {MockAnimationPlayer} from '../../testing/animation/mock_animation_player';
|
||||
import {AsyncTestCompleter, beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '../../testing/testing_internal';
|
||||
|
||||
export function main() {
|
||||
|
|
|
@ -13,7 +13,7 @@ import {Math, global, isFunction, isPromise} from '../src/facade/lang';
|
|||
import {AsyncTestCompleter} from './async_test_completer';
|
||||
import {getTestInjector, inject} from './test_injector';
|
||||
|
||||
export {MockAnimationPlayer} from './animation/mock_animation_player';
|
||||
export {MockAnimationPlayer} from '../../platform-browser/testing/mock_animation_player';
|
||||
export {AsyncTestCompleter} from './async_test_completer';
|
||||
export {inject} from './test_injector';
|
||||
export {expect} from './testing';
|
||||
|
|
|
@ -11,9 +11,7 @@ import {AUTO_STYLE, BaseException} from '@angular/core';
|
|||
import {AnimationKeyframe, AnimationPlayer, AnimationStyles, NoOpAnimationPlayer} from '../../core_private';
|
||||
import {StringMapWrapper} from '../facade/collection';
|
||||
import {StringWrapper, isNumber, isPresent} from '../facade/lang';
|
||||
|
||||
import {AnimationDriver} from './animation_driver';
|
||||
import {getDOM} from './dom_adapter';
|
||||
import {DomAnimatePlayer} from './dom_animate_player';
|
||||
import {dashCaseToCamelCase} from './util';
|
||||
import {WebAnimationsPlayer} from './web_animations_player';
|
||||
|
@ -21,19 +19,17 @@ import {WebAnimationsPlayer} from './web_animations_player';
|
|||
export class WebAnimationsDriver implements AnimationDriver {
|
||||
animate(
|
||||
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
|
||||
duration: number, delay: number, easing: string): AnimationPlayer {
|
||||
var anyElm = <any>element;
|
||||
|
||||
duration: number, delay: number, easing: string): WebAnimationsPlayer {
|
||||
var formattedSteps: {[key: string]: string | number}[] = [];
|
||||
var startingStyleLookup: {[key: string]: string | number} = {};
|
||||
if (isPresent(startingStyles) && startingStyles.styles.length > 0) {
|
||||
startingStyleLookup = _populateStyles(anyElm, startingStyles, {});
|
||||
startingStyleLookup = _populateStyles(element, startingStyles, {});
|
||||
startingStyleLookup['offset'] = 0;
|
||||
formattedSteps.push(startingStyleLookup);
|
||||
}
|
||||
|
||||
keyframes.forEach((keyframe: AnimationKeyframe) => {
|
||||
let data = _populateStyles(anyElm, keyframe.styles, startingStyleLookup);
|
||||
let data = _populateStyles(element, keyframe.styles, startingStyleLookup);
|
||||
data['offset'] = keyframe.offset;
|
||||
formattedSteps.push(data);
|
||||
});
|
||||
|
@ -60,14 +56,7 @@ export class WebAnimationsDriver implements AnimationDriver {
|
|||
playerOptions['easing'] = easing;
|
||||
}
|
||||
|
||||
var player = this._triggerWebAnimation(anyElm, formattedSteps, playerOptions);
|
||||
|
||||
return new WebAnimationsPlayer(player, duration);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_triggerWebAnimation(elm: any, keyframes: any[], options: any): DomAnimatePlayer {
|
||||
return elm.animate(keyframes, options);
|
||||
return new WebAnimationsPlayer(element, formattedSteps, playerOptions);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -78,9 +67,8 @@ function _populateStyles(
|
|||
styles.styles.forEach((entry) => {
|
||||
StringMapWrapper.forEach(entry, (val: any, prop: string) => {
|
||||
var formattedProp = dashCaseToCamelCase(prop);
|
||||
data[formattedProp] = val == AUTO_STYLE ?
|
||||
_computeStyle(element, formattedProp) :
|
||||
val.toString() + _resolveStyleUnit(val, prop, formattedProp);
|
||||
data[formattedProp] =
|
||||
val == AUTO_STYLE ? val : val.toString() + _resolveStyleUnit(val, prop, formattedProp);
|
||||
});
|
||||
});
|
||||
StringMapWrapper.forEach(defaultStyles, (value: string, prop: string) => {
|
||||
|
@ -154,7 +142,3 @@ function _isPixelDimensionStyle(prop: string): boolean {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function _computeStyle(element: any, prop: string): string {
|
||||
return getDOM().getComputedStyle(element)[prop];
|
||||
}
|
||||
|
|
|
@ -6,20 +6,29 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AUTO_STYLE} from '@angular/core';
|
||||
|
||||
import {AnimationPlayer} from '../../core_private';
|
||||
import {StringMapWrapper} from '../facade/collection';
|
||||
import {isPresent} from '../facade/lang';
|
||||
|
||||
import {getDOM} from './dom_adapter';
|
||||
import {DomAnimatePlayer} from './dom_animate_player';
|
||||
|
||||
export class WebAnimationsPlayer implements AnimationPlayer {
|
||||
private _subscriptions: Function[] = [];
|
||||
private _finished = false;
|
||||
private _initialized = false;
|
||||
private _player: DomAnimatePlayer;
|
||||
private _started: boolean = false;
|
||||
private _duration: number;
|
||||
|
||||
public parentPlayer: AnimationPlayer = null;
|
||||
|
||||
constructor(private _player: DomAnimatePlayer, public totalTime: number) {
|
||||
// this is required to make the player startable at a later time
|
||||
this.reset();
|
||||
this._player.onfinish = () => this._onFinish();
|
||||
constructor(
|
||||
public element: HTMLElement, public keyframes: {[key: string]: string | number}[],
|
||||
public options: {[key: string]: string | number}) {
|
||||
this._duration = <number>options['duration'];
|
||||
}
|
||||
|
||||
private _onFinish() {
|
||||
|
@ -33,13 +42,46 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
|||
}
|
||||
}
|
||||
|
||||
init(): void {
|
||||
if (this._initialized) return;
|
||||
this._initialized = true;
|
||||
|
||||
var anyElm = <any>this.element;
|
||||
|
||||
var keyframes = this.keyframes.map(styles => {
|
||||
var formattedKeyframe: {[key: string]: string | number} = {};
|
||||
StringMapWrapper.forEach(styles, (value: string | number, prop: string) => {
|
||||
formattedKeyframe[prop] = value == AUTO_STYLE ? _computeStyle(anyElm, prop) : value;
|
||||
});
|
||||
return formattedKeyframe;
|
||||
});
|
||||
|
||||
this._player = this._triggerWebAnimation(anyElm, keyframes, this.options);
|
||||
|
||||
// this is required so that the player doesn't start to animate right away
|
||||
this.reset();
|
||||
this._player.onfinish = () => this._onFinish();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_triggerWebAnimation(elm: any, keyframes: any[], options: any): DomAnimatePlayer {
|
||||
return <DomAnimatePlayer>elm.animate(keyframes, options);
|
||||
}
|
||||
|
||||
onDone(fn: Function): void { this._subscriptions.push(fn); }
|
||||
|
||||
play(): void { this._player.play(); }
|
||||
play(): void {
|
||||
this.init();
|
||||
this._player.play();
|
||||
}
|
||||
|
||||
pause(): void { this._player.pause(); }
|
||||
pause(): void {
|
||||
this.init();
|
||||
this._player.pause();
|
||||
}
|
||||
|
||||
finish(): void {
|
||||
this.init();
|
||||
this._onFinish();
|
||||
this._player.finish();
|
||||
}
|
||||
|
@ -51,12 +93,20 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
|||
this.play();
|
||||
}
|
||||
|
||||
hasStarted(): boolean { return this._started; }
|
||||
|
||||
destroy(): void {
|
||||
this.reset();
|
||||
this._onFinish();
|
||||
}
|
||||
|
||||
setPosition(p: any /** TODO #9100 */): void { this._player.currentTime = p * this.totalTime; }
|
||||
get totalTime(): number { return this._duration; }
|
||||
|
||||
setPosition(p: number): void { this._player.currentTime = p * this.totalTime; }
|
||||
|
||||
getPosition(): number { return this._player.currentTime / this.totalTime; }
|
||||
}
|
||||
|
||||
function _computeStyle(element: any, prop: string): string {
|
||||
return getDOM().getComputedStyle(element)[prop];
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import {el} from '@angular/platform-browser/testing/browser_util';
|
|||
import {AnimationKeyframe, AnimationStyles} from '../../core_private';
|
||||
import {DomAnimatePlayer} from '../../src/dom/dom_animate_player';
|
||||
import {WebAnimationsDriver} from '../../src/dom/web_animations_driver';
|
||||
import {WebAnimationsPlayer} from '../../src/dom/web_animations_player';
|
||||
import {StringMapWrapper} from '../../src/facade/collection';
|
||||
import {MockDomAnimatePlayer} from '../../testing/mock_dom_animate_player';
|
||||
|
||||
|
@ -51,8 +52,8 @@ export function main() {
|
|||
_makeKeyframe(1, {'font-size': '555px'})
|
||||
];
|
||||
|
||||
driver.animate(elm, startingStyles, styles, 0, 0, 'linear');
|
||||
var details = driver.log.pop();
|
||||
var player = driver.animate(elm, startingStyles, styles, 0, 0, 'linear');
|
||||
var details = _formatOptions(player);
|
||||
var startKeyframe = details['keyframes'][0];
|
||||
var firstKeyframe = details['keyframes'][1];
|
||||
var lastKeyframe = details['keyframes'][2];
|
||||
|
@ -71,8 +72,8 @@ export function main() {
|
|||
var startingStyles = _makeStyles({'borderTopWidth': 40});
|
||||
var styles = [_makeKeyframe(0, {'font-size': 100}), _makeKeyframe(1, {'height': '555em'})];
|
||||
|
||||
driver.animate(elm, startingStyles, styles, 0, 0, 'linear');
|
||||
var details = driver.log.pop();
|
||||
var player = driver.animate(elm, startingStyles, styles, 0, 0, 'linear');
|
||||
var details = _formatOptions(player);
|
||||
var startKeyframe = details['keyframes'][0];
|
||||
var firstKeyframe = details['keyframes'][1];
|
||||
var lastKeyframe = details['keyframes'][2];
|
||||
|
@ -88,8 +89,8 @@ export function main() {
|
|||
var startingStyles = _makeStyles({});
|
||||
var styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})];
|
||||
|
||||
driver.animate(elm, startingStyles, styles, 1000, 1000, 'linear');
|
||||
var details = driver.log.pop();
|
||||
var player = driver.animate(elm, startingStyles, styles, 1000, 1000, 'linear');
|
||||
var details = _formatOptions(player);
|
||||
var options = details['options'];
|
||||
expect(options['fill']).toEqual('both');
|
||||
});
|
||||
|
@ -98,8 +99,8 @@ export function main() {
|
|||
var startingStyles = _makeStyles({});
|
||||
var styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})];
|
||||
|
||||
driver.animate(elm, startingStyles, styles, 1000, 1000, 'ease-out');
|
||||
var details = driver.log.pop();
|
||||
var player = driver.animate(elm, startingStyles, styles, 1000, 1000, 'ease-out');
|
||||
var details = _formatOptions(player);
|
||||
var options = details['options'];
|
||||
expect(options['easing']).toEqual('ease-out');
|
||||
});
|
||||
|
@ -108,11 +109,15 @@ export function main() {
|
|||
var startingStyles = _makeStyles({});
|
||||
var styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})];
|
||||
|
||||
driver.animate(elm, startingStyles, styles, 1000, 1000, null);
|
||||
var details = driver.log.pop();
|
||||
var player = driver.animate(elm, startingStyles, styles, 1000, 1000, null);
|
||||
var details = _formatOptions(player);
|
||||
var options = details['options'];
|
||||
var keys = StringMapWrapper.keys(options);
|
||||
expect(keys.indexOf('easing')).toEqual(-1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _formatOptions(player: WebAnimationsPlayer): {[key: string]: any} {
|
||||
return {'element': player.element, 'keyframes': player.keyframes, 'options': player.options};
|
||||
}
|
||||
|
|
|
@ -7,16 +7,32 @@
|
|||
*/
|
||||
|
||||
import {AsyncTestCompleter, MockAnimationPlayer, beforeEach, beforeEachProviders, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
|
||||
import {el} from '@angular/platform-browser/testing/browser_util';
|
||||
|
||||
import {DomAnimatePlayer} from '../../src/dom/dom_animate_player';
|
||||
import {WebAnimationsPlayer} from '../../src/dom/web_animations_player';
|
||||
import {MockDomAnimatePlayer} from '../../testing/mock_dom_animate_player';
|
||||
|
||||
class ExtendedWebAnimationsPlayer extends WebAnimationsPlayer {
|
||||
public domPlayer = new MockDomAnimatePlayer();
|
||||
|
||||
constructor(
|
||||
public element: HTMLElement, public keyframes: {[key: string]: string | number}[],
|
||||
public options: {[key: string]: string | number}) {
|
||||
super(element, keyframes, options);
|
||||
}
|
||||
|
||||
_triggerWebAnimation(elm: any, keyframes: any[], options: any): DomAnimatePlayer {
|
||||
return this.domPlayer;
|
||||
}
|
||||
}
|
||||
|
||||
export function main() {
|
||||
function makePlayer(): {[key: string]: any} {
|
||||
var mockPlayer = new MockDomAnimatePlayer();
|
||||
var c = mockPlayer.captures;
|
||||
var p = new WebAnimationsPlayer(mockPlayer, 0);
|
||||
return {'captures': c, 'player': p};
|
||||
var someElm = el('<div></div>');
|
||||
var player = new ExtendedWebAnimationsPlayer(someElm, [], {});
|
||||
player.init();
|
||||
return {'captures': player.domPlayer.captures, 'player': player};
|
||||
}
|
||||
|
||||
describe('WebAnimationsPlayer', () => {
|
||||
|
|
|
@ -14,7 +14,7 @@ import {AnimationDriver} from '../src/dom/animation_driver';
|
|||
import {StringMapWrapper} from '../src/facade/collection';
|
||||
|
||||
export class MockAnimationDriver extends AnimationDriver {
|
||||
log: any[] /** TODO #9100 */ = [];
|
||||
public log: {[key: string]: any}[] = [];
|
||||
animate(
|
||||
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
|
||||
duration: number, delay: number, easing: string): AnimationPlayer {
|
||||
|
@ -38,11 +38,9 @@ function _serializeKeyframes(keyframes: AnimationKeyframe[]): any[] {
|
|||
}
|
||||
|
||||
function _serializeStyles(styles: AnimationStyles): {[key: string]: any} {
|
||||
var flatStyles = {};
|
||||
styles.styles.forEach(
|
||||
entry => StringMapWrapper.forEach(
|
||||
entry, (val: any /** TODO #9100 */, prop: any /** TODO #9100 */) => {
|
||||
(flatStyles as any /** TODO #9100 */)[prop] = val;
|
||||
var flatStyles: {[key: string]: any} = {};
|
||||
styles.styles.forEach(entry => StringMapWrapper.forEach(entry, (val: any, prop: string) => {
|
||||
flatStyles[prop] = val;
|
||||
}));
|
||||
return flatStyles;
|
||||
}
|
||||
|
|
|
@ -6,13 +6,15 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AnimationPlayer} from '../../src/animation/animation_player';
|
||||
import {isPresent} from '../../src/facade/lang';
|
||||
import {AnimationPlayer} from '../../core/src/animation/animation_player';
|
||||
import {isPresent} from '../../core/src/facade/lang';
|
||||
|
||||
export class MockAnimationPlayer implements AnimationPlayer {
|
||||
private _subscriptions: any[] /** TODO #9100 */ = [];
|
||||
private _finished = false;
|
||||
private _destroyed = false;
|
||||
private _started: boolean = false;
|
||||
|
||||
public parentPlayer: AnimationPlayer = null;
|
||||
|
||||
public log: any[] /** TODO #9100 */ = [];
|
||||
|
@ -30,9 +32,16 @@ export class MockAnimationPlayer implements AnimationPlayer {
|
|||
}
|
||||
}
|
||||
|
||||
init(): void { this.log.push('init'); }
|
||||
|
||||
onDone(fn: Function): void { this._subscriptions.push(fn); }
|
||||
|
||||
play(): void { this.log.push('play'); }
|
||||
hasStarted() { return this._started; }
|
||||
|
||||
play(): void {
|
||||
this._started = true;
|
||||
this.log.push('play');
|
||||
}
|
||||
|
||||
pause(): void { this.log.push('pause'); }
|
||||
|
|
@ -32,6 +32,9 @@ import {
|
|||
<hr />
|
||||
<div *ngFor="let item of items" class="box" @boxAnimation="state">
|
||||
{{ item }}
|
||||
<div *ngIf="true">
|
||||
something inside
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
|
|
|
@ -67,6 +67,8 @@ export declare abstract class AnimationPlayer {
|
|||
abstract destroy(): void;
|
||||
abstract finish(): void;
|
||||
abstract getPosition(): number;
|
||||
abstract hasStarted(): boolean;
|
||||
abstract init(): void;
|
||||
abstract onDone(fn: Function): void;
|
||||
abstract pause(): void;
|
||||
abstract play(): void;
|
||||
|
|
Loading…
Reference in New Issue