feat(animations): introduce a wave of new animation features
This commit is contained in:
parent
d761059e4d
commit
16c8167886
|
@ -8,15 +8,19 @@
|
|||
import {AnimationPlayer, AnimationTriggerMetadata} from '@angular/animations';
|
||||
|
||||
export abstract class AnimationEngine {
|
||||
abstract registerTrigger(trigger: AnimationTriggerMetadata, name?: string): void;
|
||||
abstract onInsert(element: any, domFn: () => any): void;
|
||||
abstract onRemove(element: any, domFn: () => any): void;
|
||||
abstract setProperty(element: any, property: string, value: any): void;
|
||||
abstract registerTrigger(
|
||||
componentId: string, namespaceId: string, hostElement: any, name: string,
|
||||
metadata: AnimationTriggerMetadata): void;
|
||||
abstract onInsert(namespaceId: string, element: any, parent: any, insertBefore: boolean): void;
|
||||
abstract onRemove(namespaceId: string, element: any, context: any): void;
|
||||
abstract setProperty(namespaceId: string, element: any, property: string, value: any): void;
|
||||
abstract listen(
|
||||
element: any, eventName: string, eventPhase: string,
|
||||
namespaceId: string, element: any, eventName: string, eventPhase: string,
|
||||
callback: (event: any) => any): () => any;
|
||||
abstract flush(): void;
|
||||
abstract destroy(namespaceId: string, context: any): void;
|
||||
|
||||
get activePlayers(): AnimationPlayer[] { throw new Error('...'); }
|
||||
get queuedPlayers(): AnimationPlayer[] { throw new Error('...'); }
|
||||
onRemovalComplete: (delegate: any, element: any) => void;
|
||||
|
||||
public players: AnimationPlayer[];
|
||||
}
|
||||
|
|
|
@ -5,23 +5,19 @@
|
|||
* 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 {AnimationMetadata, AnimationPlayer, AnimationStyleMetadata, sequence, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {AnimationDriver} from '../render/animation_driver';
|
||||
import {DomAnimationEngine} from '../render/dom_animation_engine';
|
||||
import {AnimationMetadata, AnimationOptions, ɵStyleData} from '@angular/animations';
|
||||
import {normalizeStyles} from '../util';
|
||||
|
||||
import {Ast} from './animation_ast';
|
||||
import {buildAnimationAst} from './animation_ast_builder';
|
||||
import {buildAnimationTimelines} from './animation_timeline_builder';
|
||||
import {AnimationTimelineInstruction} from './animation_timeline_instruction';
|
||||
import {buildAnimationKeyframes} from './animation_timeline_visitor';
|
||||
import {validateAnimationSequence} from './animation_validator_visitor';
|
||||
import {AnimationStyleNormalizer} from './style_normalization/animation_style_normalizer';
|
||||
import {ElementInstructionMap} from './element_instruction_map';
|
||||
|
||||
export class Animation {
|
||||
private _animationAst: AnimationMetadata;
|
||||
private _animationAst: Ast;
|
||||
constructor(input: AnimationMetadata|AnimationMetadata[]) {
|
||||
const ast =
|
||||
Array.isArray(input) ? sequence(<AnimationMetadata[]>input) : <AnimationMetadata>input;
|
||||
const errors = validateAnimationSequence(ast);
|
||||
const errors: any[] = [];
|
||||
const ast = buildAnimationAst(input, errors);
|
||||
if (errors.length) {
|
||||
const errorMessage = `animation validation failed:\n${errors.join("\n")}`;
|
||||
throw new Error(errorMessage);
|
||||
|
@ -30,26 +26,21 @@ export class Animation {
|
|||
}
|
||||
|
||||
buildTimelines(
|
||||
startingStyles: ɵStyleData|ɵStyleData[],
|
||||
destinationStyles: ɵStyleData|ɵStyleData[]): AnimationTimelineInstruction[] {
|
||||
element: any, startingStyles: ɵStyleData|ɵStyleData[],
|
||||
destinationStyles: ɵStyleData|ɵStyleData[], options: AnimationOptions,
|
||||
subInstructions?: ElementInstructionMap): AnimationTimelineInstruction[] {
|
||||
const start = Array.isArray(startingStyles) ? normalizeStyles(startingStyles) :
|
||||
<ɵStyleData>startingStyles;
|
||||
const dest = Array.isArray(destinationStyles) ? normalizeStyles(destinationStyles) :
|
||||
<ɵStyleData>destinationStyles;
|
||||
return buildAnimationKeyframes(this._animationAst, start, dest);
|
||||
}
|
||||
|
||||
// this is only used for development demo purposes for now
|
||||
private create(
|
||||
injector: any, element: any, startingStyles: ɵStyleData = {},
|
||||
destinationStyles: ɵStyleData = {}): AnimationPlayer {
|
||||
const instructions = this.buildTimelines(startingStyles, destinationStyles);
|
||||
|
||||
// note the code below is only here to make the tests happy (once the new renderer is
|
||||
// within core then the code below will interact with Renderer.transition(...))
|
||||
const driver: AnimationDriver = injector.get(AnimationDriver);
|
||||
const normalizer: AnimationStyleNormalizer = injector.get(AnimationStyleNormalizer);
|
||||
const engine = new DomAnimationEngine(driver, normalizer);
|
||||
return engine.animateTimeline(element, instructions);
|
||||
const errors: any = [];
|
||||
subInstructions = subInstructions || new ElementInstructionMap();
|
||||
const result = buildAnimationTimelines(
|
||||
element, this._animationAst, start, dest, options, subInstructions, errors);
|
||||
if (errors.length) {
|
||||
const errorMessage = `animation building failed:\n${errors.join("\n")}`;
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,156 @@
|
|||
/**
|
||||
* @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
|
||||
/**
|
||||
* @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 {AnimateTimings, AnimationOptions, ɵStyleData} from '@angular/animations';
|
||||
|
||||
const EMPTY_ANIMATION_OPTIONS: AnimationOptions = {};
|
||||
|
||||
export interface AstVisitor {
|
||||
visitTrigger(ast: TriggerAst, context: any): any;
|
||||
visitState(ast: StateAst, context: any): any;
|
||||
visitTransition(ast: TransitionAst, context: any): any;
|
||||
visitSequence(ast: SequenceAst, context: any): any;
|
||||
visitGroup(ast: GroupAst, context: any): any;
|
||||
visitAnimate(ast: AnimateAst, context: any): any;
|
||||
visitStyle(ast: StyleAst, context: any): any;
|
||||
visitKeyframes(ast: KeyframesAst, context: any): any;
|
||||
visitReference(ast: ReferenceAst, context: any): any;
|
||||
visitAnimateChild(ast: AnimateChildAst, context: any): any;
|
||||
visitAnimateRef(ast: AnimateRefAst, context: any): any;
|
||||
visitQuery(ast: QueryAst, context: any): any;
|
||||
visitStagger(ast: StaggerAst, context: any): any;
|
||||
visitTiming(ast: TimingAst, context: any): any;
|
||||
}
|
||||
|
||||
export abstract class Ast {
|
||||
abstract visit(ast: AstVisitor, context: any): any;
|
||||
public options: AnimationOptions = EMPTY_ANIMATION_OPTIONS;
|
||||
|
||||
get params(): {[name: string]: any}|null { return this.options['params'] || null; }
|
||||
}
|
||||
|
||||
export class TriggerAst extends Ast {
|
||||
public queryCount: number = 0;
|
||||
public depCount: number = 0;
|
||||
|
||||
constructor(public name: string, public states: StateAst[], public transitions: TransitionAst[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitTrigger(this, context); }
|
||||
}
|
||||
|
||||
export class StateAst extends Ast {
|
||||
constructor(public name: string, public style: StyleAst) { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitState(this, context); }
|
||||
}
|
||||
|
||||
export class TransitionAst extends Ast {
|
||||
public queryCount: number = 0;
|
||||
public depCount: number = 0;
|
||||
|
||||
constructor(
|
||||
public matchers: ((fromState: string, toState: string) => boolean)[], public animation: Ast) {
|
||||
super();
|
||||
}
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitTransition(this, context); }
|
||||
}
|
||||
|
||||
export class SequenceAst extends Ast {
|
||||
constructor(public steps: Ast[]) { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitSequence(this, context); }
|
||||
}
|
||||
|
||||
export class GroupAst extends Ast {
|
||||
constructor(public steps: Ast[]) { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitGroup(this, context); }
|
||||
}
|
||||
|
||||
export class AnimateAst extends Ast {
|
||||
constructor(public timings: TimingAst, public style: StyleAst|KeyframesAst) { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitAnimate(this, context); }
|
||||
}
|
||||
|
||||
export class StyleAst extends Ast {
|
||||
public isEmptyStep = false;
|
||||
|
||||
constructor(
|
||||
public styles: (ɵStyleData|string)[], public easing: string|null,
|
||||
public offset: number|null) {
|
||||
super();
|
||||
}
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitStyle(this, context); }
|
||||
}
|
||||
|
||||
export class KeyframesAst extends Ast {
|
||||
constructor(public styles: StyleAst[]) { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitKeyframes(this, context); }
|
||||
}
|
||||
|
||||
export class ReferenceAst extends Ast {
|
||||
constructor(public animation: Ast) { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitReference(this, context); }
|
||||
}
|
||||
|
||||
export class AnimateChildAst extends Ast {
|
||||
constructor() { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitAnimateChild(this, context); }
|
||||
}
|
||||
|
||||
export class AnimateRefAst extends Ast {
|
||||
constructor(public animation: ReferenceAst) { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitAnimateRef(this, context); }
|
||||
}
|
||||
|
||||
export class QueryAst extends Ast {
|
||||
public originalSelector: string;
|
||||
|
||||
constructor(
|
||||
public selector: string, public limit: number, public optional: boolean,
|
||||
public includeSelf: boolean, public animation: Ast) {
|
||||
super();
|
||||
}
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitQuery(this, context); }
|
||||
}
|
||||
|
||||
export class StaggerAst extends Ast {
|
||||
constructor(public timings: AnimateTimings, public animation: Ast) { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitStagger(this, context); }
|
||||
}
|
||||
|
||||
export class TimingAst extends Ast {
|
||||
constructor(
|
||||
public duration: number, public delay: number = 0, public easing: string|null = null) {
|
||||
super();
|
||||
}
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitTiming(this, context); }
|
||||
}
|
||||
|
||||
export class DynamicTimingAst extends TimingAst {
|
||||
constructor(public value: string) { super(0, 0, ''); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitTiming(this, context); }
|
||||
}
|
|
@ -0,0 +1,475 @@
|
|||
/**
|
||||
* @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 {AUTO_STYLE, AnimateTimings, AnimationAnimateChildMetadata, AnimationAnimateMetadata, AnimationAnimateRefMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationOptions, AnimationQueryMetadata, AnimationQueryOptions, AnimationReferenceMetadata, AnimationSequenceMetadata, AnimationStaggerMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, AnimationTriggerMetadata, style, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {getOrSetAsInMap} from '../render/shared';
|
||||
import {ENTER_SELECTOR, LEAVE_SELECTOR, NG_ANIMATING_SELECTOR, NG_TRIGGER_SELECTOR, copyObj, normalizeAnimationEntry, resolveTiming, validateStyleParams} from '../util';
|
||||
|
||||
import {AnimateAst, AnimateChildAst, AnimateRefAst, Ast, DynamicTimingAst, GroupAst, KeyframesAst, QueryAst, ReferenceAst, SequenceAst, StaggerAst, StateAst, StyleAst, TimingAst, TransitionAst, TriggerAst} from './animation_ast';
|
||||
import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor';
|
||||
import {parseTransitionExpr} from './animation_transition_expr';
|
||||
|
||||
const SELF_TOKEN = ':self';
|
||||
const SELF_TOKEN_REGEX = new RegExp(`\s*${SELF_TOKEN}\s*,?`, 'g');
|
||||
|
||||
/*
|
||||
* [Validation]
|
||||
* The visitor code below will traverse the animation AST generated by the animation verb functions
|
||||
* (the output is a tree of objects) and attempt to perform a series of validations on the data. The
|
||||
* following corner-cases will be validated:
|
||||
*
|
||||
* 1. Overlap of animations
|
||||
* Given that a CSS property cannot be animated in more than one place at the same time, it's
|
||||
* important that this behaviour is detected and validated. The way in which this occurs is that
|
||||
* each time a style property is examined, a string-map containing the property will be updated with
|
||||
* the start and end times for when the property is used within an animation step.
|
||||
*
|
||||
* If there are two or more parallel animations that are currently running (these are invoked by the
|
||||
* group()) on the same element then the validator will throw an error. Since the start/end timing
|
||||
* values are collected for each property then if the current animation step is animating the same
|
||||
* property and its timing values fall anywhere into the window of time that the property is
|
||||
* currently being animated within then this is what causes an error.
|
||||
*
|
||||
* 2. Timing values
|
||||
* The validator will validate to see if a timing value of `duration delay easing` or
|
||||
* `durationNumber` is valid or not.
|
||||
*
|
||||
* (note that upon validation the code below will replace the timing data with an object containing
|
||||
* {duration,delay,easing}.
|
||||
*
|
||||
* 3. Offset Validation
|
||||
* Each of the style() calls are allowed to have an offset value when placed inside of keyframes().
|
||||
* Offsets within keyframes() are considered valid when:
|
||||
*
|
||||
* - No offsets are used at all
|
||||
* - Each style() entry contains an offset value
|
||||
* - Each offset is between 0 and 1
|
||||
* - Each offset is greater to or equal than the previous one
|
||||
*
|
||||
* Otherwise an error will be thrown.
|
||||
*/
|
||||
export function buildAnimationAst(
|
||||
metadata: AnimationMetadata | AnimationMetadata[], errors: any[]): Ast {
|
||||
return new AnimationAstBuilderVisitor().build(metadata, errors);
|
||||
}
|
||||
|
||||
const LEAVE_TOKEN = ':leave';
|
||||
const LEAVE_TOKEN_REGEX = new RegExp(LEAVE_TOKEN, 'g');
|
||||
const ENTER_TOKEN = ':enter';
|
||||
const ENTER_TOKEN_REGEX = new RegExp(ENTER_TOKEN, 'g');
|
||||
const ROOT_SELECTOR = '';
|
||||
|
||||
export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
|
||||
build(metadata: AnimationMetadata|AnimationMetadata[], errors: any[]): Ast {
|
||||
const context = new AnimationAstBuilderContext(errors);
|
||||
this._resetContextStyleTimingState(context);
|
||||
return visitAnimationNode(this, normalizeAnimationEntry(metadata), context) as Ast;
|
||||
}
|
||||
|
||||
private _resetContextStyleTimingState(context: AnimationAstBuilderContext) {
|
||||
context.currentQuerySelector = ROOT_SELECTOR;
|
||||
context.collectedStyles[ROOT_SELECTOR] = {};
|
||||
context.currentTime = 0;
|
||||
}
|
||||
|
||||
visitTrigger(metadata: AnimationTriggerMetadata, context: AnimationAstBuilderContext):
|
||||
TriggerAst {
|
||||
let queryCount = context.queryCount = 0;
|
||||
let depCount = context.depCount = 0;
|
||||
const states: StateAst[] = [];
|
||||
const transitions: TransitionAst[] = [];
|
||||
metadata.definitions.forEach(def => {
|
||||
this._resetContextStyleTimingState(context);
|
||||
if (def.type == AnimationMetadataType.State) {
|
||||
const stateDef = def as AnimationStateMetadata;
|
||||
const name = stateDef.name;
|
||||
name.split(/\s*,\s*/).forEach(n => {
|
||||
stateDef.name = n;
|
||||
states.push(this.visitState(stateDef, context));
|
||||
});
|
||||
stateDef.name = name;
|
||||
} else if (def.type == AnimationMetadataType.Transition) {
|
||||
const transition = this.visitTransition(def as AnimationTransitionMetadata, context);
|
||||
queryCount += transition.queryCount;
|
||||
depCount += transition.depCount;
|
||||
transitions.push(transition);
|
||||
} else {
|
||||
context.errors.push(
|
||||
'only state() and transition() definitions can sit inside of a trigger()');
|
||||
}
|
||||
});
|
||||
const ast = new TriggerAst(metadata.name, states, transitions);
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
ast.queryCount = queryCount;
|
||||
ast.depCount = depCount;
|
||||
return ast;
|
||||
}
|
||||
|
||||
visitState(metadata: AnimationStateMetadata, context: AnimationAstBuilderContext): StateAst {
|
||||
return new StateAst(metadata.name, this.visitStyle(metadata.styles, context));
|
||||
}
|
||||
|
||||
visitTransition(metadata: AnimationTransitionMetadata, context: AnimationAstBuilderContext):
|
||||
TransitionAst {
|
||||
context.queryCount = 0;
|
||||
context.depCount = 0;
|
||||
const entry = visitAnimationNode(this, normalizeAnimationEntry(metadata.animation), context);
|
||||
const matchers = parseTransitionExpr(metadata.expr, context.errors);
|
||||
const ast = new TransitionAst(matchers, entry);
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
ast.queryCount = context.queryCount;
|
||||
ast.depCount = context.depCount;
|
||||
return ast;
|
||||
}
|
||||
|
||||
visitSequence(metadata: AnimationSequenceMetadata, context: AnimationAstBuilderContext):
|
||||
SequenceAst {
|
||||
const ast = new SequenceAst(metadata.steps.map(s => visitAnimationNode(this, s, context)));
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
return ast;
|
||||
}
|
||||
|
||||
visitGroup(metadata: AnimationGroupMetadata, context: AnimationAstBuilderContext): GroupAst {
|
||||
const currentTime = context.currentTime;
|
||||
let furthestTime = 0;
|
||||
const steps = metadata.steps.map(step => {
|
||||
context.currentTime = currentTime;
|
||||
const innerAst = visitAnimationNode(this, step, context);
|
||||
furthestTime = Math.max(furthestTime, context.currentTime);
|
||||
return innerAst;
|
||||
});
|
||||
|
||||
context.currentTime = furthestTime;
|
||||
const ast = new GroupAst(steps);
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
return ast;
|
||||
}
|
||||
|
||||
visitAnimate(metadata: AnimationAnimateMetadata, context: AnimationAstBuilderContext):
|
||||
AnimateAst {
|
||||
const timingAst = constructTimingAst(metadata.timings, context.errors);
|
||||
context.currentAnimateTimings = timingAst;
|
||||
|
||||
let styles: StyleAst|KeyframesAst;
|
||||
let styleMetadata: AnimationMetadata = metadata.styles ? metadata.styles : style({});
|
||||
if (styleMetadata.type == AnimationMetadataType.Keyframes) {
|
||||
styles = this.visitKeyframes(styleMetadata as AnimationKeyframesSequenceMetadata, context);
|
||||
} else {
|
||||
let styleMetadata = metadata.styles as AnimationStyleMetadata;
|
||||
let isEmpty = false;
|
||||
if (!styleMetadata) {
|
||||
isEmpty = true;
|
||||
const newStyleData: {[prop: string]: string | number} = {};
|
||||
if (timingAst.easing) {
|
||||
newStyleData['easing'] = timingAst.easing;
|
||||
}
|
||||
styleMetadata = style(newStyleData);
|
||||
}
|
||||
context.currentTime += timingAst.duration + timingAst.delay;
|
||||
const styleAst = this.visitStyle(styleMetadata, context);
|
||||
styleAst.isEmptyStep = isEmpty;
|
||||
styles = styleAst;
|
||||
}
|
||||
|
||||
context.currentAnimateTimings = null;
|
||||
return new AnimateAst(timingAst, styles);
|
||||
}
|
||||
|
||||
visitStyle(metadata: AnimationStyleMetadata, context: AnimationAstBuilderContext): StyleAst {
|
||||
const ast = this._makeStyleAst(metadata, context);
|
||||
this._validateStyleAst(ast, context);
|
||||
return ast;
|
||||
}
|
||||
|
||||
private _makeStyleAst(metadata: AnimationStyleMetadata, context: AnimationAstBuilderContext):
|
||||
StyleAst {
|
||||
const styles: (ɵStyleData | string)[] = [];
|
||||
if (Array.isArray(metadata.styles)) {
|
||||
(metadata.styles as(ɵStyleData | string)[]).forEach(styleTuple => {
|
||||
if (typeof styleTuple == 'string') {
|
||||
if (styleTuple == AUTO_STYLE) {
|
||||
styles.push(styleTuple as string);
|
||||
} else {
|
||||
context.errors.push(`The provided style string value ${styleTuple} is not allowed.`);
|
||||
}
|
||||
} else {
|
||||
styles.push(styleTuple as ɵStyleData);
|
||||
}
|
||||
})
|
||||
} else {
|
||||
styles.push(metadata.styles);
|
||||
}
|
||||
|
||||
let collectedEasing: string|null = null;
|
||||
styles.forEach(styleData => {
|
||||
if (isObject(styleData)) {
|
||||
const styleMap = styleData as ɵStyleData;
|
||||
const easing = styleMap['easing'];
|
||||
if (easing) {
|
||||
collectedEasing = easing as string;
|
||||
delete styleMap['easing'];
|
||||
}
|
||||
}
|
||||
});
|
||||
return new StyleAst(styles, collectedEasing, metadata.offset);
|
||||
}
|
||||
|
||||
private _validateStyleAst(ast: StyleAst, context: AnimationAstBuilderContext): void {
|
||||
const timings = context.currentAnimateTimings;
|
||||
let endTime = context.currentTime;
|
||||
let startTime = context.currentTime;
|
||||
if (timings && startTime > 0) {
|
||||
startTime -= timings.duration + timings.delay;
|
||||
}
|
||||
|
||||
ast.styles.forEach(tuple => {
|
||||
if (typeof tuple == 'string') return;
|
||||
|
||||
Object.keys(tuple).forEach(prop => {
|
||||
const collectedStyles = context.collectedStyles[context.currentQuerySelector !];
|
||||
const collectedEntry = collectedStyles[prop];
|
||||
let updateCollectedStyle = true;
|
||||
if (collectedEntry) {
|
||||
if (startTime != endTime && startTime >= collectedEntry.startTime &&
|
||||
endTime <= collectedEntry.endTime) {
|
||||
context.errors.push(
|
||||
`The CSS property "${prop}" that exists between the times of "${collectedEntry.startTime}ms" and "${collectedEntry.endTime}ms" is also being animated in a parallel animation between the times of "${startTime}ms" and "${endTime}ms"`);
|
||||
updateCollectedStyle = false;
|
||||
}
|
||||
|
||||
// we always choose the smaller start time value since we
|
||||
// want to have a record of the entire animation window where
|
||||
// the style property is being animated in between
|
||||
startTime = collectedEntry.startTime;
|
||||
}
|
||||
|
||||
if (updateCollectedStyle) {
|
||||
collectedStyles[prop] = {startTime, endTime};
|
||||
}
|
||||
|
||||
if (context.options) {
|
||||
validateStyleParams(tuple[prop], context.options, context.errors);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
visitKeyframes(metadata: AnimationKeyframesSequenceMetadata, context: AnimationAstBuilderContext):
|
||||
KeyframesAst {
|
||||
if (!context.currentAnimateTimings) {
|
||||
context.errors.push(`keyframes() must be placed inside of a call to animate()`);
|
||||
return new KeyframesAst([]);
|
||||
}
|
||||
|
||||
const MAX_KEYFRAME_OFFSET = 1;
|
||||
|
||||
let totalKeyframesWithOffsets = 0;
|
||||
const offsets: number[] = [];
|
||||
let offsetsOutOfOrder = false;
|
||||
let keyframesOutOfRange = false;
|
||||
let previousOffset: number = 0;
|
||||
|
||||
const keyframes: StyleAst[] = metadata.steps.map(styles => {
|
||||
const style = this._makeStyleAst(styles, context);
|
||||
let offsetVal: number|null =
|
||||
style.offset != null ? style.offset : consumeOffset(style.styles);
|
||||
let offset: number = 0;
|
||||
if (offsetVal != null) {
|
||||
totalKeyframesWithOffsets++;
|
||||
offset = style.offset = offsetVal;
|
||||
}
|
||||
keyframesOutOfRange = keyframesOutOfRange || offset < 0 || offset > 1;
|
||||
offsetsOutOfOrder = offsetsOutOfOrder || offset < previousOffset;
|
||||
previousOffset = offset;
|
||||
offsets.push(offset);
|
||||
return style;
|
||||
});
|
||||
|
||||
if (keyframesOutOfRange) {
|
||||
context.errors.push(`Please ensure that all keyframe offsets are between 0 and 1`);
|
||||
}
|
||||
|
||||
if (offsetsOutOfOrder) {
|
||||
context.errors.push(`Please ensure that all keyframe offsets are in order`);
|
||||
}
|
||||
|
||||
const length = metadata.steps.length;
|
||||
let generatedOffset = 0;
|
||||
if (totalKeyframesWithOffsets > 0 && totalKeyframesWithOffsets < length) {
|
||||
context.errors.push(`Not all style() steps within the declared keyframes() contain offsets`);
|
||||
} else if (totalKeyframesWithOffsets == 0) {
|
||||
generatedOffset = MAX_KEYFRAME_OFFSET / (length - 1);
|
||||
}
|
||||
|
||||
const limit = length - 1;
|
||||
const currentTime = context.currentTime;
|
||||
const currentAnimateTimings = context.currentAnimateTimings !;
|
||||
const animateDuration = currentAnimateTimings.duration;
|
||||
keyframes.forEach((kf, i) => {
|
||||
const offset = generatedOffset > 0 ? (i == limit ? 1 : (generatedOffset * i)) : offsets[i];
|
||||
const durationUpToThisFrame = offset * animateDuration;
|
||||
context.currentTime = currentTime + currentAnimateTimings.delay + durationUpToThisFrame;
|
||||
currentAnimateTimings.duration = durationUpToThisFrame;
|
||||
this._validateStyleAst(kf, context);
|
||||
kf.offset = offset;
|
||||
});
|
||||
|
||||
return new KeyframesAst(keyframes);
|
||||
}
|
||||
|
||||
visitReference(metadata: AnimationReferenceMetadata, context: AnimationAstBuilderContext):
|
||||
ReferenceAst {
|
||||
const entry = visitAnimationNode(this, normalizeAnimationEntry(metadata.animation), context);
|
||||
const ast = new ReferenceAst(entry);
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
return ast;
|
||||
}
|
||||
|
||||
visitAnimateChild(metadata: AnimationAnimateChildMetadata, context: AnimationAstBuilderContext):
|
||||
AnimateChildAst {
|
||||
context.depCount++;
|
||||
const ast = new AnimateChildAst();
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
return ast;
|
||||
}
|
||||
|
||||
visitAnimateRef(metadata: AnimationAnimateRefMetadata, context: AnimationAstBuilderContext):
|
||||
AnimateRefAst {
|
||||
const animation = this.visitReference(metadata.animation, context);
|
||||
const ast = new AnimateRefAst(animation);
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
return ast;
|
||||
}
|
||||
|
||||
visitQuery(metadata: AnimationQueryMetadata, context: AnimationAstBuilderContext): QueryAst {
|
||||
const parentSelector = context.currentQuerySelector !;
|
||||
const options = (metadata.options || {}) as AnimationQueryOptions;
|
||||
|
||||
context.queryCount++;
|
||||
context.currentQuery = metadata;
|
||||
const [selector, includeSelf] = normalizeSelector(metadata.selector);
|
||||
context.currentQuerySelector =
|
||||
parentSelector.length ? (parentSelector + ' ' + selector) : selector;
|
||||
getOrSetAsInMap(context.collectedStyles, context.currentQuerySelector, {});
|
||||
|
||||
const entry = visitAnimationNode(this, normalizeAnimationEntry(metadata.animation), context);
|
||||
context.currentQuery = null;
|
||||
context.currentQuerySelector = parentSelector;
|
||||
|
||||
const ast = new QueryAst(selector, options.limit || 0, !!options.optional, includeSelf, entry);
|
||||
ast.originalSelector = metadata.selector;
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
return ast;
|
||||
}
|
||||
|
||||
visitStagger(metadata: AnimationStaggerMetadata, context: AnimationAstBuilderContext):
|
||||
StaggerAst {
|
||||
if (!context.currentQuery) {
|
||||
context.errors.push(`stagger() can only be used inside of query()`);
|
||||
}
|
||||
const timings = metadata.timings === 'full' ?
|
||||
{duration: 0, delay: 0, easing: 'full'} :
|
||||
resolveTiming(metadata.timings, context.errors, true);
|
||||
const animation =
|
||||
visitAnimationNode(this, normalizeAnimationEntry(metadata.animation), context);
|
||||
return new StaggerAst(timings, animation);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSelector(selector: string): [string, boolean] {
|
||||
const hasAmpersand = selector.split(/\s*,\s*/).find(token => token == SELF_TOKEN) ? true : false;
|
||||
if (hasAmpersand) {
|
||||
selector = selector.replace(SELF_TOKEN_REGEX, '');
|
||||
}
|
||||
|
||||
selector = selector.replace(ENTER_TOKEN_REGEX, ENTER_SELECTOR)
|
||||
.replace(LEAVE_TOKEN_REGEX, LEAVE_SELECTOR)
|
||||
.replace(/@\*/g, NG_TRIGGER_SELECTOR)
|
||||
.replace(/@\w+/g, match => NG_TRIGGER_SELECTOR + '-' + match.substr(1))
|
||||
.replace(/:animating/g, NG_ANIMATING_SELECTOR);
|
||||
|
||||
return [selector, hasAmpersand];
|
||||
}
|
||||
|
||||
|
||||
function normalizeParams(obj: {[key: string]: any} | any): {[key: string]: any}|null {
|
||||
return obj ? copyObj(obj) : null;
|
||||
}
|
||||
|
||||
export type StyleTimeTuple = {
|
||||
startTime: number; endTime: number;
|
||||
};
|
||||
|
||||
export class AnimationAstBuilderContext {
|
||||
public queryCount: number = 0;
|
||||
public depCount: number = 0;
|
||||
public currentTransition: AnimationTransitionMetadata|null = null;
|
||||
public currentQuery: AnimationQueryMetadata|null = null;
|
||||
public currentQuerySelector: string|null = null;
|
||||
public currentAnimateTimings: TimingAst|null = null;
|
||||
public currentTime: number = 0;
|
||||
public collectedStyles: {[selectorName: string]: {[propName: string]: StyleTimeTuple}} = {};
|
||||
public options: AnimationOptions|null = null;
|
||||
constructor(public errors: any[]) {}
|
||||
}
|
||||
|
||||
function consumeOffset(styles: ɵStyleData | string | (ɵStyleData | string)[]): number|null {
|
||||
if (typeof styles == 'string') return null;
|
||||
|
||||
let offset: number|null = null;
|
||||
|
||||
if (Array.isArray(styles)) {
|
||||
styles.forEach(styleTuple => {
|
||||
if (isObject(styleTuple) && styleTuple.hasOwnProperty('offset')) {
|
||||
const obj = styleTuple as ɵStyleData;
|
||||
offset = parseFloat(obj['offset'] as string);
|
||||
delete obj['offset'];
|
||||
}
|
||||
});
|
||||
} else if (isObject(styles) && styles.hasOwnProperty('offset')) {
|
||||
const obj = styles as ɵStyleData;
|
||||
offset = parseFloat(obj['offset'] as string);
|
||||
delete obj['offset'];
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
function isObject(value: any): boolean {
|
||||
return !Array.isArray(value) && typeof value == 'object';
|
||||
}
|
||||
|
||||
function constructTimingAst(value: string | number | AnimateTimings, errors: any[]) {
|
||||
let timings: AnimateTimings|null = null;
|
||||
if (value.hasOwnProperty('duration')) {
|
||||
timings = value as AnimateTimings;
|
||||
} else if (typeof value == 'number') {
|
||||
const duration = resolveTiming(value as number, errors).duration;
|
||||
return new TimingAst(value as number, 0, '');
|
||||
}
|
||||
|
||||
const strValue = value as string;
|
||||
const isDynamic = strValue.split(/\s+/).some(v => v.charAt(0) == '{' && v.charAt(1) == '{');
|
||||
if (isDynamic) {
|
||||
return new DynamicTimingAst(strValue);
|
||||
}
|
||||
|
||||
timings = timings || resolveTiming(strValue, errors);
|
||||
return new TimingAst(timings.duration, timings.delay, timings.easing);
|
||||
}
|
||||
|
||||
function normalizeAnimationOptions(options: AnimationOptions | null): AnimationOptions {
|
||||
if (options) {
|
||||
options = copyObj(options);
|
||||
if (options['params']) {
|
||||
options['params'] = normalizeParams(options['params']) !;
|
||||
}
|
||||
} else {
|
||||
options = {};
|
||||
}
|
||||
return options;
|
||||
}
|
|
@ -5,35 +5,53 @@
|
|||
* 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 {AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata} from '@angular/animations';
|
||||
import {AnimationAnimateChildMetadata, AnimationAnimateMetadata, AnimationAnimateRefMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationQueryMetadata, AnimationReferenceMetadata, AnimationSequenceMetadata, AnimationStaggerMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, AnimationTriggerMetadata} from '@angular/animations';
|
||||
|
||||
export interface AnimationDslVisitor {
|
||||
visitTrigger(ast: AnimationTriggerMetadata, context: any): any;
|
||||
visitState(ast: AnimationStateMetadata, context: any): any;
|
||||
visitTransition(ast: AnimationTransitionMetadata, context: any): any;
|
||||
visitSequence(ast: AnimationSequenceMetadata, context: any): any;
|
||||
visitGroup(ast: AnimationGroupMetadata, context: any): any;
|
||||
visitAnimate(ast: AnimationAnimateMetadata, context: any): any;
|
||||
visitStyle(ast: AnimationStyleMetadata, context: any): any;
|
||||
visitKeyframeSequence(ast: AnimationKeyframesSequenceMetadata, context: any): any;
|
||||
visitKeyframes(ast: AnimationKeyframesSequenceMetadata, context: any): any;
|
||||
visitReference(ast: AnimationReferenceMetadata, context: any): any;
|
||||
visitAnimateChild(ast: AnimationAnimateChildMetadata, context: any): any;
|
||||
visitAnimateRef(ast: AnimationAnimateRefMetadata, context: any): any;
|
||||
visitQuery(ast: AnimationQueryMetadata, context: any): any;
|
||||
visitStagger(ast: AnimationStaggerMetadata, context: any): any;
|
||||
}
|
||||
|
||||
export function visitAnimationNode(
|
||||
visitor: AnimationDslVisitor, node: AnimationMetadata, context: any) {
|
||||
switch (node.type) {
|
||||
case AnimationMetadataType.Trigger:
|
||||
return visitor.visitTrigger(node as AnimationTriggerMetadata, context);
|
||||
case AnimationMetadataType.State:
|
||||
return visitor.visitState(<AnimationStateMetadata>node, context);
|
||||
return visitor.visitState(node as AnimationStateMetadata, context);
|
||||
case AnimationMetadataType.Transition:
|
||||
return visitor.visitTransition(<AnimationTransitionMetadata>node, context);
|
||||
return visitor.visitTransition(node as AnimationTransitionMetadata, context);
|
||||
case AnimationMetadataType.Sequence:
|
||||
return visitor.visitSequence(<AnimationSequenceMetadata>node, context);
|
||||
return visitor.visitSequence(node as AnimationSequenceMetadata, context);
|
||||
case AnimationMetadataType.Group:
|
||||
return visitor.visitGroup(<AnimationGroupMetadata>node, context);
|
||||
return visitor.visitGroup(node as AnimationGroupMetadata, context);
|
||||
case AnimationMetadataType.Animate:
|
||||
return visitor.visitAnimate(<AnimationAnimateMetadata>node, context);
|
||||
case AnimationMetadataType.KeyframeSequence:
|
||||
return visitor.visitKeyframeSequence(<AnimationKeyframesSequenceMetadata>node, context);
|
||||
return visitor.visitAnimate(node as AnimationAnimateMetadata, context);
|
||||
case AnimationMetadataType.Keyframes:
|
||||
return visitor.visitKeyframes(node as AnimationKeyframesSequenceMetadata, context);
|
||||
case AnimationMetadataType.Style:
|
||||
return visitor.visitStyle(<AnimationStyleMetadata>node, context);
|
||||
return visitor.visitStyle(node as AnimationStyleMetadata, context);
|
||||
case AnimationMetadataType.Reference:
|
||||
return visitor.visitReference(node as AnimationReferenceMetadata, context);
|
||||
case AnimationMetadataType.AnimateChild:
|
||||
return visitor.visitAnimateChild(node as AnimationAnimateChildMetadata, context);
|
||||
case AnimationMetadataType.AnimateRef:
|
||||
return visitor.visitAnimateRef(node as AnimationAnimateRefMetadata, context);
|
||||
case AnimationMetadataType.Query:
|
||||
return visitor.visitQuery(node as AnimationQueryMetadata, context);
|
||||
case AnimationMetadataType.Stagger:
|
||||
return visitor.visitStagger(node as AnimationStaggerMetadata, context);
|
||||
default:
|
||||
throw new Error(`Unable to resolve animation metadata node #${node.type}`);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,873 @@
|
|||
/**
|
||||
* @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 {AUTO_STYLE, AnimateTimings, AnimationOptions, AnimationQueryOptions, ɵPRE_STYLE as PRE_STYLE, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {copyObj, copyStyles, interpolateParams, iteratorToArray, resolveTiming, resolveTimingValue} from '../util';
|
||||
|
||||
import {AnimateAst, AnimateChildAst, AnimateRefAst, Ast, AstVisitor, DynamicTimingAst, GroupAst, KeyframesAst, QueryAst, ReferenceAst, SequenceAst, StaggerAst, StateAst, StyleAst, TimingAst, TransitionAst, TriggerAst} from './animation_ast';
|
||||
import {AnimationTimelineInstruction, createTimelineInstruction} from './animation_timeline_instruction';
|
||||
import {ElementInstructionMap} from './element_instruction_map';
|
||||
|
||||
const ONE_FRAME_IN_MILLISECONDS = 1;
|
||||
|
||||
/*
|
||||
* The code within this file aims to generate web-animations-compatible keyframes from Angular's
|
||||
* animation DSL code.
|
||||
*
|
||||
* The code below will be converted from:
|
||||
*
|
||||
* ```
|
||||
* sequence([
|
||||
* style({ opacity: 0 }),
|
||||
* animate(1000, style({ opacity: 0 }))
|
||||
* ])
|
||||
* ```
|
||||
*
|
||||
* To:
|
||||
* ```
|
||||
* keyframes = [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }]
|
||||
* duration = 1000
|
||||
* delay = 0
|
||||
* easing = ''
|
||||
* ```
|
||||
*
|
||||
* For this operation to cover the combination of animation verbs (style, animate, group, etc...) a
|
||||
* combination of prototypical inheritance, AST traversal and merge-sort-like algorithms are used.
|
||||
*
|
||||
* [AST Traversal]
|
||||
* Each of the animation verbs, when executed, will return an string-map object representing what
|
||||
* type of action it is (style, animate, group, etc...) and the data associated with it. This means
|
||||
* that when functional composition mix of these functions is evaluated (like in the example above)
|
||||
* then it will end up producing a tree of objects representing the animation itself.
|
||||
*
|
||||
* When this animation object tree is processed by the visitor code below it will visit each of the
|
||||
* verb statements within the visitor. And during each visit it will build the context of the
|
||||
* animation keyframes by interacting with the `TimelineBuilder`.
|
||||
*
|
||||
* [TimelineBuilder]
|
||||
* This class is responsible for tracking the styles and building a series of keyframe objects for a
|
||||
* timeline between a start and end time. The builder starts off with an initial timeline and each
|
||||
* time the AST comes across a `group()`, `keyframes()` or a combination of the two wihtin a
|
||||
* `sequence()` then it will generate a sub timeline for each step as well as a new one after
|
||||
* they are complete.
|
||||
*
|
||||
* As the AST is traversed, the timing state on each of the timelines will be incremented. If a sub
|
||||
* timeline was created (based on one of the cases above) then the parent timeline will attempt to
|
||||
* merge the styles used within the sub timelines into itself (only with group() this will happen).
|
||||
* This happens with a merge operation (much like how the merge works in mergesort) and it will only
|
||||
* copy the most recently used styles from the sub timelines into the parent timeline. This ensures
|
||||
* that if the styles are used later on in another phase of the animation then they will be the most
|
||||
* up-to-date values.
|
||||
*
|
||||
* [How Missing Styles Are Updated]
|
||||
* Each timeline has a `backFill` property which is responsible for filling in new styles into
|
||||
* already processed keyframes if a new style shows up later within the animation sequence.
|
||||
*
|
||||
* ```
|
||||
* sequence([
|
||||
* style({ width: 0 }),
|
||||
* animate(1000, style({ width: 100 })),
|
||||
* animate(1000, style({ width: 200 })),
|
||||
* animate(1000, style({ width: 300 }))
|
||||
* animate(1000, style({ width: 400, height: 400 })) // notice how `height` doesn't exist anywhere
|
||||
* else
|
||||
* ])
|
||||
* ```
|
||||
*
|
||||
* What is happening here is that the `height` value is added later in the sequence, but is missing
|
||||
* from all previous animation steps. Therefore when a keyframe is created it would also be missing
|
||||
* from all previous keyframes up until where it is first used. For the timeline keyframe generation
|
||||
* to properly fill in the style it will place the previous value (the value from the parent
|
||||
* timeline) or a default value of `*` into the backFill object. Given that each of the keyframe
|
||||
* styles are objects that prototypically inhert from the backFill object, this means that if a
|
||||
* value is added into the backFill then it will automatically propagate any missing values to all
|
||||
* keyframes. Therefore the missing `height` value will be properly filled into the already
|
||||
* processed keyframes.
|
||||
*
|
||||
* When a sub-timeline is created it will have its own backFill property. This is done so that
|
||||
* styles present within the sub-timeline do not accidentally seep into the previous/future timeline
|
||||
* keyframes
|
||||
*
|
||||
* (For prototypically-inherited contents to be detected a `for(i in obj)` loop must be used.)
|
||||
*
|
||||
* [Validation]
|
||||
* The code in this file is not responsible for validation. That functionality happens with within
|
||||
* the `AnimationValidatorVisitor` code.
|
||||
*/
|
||||
export function buildAnimationTimelines(
|
||||
rootElement: any, ast: Ast, startingStyles: ɵStyleData = {}, finalStyles: ɵStyleData = {},
|
||||
options: AnimationOptions, subInstructions?: ElementInstructionMap,
|
||||
errors: any[] = []): AnimationTimelineInstruction[] {
|
||||
return new AnimationTimelineBuilderVisitor().buildKeyframes(
|
||||
rootElement, ast, startingStyles, finalStyles, options, subInstructions, errors);
|
||||
}
|
||||
|
||||
export declare type StyleAtTime = {
|
||||
time: number; value: string | number;
|
||||
};
|
||||
|
||||
const DEFAULT_NOOP_PREVIOUS_NODE = <Ast>{};
|
||||
export class AnimationTimelineContext {
|
||||
public parentContext: AnimationTimelineContext|null = null;
|
||||
public currentTimeline: TimelineBuilder;
|
||||
public currentAnimateTimings: AnimateTimings|null = null;
|
||||
public previousNode: Ast = DEFAULT_NOOP_PREVIOUS_NODE;
|
||||
public subContextCount = 0;
|
||||
public options: AnimationOptions = {};
|
||||
public currentQueryIndex: number = 0;
|
||||
public currentQueryTotal: number = 0;
|
||||
public currentStaggerTime: number = 0;
|
||||
|
||||
constructor(
|
||||
public element: any, public subInstructions: ElementInstructionMap, public errors: any[],
|
||||
public timelines: TimelineBuilder[], initialTimeline?: TimelineBuilder) {
|
||||
this.currentTimeline = initialTimeline || new TimelineBuilder(element, 0);
|
||||
timelines.push(this.currentTimeline);
|
||||
}
|
||||
|
||||
get params() { return this.options.params; }
|
||||
|
||||
updateOptions(newOptions: AnimationOptions|null, skipIfExists?: boolean) {
|
||||
if (!newOptions) return;
|
||||
|
||||
if (newOptions.duration != null) {
|
||||
this.options.duration = resolveTimingValue(newOptions.duration);
|
||||
}
|
||||
|
||||
if (newOptions.delay != null) {
|
||||
this.options.delay = resolveTimingValue(newOptions.delay);
|
||||
}
|
||||
|
||||
const newParams = newOptions.params;
|
||||
if (newParams) {
|
||||
let params: {[name: string]: any} = this.options && this.options.params !;
|
||||
if (!params) {
|
||||
params = this.options.params = {};
|
||||
}
|
||||
|
||||
Object.keys(params).forEach(name => {
|
||||
const value = params[name];
|
||||
if (!skipIfExists || !newOptions.hasOwnProperty(name)) {
|
||||
params[name] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _copyOptions() {
|
||||
const options: AnimationOptions = {};
|
||||
if (this.options) {
|
||||
const oldParams = this.options.params;
|
||||
if (oldParams) {
|
||||
const params: {[name: string]: any} = options['params'] = {};
|
||||
Object.keys(this.options.params).forEach(name => { params[name] = oldParams[name]; });
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
createSubContext(options: AnimationOptions|null = null, element?: any, newTime?: number):
|
||||
AnimationTimelineContext {
|
||||
const target = element || this.element;
|
||||
const context = new AnimationTimelineContext(
|
||||
target, this.subInstructions, this.errors, this.timelines,
|
||||
this.currentTimeline.fork(target, newTime || 0));
|
||||
context.previousNode = this.previousNode;
|
||||
context.currentAnimateTimings = this.currentAnimateTimings;
|
||||
|
||||
context.options = this._copyOptions();
|
||||
context.updateOptions(options);
|
||||
|
||||
context.currentQueryIndex = this.currentQueryIndex;
|
||||
context.currentQueryTotal = this.currentQueryTotal;
|
||||
context.parentContext = this;
|
||||
this.subContextCount++;
|
||||
return context;
|
||||
}
|
||||
|
||||
transformIntoNewTimeline(newTime?: number) {
|
||||
this.previousNode = DEFAULT_NOOP_PREVIOUS_NODE;
|
||||
this.currentTimeline = this.currentTimeline.fork(this.element, newTime);
|
||||
this.timelines.push(this.currentTimeline);
|
||||
return this.currentTimeline;
|
||||
}
|
||||
|
||||
appendInstructionToTimeline(
|
||||
instruction: AnimationTimelineInstruction, duration: number|null,
|
||||
delay: number|null): AnimateTimings {
|
||||
const updatedTimings: AnimateTimings = {
|
||||
duration: duration != null ? duration : instruction.duration,
|
||||
delay: this.currentTimeline.currentTime + (delay != null ? delay : 0) + instruction.delay,
|
||||
easing: ''
|
||||
};
|
||||
const builder = new SubTimelineBuilder(
|
||||
instruction.element, instruction.keyframes, instruction.preStyleProps,
|
||||
instruction.postStyleProps, updatedTimings, instruction.stretchStartingKeyframe);
|
||||
this.timelines.push(builder);
|
||||
return updatedTimings;
|
||||
}
|
||||
|
||||
incrementTime(time: number) {
|
||||
this.currentTimeline.forwardTime(this.currentTimeline.duration + time);
|
||||
}
|
||||
|
||||
delayNextStep(delay: number) {
|
||||
// negative delays are not yet supported
|
||||
if (delay > 0) {
|
||||
this.currentTimeline.delayNextStep(delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class AnimationTimelineBuilderVisitor implements AstVisitor {
|
||||
buildKeyframes(
|
||||
rootElement: any, ast: Ast, startingStyles: ɵStyleData, finalStyles: ɵStyleData,
|
||||
options: AnimationOptions, subInstructions?: ElementInstructionMap,
|
||||
errors: any[] = []): AnimationTimelineInstruction[] {
|
||||
subInstructions = subInstructions || new ElementInstructionMap();
|
||||
const context = new AnimationTimelineContext(rootElement, subInstructions, errors, []);
|
||||
context.options = options;
|
||||
context.currentTimeline.setStyles([startingStyles], null, context.errors, options);
|
||||
|
||||
ast.visit(this, context);
|
||||
|
||||
// this checks to see if an actual animation happened
|
||||
const timelines = context.timelines.filter(timeline => timeline.containsAnimation());
|
||||
if (timelines.length && Object.keys(finalStyles).length) {
|
||||
const tl = timelines[timelines.length - 1];
|
||||
if (!tl.allowOnlyTimelineStyles()) {
|
||||
tl.setStyles([finalStyles], null, context.errors, options);
|
||||
}
|
||||
}
|
||||
|
||||
return timelines.length ? timelines.map(timeline => timeline.buildKeyframes()) :
|
||||
[createTimelineInstruction(rootElement, [], [], [], 0, 0, '', false)];
|
||||
}
|
||||
|
||||
visitTrigger(ast: TriggerAst, context: AnimationTimelineContext): any {
|
||||
// these values are not visited in this AST
|
||||
}
|
||||
|
||||
visitState(ast: StateAst, context: AnimationTimelineContext): any {
|
||||
// these values are not visited in this AST
|
||||
}
|
||||
|
||||
visitTransition(ast: TransitionAst, context: AnimationTimelineContext): any {
|
||||
// these values are not visited in this AST
|
||||
}
|
||||
|
||||
visitAnimateChild(ast: AnimateChildAst, context: AnimationTimelineContext): any {
|
||||
const elementInstructions = context.subInstructions.consume(context.element);
|
||||
if (elementInstructions) {
|
||||
const innerContext = context.createSubContext(ast.options);
|
||||
const startTime = context.currentTimeline.currentTime;
|
||||
const endTime = this._visitSubInstructions(elementInstructions, innerContext);
|
||||
if (startTime != endTime) {
|
||||
// we do this on the upper context because we created a sub context for
|
||||
// the sub child animations
|
||||
context.transformIntoNewTimeline(endTime);
|
||||
}
|
||||
}
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitAnimateRef(ast: AnimateRefAst, context: AnimationTimelineContext): any {
|
||||
const innerContext = context.createSubContext(ast.options);
|
||||
innerContext.transformIntoNewTimeline();
|
||||
this.visitReference(ast.animation, innerContext);
|
||||
context.transformIntoNewTimeline(innerContext.currentTimeline.currentTime);
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
private _visitSubInstructions(
|
||||
instructions: AnimationTimelineInstruction[], context: AnimationTimelineContext): number {
|
||||
const options = context.options;
|
||||
const startTime = context.currentTimeline.currentTime;
|
||||
let furthestTime = startTime;
|
||||
|
||||
// this is a special-case for when a user wants to skip a sub
|
||||
// animation from being fired entirely.
|
||||
const duration = options.duration != null ? resolveTimingValue(options.duration) : null;
|
||||
const delay = options.delay != null ? resolveTimingValue(options.delay) : null;
|
||||
if (duration !== 0) {
|
||||
instructions.forEach(instruction => {
|
||||
const instructionTimings =
|
||||
context.appendInstructionToTimeline(instruction, duration, delay);
|
||||
furthestTime =
|
||||
Math.max(furthestTime, instructionTimings.duration + instructionTimings.delay);
|
||||
});
|
||||
}
|
||||
|
||||
return furthestTime;
|
||||
}
|
||||
|
||||
visitReference(ast: ReferenceAst, context: AnimationTimelineContext) {
|
||||
context.updateOptions(ast.options, true);
|
||||
ast.animation.visit(this, context);
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitSequence(ast: SequenceAst, context: AnimationTimelineContext) {
|
||||
const subContextCount = context.subContextCount;
|
||||
const options = ast.options;
|
||||
|
||||
if (options && (options.params || options.delay)) {
|
||||
context.createSubContext(options);
|
||||
context.transformIntoNewTimeline();
|
||||
|
||||
if (options.delay != null) {
|
||||
if (context.previousNode instanceof StyleAst) {
|
||||
context.currentTimeline.snapshotCurrentStyles();
|
||||
context.previousNode = DEFAULT_NOOP_PREVIOUS_NODE;
|
||||
}
|
||||
|
||||
const delay = resolveTimingValue(options.delay);
|
||||
context.delayNextStep(delay);
|
||||
}
|
||||
}
|
||||
|
||||
if (ast.steps.length) {
|
||||
ast.steps.forEach(s => s.visit(this, context));
|
||||
|
||||
// this is here just incase the inner steps only contain or end with a style() call
|
||||
context.currentTimeline.applyStylesToKeyframe();
|
||||
|
||||
// this means that some animation function within the sequence
|
||||
// ended up creating a sub timeline (which means the current
|
||||
// timeline cannot overlap with the contents of the sequence)
|
||||
if (context.subContextCount > subContextCount) {
|
||||
context.transformIntoNewTimeline();
|
||||
}
|
||||
}
|
||||
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitGroup(ast: GroupAst, context: AnimationTimelineContext) {
|
||||
const innerTimelines: TimelineBuilder[] = [];
|
||||
let furthestTime = context.currentTimeline.currentTime;
|
||||
const delay = ast.options && ast.options.delay ? resolveTimingValue(ast.options.delay) : 0;
|
||||
|
||||
ast.steps.forEach(s => {
|
||||
const innerContext = context.createSubContext(ast.options);
|
||||
if (delay) {
|
||||
innerContext.delayNextStep(delay);
|
||||
}
|
||||
|
||||
s.visit(this, innerContext);
|
||||
furthestTime = Math.max(furthestTime, innerContext.currentTimeline.currentTime);
|
||||
innerTimelines.push(innerContext.currentTimeline);
|
||||
});
|
||||
|
||||
// this operation is run after the AST loop because otherwise
|
||||
// if the parent timeline's collected styles were updated then
|
||||
// it would pass in invalid data into the new-to-be forked items
|
||||
innerTimelines.forEach(
|
||||
timeline => context.currentTimeline.mergeTimelineCollectedStyles(timeline));
|
||||
context.transformIntoNewTimeline(furthestTime);
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitTiming(ast: TimingAst, context: AnimationTimelineContext): AnimateTimings {
|
||||
if (ast instanceof DynamicTimingAst) {
|
||||
const strValue = context.params ?
|
||||
interpolateParams(ast.value, context.params, context.errors) :
|
||||
ast.value.toString();
|
||||
return resolveTiming(strValue, context.errors);
|
||||
} else {
|
||||
return {duration: ast.duration, delay: ast.delay, easing: ast.easing};
|
||||
}
|
||||
}
|
||||
|
||||
visitAnimate(ast: AnimateAst, context: AnimationTimelineContext) {
|
||||
const timings = context.currentAnimateTimings = this.visitTiming(ast.timings, context);
|
||||
const timeline = context.currentTimeline;
|
||||
if (timings.delay) {
|
||||
context.incrementTime(timings.delay);
|
||||
timeline.snapshotCurrentStyles();
|
||||
}
|
||||
|
||||
const style = ast.style;
|
||||
if (style instanceof KeyframesAst) {
|
||||
this.visitKeyframes(style, context);
|
||||
} else {
|
||||
context.incrementTime(timings.duration);
|
||||
this.visitStyle(style as StyleAst, context);
|
||||
timeline.applyStylesToKeyframe();
|
||||
}
|
||||
|
||||
context.currentAnimateTimings = null;
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitStyle(ast: StyleAst, context: AnimationTimelineContext) {
|
||||
const timeline = context.currentTimeline;
|
||||
const timings = context.currentAnimateTimings !;
|
||||
|
||||
// this is a special case for when a style() call
|
||||
// directly follows an animate() call (but not inside of an animate() call)
|
||||
if (!timings && timeline.getCurrentStyleProperties().length) {
|
||||
timeline.forwardFrame();
|
||||
}
|
||||
|
||||
const easing = (timings && timings.easing) || ast.easing;
|
||||
if (ast.isEmptyStep) {
|
||||
timeline.applyEmptyStep(easing);
|
||||
} else {
|
||||
timeline.setStyles(ast.styles, easing, context.errors, context.options);
|
||||
}
|
||||
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitKeyframes(ast: KeyframesAst, context: AnimationTimelineContext) {
|
||||
const currentAnimateTimings = context.currentAnimateTimings !;
|
||||
const startTime = (context.currentTimeline !).duration;
|
||||
const duration = currentAnimateTimings.duration;
|
||||
const innerContext = context.createSubContext();
|
||||
const innerTimeline = innerContext.currentTimeline;
|
||||
innerTimeline.easing = currentAnimateTimings.easing;
|
||||
|
||||
ast.styles.forEach(step => {
|
||||
const offset: number = step.offset || 0;
|
||||
innerTimeline.forwardTime(offset * duration);
|
||||
innerTimeline.setStyles(step.styles, step.easing, context.errors, context.options);
|
||||
innerTimeline.applyStylesToKeyframe();
|
||||
});
|
||||
|
||||
// this will ensure that the parent timeline gets all the styles from
|
||||
// the child even if the new timeline below is not used
|
||||
context.currentTimeline.mergeTimelineCollectedStyles(innerTimeline);
|
||||
|
||||
// we do this because the window between this timeline and the sub timeline
|
||||
// should ensure that the styles within are exactly the same as they were before
|
||||
context.transformIntoNewTimeline(startTime + duration);
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitQuery(ast: QueryAst, context: AnimationTimelineContext) {
|
||||
// in the event that the first step before this is a style step we need
|
||||
// to ensure the styles are applied before the children are animated
|
||||
const startTime = context.currentTimeline.currentTime;
|
||||
const options = (ast.options || {}) as AnimationQueryOptions;
|
||||
const delay = options.delay ? resolveTimingValue(options.delay) : 0;
|
||||
|
||||
if (delay && (context.previousNode instanceof StyleAst ||
|
||||
(startTime == 0 && context.currentTimeline.getCurrentStyleProperties().length))) {
|
||||
context.currentTimeline.snapshotCurrentStyles();
|
||||
context.previousNode = DEFAULT_NOOP_PREVIOUS_NODE;
|
||||
}
|
||||
|
||||
let furthestTime = startTime;
|
||||
const elms = invokeQuery(
|
||||
context.element, ast.selector, ast.originalSelector, ast.limit, ast.includeSelf,
|
||||
options.optional ? true : false, context.errors);
|
||||
|
||||
context.currentQueryTotal = elms.length;
|
||||
let sameElementTimeline: TimelineBuilder|null = null;
|
||||
elms.forEach((element, i) => {
|
||||
|
||||
context.currentQueryIndex = i;
|
||||
const innerContext = context.createSubContext(ast.options, element);
|
||||
if (delay) {
|
||||
innerContext.delayNextStep(delay);
|
||||
}
|
||||
|
||||
if (element === context.element) {
|
||||
sameElementTimeline = innerContext.currentTimeline;
|
||||
}
|
||||
|
||||
ast.animation.visit(this, innerContext);
|
||||
|
||||
// this is here just incase the inner steps only contain or end
|
||||
// with a style() call (which is here to signal that this is a preparatory
|
||||
// call to style an element before it is animated again)
|
||||
innerContext.currentTimeline.applyStylesToKeyframe();
|
||||
|
||||
const endTime = innerContext.currentTimeline.currentTime;
|
||||
furthestTime = Math.max(furthestTime, endTime);
|
||||
});
|
||||
|
||||
context.currentQueryIndex = 0;
|
||||
context.currentQueryTotal = 0;
|
||||
context.transformIntoNewTimeline(furthestTime);
|
||||
|
||||
if (sameElementTimeline) {
|
||||
context.currentTimeline.mergeTimelineCollectedStyles(sameElementTimeline);
|
||||
context.currentTimeline.snapshotCurrentStyles();
|
||||
}
|
||||
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitStagger(ast: StaggerAst, context: AnimationTimelineContext) {
|
||||
const parentContext = context.parentContext !;
|
||||
const tl = context.currentTimeline;
|
||||
const timings = ast.timings;
|
||||
const duration = Math.abs(timings.duration);
|
||||
const maxTime = duration * (context.currentQueryTotal - 1);
|
||||
let delay = duration * context.currentQueryIndex;
|
||||
|
||||
let staggerTransformer = timings.duration < 0 ? 'reverse' : timings.easing;
|
||||
switch (staggerTransformer) {
|
||||
case 'reverse':
|
||||
delay = maxTime - delay;
|
||||
break;
|
||||
case 'full':
|
||||
delay = parentContext.currentStaggerTime;
|
||||
break;
|
||||
}
|
||||
|
||||
if (delay) {
|
||||
context.currentTimeline.delayNextStep(delay);
|
||||
}
|
||||
|
||||
const startingTime = context.currentTimeline.currentTime;
|
||||
ast.animation.visit(this, context);
|
||||
context.previousNode = ast;
|
||||
|
||||
// time = duration + delay
|
||||
// the reason why this computation is so complex is because
|
||||
// the inner timeline may either have a delay value or a stretched
|
||||
// keyframe depending on if a subtimeline is not used or is used.
|
||||
parentContext.currentStaggerTime =
|
||||
(tl.currentTime - startingTime) + (tl.startTime - parentContext.currentTimeline.startTime);
|
||||
}
|
||||
}
|
||||
|
||||
export class TimelineBuilder {
|
||||
public duration: number = 0;
|
||||
public easing: string|null;
|
||||
private _previousKeyframe: ɵStyleData = {};
|
||||
private _currentKeyframe: ɵStyleData = {};
|
||||
private _keyframes = new Map<number, ɵStyleData>();
|
||||
private _styleSummary: {[prop: string]: StyleAtTime} = {};
|
||||
private _localTimelineStyles: ɵStyleData;
|
||||
private _globalTimelineStyles: ɵStyleData;
|
||||
private _pendingStyles: ɵStyleData = {};
|
||||
private _backFill: ɵStyleData = {};
|
||||
private _currentEmptyStepKeyframe: ɵStyleData|null = null;
|
||||
|
||||
constructor(
|
||||
public element: any, public startTime: number,
|
||||
private _elementTimelineStylesLookup?: Map<any, ɵStyleData>) {
|
||||
if (!this._elementTimelineStylesLookup) {
|
||||
this._elementTimelineStylesLookup = new Map<any, ɵStyleData>();
|
||||
}
|
||||
|
||||
this._localTimelineStyles = Object.create(this._backFill, {});
|
||||
this._globalTimelineStyles = this._elementTimelineStylesLookup.get(element) !;
|
||||
if (!this._globalTimelineStyles) {
|
||||
this._globalTimelineStyles = this._localTimelineStyles;
|
||||
this._elementTimelineStylesLookup.set(element, this._localTimelineStyles);
|
||||
}
|
||||
this._loadKeyframe();
|
||||
}
|
||||
|
||||
containsAnimation(): boolean {
|
||||
switch (this._keyframes.size) {
|
||||
case 0:
|
||||
return false;
|
||||
case 1:
|
||||
return this.getCurrentStyleProperties().length > 0;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentStyleProperties(): string[] { return Object.keys(this._currentKeyframe); }
|
||||
|
||||
get currentTime() { return this.startTime + this.duration; }
|
||||
|
||||
delayNextStep(delay: number) {
|
||||
if (this.duration == 0) {
|
||||
this.startTime += delay;
|
||||
} else {
|
||||
this.forwardTime(this.currentTime + delay);
|
||||
}
|
||||
}
|
||||
|
||||
fork(element: any, currentTime?: number): TimelineBuilder {
|
||||
this.applyStylesToKeyframe();
|
||||
return new TimelineBuilder(
|
||||
element, currentTime || this.currentTime, this._elementTimelineStylesLookup);
|
||||
}
|
||||
|
||||
private _loadKeyframe() {
|
||||
if (this._currentKeyframe) {
|
||||
this._previousKeyframe = this._currentKeyframe;
|
||||
}
|
||||
this._currentKeyframe = this._keyframes.get(this.duration) !;
|
||||
if (!this._currentKeyframe) {
|
||||
this._currentKeyframe = Object.create(this._backFill, {});
|
||||
this._keyframes.set(this.duration, this._currentKeyframe);
|
||||
}
|
||||
}
|
||||
|
||||
forwardFrame() {
|
||||
this.duration += ONE_FRAME_IN_MILLISECONDS;
|
||||
this._loadKeyframe();
|
||||
}
|
||||
|
||||
forwardTime(time: number) {
|
||||
this.applyStylesToKeyframe();
|
||||
this.duration = time;
|
||||
this._loadKeyframe();
|
||||
}
|
||||
|
||||
private _updateStyle(prop: string, value: string|number) {
|
||||
this._localTimelineStyles[prop] = value;
|
||||
this._globalTimelineStyles[prop] = value;
|
||||
this._styleSummary[prop] = {time: this.currentTime, value};
|
||||
}
|
||||
|
||||
allowOnlyTimelineStyles() { return this._currentEmptyStepKeyframe !== this._currentKeyframe; }
|
||||
|
||||
applyEmptyStep(easing: string|null) {
|
||||
if (easing) {
|
||||
this._previousKeyframe['easing'] = easing;
|
||||
}
|
||||
|
||||
// special case for animate(duration):
|
||||
// all missing styles are filled with a `*` value then
|
||||
// if any destination styles are filled in later on the same
|
||||
// keyframe then they will override the overridden styles
|
||||
// We use `_globalTimelineStyles` here because there may be
|
||||
// styles in previous keyframes that are not present in this timeline
|
||||
Object.keys(this._globalTimelineStyles).forEach(prop => {
|
||||
this._backFill[prop] = this._globalTimelineStyles[prop] || AUTO_STYLE;
|
||||
this._currentKeyframe[prop] = AUTO_STYLE;
|
||||
});
|
||||
this._currentEmptyStepKeyframe = this._currentKeyframe;
|
||||
}
|
||||
|
||||
setStyles(
|
||||
input: (ɵStyleData|string)[], easing: string|null, errors: any[],
|
||||
options?: AnimationOptions) {
|
||||
if (easing) {
|
||||
this._previousKeyframe['easing'] = easing;
|
||||
}
|
||||
|
||||
const params = (options && options.params) || {};
|
||||
const styles = flattenStyles(input, this._globalTimelineStyles);
|
||||
Object.keys(styles).forEach(prop => {
|
||||
const val = interpolateParams(styles[prop], params, errors);
|
||||
this._pendingStyles[prop] = val;
|
||||
if (!this._localTimelineStyles.hasOwnProperty(prop)) {
|
||||
this._backFill[prop] = this._globalTimelineStyles.hasOwnProperty(prop) ?
|
||||
this._globalTimelineStyles[prop] :
|
||||
AUTO_STYLE;
|
||||
}
|
||||
this._updateStyle(prop, val);
|
||||
});
|
||||
}
|
||||
|
||||
applyStylesToKeyframe() {
|
||||
const styles = this._pendingStyles;
|
||||
const props = Object.keys(styles);
|
||||
if (props.length == 0) return;
|
||||
|
||||
this._pendingStyles = {};
|
||||
|
||||
props.forEach(prop => {
|
||||
const val = styles[prop];
|
||||
this._currentKeyframe[prop] = val;
|
||||
});
|
||||
|
||||
Object.keys(this._localTimelineStyles).forEach(prop => {
|
||||
if (!this._currentKeyframe.hasOwnProperty(prop)) {
|
||||
this._currentKeyframe[prop] = this._localTimelineStyles[prop];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
snapshotCurrentStyles() {
|
||||
Object.keys(this._localTimelineStyles).forEach(prop => {
|
||||
const val = this._localTimelineStyles[prop];
|
||||
this._pendingStyles[prop] = val;
|
||||
this._updateStyle(prop, val);
|
||||
});
|
||||
}
|
||||
|
||||
getFinalKeyframe() { return this._keyframes.get(this.duration); }
|
||||
|
||||
get properties() {
|
||||
const properties: string[] = [];
|
||||
for (let prop in this._currentKeyframe) {
|
||||
properties.push(prop);
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
mergeTimelineCollectedStyles(timeline: TimelineBuilder) {
|
||||
Object.keys(timeline._styleSummary).forEach(prop => {
|
||||
const details0 = this._styleSummary[prop];
|
||||
const details1 = timeline._styleSummary[prop];
|
||||
if (!details0 || details1.time > details0.time) {
|
||||
this._updateStyle(prop, details1.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
buildKeyframes(): AnimationTimelineInstruction {
|
||||
this.applyStylesToKeyframe();
|
||||
const preStyleProps = new Set<string>();
|
||||
const postStyleProps = new Set<string>();
|
||||
const isEmpty = this._keyframes.size === 1 && this.duration === 0;
|
||||
|
||||
let finalKeyframes: ɵStyleData[] = [];
|
||||
this._keyframes.forEach((keyframe, time) => {
|
||||
const finalKeyframe = copyStyles(keyframe, true);
|
||||
Object.keys(finalKeyframe).forEach(prop => {
|
||||
const value = finalKeyframe[prop];
|
||||
if (value == PRE_STYLE) {
|
||||
preStyleProps.add(prop);
|
||||
} else if (value == AUTO_STYLE) {
|
||||
postStyleProps.add(prop);
|
||||
}
|
||||
});
|
||||
if (!isEmpty) {
|
||||
finalKeyframe['offset'] = time / this.duration;
|
||||
}
|
||||
finalKeyframes.push(finalKeyframe);
|
||||
});
|
||||
|
||||
const preProps: string[] = preStyleProps.size ? iteratorToArray(preStyleProps.values()) : [];
|
||||
const postProps: string[] = postStyleProps.size ? iteratorToArray(postStyleProps.values()) : [];
|
||||
|
||||
// special case for a 0-second animation (which is designed just to place styles onscreen)
|
||||
if (isEmpty) {
|
||||
const kf0 = finalKeyframes[0];
|
||||
const kf1 = copyObj(kf0);
|
||||
kf0['offset'] = 0;
|
||||
kf1['offset'] = 1;
|
||||
finalKeyframes = [kf0, kf1];
|
||||
}
|
||||
|
||||
return createTimelineInstruction(
|
||||
this.element, finalKeyframes, preProps, postProps, this.duration, this.startTime,
|
||||
this.easing, false);
|
||||
}
|
||||
}
|
||||
|
||||
class SubTimelineBuilder extends TimelineBuilder {
|
||||
public timings: AnimateTimings;
|
||||
|
||||
constructor(
|
||||
public element: any, public keyframes: ɵStyleData[], public preStyleProps: string[],
|
||||
public postStyleProps: string[], timings: AnimateTimings,
|
||||
private _stretchStartingKeyframe: boolean = false) {
|
||||
super(element, timings.delay);
|
||||
this.timings = {duration: timings.duration, delay: timings.delay, easing: timings.easing};
|
||||
}
|
||||
|
||||
containsAnimation(): boolean { return this.keyframes.length > 1; }
|
||||
|
||||
buildKeyframes(): AnimationTimelineInstruction {
|
||||
let keyframes = this.keyframes;
|
||||
let {delay, duration, easing} = this.timings;
|
||||
if (this._stretchStartingKeyframe && delay) {
|
||||
const newKeyframes: ɵStyleData[] = [];
|
||||
const totalTime = duration + delay;
|
||||
const startingGap = delay / totalTime;
|
||||
|
||||
// the original starting keyframe now starts once the delay is done
|
||||
const newFirstKeyframe = copyStyles(keyframes[0], false);
|
||||
newFirstKeyframe['offset'] = 0;
|
||||
newKeyframes.push(newFirstKeyframe);
|
||||
|
||||
const oldFirstKeyframe = copyStyles(keyframes[0], false);
|
||||
oldFirstKeyframe['offset'] = roundOffset(startingGap);
|
||||
newKeyframes.push(oldFirstKeyframe);
|
||||
|
||||
/*
|
||||
When the keyframe is stretched then it means that the delay before the animation
|
||||
starts is gone. Instead the first keyframe is placed at the start of the animation
|
||||
and it is then copied to where it starts when the original delay is over. This basically
|
||||
means nothing animates during that delay, but the styles are still renderered. For this
|
||||
to work the original offset values that exist in the original keyframes must be "warped"
|
||||
so that they can take the new keyframe + delay into account.
|
||||
|
||||
delay=1000, duration=1000, keyframes = 0 .5 1
|
||||
|
||||
turns into
|
||||
|
||||
delay=0, duration=2000, keyframes = 0 .33 .66 1
|
||||
*/
|
||||
|
||||
// offsets between 1 ... n -1 are all warped by the keyframe stretch
|
||||
const limit = keyframes.length - 1;
|
||||
for (let i = 1; i <= limit; i++) {
|
||||
let kf = copyStyles(keyframes[i], false);
|
||||
const oldOffset = kf['offset'] as number;
|
||||
const timeAtKeyframe = delay + oldOffset * duration;
|
||||
kf['offset'] = roundOffset(timeAtKeyframe / totalTime);
|
||||
newKeyframes.push(kf);
|
||||
}
|
||||
|
||||
// the new starting keyframe should be added at the start
|
||||
duration = totalTime;
|
||||
delay = 0;
|
||||
easing = '';
|
||||
|
||||
keyframes = newKeyframes;
|
||||
}
|
||||
|
||||
return createTimelineInstruction(
|
||||
this.element, keyframes, this.preStyleProps, this.postStyleProps, duration, delay, easing,
|
||||
true);
|
||||
}
|
||||
}
|
||||
|
||||
function invokeQuery(
|
||||
rootElement: any, selector: string, originalSelector: string, limit: number,
|
||||
includeSelf: boolean, optional: boolean, errors: any[]): any[] {
|
||||
const multi = limit != 1;
|
||||
let results: any[] = [];
|
||||
if (includeSelf) {
|
||||
results.push(rootElement);
|
||||
}
|
||||
if (selector.length > 0) { // if :self is only used then the selector is empty
|
||||
if (multi) {
|
||||
results.push(...rootElement.querySelectorAll(selector));
|
||||
if (limit > 1) {
|
||||
results = results.slice(0, limit);
|
||||
}
|
||||
} else {
|
||||
const elm = rootElement.querySelector(selector);
|
||||
if (elm) {
|
||||
results.push(elm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!optional && results.length == 0) {
|
||||
errors.push(
|
||||
`\`query("${originalSelector}")\` returned zero elements. (Use \`query("${originalSelector}", { optional: true })\` if you wish to allow this.)`);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function roundOffset(offset: number, decimalPoints = 3): number {
|
||||
const mult = Math.pow(10, decimalPoints - 1);
|
||||
return Math.round(offset * mult) / mult;
|
||||
}
|
||||
|
||||
function flattenStyles(input: (ɵStyleData | string)[], allStyles: ɵStyleData) {
|
||||
const styles: ɵStyleData = {};
|
||||
let allProperties: string[];
|
||||
input.forEach(token => {
|
||||
if (token === '*') {
|
||||
allProperties = allProperties || Object.keys(allStyles);
|
||||
allProperties.forEach(prop => { styles[prop] = AUTO_STYLE; });
|
||||
} else {
|
||||
copyStyles(token as ɵStyleData, false, styles);
|
||||
}
|
||||
});
|
||||
return styles;
|
||||
}
|
|
@ -9,21 +9,30 @@ import {ɵStyleData} from '@angular/animations';
|
|||
import {AnimationEngineInstruction, AnimationTransitionInstructionType} from '../render/animation_engine_instruction';
|
||||
|
||||
export interface AnimationTimelineInstruction extends AnimationEngineInstruction {
|
||||
element: any;
|
||||
keyframes: ɵStyleData[];
|
||||
preStyleProps: string[];
|
||||
postStyleProps: string[];
|
||||
duration: number;
|
||||
delay: number;
|
||||
totalTime: number;
|
||||
easing: string|null|undefined;
|
||||
easing: string|null;
|
||||
stretchStartingKeyframe?: boolean;
|
||||
subTimeline: boolean;
|
||||
}
|
||||
|
||||
export function createTimelineInstruction(
|
||||
keyframes: ɵStyleData[], duration: number, delay: number,
|
||||
easing: string | null | undefined): AnimationTimelineInstruction {
|
||||
element: any, keyframes: ɵStyleData[], preStyleProps: string[], postStyleProps: string[],
|
||||
duration: number, delay: number, easing: string | null = null,
|
||||
subTimeline: boolean = false): AnimationTimelineInstruction {
|
||||
return {
|
||||
type: AnimationTransitionInstructionType.TimelineAnimation,
|
||||
element,
|
||||
keyframes,
|
||||
preStyleProps,
|
||||
postStyleProps,
|
||||
duration,
|
||||
delay,
|
||||
totalTime: duration + delay, easing
|
||||
totalTime: duration + delay, easing, subTimeline
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,470 +0,0 @@
|
|||
/**
|
||||
* @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 {AUTO_STYLE, AnimateTimings, AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, sequence, style, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {copyStyles, normalizeStyles, parseTimeExpression} from '../util';
|
||||
|
||||
import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor';
|
||||
import {AnimationTimelineInstruction, createTimelineInstruction} from './animation_timeline_instruction';
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* The code within this file aims to generate web-animations-compatible keyframes from Angular's
|
||||
* animation DSL code.
|
||||
*
|
||||
* The code below will be converted from:
|
||||
*
|
||||
* ```
|
||||
* sequence([
|
||||
* style({ opacity: 0 }),
|
||||
* animate(1000, style({ opacity: 0 }))
|
||||
* ])
|
||||
* ```
|
||||
*
|
||||
* To:
|
||||
* ```
|
||||
* keyframes = [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }]
|
||||
* duration = 1000
|
||||
* delay = 0
|
||||
* easing = ''
|
||||
* ```
|
||||
*
|
||||
* For this operation to cover the combination of animation verbs (style, animate, group, etc...) a
|
||||
* combination of prototypical inheritance, AST traversal and merge-sort-like algorithms are used.
|
||||
*
|
||||
* [AST Traversal]
|
||||
* Each of the animation verbs, when executed, will return an string-map object representing what
|
||||
* type of action it is (style, animate, group, etc...) and the data associated with it. This means
|
||||
* that when functional composition mix of these functions is evaluated (like in the example above)
|
||||
* then it will end up producing a tree of objects representing the animation itself.
|
||||
*
|
||||
* When this animation object tree is processed by the visitor code below it will visit each of the
|
||||
* verb statements within the visitor. And during each visit it will build the context of the
|
||||
* animation keyframes by interacting with the `TimelineBuilder`.
|
||||
*
|
||||
* [TimelineBuilder]
|
||||
* This class is responsible for tracking the styles and building a series of keyframe objects for a
|
||||
* timeline between a start and end time. The builder starts off with an initial timeline and each
|
||||
* time the AST comes across a `group()`, `keyframes()` or a combination of the two within a
|
||||
* `sequence()` then it will generate a sub timeline for each step as well as a new one after
|
||||
* they are complete.
|
||||
*
|
||||
* As the AST is traversed, the timing state on each of the timelines will be incremented. If a sub
|
||||
* timeline was created (based on one of the cases above) then the parent timeline will attempt to
|
||||
* merge the styles used within the sub timelines into itself (only with group() this will happen).
|
||||
* This happens with a merge operation (much like how the merge works in mergesort) and it will only
|
||||
* copy the most recently used styles from the sub timelines into the parent timeline. This ensures
|
||||
* that if the styles are used later on in another phase of the animation then they will be the most
|
||||
* up-to-date values.
|
||||
*
|
||||
* [How Missing Styles Are Updated]
|
||||
* Each timeline has a `backFill` property which is responsible for filling in new styles into
|
||||
* already processed keyframes if a new style shows up later within the animation sequence.
|
||||
*
|
||||
* ```
|
||||
* sequence([
|
||||
* style({ width: 0 }),
|
||||
* animate(1000, style({ width: 100 })),
|
||||
* animate(1000, style({ width: 200 })),
|
||||
* animate(1000, style({ width: 300 }))
|
||||
* animate(1000, style({ width: 400, height: 400 })) // notice how `height` doesn't exist anywhere
|
||||
* else
|
||||
* ])
|
||||
* ```
|
||||
*
|
||||
* What is happening here is that the `height` value is added later in the sequence, but is missing
|
||||
* from all previous animation steps. Therefore when a keyframe is created it would also be missing
|
||||
* from all previous keyframes up until where it is first used. For the timeline keyframe generation
|
||||
* to properly fill in the style it will place the previous value (the value from the parent
|
||||
* timeline) or a default value of `*` into the backFill object. Given that each of the keyframe
|
||||
* styles are objects that prototypically inherited from the backFill object, this means that if a
|
||||
* value is added into the backFill then it will automatically propagate any missing values to all
|
||||
* keyframes. Therefore the missing `height` value will be properly filled into the already
|
||||
* processed keyframes.
|
||||
*
|
||||
* When a sub-timeline is created it will have its own backFill property. This is done so that
|
||||
* styles present within the sub-timeline do not accidentally seep into the previous/future timeline
|
||||
* keyframes
|
||||
*
|
||||
* (For prototypically inherited contents to be detected a `for(i in obj)` loop must be used.)
|
||||
*
|
||||
* [Validation]
|
||||
* The code in this file is not responsible for validation. That functionality happens with within
|
||||
* the `AnimationValidatorVisitor` code.
|
||||
*/
|
||||
export function buildAnimationKeyframes(
|
||||
ast: AnimationMetadata | AnimationMetadata[], startingStyles: ɵStyleData = {},
|
||||
finalStyles: ɵStyleData = {}): AnimationTimelineInstruction[] {
|
||||
const normalizedAst =
|
||||
Array.isArray(ast) ? sequence(<AnimationMetadata[]>ast) : <AnimationMetadata>ast;
|
||||
return new AnimationTimelineVisitor().buildKeyframes(normalizedAst, startingStyles, finalStyles);
|
||||
}
|
||||
|
||||
export declare type StyleAtTime = {
|
||||
time: number; value: string | number;
|
||||
};
|
||||
|
||||
export class AnimationTimelineContext {
|
||||
currentTimeline: TimelineBuilder;
|
||||
currentAnimateTimings: AnimateTimings|null;
|
||||
previousNode: AnimationMetadata = <AnimationMetadata>{};
|
||||
subContextCount = 0;
|
||||
|
||||
constructor(
|
||||
public errors: any[], public timelines: TimelineBuilder[],
|
||||
initialTimeline?: TimelineBuilder) {
|
||||
this.currentTimeline = initialTimeline || new TimelineBuilder(0);
|
||||
timelines.push(this.currentTimeline);
|
||||
}
|
||||
|
||||
createSubContext(): AnimationTimelineContext {
|
||||
const context =
|
||||
new AnimationTimelineContext(this.errors, this.timelines, this.currentTimeline.fork());
|
||||
context.previousNode = this.previousNode;
|
||||
context.currentAnimateTimings = this.currentAnimateTimings;
|
||||
this.subContextCount++;
|
||||
return context;
|
||||
}
|
||||
|
||||
transformIntoNewTimeline(newTime = 0) {
|
||||
this.currentTimeline = this.currentTimeline.fork(newTime);
|
||||
this.timelines.push(this.currentTimeline);
|
||||
return this.currentTimeline;
|
||||
}
|
||||
|
||||
incrementTime(time: number) {
|
||||
this.currentTimeline.forwardTime(this.currentTimeline.duration + time);
|
||||
}
|
||||
}
|
||||
|
||||
export class AnimationTimelineVisitor implements AnimationDslVisitor {
|
||||
buildKeyframes(ast: AnimationMetadata, startingStyles: ɵStyleData, finalStyles: ɵStyleData):
|
||||
AnimationTimelineInstruction[] {
|
||||
const context = new AnimationTimelineContext([], []);
|
||||
context.currentTimeline.setStyles(startingStyles);
|
||||
|
||||
visitAnimationNode(this, ast, context);
|
||||
|
||||
// this checks to see if an actual animation happened
|
||||
const timelines = context.timelines.filter(timeline => timeline.hasStyling());
|
||||
if (timelines.length && Object.keys(finalStyles).length) {
|
||||
const tl = timelines[timelines.length - 1];
|
||||
if (!tl.allowOnlyTimelineStyles()) {
|
||||
tl.setStyles(finalStyles);
|
||||
}
|
||||
}
|
||||
|
||||
return timelines.length ? timelines.map(timeline => timeline.buildKeyframes()) :
|
||||
[createTimelineInstruction([], 0, 0, '')];
|
||||
}
|
||||
|
||||
visitState(ast: AnimationStateMetadata, context: any): any {
|
||||
// these values are not visited in this AST
|
||||
}
|
||||
|
||||
visitTransition(ast: AnimationTransitionMetadata, context: any): any {
|
||||
// these values are not visited in this AST
|
||||
}
|
||||
|
||||
visitSequence(ast: AnimationSequenceMetadata, context: AnimationTimelineContext) {
|
||||
const subContextCount = context.subContextCount;
|
||||
if (context.previousNode.type == AnimationMetadataType.Style) {
|
||||
context.currentTimeline.forwardFrame();
|
||||
context.currentTimeline.snapshotCurrentStyles();
|
||||
}
|
||||
|
||||
ast.steps.forEach(s => visitAnimationNode(this, s, context));
|
||||
|
||||
// this means that some animation function within the sequence
|
||||
// ended up creating a sub timeline (which means the current
|
||||
// timeline cannot overlap with the contents of the sequence)
|
||||
if (context.subContextCount > subContextCount) {
|
||||
context.transformIntoNewTimeline();
|
||||
}
|
||||
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitGroup(ast: AnimationGroupMetadata, context: AnimationTimelineContext) {
|
||||
const innerTimelines: TimelineBuilder[] = [];
|
||||
let furthestTime = context.currentTimeline.currentTime;
|
||||
ast.steps.forEach(s => {
|
||||
const innerContext = context.createSubContext();
|
||||
visitAnimationNode(this, s, innerContext);
|
||||
furthestTime = Math.max(furthestTime, innerContext.currentTimeline.currentTime);
|
||||
innerTimelines.push(innerContext.currentTimeline);
|
||||
});
|
||||
|
||||
// this operation is run after the AST loop because otherwise
|
||||
// if the parent timeline's collected styles were updated then
|
||||
// it would pass in invalid data into the new-to-be forked items
|
||||
innerTimelines.forEach(
|
||||
timeline => context.currentTimeline.mergeTimelineCollectedStyles(timeline));
|
||||
context.transformIntoNewTimeline(furthestTime);
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitAnimate(ast: AnimationAnimateMetadata, context: AnimationTimelineContext) {
|
||||
const timings = ast.timings.hasOwnProperty('duration') ?
|
||||
<AnimateTimings>ast.timings :
|
||||
parseTimeExpression(<string|number>ast.timings, context.errors);
|
||||
context.currentAnimateTimings = timings;
|
||||
|
||||
if (timings.delay) {
|
||||
context.incrementTime(timings.delay);
|
||||
context.currentTimeline.snapshotCurrentStyles();
|
||||
}
|
||||
|
||||
const astType = ast.styles ? ast.styles.type : -1;
|
||||
if (astType == AnimationMetadataType.KeyframeSequence) {
|
||||
this.visitKeyframeSequence(<AnimationKeyframesSequenceMetadata>ast.styles, context);
|
||||
} else {
|
||||
let styleAst = ast.styles as AnimationStyleMetadata;
|
||||
if (!styleAst) {
|
||||
const newStyleData: {[prop: string]: string | number} = {};
|
||||
if (timings.easing) {
|
||||
newStyleData['easing'] = timings.easing;
|
||||
}
|
||||
styleAst = style(newStyleData);
|
||||
(styleAst as any)['treatAsEmptyStep'] = true;
|
||||
}
|
||||
context.incrementTime(timings.duration);
|
||||
if (styleAst) {
|
||||
this.visitStyle(styleAst, context);
|
||||
}
|
||||
}
|
||||
|
||||
context.currentAnimateTimings = null;
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitStyle(ast: AnimationStyleMetadata, context: AnimationTimelineContext) {
|
||||
// this is a special case when a style() call is issued directly after
|
||||
// a call to animate(). If the clock is not forwarded by one frame then
|
||||
// the style() calls will be merged into the previous animate() call
|
||||
// which is incorrect.
|
||||
if (!context.currentAnimateTimings &&
|
||||
context.previousNode.type == AnimationMetadataType.Animate) {
|
||||
context.currentTimeline.forwardFrame();
|
||||
}
|
||||
|
||||
const normalizedStyles = normalizeStyles(ast.styles);
|
||||
const easing = context.currentAnimateTimings && context.currentAnimateTimings.easing;
|
||||
this._applyStyles(
|
||||
normalizedStyles, easing, (ast as any)['treatAsEmptyStep'] ? true : false, context);
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
private _applyStyles(
|
||||
styles: ɵStyleData, easing: string|null, treatAsEmptyStep: boolean,
|
||||
context: AnimationTimelineContext) {
|
||||
if (styles.hasOwnProperty('easing')) {
|
||||
easing = easing || styles['easing'] as string;
|
||||
delete styles['easing'];
|
||||
}
|
||||
context.currentTimeline.setStyles(styles, easing, treatAsEmptyStep);
|
||||
}
|
||||
|
||||
visitKeyframeSequence(
|
||||
ast: AnimationKeyframesSequenceMetadata, context: AnimationTimelineContext) {
|
||||
const MAX_KEYFRAME_OFFSET = 1;
|
||||
const limit = ast.steps.length - 1;
|
||||
const firstKeyframe = ast.steps[0];
|
||||
|
||||
let offsetGap = 0;
|
||||
const containsOffsets = getOffset(firstKeyframe) != null;
|
||||
if (!containsOffsets) {
|
||||
offsetGap = MAX_KEYFRAME_OFFSET / limit;
|
||||
}
|
||||
|
||||
const startTime = context.currentTimeline.duration;
|
||||
const duration = context.currentAnimateTimings !.duration;
|
||||
const innerContext = context.createSubContext();
|
||||
const innerTimeline = innerContext.currentTimeline;
|
||||
innerTimeline.easing = context.currentAnimateTimings !.easing;
|
||||
|
||||
ast.steps.forEach((step: AnimationStyleMetadata, i: number) => {
|
||||
const normalizedStyles = normalizeStyles(step.styles);
|
||||
const offset = containsOffsets ?
|
||||
(step.offset != null ? step.offset : parseFloat(normalizedStyles['offset'] as string)) :
|
||||
(i == limit ? MAX_KEYFRAME_OFFSET : i * offsetGap);
|
||||
innerTimeline.forwardTime(offset * duration);
|
||||
this._applyStyles(normalizedStyles, null, false, innerContext);
|
||||
});
|
||||
|
||||
// this will ensure that the parent timeline gets all the styles from
|
||||
// the child even if the new timeline below is not used
|
||||
context.currentTimeline.mergeTimelineCollectedStyles(innerTimeline);
|
||||
|
||||
// we do this because the window between this timeline and the sub timeline
|
||||
// should ensure that the styles within are exactly the same as they were before
|
||||
context.transformIntoNewTimeline(startTime + duration);
|
||||
context.previousNode = ast;
|
||||
}
|
||||
}
|
||||
|
||||
export class TimelineBuilder {
|
||||
public duration: number = 0;
|
||||
public easing: string|null = '';
|
||||
private _previousKeyframe: ɵStyleData = {};
|
||||
private _currentKeyframe: ɵStyleData;
|
||||
private _keyframes = new Map<number, ɵStyleData>();
|
||||
private _styleSummary: {[prop: string]: StyleAtTime} = {};
|
||||
private _localTimelineStyles: ɵStyleData;
|
||||
private _backFill: ɵStyleData = {};
|
||||
private _currentEmptyStepKeyframe: ɵStyleData|null = null;
|
||||
private _globalTimelineStyles: ɵStyleData;
|
||||
|
||||
constructor(public startTime: number, globalTimelineStyles?: ɵStyleData) {
|
||||
this._localTimelineStyles = Object.create(this._backFill, {});
|
||||
this._globalTimelineStyles =
|
||||
globalTimelineStyles ? globalTimelineStyles : this._localTimelineStyles;
|
||||
this._loadKeyframe();
|
||||
}
|
||||
|
||||
hasStyling(): boolean { return this._keyframes.size > 1; }
|
||||
|
||||
get currentTime() { return this.startTime + this.duration; }
|
||||
|
||||
fork(currentTime = 0): TimelineBuilder {
|
||||
return new TimelineBuilder(currentTime || this.currentTime, this._globalTimelineStyles);
|
||||
}
|
||||
|
||||
private _loadKeyframe() {
|
||||
if (this._currentKeyframe) {
|
||||
this._previousKeyframe = this._currentKeyframe;
|
||||
}
|
||||
this._currentKeyframe = this._keyframes.get(this.duration) !;
|
||||
if (!this._currentKeyframe) {
|
||||
this._currentKeyframe = Object.create(this._backFill, {});
|
||||
this._keyframes.set(this.duration, this._currentKeyframe);
|
||||
}
|
||||
}
|
||||
|
||||
forwardFrame() {
|
||||
this.duration++;
|
||||
this._loadKeyframe();
|
||||
}
|
||||
|
||||
forwardTime(time: number) {
|
||||
this.duration = time;
|
||||
this._loadKeyframe();
|
||||
}
|
||||
|
||||
private _updateStyle(prop: string, value: string|number) {
|
||||
this._localTimelineStyles[prop] = value;
|
||||
this._globalTimelineStyles ![prop] = value;
|
||||
this._styleSummary[prop] = {time: this.currentTime, value};
|
||||
}
|
||||
|
||||
allowOnlyTimelineStyles() { return this._currentEmptyStepKeyframe !== this._currentKeyframe; }
|
||||
|
||||
setStyles(styles: ɵStyleData, easing: string|null = null, treatAsEmptyStep: boolean = false) {
|
||||
if (easing) {
|
||||
this._previousKeyframe !['easing'] = easing;
|
||||
}
|
||||
|
||||
if (treatAsEmptyStep) {
|
||||
// special case for animate(duration):
|
||||
// all missing styles are filled with a `*` value then
|
||||
// if any destination styles are filled in later on the same
|
||||
// keyframe then they will override the overridden styles
|
||||
// We use `_globalTimelineStyles` here because there may be
|
||||
// styles in previous keyframes that are not present in this timeline
|
||||
Object.keys(this._globalTimelineStyles).forEach(prop => {
|
||||
this._backFill[prop] = this._globalTimelineStyles[prop] || AUTO_STYLE;
|
||||
this._currentKeyframe[prop] = AUTO_STYLE;
|
||||
});
|
||||
this._currentEmptyStepKeyframe = this._currentKeyframe;
|
||||
} else {
|
||||
Object.keys(styles).forEach(prop => {
|
||||
if (prop !== 'offset') {
|
||||
const val = styles[prop];
|
||||
this._currentKeyframe[prop] = val;
|
||||
if (!this._localTimelineStyles[prop]) {
|
||||
this._backFill[prop] = this._globalTimelineStyles[prop] || AUTO_STYLE;
|
||||
}
|
||||
this._updateStyle(prop, val);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(this._localTimelineStyles).forEach(prop => {
|
||||
if (!this._currentKeyframe.hasOwnProperty(prop)) {
|
||||
this._currentKeyframe[prop] = this._localTimelineStyles[prop];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
snapshotCurrentStyles() { copyStyles(this._localTimelineStyles, false, this._currentKeyframe); }
|
||||
|
||||
getFinalKeyframe(): ɵStyleData { return this._keyframes.get(this.duration) !; }
|
||||
|
||||
get properties() {
|
||||
const properties: string[] = [];
|
||||
for (let prop in this._currentKeyframe) {
|
||||
properties.push(prop);
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
mergeTimelineCollectedStyles(timeline: TimelineBuilder) {
|
||||
Object.keys(timeline._styleSummary).forEach(prop => {
|
||||
const details0 = this._styleSummary[prop];
|
||||
const details1 = timeline._styleSummary[prop];
|
||||
if (!details0 || details1.time > details0.time) {
|
||||
this._updateStyle(prop, details1.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
buildKeyframes(): AnimationTimelineInstruction {
|
||||
const finalKeyframes: ɵStyleData[] = [];
|
||||
// special case for when there are only start/destination
|
||||
// styles but no actual animation animate steps...
|
||||
if (this.duration == 0) {
|
||||
const targetKeyframe = this.getFinalKeyframe();
|
||||
|
||||
const firstKeyframe = copyStyles(targetKeyframe, true);
|
||||
firstKeyframe['offset'] = 0;
|
||||
finalKeyframes.push(firstKeyframe);
|
||||
|
||||
const lastKeyframe = copyStyles(targetKeyframe, true);
|
||||
lastKeyframe['offset'] = 1;
|
||||
finalKeyframes.push(lastKeyframe);
|
||||
} else {
|
||||
this._keyframes.forEach((keyframe, time) => {
|
||||
const finalKeyframe = copyStyles(keyframe, true);
|
||||
finalKeyframe['offset'] = time / this.duration;
|
||||
finalKeyframes.push(finalKeyframe);
|
||||
});
|
||||
}
|
||||
|
||||
return createTimelineInstruction(finalKeyframes, this.duration, this.startTime, this.easing);
|
||||
}
|
||||
}
|
||||
|
||||
function getOffset(ast: AnimationStyleMetadata): number {
|
||||
let offset = ast.offset;
|
||||
if (offset == null) {
|
||||
const styles = ast.styles;
|
||||
if (Array.isArray(styles)) {
|
||||
for (let i = 0; i < styles.length; i++) {
|
||||
const o = styles[i]['offset'] as number;
|
||||
if (o != null) {
|
||||
offset = o;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
offset = styles['offset'] as number;
|
||||
}
|
||||
}
|
||||
return offset !;
|
||||
}
|
|
@ -57,8 +57,16 @@ function parseAnimationAlias(alias: string, errors: string[]): string {
|
|||
|
||||
function makeLambdaFromStates(lhs: string, rhs: string): TransitionMatcherFn {
|
||||
return (fromState: any, toState: any): boolean => {
|
||||
const lhsMatch = lhs == ANY_STATE || lhs == fromState;
|
||||
const rhsMatch = rhs == ANY_STATE || rhs == toState;
|
||||
let lhsMatch = lhs == ANY_STATE || lhs == fromState;
|
||||
let rhsMatch = rhs == ANY_STATE || rhs == toState;
|
||||
|
||||
if (!lhsMatch && typeof fromState === 'boolean') {
|
||||
lhsMatch = fromState ? lhs === 'true' : lhs === 'false';
|
||||
}
|
||||
if (!rhsMatch && typeof toState === 'boolean') {
|
||||
rhsMatch = toState ? rhs === 'true' : rhs === 'false';
|
||||
}
|
||||
|
||||
return lhsMatch && rhsMatch;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,38 +5,66 @@
|
|||
* 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 {AnimationMetadata, AnimationTransitionMetadata, sequence, ɵStyleData} from '@angular/animations';
|
||||
import {AnimationOptions, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {buildAnimationKeyframes} from './animation_timeline_visitor';
|
||||
import {getOrSetAsInMap} from '../render/shared';
|
||||
import {iteratorToArray, mergeAnimationOptions} from '../util';
|
||||
|
||||
import {TransitionAst} from './animation_ast';
|
||||
import {buildAnimationTimelines} from './animation_timeline_builder';
|
||||
import {TransitionMatcherFn} from './animation_transition_expr';
|
||||
import {AnimationTransitionInstruction, createTransitionInstruction} from './animation_transition_instruction';
|
||||
import {ElementInstructionMap} from './element_instruction_map';
|
||||
|
||||
export class AnimationTransitionFactory {
|
||||
private _animationAst: AnimationMetadata;
|
||||
|
||||
constructor(
|
||||
private _triggerName: string, ast: AnimationTransitionMetadata,
|
||||
private matchFns: TransitionMatcherFn[],
|
||||
private _stateStyles: {[stateName: string]: ɵStyleData}) {
|
||||
const normalizedAst = Array.isArray(ast.animation) ?
|
||||
sequence(<AnimationMetadata[]>ast.animation) :
|
||||
<AnimationMetadata>ast.animation;
|
||||
this._animationAst = normalizedAst;
|
||||
private _triggerName: string, public ast: TransitionAst,
|
||||
private _stateStyles: {[stateName: string]: ɵStyleData}) {}
|
||||
|
||||
match(currentState: any, nextState: any): boolean {
|
||||
return oneOrMoreTransitionsMatch(this.ast.matchers, currentState, nextState);
|
||||
}
|
||||
|
||||
match(currentState: any, nextState: any): AnimationTransitionInstruction|undefined {
|
||||
if (!oneOrMoreTransitionsMatch(this.matchFns, currentState, nextState)) return;
|
||||
build(
|
||||
element: any, currentState: any, nextState: any, options?: AnimationOptions,
|
||||
subInstructions?: ElementInstructionMap): AnimationTransitionInstruction|undefined {
|
||||
const animationOptions = mergeAnimationOptions(this.ast.options || {}, options || {});
|
||||
|
||||
const backupStateStyles = this._stateStyles['*'] || {};
|
||||
const currentStateStyles = this._stateStyles[currentState] || backupStateStyles;
|
||||
const nextStateStyles = this._stateStyles[nextState] || backupStateStyles;
|
||||
|
||||
const timelines =
|
||||
buildAnimationKeyframes(this._animationAst, currentStateStyles, nextStateStyles);
|
||||
const errors: any[] = [];
|
||||
const timelines = buildAnimationTimelines(
|
||||
element, this.ast.animation, currentStateStyles, nextStateStyles, animationOptions,
|
||||
subInstructions, errors);
|
||||
|
||||
if (errors.length) {
|
||||
const errorMessage = `animation building failed:\n${errors.join("\n")}`;
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const preStyleMap = new Map<any, {[prop: string]: boolean}>();
|
||||
const postStyleMap = new Map<any, {[prop: string]: boolean}>();
|
||||
const queriedElements = new Set<any>();
|
||||
timelines.forEach(tl => {
|
||||
const elm = tl.element;
|
||||
const preProps = getOrSetAsInMap(preStyleMap, elm, {});
|
||||
tl.preStyleProps.forEach(prop => preProps[prop] = true);
|
||||
|
||||
const postProps = getOrSetAsInMap(postStyleMap, elm, {});
|
||||
tl.postStyleProps.forEach(prop => postProps[prop] = true);
|
||||
|
||||
if (elm !== element) {
|
||||
queriedElements.add(elm);
|
||||
}
|
||||
});
|
||||
|
||||
const queriedElementsList = iteratorToArray(queriedElements.values());
|
||||
return createTransitionInstruction(
|
||||
this._triggerName, currentState, nextState, nextState === 'void', currentStateStyles,
|
||||
nextStateStyles, timelines);
|
||||
element, this._triggerName, currentState, nextState, nextState === 'void',
|
||||
currentStateStyles, nextStateStyles, timelines, queriedElementsList, preStyleMap,
|
||||
postStyleMap);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import {AnimationEngineInstruction, AnimationTransitionInstructionType} from '..
|
|||
import {AnimationTimelineInstruction} from './animation_timeline_instruction';
|
||||
|
||||
export interface AnimationTransitionInstruction extends AnimationEngineInstruction {
|
||||
element: any;
|
||||
triggerName: string;
|
||||
isRemovalTransition: boolean;
|
||||
fromState: string;
|
||||
|
@ -17,20 +18,29 @@ export interface AnimationTransitionInstruction extends AnimationEngineInstructi
|
|||
toState: string;
|
||||
toStyles: ɵStyleData;
|
||||
timelines: AnimationTimelineInstruction[];
|
||||
queriedElements: any[];
|
||||
preStyleProps: Map<any, {[prop: string]: boolean}>;
|
||||
postStyleProps: Map<any, {[prop: string]: boolean}>;
|
||||
}
|
||||
|
||||
export function createTransitionInstruction(
|
||||
triggerName: string, fromState: string, toState: string, isRemovalTransition: boolean,
|
||||
fromStyles: ɵStyleData, toStyles: ɵStyleData,
|
||||
timelines: AnimationTimelineInstruction[]): AnimationTransitionInstruction {
|
||||
element: any, triggerName: string, fromState: string, toState: string,
|
||||
isRemovalTransition: boolean, fromStyles: ɵStyleData, toStyles: ɵStyleData,
|
||||
timelines: AnimationTimelineInstruction[], queriedElements: any[],
|
||||
preStyleProps: Map<any, {[prop: string]: boolean}>,
|
||||
postStyleProps: Map<any, {[prop: string]: boolean}>): AnimationTransitionInstruction {
|
||||
return {
|
||||
type: AnimationTransitionInstructionType.TransitionAnimation,
|
||||
element,
|
||||
triggerName,
|
||||
isRemovalTransition,
|
||||
fromState,
|
||||
fromStyles,
|
||||
toState,
|
||||
toStyles,
|
||||
timelines
|
||||
timelines,
|
||||
queriedElements,
|
||||
preStyleProps,
|
||||
postStyleProps
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,22 +5,18 @@
|
|||
* 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 {AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, ɵStyleData} from '@angular/animations';
|
||||
import {ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {copyStyles, normalizeStyles} from '../util';
|
||||
import {copyStyles} from '../util';
|
||||
|
||||
import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor';
|
||||
import {parseTransitionExpr} from './animation_transition_expr';
|
||||
import {SequenceAst, TransitionAst, TriggerAst} from './animation_ast';
|
||||
import {AnimationTransitionFactory} from './animation_transition_factory';
|
||||
import {AnimationTransitionInstruction, createTransitionInstruction} from './animation_transition_instruction';
|
||||
import {validateAnimationSequence} from './animation_validator_visitor';
|
||||
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export function buildTrigger(name: string, definitions: AnimationMetadata[]): AnimationTrigger {
|
||||
return new AnimationTriggerVisitor().buildTrigger(name, definitions);
|
||||
export function buildTrigger(name: string, ast: TriggerAst): AnimationTrigger {
|
||||
return new AnimationTrigger(name, ast);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -28,90 +24,51 @@ export function buildTrigger(name: string, definitions: AnimationMetadata[]): An
|
|||
*/
|
||||
export class AnimationTrigger {
|
||||
public transitionFactories: AnimationTransitionFactory[] = [];
|
||||
public fallbackTransition: AnimationTransitionFactory;
|
||||
public states: {[stateName: string]: ɵStyleData} = {};
|
||||
|
||||
constructor(
|
||||
public name: string, states: {[stateName: string]: ɵStyleData},
|
||||
private _transitionAsts: AnimationTransitionMetadata[]) {
|
||||
Object.keys(states).forEach(
|
||||
stateName => { this.states[stateName] = copyStyles(states[stateName], false); });
|
||||
|
||||
const errors: string[] = [];
|
||||
_transitionAsts.forEach(ast => {
|
||||
const exprs = parseTransitionExpr(ast.expr, errors);
|
||||
const sequenceErrors = validateAnimationSequence(ast);
|
||||
if (sequenceErrors.length) {
|
||||
errors.push(...sequenceErrors);
|
||||
} else {
|
||||
this.transitionFactories.push(
|
||||
new AnimationTransitionFactory(this.name, ast, exprs, states));
|
||||
}
|
||||
constructor(public name: string, public ast: TriggerAst) {
|
||||
ast.states.forEach(ast => {
|
||||
const obj = this.states[ast.name] = {};
|
||||
ast.style.styles.forEach(styleTuple => {
|
||||
if (typeof styleTuple == 'object') {
|
||||
copyStyles(styleTuple as ɵStyleData, false, obj);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (errors.length) {
|
||||
const LINE_START = '\n - ';
|
||||
throw new Error(
|
||||
`Animation parsing for the ${name} trigger have failed:${LINE_START}${errors.join(LINE_START)}`);
|
||||
balanceProperties(this.states, 'true', '1');
|
||||
balanceProperties(this.states, 'false', '0');
|
||||
|
||||
ast.transitions.forEach(ast => {
|
||||
this.transitionFactories.push(new AnimationTransitionFactory(name, ast, this.states));
|
||||
});
|
||||
|
||||
this.fallbackTransition = createFallbackTransition(name, this.states);
|
||||
}
|
||||
|
||||
get containsQueries() { return this.ast.queryCount > 0; }
|
||||
|
||||
matchTransition(currentState: any, nextState: any): AnimationTransitionFactory|null {
|
||||
const entry = this.transitionFactories.find(f => f.match(currentState, nextState));
|
||||
return entry || null;
|
||||
}
|
||||
}
|
||||
|
||||
function createFallbackTransition(
|
||||
triggerName: string, states: {[stateName: string]: ɵStyleData}): AnimationTransitionFactory {
|
||||
const matchers = [(fromState: any, toState: any) => true];
|
||||
const animation = new SequenceAst([]);
|
||||
const transition = new TransitionAst(matchers, animation);
|
||||
return new AnimationTransitionFactory(triggerName, transition, states);
|
||||
}
|
||||
|
||||
function balanceProperties(obj: {[key: string]: any}, key1: string, key2: string) {
|
||||
if (obj.hasOwnProperty(key1)) {
|
||||
if (!obj.hasOwnProperty(key2)) {
|
||||
obj[key2] = obj[key1];
|
||||
}
|
||||
}
|
||||
|
||||
createFallbackInstruction(currentState: any, nextState: any): AnimationTransitionInstruction {
|
||||
const backupStateStyles = this.states['*'] || {};
|
||||
const currentStateStyles = this.states[currentState] || backupStateStyles;
|
||||
const nextStateStyles = this.states[nextState] || backupStateStyles;
|
||||
return createTransitionInstruction(
|
||||
this.name, currentState, nextState, nextState == 'void', currentStateStyles,
|
||||
nextStateStyles, []);
|
||||
}
|
||||
|
||||
matchTransition(currentState: any, nextState: any): AnimationTransitionInstruction|null {
|
||||
for (let i = 0; i < this.transitionFactories.length; i++) {
|
||||
let result = this.transitionFactories[i].match(currentState, nextState);
|
||||
if (result) return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class AnimationTriggerContext {
|
||||
public errors: string[] = [];
|
||||
public states: {[stateName: string]: ɵStyleData} = {};
|
||||
public transitions: AnimationTransitionMetadata[] = [];
|
||||
}
|
||||
|
||||
class AnimationTriggerVisitor implements AnimationDslVisitor {
|
||||
buildTrigger(name: string, definitions: AnimationMetadata[]): AnimationTrigger {
|
||||
const context = new AnimationTriggerContext();
|
||||
definitions.forEach(def => visitAnimationNode(this, def, context));
|
||||
return new AnimationTrigger(name, context.states, context.transitions);
|
||||
}
|
||||
|
||||
visitState(ast: AnimationStateMetadata, context: any): any {
|
||||
const styles = normalizeStyles(ast.styles.styles);
|
||||
ast.name.split(/\s*,\s*/).forEach(name => { context.states[name] = styles; });
|
||||
}
|
||||
|
||||
visitTransition(ast: AnimationTransitionMetadata, context: any): any {
|
||||
context.transitions.push(ast);
|
||||
}
|
||||
|
||||
visitSequence(ast: AnimationSequenceMetadata, context: any) {
|
||||
// these values are not visited in this AST
|
||||
}
|
||||
|
||||
visitGroup(ast: AnimationGroupMetadata, context: any) {
|
||||
// these values are not visited in this AST
|
||||
}
|
||||
|
||||
visitAnimate(ast: AnimationAnimateMetadata, context: any) {
|
||||
// these values are not visited in this AST
|
||||
}
|
||||
|
||||
visitStyle(ast: AnimationStyleMetadata, context: any) {
|
||||
// these values are not visited in this AST
|
||||
}
|
||||
|
||||
visitKeyframeSequence(ast: AnimationKeyframesSequenceMetadata, context: any) {
|
||||
// these values are not visited in this AST
|
||||
} else if (obj.hasOwnProperty(key2)) {
|
||||
obj[key1] = obj[key2];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,195 +0,0 @@
|
|||
/**
|
||||
* @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 {AnimateTimings, AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, sequence} from '@angular/animations';
|
||||
|
||||
import {normalizeStyles, parseTimeExpression} from '../util';
|
||||
|
||||
import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor';
|
||||
|
||||
export type StyleTimeTuple = {
|
||||
startTime: number; endTime: number;
|
||||
};
|
||||
|
||||
/*
|
||||
* [Validation]
|
||||
* The visitor code below will traverse the animation AST generated by the animation verb functions
|
||||
* (the output is a tree of objects) and attempt to perform a series of validations on the data. The
|
||||
* following corner-cases will be validated:
|
||||
*
|
||||
* 1. Overlap of animations
|
||||
* Given that a CSS property cannot be animated in more than one place at the same time, it's
|
||||
* important that this behaviour is detected and validated. The way in which this occurs is that
|
||||
* each time a style property is examined, a string-map containing the property will be updated with
|
||||
* the start and end times for when the property is used within an animation step.
|
||||
*
|
||||
* If there are two or more parallel animations that are currently running (these are invoked by the
|
||||
* group()) on the same element then the validator will throw an error. Since the start/end timing
|
||||
* values are collected for each property then if the current animation step is animating the same
|
||||
* property and its timing values fall anywhere into the window of time that the property is
|
||||
* currently being animated within then this is what causes an error.
|
||||
*
|
||||
* 2. Timing values
|
||||
* The validator will validate to see if a timing value of `duration delay easing` or
|
||||
* `durationNumber` is valid or not.
|
||||
*
|
||||
* (note that upon validation the code below will replace the timing data with an object containing
|
||||
* {duration,delay,easing}.
|
||||
*
|
||||
* 3. Offset Validation
|
||||
* Each of the style() calls are allowed to have an offset value when placed inside of keyframes().
|
||||
* Offsets within keyframes() are considered valid when:
|
||||
*
|
||||
* - No offsets are used at all
|
||||
* - Each style() entry contains an offset value
|
||||
* - Each offset is between 0 and 1
|
||||
* - Each offset is greater to or equal than the previous one
|
||||
*
|
||||
* Otherwise an error will be thrown.
|
||||
*/
|
||||
export function validateAnimationSequence(ast: AnimationMetadata) {
|
||||
const normalizedAst =
|
||||
Array.isArray(ast) ? sequence(<AnimationMetadata[]>ast) : <AnimationMetadata>ast;
|
||||
return new AnimationValidatorVisitor().validate(normalizedAst);
|
||||
}
|
||||
|
||||
export class AnimationValidatorVisitor implements AnimationDslVisitor {
|
||||
validate(ast: AnimationMetadata): string[] {
|
||||
const context = new AnimationValidatorContext();
|
||||
visitAnimationNode(this, ast, context);
|
||||
return context.errors;
|
||||
}
|
||||
|
||||
visitState(ast: AnimationStateMetadata, context: any): any {
|
||||
// these values are not visited in this AST
|
||||
}
|
||||
|
||||
visitTransition(ast: AnimationTransitionMetadata, context: any): any {
|
||||
// these values are not visited in this AST
|
||||
}
|
||||
|
||||
visitSequence(ast: AnimationSequenceMetadata, context: AnimationValidatorContext): any {
|
||||
ast.steps.forEach(step => visitAnimationNode(this, step, context));
|
||||
}
|
||||
|
||||
visitGroup(ast: AnimationGroupMetadata, context: AnimationValidatorContext): any {
|
||||
const currentTime = context.currentTime;
|
||||
let furthestTime = 0;
|
||||
ast.steps.forEach(step => {
|
||||
context.currentTime = currentTime;
|
||||
visitAnimationNode(this, step, context);
|
||||
furthestTime = Math.max(furthestTime, context.currentTime);
|
||||
});
|
||||
context.currentTime = furthestTime;
|
||||
}
|
||||
|
||||
visitAnimate(ast: AnimationAnimateMetadata, context: AnimationValidatorContext): any {
|
||||
// we reassign the timings here so that they are not reparsed each
|
||||
// time an animation occurs
|
||||
context.currentAnimateTimings = ast.timings =
|
||||
parseTimeExpression(<string|number>ast.timings, context.errors);
|
||||
|
||||
const astType = ast.styles && ast.styles.type;
|
||||
if (astType == AnimationMetadataType.KeyframeSequence) {
|
||||
this.visitKeyframeSequence(<AnimationKeyframesSequenceMetadata>ast.styles, context);
|
||||
} else {
|
||||
context.currentTime +=
|
||||
context.currentAnimateTimings.duration + context.currentAnimateTimings.delay;
|
||||
if (astType == AnimationMetadataType.Style) {
|
||||
this.visitStyle(<AnimationStyleMetadata>ast.styles, context);
|
||||
}
|
||||
}
|
||||
|
||||
context.currentAnimateTimings = null;
|
||||
}
|
||||
|
||||
visitStyle(ast: AnimationStyleMetadata, context: AnimationValidatorContext): any {
|
||||
const styleData = normalizeStyles(ast.styles);
|
||||
const timings = context.currentAnimateTimings;
|
||||
let endTime = context.currentTime;
|
||||
let startTime = context.currentTime;
|
||||
if (timings && startTime > 0) {
|
||||
startTime -= timings.duration + timings.delay;
|
||||
}
|
||||
Object.keys(styleData).forEach(prop => {
|
||||
const collectedEntry = context.collectedStyles[prop];
|
||||
let updateCollectedStyle = true;
|
||||
if (collectedEntry) {
|
||||
if (startTime != endTime && startTime >= collectedEntry.startTime &&
|
||||
endTime <= collectedEntry.endTime) {
|
||||
context.errors.push(
|
||||
`The CSS property "${prop}" that exists between the times of "${collectedEntry.startTime}ms" and "${collectedEntry.endTime}ms" is also being animated in a parallel animation between the times of "${startTime}ms" and "${endTime}ms"`);
|
||||
updateCollectedStyle = false;
|
||||
}
|
||||
|
||||
// we always choose the smaller start time value since we
|
||||
// want to have a record of the entire animation window where
|
||||
// the style property is being animated in between
|
||||
startTime = collectedEntry.startTime;
|
||||
}
|
||||
if (updateCollectedStyle) {
|
||||
context.collectedStyles[prop] = {startTime, endTime};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
visitKeyframeSequence(
|
||||
ast: AnimationKeyframesSequenceMetadata, context: AnimationValidatorContext): any {
|
||||
let totalKeyframesWithOffsets = 0;
|
||||
const offsets: number[] = [];
|
||||
let offsetsOutOfOrder = false;
|
||||
let keyframesOutOfRange = false;
|
||||
let previousOffset: number = 0;
|
||||
ast.steps.forEach(step => {
|
||||
const styleData = normalizeStyles(step.styles);
|
||||
let offset = 0;
|
||||
if (styleData.hasOwnProperty('offset')) {
|
||||
totalKeyframesWithOffsets++;
|
||||
offset = <number>styleData['offset'];
|
||||
}
|
||||
keyframesOutOfRange = keyframesOutOfRange || offset < 0 || offset > 1;
|
||||
offsetsOutOfOrder = offsetsOutOfOrder || offset < previousOffset;
|
||||
previousOffset = offset;
|
||||
offsets.push(offset);
|
||||
});
|
||||
|
||||
if (keyframesOutOfRange) {
|
||||
context.errors.push(`Please ensure that all keyframe offsets are between 0 and 1`);
|
||||
}
|
||||
|
||||
if (offsetsOutOfOrder) {
|
||||
context.errors.push(`Please ensure that all keyframe offsets are in order`);
|
||||
}
|
||||
|
||||
const length = ast.steps.length;
|
||||
let generatedOffset = 0;
|
||||
if (totalKeyframesWithOffsets > 0 && totalKeyframesWithOffsets < length) {
|
||||
context.errors.push(`Not all style() steps within the declared keyframes() contain offsets`);
|
||||
} else if (totalKeyframesWithOffsets == 0) {
|
||||
generatedOffset = 1 / length;
|
||||
}
|
||||
|
||||
const limit = length - 1;
|
||||
const currentTime = context.currentTime;
|
||||
const animateDuration = context.currentAnimateTimings !.duration;
|
||||
ast.steps.forEach((step, i) => {
|
||||
const offset = generatedOffset > 0 ? (i == limit ? 1 : (generatedOffset * i)) : offsets[i];
|
||||
const durationUpToThisFrame = offset * animateDuration;
|
||||
context.currentTime =
|
||||
currentTime + context.currentAnimateTimings !.delay + durationUpToThisFrame;
|
||||
context.currentAnimateTimings !.duration = durationUpToThisFrame;
|
||||
this.visitStyle(step, context);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class AnimationValidatorContext {
|
||||
public errors: string[] = [];
|
||||
public currentTime: number = 0;
|
||||
public currentAnimateTimings: AnimateTimings|null;
|
||||
public collectedStyles: {[propName: string]: StyleTimeTuple} = {};
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* @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 {AnimationTimelineInstruction} from './animation_timeline_instruction';
|
||||
|
||||
export class ElementInstructionMap {
|
||||
private _map = new Map<any, AnimationTimelineInstruction[]>();
|
||||
|
||||
consume(element: any): AnimationTimelineInstruction[] {
|
||||
let instructions = this._map.get(element);
|
||||
if (instructions) {
|
||||
this._map.delete(element);
|
||||
} else {
|
||||
instructions = [];
|
||||
}
|
||||
return instructions;
|
||||
}
|
||||
|
||||
append(element: any, instructions: AnimationTimelineInstruction[]) {
|
||||
let existingInstructions = this._map.get(element);
|
||||
if (!existingInstructions) {
|
||||
this._map.set(element, existingInstructions = []);
|
||||
}
|
||||
existingInstructions.push(...instructions);
|
||||
}
|
||||
|
||||
has(element: any): boolean { return this._map.has(element); }
|
||||
|
||||
clear() { this._map.clear(); }
|
||||
}
|
|
@ -10,7 +10,7 @@ export {Animation as ɵAnimation} from './dsl/animation';
|
|||
export {AnimationStyleNormalizer as ɵAnimationStyleNormalizer, NoopAnimationStyleNormalizer as ɵNoopAnimationStyleNormalizer} from './dsl/style_normalization/animation_style_normalizer';
|
||||
export {WebAnimationsStyleNormalizer as ɵWebAnimationsStyleNormalizer} from './dsl/style_normalization/web_animations_style_normalizer';
|
||||
export {NoopAnimationDriver as ɵNoopAnimationDriver} from './render/animation_driver';
|
||||
export {DomAnimationEngine as ɵDomAnimationEngine} from './render/dom_animation_engine';
|
||||
export {DomAnimationEngine as ɵDomAnimationEngine} from './render/dom_animation_engine_next';
|
||||
export {NoopAnimationEngine as ɵNoopAnimationEngine} from './render/noop_animation_engine';
|
||||
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';
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ɵStyleData} from '@angular/animations';
|
||||
import {AnimationPlayer, NoopAnimationPlayer} from '@angular/animations';
|
||||
|
||||
|
||||
|
@ -14,6 +15,10 @@ import {AnimationPlayer, NoopAnimationPlayer} from '@angular/animations';
|
|||
* @experimental
|
||||
*/
|
||||
export class NoopAnimationDriver implements AnimationDriver {
|
||||
computeStyle(element: any, prop: string, defaultValue?: string): string {
|
||||
return defaultValue || '';
|
||||
}
|
||||
|
||||
animate(
|
||||
element: any, keyframes: {[key: string]: string | number}[], duration: number, delay: number,
|
||||
easing: string, previousPlayers: any[] = []): AnimationPlayer {
|
||||
|
@ -26,6 +31,9 @@ export class NoopAnimationDriver implements AnimationDriver {
|
|||
*/
|
||||
export abstract class AnimationDriver {
|
||||
static NOOP: AnimationDriver = new NoopAnimationDriver();
|
||||
|
||||
abstract computeStyle(element: any, prop: string, defaultValue?: string): string;
|
||||
|
||||
abstract animate(
|
||||
element: any, keyframes: {[key: string]: string | number}[], duration: number, delay: number,
|
||||
easing?: string|null, previousPlayers?: any[]): any;
|
||||
|
|
|
@ -1,527 +0,0 @@
|
|||
/**
|
||||
* @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 {AnimationEvent, AnimationPlayer, AnimationTriggerMetadata, NoopAnimationPlayer, ɵAnimationGroupPlayer, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {AnimationTimelineInstruction} from '../dsl/animation_timeline_instruction';
|
||||
import {AnimationTransitionInstruction} from '../dsl/animation_transition_instruction';
|
||||
import {AnimationTrigger, buildTrigger} from '../dsl/animation_trigger';
|
||||
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
|
||||
import {eraseStyles, setStyles} from '../util';
|
||||
|
||||
import {AnimationDriver} from './animation_driver';
|
||||
|
||||
export interface QueuedAnimationTransitionTuple {
|
||||
element: any;
|
||||
player: AnimationPlayer;
|
||||
triggerName: string;
|
||||
event: AnimationEvent;
|
||||
}
|
||||
|
||||
export interface TriggerListenerTuple {
|
||||
triggerName: string;
|
||||
phase: string;
|
||||
callback: (event: any) => any;
|
||||
}
|
||||
|
||||
const MARKED_FOR_ANIMATION_CLASSNAME = 'ng-animating';
|
||||
const MARKED_FOR_ANIMATION_SELECTOR = '.ng-animating';
|
||||
const MARKED_FOR_REMOVAL = '$$ngRemove';
|
||||
const VOID_STATE = 'void';
|
||||
|
||||
export class DomAnimationEngine {
|
||||
private _flaggedInserts = new Set<any>();
|
||||
private _queuedRemovals = new Map<any, () => any>();
|
||||
private _queuedTransitionAnimations: QueuedAnimationTransitionTuple[] = [];
|
||||
private _activeTransitionAnimations = new Map<any, {[triggerName: string]: AnimationPlayer}>();
|
||||
private _activeElementAnimations = new Map<any, AnimationPlayer[]>();
|
||||
|
||||
private _elementTriggerStates = new Map<any, {[triggerName: string]: string}>();
|
||||
|
||||
private _triggers: {[triggerName: string]: AnimationTrigger} = Object.create(null);
|
||||
private _triggerListeners = new Map<any, TriggerListenerTuple[]>();
|
||||
|
||||
private _pendingListenerRemovals = new Map<any, TriggerListenerTuple[]>();
|
||||
|
||||
constructor(private _driver: AnimationDriver, private _normalizer: AnimationStyleNormalizer) {}
|
||||
|
||||
get queuedPlayers(): AnimationPlayer[] {
|
||||
return this._queuedTransitionAnimations.map(q => q.player);
|
||||
}
|
||||
|
||||
get activePlayers(): AnimationPlayer[] {
|
||||
const players: AnimationPlayer[] = [];
|
||||
this._activeElementAnimations.forEach(activePlayers => players.push(...activePlayers));
|
||||
return players;
|
||||
}
|
||||
|
||||
registerTrigger(trigger: AnimationTriggerMetadata, name?: string): void {
|
||||
name = name || trigger.name;
|
||||
if (this._triggers[name]) {
|
||||
return;
|
||||
}
|
||||
this._triggers[name] = buildTrigger(name, trigger.definitions);
|
||||
}
|
||||
|
||||
onInsert(element: any, domFn: () => any): void {
|
||||
if (element['nodeType'] == 1) {
|
||||
this._flaggedInserts.add(element);
|
||||
}
|
||||
domFn();
|
||||
}
|
||||
|
||||
onRemove(element: any, domFn: () => any): void {
|
||||
if (element['nodeType'] != 1) {
|
||||
domFn();
|
||||
return;
|
||||
}
|
||||
|
||||
let lookupRef = this._elementTriggerStates.get(element);
|
||||
if (lookupRef) {
|
||||
const possibleTriggers = Object.keys(lookupRef);
|
||||
const hasRemoval = possibleTriggers.some(triggerName => {
|
||||
const oldValue = lookupRef ![triggerName];
|
||||
const instruction = this._triggers[triggerName].matchTransition(oldValue, VOID_STATE);
|
||||
return !!instruction;
|
||||
});
|
||||
if (hasRemoval) {
|
||||
element[MARKED_FOR_REMOVAL] = true;
|
||||
this._queuedRemovals.set(element, domFn);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// this means that there are no animations to take on this
|
||||
// leave operation therefore we should fire the done|start callbacks
|
||||
if (this._triggerListeners.has(element)) {
|
||||
element[MARKED_FOR_REMOVAL] = true;
|
||||
this._queuedRemovals.set(element, () => {});
|
||||
}
|
||||
this._onRemovalTransition(element).forEach(player => player.destroy());
|
||||
domFn();
|
||||
}
|
||||
|
||||
setProperty(element: any, property: string, value: any): void {
|
||||
const trigger = this._triggers[property];
|
||||
if (!trigger) {
|
||||
throw new Error(`The provided animation trigger "${property}" has not been registered!`);
|
||||
}
|
||||
|
||||
let lookupRef = this._elementTriggerStates.get(element);
|
||||
if (!lookupRef) {
|
||||
this._elementTriggerStates.set(element, lookupRef = {});
|
||||
}
|
||||
|
||||
let oldValue = lookupRef.hasOwnProperty(property) ? lookupRef[property] : VOID_STATE;
|
||||
if (oldValue !== value) {
|
||||
value = normalizeTriggerValue(value);
|
||||
let instruction = trigger.matchTransition(oldValue, value);
|
||||
if (!instruction) {
|
||||
// we do this to make sure we always have an animation player so
|
||||
// that callback operations are properly called
|
||||
instruction = trigger.createFallbackInstruction(oldValue, value);
|
||||
}
|
||||
this.animateTransition(element, instruction);
|
||||
lookupRef[property] = value;
|
||||
}
|
||||
}
|
||||
|
||||
listen(element: any, eventName: string, eventPhase: string, callback: (event: any) => any):
|
||||
() => void {
|
||||
if (!eventPhase) {
|
||||
throw new Error(
|
||||
`Unable to listen on the animation trigger "${eventName}" because the provided event is undefined!`);
|
||||
}
|
||||
if (!this._triggers[eventName]) {
|
||||
throw new Error(
|
||||
`Unable to listen on the animation trigger event "${eventPhase}" because the animation trigger "${eventName}" doesn't exist!`);
|
||||
}
|
||||
let elementListeners = this._triggerListeners.get(element);
|
||||
if (!elementListeners) {
|
||||
this._triggerListeners.set(element, elementListeners = []);
|
||||
}
|
||||
validatePlayerEvent(eventName, eventPhase);
|
||||
const tuple = <TriggerListenerTuple>{triggerName: eventName, phase: eventPhase, callback};
|
||||
elementListeners.push(tuple);
|
||||
return () => {
|
||||
// this is queued up in the event that a removal animation is set
|
||||
// to fire on the element (the listeners need to be set during flush)
|
||||
getOrSetAsInMap(this._pendingListenerRemovals, element, []).push(tuple);
|
||||
};
|
||||
}
|
||||
|
||||
private _clearPendingListenerRemovals() {
|
||||
this._pendingListenerRemovals.forEach((tuples: TriggerListenerTuple[], element: any) => {
|
||||
const elementListeners = this._triggerListeners.get(element);
|
||||
if (elementListeners) {
|
||||
tuples.forEach(tuple => {
|
||||
const index = elementListeners.indexOf(tuple);
|
||||
if (index >= 0) {
|
||||
elementListeners.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
this._pendingListenerRemovals.clear();
|
||||
}
|
||||
|
||||
private _onRemovalTransition(element: any): AnimationPlayer[] {
|
||||
// when a parent animation is set to trigger a removal we want to
|
||||
// find all of the children that are currently animating and clear
|
||||
// them out by destroying each of them.
|
||||
const elms = element.querySelectorAll(MARKED_FOR_ANIMATION_SELECTOR);
|
||||
for (let i = 0; i < elms.length; i++) {
|
||||
const elm = elms[i];
|
||||
const activePlayers = this._activeElementAnimations.get(elm);
|
||||
if (activePlayers) {
|
||||
activePlayers.forEach(player => player.destroy());
|
||||
}
|
||||
|
||||
const activeTransitions = this._activeTransitionAnimations.get(elm);
|
||||
if (activeTransitions) {
|
||||
Object.keys(activeTransitions).forEach(triggerName => {
|
||||
const player = activeTransitions[triggerName];
|
||||
if (player) {
|
||||
player.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// we make a copy of the array because the actual source array is modified
|
||||
// each time a player is finished/destroyed (the forEach loop would fail otherwise)
|
||||
return copyArray(this._activeElementAnimations.get(element) !);
|
||||
}
|
||||
|
||||
animateTransition(element: any, instruction: AnimationTransitionInstruction): AnimationPlayer {
|
||||
const triggerName = instruction.triggerName;
|
||||
|
||||
let previousPlayers: AnimationPlayer[];
|
||||
if (instruction.isRemovalTransition) {
|
||||
previousPlayers = this._onRemovalTransition(element);
|
||||
} else {
|
||||
previousPlayers = [];
|
||||
const existingTransitions = this._activeTransitionAnimations.get(element);
|
||||
const existingPlayer = existingTransitions ? existingTransitions[triggerName] : null;
|
||||
if (existingPlayer) {
|
||||
previousPlayers.push(existingPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
// it's important to do this step before destroying the players
|
||||
// so that the onDone callback below won't fire before this
|
||||
eraseStyles(element, instruction.fromStyles);
|
||||
|
||||
// we first run this so that the previous animation player
|
||||
// data can be passed into the successive animation players
|
||||
let totalTime = 0;
|
||||
const players = instruction.timelines.map((timelineInstruction, i) => {
|
||||
totalTime = Math.max(totalTime, timelineInstruction.totalTime);
|
||||
return this._buildPlayer(element, timelineInstruction, previousPlayers, i);
|
||||
});
|
||||
|
||||
previousPlayers.forEach(previousPlayer => previousPlayer.destroy());
|
||||
const player = optimizeGroupPlayer(players);
|
||||
player.onDone(() => {
|
||||
player.destroy();
|
||||
const elmTransitionMap = this._activeTransitionAnimations.get(element);
|
||||
if (elmTransitionMap) {
|
||||
delete elmTransitionMap[triggerName];
|
||||
if (Object.keys(elmTransitionMap).length == 0) {
|
||||
this._activeTransitionAnimations.delete(element);
|
||||
}
|
||||
}
|
||||
deleteFromArrayMap(this._activeElementAnimations, element, player);
|
||||
setStyles(element, instruction.toStyles);
|
||||
});
|
||||
|
||||
const elmTransitionMap = getOrSetAsInMap(this._activeTransitionAnimations, element, {});
|
||||
elmTransitionMap[triggerName] = player;
|
||||
|
||||
this._queuePlayer(
|
||||
element, triggerName, player,
|
||||
makeAnimationEvent(
|
||||
element, triggerName, instruction.fromState, instruction.toState,
|
||||
null, // this will be filled in during event creation
|
||||
totalTime));
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
public animateTimeline(
|
||||
element: any, instructions: AnimationTimelineInstruction[],
|
||||
previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
|
||||
const players = instructions.map((instruction, i) => {
|
||||
const player = this._buildPlayer(element, instruction, previousPlayers, i);
|
||||
player.onDestroy(
|
||||
() => { deleteFromArrayMap(this._activeElementAnimations, element, player); });
|
||||
this._markPlayerAsActive(element, player);
|
||||
return player;
|
||||
});
|
||||
return optimizeGroupPlayer(players);
|
||||
}
|
||||
|
||||
private _buildPlayer(
|
||||
element: any, instruction: AnimationTimelineInstruction, previousPlayers: AnimationPlayer[],
|
||||
index: number = 0): AnimationPlayer {
|
||||
// only the very first animation can absorb the previous styles. This
|
||||
// is here to prevent the an overlap situation where a group animation
|
||||
// absorbs previous styles multiple times for the same element.
|
||||
if (index && previousPlayers.length) {
|
||||
previousPlayers = [];
|
||||
}
|
||||
return this._driver.animate(
|
||||
element, this._normalizeKeyframes(instruction.keyframes), instruction.duration,
|
||||
instruction.delay, instruction.easing, previousPlayers);
|
||||
}
|
||||
|
||||
private _normalizeKeyframes(keyframes: ɵStyleData[]): ɵStyleData[] {
|
||||
const errors: string[] = [];
|
||||
const normalizedKeyframes: ɵStyleData[] = [];
|
||||
keyframes.forEach(kf => {
|
||||
const normalizedKeyframe: ɵStyleData = {};
|
||||
Object.keys(kf).forEach(prop => {
|
||||
let normalizedProp = prop;
|
||||
let normalizedValue = kf[prop];
|
||||
if (prop != 'offset') {
|
||||
normalizedProp = this._normalizer.normalizePropertyName(prop, errors);
|
||||
normalizedValue =
|
||||
this._normalizer.normalizeStyleValue(prop, normalizedProp, kf[prop], errors);
|
||||
}
|
||||
normalizedKeyframe[normalizedProp] = normalizedValue;
|
||||
});
|
||||
normalizedKeyframes.push(normalizedKeyframe);
|
||||
});
|
||||
if (errors.length) {
|
||||
const LINE_START = '\n - ';
|
||||
throw new Error(
|
||||
`Unable to animate due to the following errors:${LINE_START}${errors.join(LINE_START)}`);
|
||||
}
|
||||
return normalizedKeyframes;
|
||||
}
|
||||
|
||||
private _markPlayerAsActive(element: any, player: AnimationPlayer) {
|
||||
const elementAnimations = getOrSetAsInMap(this._activeElementAnimations, element, []);
|
||||
elementAnimations.push(player);
|
||||
}
|
||||
|
||||
private _queuePlayer(
|
||||
element: any, triggerName: string, player: AnimationPlayer, event: AnimationEvent) {
|
||||
const tuple = <QueuedAnimationTransitionTuple>{element, player, triggerName, event};
|
||||
this._queuedTransitionAnimations.push(tuple);
|
||||
player.init();
|
||||
|
||||
element.classList.add(MARKED_FOR_ANIMATION_CLASSNAME);
|
||||
player.onDone(() => { element.classList.remove(MARKED_FOR_ANIMATION_CLASSNAME); });
|
||||
}
|
||||
|
||||
private _flushQueuedAnimations() {
|
||||
parentLoop: while (this._queuedTransitionAnimations.length) {
|
||||
const {player, element, triggerName, event} = this._queuedTransitionAnimations.shift() !;
|
||||
|
||||
let parent = element;
|
||||
while (parent = parent.parentNode) {
|
||||
// this means that a parent element will or will not
|
||||
// have its own animation operation which in this case
|
||||
// there's no point in even trying to do an animation
|
||||
if (parent[MARKED_FOR_REMOVAL]) continue parentLoop;
|
||||
}
|
||||
|
||||
const listeners = this._triggerListeners.get(element);
|
||||
if (listeners) {
|
||||
listeners.forEach(tuple => {
|
||||
if (tuple.triggerName == triggerName) {
|
||||
listenOnPlayer(player, tuple.phase, event, tuple.callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// if a removal exists for the given element then we need cancel
|
||||
// all the queued players so that a proper removal animation can go
|
||||
if (this._queuedRemovals.has(element)) {
|
||||
player.destroy();
|
||||
continue;
|
||||
}
|
||||
|
||||
this._markPlayerAsActive(element, player);
|
||||
|
||||
// in the event that an animation throws an error then we do
|
||||
// not want to re-run animations on any previous animations
|
||||
// if they have already been kicked off beforehand
|
||||
player.init();
|
||||
if (!player.hasStarted()) {
|
||||
player.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flush() {
|
||||
const leaveListeners = new Map<any, TriggerListenerTuple[]>();
|
||||
this._queuedRemovals.forEach((callback, element) => {
|
||||
const tuple = this._pendingListenerRemovals.get(element);
|
||||
if (tuple) {
|
||||
leaveListeners.set(element, tuple);
|
||||
this._pendingListenerRemovals.delete(element);
|
||||
}
|
||||
});
|
||||
|
||||
this._clearPendingListenerRemovals();
|
||||
this._pendingListenerRemovals = leaveListeners;
|
||||
|
||||
this._flushQueuedAnimations();
|
||||
|
||||
let flushAgain = false;
|
||||
this._queuedRemovals.forEach((callback, element) => {
|
||||
// an item that was inserted/removed in the same flush means
|
||||
// that an animation should not happen anyway
|
||||
if (this._flaggedInserts.has(element)) return;
|
||||
|
||||
let parent = element;
|
||||
let players: AnimationPlayer[] = [];
|
||||
while (parent = parent.parentNode) {
|
||||
// there is no reason to even try to
|
||||
if (parent[MARKED_FOR_REMOVAL]) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const match = this._activeElementAnimations.get(parent);
|
||||
if (match) {
|
||||
players.push(...match);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// the loop was unable to find an parent that is animating even
|
||||
// though this element has set to be removed, so the algorithm
|
||||
// should check to see if there are any triggers on the element
|
||||
// that are present to handle a leave animation and then setup
|
||||
// those players to facilitate the callback after done
|
||||
if (players.length == 0) {
|
||||
// this means that the element has valid state triggers
|
||||
const stateDetails = this._elementTriggerStates.get(element);
|
||||
if (stateDetails) {
|
||||
Object.keys(stateDetails).forEach(triggerName => {
|
||||
flushAgain = true;
|
||||
const oldValue = stateDetails[triggerName];
|
||||
const instruction = this._triggers[triggerName].matchTransition(oldValue, VOID_STATE);
|
||||
if (instruction) {
|
||||
players.push(this.animateTransition(element, instruction));
|
||||
} else {
|
||||
const event = makeAnimationEvent(element, triggerName, oldValue, VOID_STATE, '', 0);
|
||||
const player = new NoopAnimationPlayer();
|
||||
this._queuePlayer(element, triggerName, player, event);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (players.length) {
|
||||
optimizeGroupPlayer(players).onDone(callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
this._queuedRemovals.clear();
|
||||
this._flaggedInserts.clear();
|
||||
|
||||
// this means that one or more leave animations were detected
|
||||
if (flushAgain) {
|
||||
this._flushQueuedAnimations();
|
||||
this._clearPendingListenerRemovals();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getOrSetAsInMap(map: Map<any, any>, key: any, defaultValue: any) {
|
||||
let value = map.get(key);
|
||||
if (!value) {
|
||||
map.set(key, value = defaultValue);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function deleteFromArrayMap(map: Map<any, any[]>, key: any, value: any) {
|
||||
let arr = map.get(key);
|
||||
if (arr) {
|
||||
const index = arr.indexOf(value);
|
||||
if (index >= 0) {
|
||||
arr.splice(index, 1);
|
||||
if (arr.length == 0) {
|
||||
map.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function optimizeGroupPlayer(players: AnimationPlayer[]): AnimationPlayer {
|
||||
switch (players.length) {
|
||||
case 0:
|
||||
return new NoopAnimationPlayer();
|
||||
case 1:
|
||||
return players[0];
|
||||
default:
|
||||
return new ɵAnimationGroupPlayer(players);
|
||||
}
|
||||
}
|
||||
|
||||
function copyArray(source: any[]): any[] {
|
||||
return source ? source.splice(0) : [];
|
||||
}
|
||||
|
||||
function validatePlayerEvent(triggerName: string, eventName: string) {
|
||||
switch (eventName) {
|
||||
case 'start':
|
||||
case 'done':
|
||||
return;
|
||||
default:
|
||||
throw new Error(
|
||||
`The provided animation trigger event "${eventName}" for the animation trigger "${triggerName}" is not supported!`);
|
||||
}
|
||||
}
|
||||
|
||||
function listenOnPlayer(
|
||||
player: AnimationPlayer, eventName: string, baseEvent: AnimationEvent,
|
||||
callback: (event: any) => any) {
|
||||
switch (eventName) {
|
||||
case 'start':
|
||||
player.onStart(() => {
|
||||
const event = copyAnimationEvent(baseEvent);
|
||||
event.phaseName = 'start';
|
||||
callback(event);
|
||||
});
|
||||
break;
|
||||
case 'done':
|
||||
player.onDone(() => {
|
||||
const event = copyAnimationEvent(baseEvent);
|
||||
event.phaseName = 'done';
|
||||
callback(event);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function copyAnimationEvent(e: AnimationEvent): AnimationEvent {
|
||||
return makeAnimationEvent(
|
||||
e.element, e.triggerName, e.fromState, e.toState, e.phaseName, e.totalTime);
|
||||
}
|
||||
|
||||
function makeAnimationEvent(
|
||||
element: any, triggerName: string, fromState: string, toState: string, phaseName: string | null,
|
||||
totalTime: number): AnimationEvent {
|
||||
return <AnimationEvent>{element, triggerName, fromState, toState, phaseName, totalTime};
|
||||
}
|
||||
|
||||
function normalizeTriggerValue(value: any): string {
|
||||
switch (typeof value) {
|
||||
case 'boolean':
|
||||
return value ? '1' : '0';
|
||||
default:
|
||||
return value ? value.toString() : null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* @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 {AnimationMetadata, AnimationPlayer, AnimationTriggerMetadata} from '@angular/animations';
|
||||
|
||||
import {AnimationEngine} from '../animation_engine';
|
||||
import {TriggerAst} from '../dsl/animation_ast';
|
||||
import {buildAnimationAst} from '../dsl/animation_ast_builder';
|
||||
import {AnimationTrigger, buildTrigger} from '../dsl/animation_trigger';
|
||||
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
|
||||
|
||||
import {AnimationDriver} from './animation_driver';
|
||||
import {parseTimelineCommand} from './shared';
|
||||
import {TimelineAnimationEngine} from './timeline_animation_engine';
|
||||
import {TransitionAnimationEngine} from './transition_animation_engine';
|
||||
|
||||
export class DomAnimationEngine implements AnimationEngine {
|
||||
private _transitionEngine: TransitionAnimationEngine;
|
||||
private _timelineEngine: TimelineAnimationEngine;
|
||||
|
||||
private _triggerCache: {[key: string]: AnimationTrigger} = {};
|
||||
|
||||
// this method is designed to be overridden by the code that uses this engine
|
||||
public onRemovalComplete = (element: any, context: any) => {};
|
||||
|
||||
constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) {
|
||||
this._transitionEngine = new TransitionAnimationEngine(driver, normalizer);
|
||||
this._timelineEngine = new TimelineAnimationEngine(driver, normalizer);
|
||||
|
||||
this._transitionEngine.onRemovalComplete =
|
||||
(element: any, context: any) => { this.onRemovalComplete(element, context); }
|
||||
}
|
||||
|
||||
registerTrigger(
|
||||
componentId: string, namespaceId: string, hostElement: any, name: string,
|
||||
metadata: AnimationTriggerMetadata): void {
|
||||
const cacheKey = componentId + '-' + name;
|
||||
let trigger = this._triggerCache[cacheKey];
|
||||
if (!trigger) {
|
||||
const errors: any[] = [];
|
||||
const ast = buildAnimationAst(metadata as AnimationMetadata, errors) as TriggerAst;
|
||||
if (errors.length) {
|
||||
throw new Error(
|
||||
`The animation trigger "${name}" has failed to build due to the following errors:\n - ${errors.join("\n - ")}`);
|
||||
}
|
||||
trigger = buildTrigger(name, ast);
|
||||
this._triggerCache[cacheKey] = trigger;
|
||||
}
|
||||
|
||||
this._transitionEngine.register(namespaceId, hostElement, name, trigger);
|
||||
}
|
||||
|
||||
destroy(namespaceId: string, context: any) {
|
||||
this._transitionEngine.destroy(namespaceId, context);
|
||||
}
|
||||
|
||||
onInsert(namespaceId: string, element: any, parent: any, insertBefore: boolean): void {
|
||||
this._transitionEngine.insertNode(namespaceId, element, parent, insertBefore);
|
||||
}
|
||||
|
||||
onRemove(namespaceId: string, element: any, context: any): void {
|
||||
this._transitionEngine.removeNode(namespaceId, element, context);
|
||||
}
|
||||
|
||||
setProperty(namespaceId: string, element: any, property: string, value: any): boolean {
|
||||
// @@property
|
||||
if (property.charAt(0) == '@') {
|
||||
const [id, action] = parseTimelineCommand(property);
|
||||
const args = value as any[];
|
||||
this._timelineEngine.command(id, element, action, args);
|
||||
return false;
|
||||
}
|
||||
return this._transitionEngine.trigger(namespaceId, element, property, value);
|
||||
}
|
||||
|
||||
listen(
|
||||
namespaceId: string, element: any, eventName: string, eventPhase: string,
|
||||
callback: (event: any) => any): () => any {
|
||||
// @@listen
|
||||
if (eventName.charAt(0) == '@') {
|
||||
const [id, action] = parseTimelineCommand(eventName);
|
||||
return this._timelineEngine.listen(id, element, action, callback);
|
||||
}
|
||||
return this._transitionEngine.listen(namespaceId, element, eventName, eventPhase, callback);
|
||||
}
|
||||
|
||||
flush(): void { this._transitionEngine.flush(); }
|
||||
|
||||
get players(): AnimationPlayer[] {
|
||||
return (this._transitionEngine.players as AnimationPlayer[])
|
||||
.concat(this._timelineEngine.players as AnimationPlayer[]);
|
||||
}
|
||||
}
|
|
@ -5,20 +5,30 @@
|
|||
* 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 {AnimationEvent, AnimationMetadataType, AnimationPlayer, AnimationStateMetadata, AnimationTriggerMetadata, ɵStyleData} from '@angular/animations';
|
||||
import {AnimationEvent, AnimationPlayer, AnimationTriggerMetadata, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {AnimationEngine} from '../animation_engine';
|
||||
import {TriggerAst} from '../dsl/animation_ast';
|
||||
import {buildAnimationAst} from '../dsl/animation_ast_builder';
|
||||
import {buildTrigger} from '../dsl/animation_trigger';
|
||||
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
|
||||
import {copyStyles, eraseStyles, normalizeStyles, setStyles} from '../util';
|
||||
|
||||
import {AnimationDriver} from './animation_driver';
|
||||
import {parseTimelineCommand} from './shared';
|
||||
import {TimelineAnimationEngine} from './timeline_animation_engine';
|
||||
|
||||
interface ListenerTuple {
|
||||
eventPhase: string;
|
||||
triggerName: string;
|
||||
namespacedName: string;
|
||||
callback: (event: any) => any;
|
||||
doRemove?: boolean;
|
||||
}
|
||||
|
||||
interface ChangeTuple {
|
||||
element: any;
|
||||
namespacedName: string;
|
||||
triggerName: string;
|
||||
oldValue: string;
|
||||
newValue: string;
|
||||
|
@ -35,36 +45,55 @@ export class NoopAnimationEngine extends AnimationEngine {
|
|||
private _triggerStyles: {[triggerName: string]: {[stateName: string]: ɵStyleData}} =
|
||||
Object.create(null);
|
||||
|
||||
registerTrigger(trigger: AnimationTriggerMetadata, name?: string): void {
|
||||
name = name || trigger.name;
|
||||
private _timelineEngine: TimelineAnimationEngine;
|
||||
|
||||
// this method is designed to be overridden by the code that uses this engine
|
||||
public onRemovalComplete = (element: any, context: any) => {};
|
||||
|
||||
constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) {
|
||||
super();
|
||||
this._timelineEngine = new TimelineAnimationEngine(driver, normalizer);
|
||||
}
|
||||
|
||||
registerTrigger(
|
||||
componentId: string, namespaceId: string, hostElement: any, name: string,
|
||||
metadata: AnimationTriggerMetadata): void {
|
||||
name = name || metadata.name;
|
||||
name = namespaceId + '#' + name;
|
||||
if (this._triggerStyles[name]) {
|
||||
return;
|
||||
}
|
||||
const stateMap: {[stateName: string]: ɵStyleData} = {};
|
||||
trigger.definitions.forEach(def => {
|
||||
if (def.type === AnimationMetadataType.State) {
|
||||
const stateDef = def as AnimationStateMetadata;
|
||||
stateMap[stateDef.name] = normalizeStyles(stateDef.styles.styles);
|
||||
}
|
||||
});
|
||||
this._triggerStyles[name] = stateMap;
|
||||
|
||||
const errors: any[] = [];
|
||||
const ast = buildAnimationAst(metadata, errors) as TriggerAst;
|
||||
const trigger = buildTrigger(name, ast);
|
||||
this._triggerStyles[name] = trigger.states;
|
||||
}
|
||||
|
||||
onInsert(element: any, domFn: () => any): void { domFn(); }
|
||||
onInsert(namespaceId: string, element: any, parent: any, insertBefore: boolean): void {}
|
||||
|
||||
onRemove(element: any, domFn: () => any): void {
|
||||
domFn();
|
||||
onRemove(namespaceId: string, element: any, context: any): void {
|
||||
this.onRemovalComplete(element, context);
|
||||
if (element['nodeType'] == 1) {
|
||||
this._flaggedRemovals.add(element);
|
||||
}
|
||||
}
|
||||
|
||||
setProperty(element: any, property: string, value: any): void {
|
||||
const storageProp = makeStorageProp(property);
|
||||
const oldValue = element[storageProp] || DEFAULT_STATE_VALUE;
|
||||
this._changes.push(<ChangeTuple>{element, oldValue, newValue: value, triggerName: property});
|
||||
setProperty(namespaceId: string, element: any, property: string, value: any): boolean {
|
||||
if (property.charAt(0) == '@') {
|
||||
const [id, action] = parseTimelineCommand(property);
|
||||
const args = value as any[];
|
||||
this._timelineEngine.command(id, element, action, args);
|
||||
return false;
|
||||
}
|
||||
|
||||
const triggerStateStyles = this._triggerStyles[property] || {};
|
||||
const namespacedName = namespaceId + '#' + property;
|
||||
const storageProp = makeStorageProp(namespacedName);
|
||||
const oldValue = element[storageProp] || DEFAULT_STATE_VALUE;
|
||||
this._changes.push(
|
||||
<ChangeTuple>{element, oldValue, newValue: value, triggerName: property, namespacedName});
|
||||
|
||||
const triggerStateStyles = this._triggerStyles[namespacedName] || {};
|
||||
const fromStateStyles =
|
||||
triggerStateStyles[oldValue] || triggerStateStyles[DEFAULT_STATE_STYLES];
|
||||
if (fromStateStyles) {
|
||||
|
@ -78,16 +107,27 @@ export class NoopAnimationEngine extends AnimationEngine {
|
|||
setStyles(element, toStateStyles);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
listen(element: any, eventName: string, eventPhase: string, callback: (event: any) => any):
|
||||
() => any {
|
||||
listen(
|
||||
namespaceId: string, element: any, eventName: string, eventPhase: string,
|
||||
callback: (event: any) => any): () => any {
|
||||
if (eventName.charAt(0) == '@') {
|
||||
const [id, action] = parseTimelineCommand(eventName);
|
||||
return this._timelineEngine.listen(id, element, action, callback);
|
||||
}
|
||||
|
||||
let listeners = this._listeners.get(element);
|
||||
if (!listeners) {
|
||||
this._listeners.set(element, listeners = []);
|
||||
}
|
||||
|
||||
const tuple = <ListenerTuple>{triggerName: eventName, eventPhase, callback};
|
||||
const tuple = <ListenerTuple>{
|
||||
namespacedName: namespaceId + '#' + eventName,
|
||||
triggerName: eventName, eventPhase, callback
|
||||
};
|
||||
listeners.push(tuple);
|
||||
|
||||
return () => tuple.doRemove = true;
|
||||
|
@ -113,7 +153,7 @@ export class NoopAnimationEngine extends AnimationEngine {
|
|||
const listeners = this._listeners.get(element);
|
||||
if (listeners) {
|
||||
listeners.forEach(listener => {
|
||||
if (listener.triggerName == change.triggerName) {
|
||||
if (listener.namespacedName == change.namespacedName) {
|
||||
handleListener(listener, change);
|
||||
}
|
||||
});
|
||||
|
@ -126,10 +166,12 @@ export class NoopAnimationEngine extends AnimationEngine {
|
|||
if (listeners) {
|
||||
listeners.forEach(listener => {
|
||||
const triggerName = listener.triggerName;
|
||||
const storageProp = makeStorageProp(triggerName);
|
||||
const namespacedName = listener.namespacedName;
|
||||
const storageProp = makeStorageProp(namespacedName);
|
||||
handleListener(listener, <ChangeTuple>{
|
||||
element: element,
|
||||
triggerName: triggerName,
|
||||
element,
|
||||
triggerName,
|
||||
namespacedName: listener.namespacedName,
|
||||
oldValue: element[storageProp] || DEFAULT_STATE_VALUE,
|
||||
newValue: DEFAULT_STATE_VALUE
|
||||
});
|
||||
|
@ -156,8 +198,9 @@ export class NoopAnimationEngine extends AnimationEngine {
|
|||
this._onDoneFns = [];
|
||||
}
|
||||
|
||||
get activePlayers(): AnimationPlayer[] { return []; }
|
||||
get queuedPlayers(): AnimationPlayer[] { return []; }
|
||||
get players(): AnimationPlayer[] { return []; }
|
||||
|
||||
destroy(namespaceId: string) {}
|
||||
}
|
||||
|
||||
function makeAnimationEvent(
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
/**
|
||||
* @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 {AUTO_STYLE, AnimationEvent, AnimationPlayer, NoopAnimationPlayer, ɵAnimationGroupPlayer, ɵPRE_STYLE as PRE_STYLE, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {AnimationStyleNormalizer} from '../../src/dsl/style_normalization/animation_style_normalizer';
|
||||
import {AnimationDriver} from '../../src/render/animation_driver';
|
||||
|
||||
export function optimizeGroupPlayer(players: AnimationPlayer[]): AnimationPlayer {
|
||||
switch (players.length) {
|
||||
case 0:
|
||||
return new NoopAnimationPlayer();
|
||||
case 1:
|
||||
return players[0];
|
||||
default:
|
||||
return new ɵAnimationGroupPlayer(players);
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeKeyframes(
|
||||
driver: AnimationDriver, normalizer: AnimationStyleNormalizer, element: any,
|
||||
keyframes: ɵStyleData[], preStyles: ɵStyleData = {},
|
||||
postStyles: ɵStyleData = {}): ɵStyleData[] {
|
||||
const errors: string[] = [];
|
||||
const normalizedKeyframes: ɵStyleData[] = [];
|
||||
let previousOffset = -1;
|
||||
let previousKeyframe: ɵStyleData|null = null;
|
||||
keyframes.forEach(kf => {
|
||||
const offset = kf['offset'] as number;
|
||||
const isSameOffset = offset == previousOffset;
|
||||
const normalizedKeyframe: ɵStyleData = (isSameOffset && previousKeyframe) || {};
|
||||
Object.keys(kf).forEach(prop => {
|
||||
let normalizedProp = prop;
|
||||
let normalizedValue = kf[prop];
|
||||
if (normalizedValue == PRE_STYLE) {
|
||||
normalizedValue = preStyles[prop];
|
||||
} else if (normalizedValue == AUTO_STYLE) {
|
||||
normalizedValue = postStyles[prop];
|
||||
} else if (prop != 'offset') {
|
||||
normalizedProp = normalizer.normalizePropertyName(prop, errors);
|
||||
normalizedValue = normalizer.normalizeStyleValue(prop, normalizedProp, kf[prop], errors);
|
||||
}
|
||||
normalizedKeyframe[normalizedProp] = normalizedValue;
|
||||
});
|
||||
if (!isSameOffset) {
|
||||
normalizedKeyframes.push(normalizedKeyframe);
|
||||
}
|
||||
previousKeyframe = normalizedKeyframe;
|
||||
previousOffset = offset;
|
||||
});
|
||||
if (errors.length) {
|
||||
const LINE_START = '\n - ';
|
||||
throw new Error(
|
||||
`Unable to animate due to the following errors:${LINE_START}${errors.join(LINE_START)}`);
|
||||
}
|
||||
|
||||
return normalizedKeyframes;
|
||||
}
|
||||
|
||||
export function listenOnPlayer(
|
||||
player: AnimationPlayer, eventName: string, event: AnimationEvent | undefined,
|
||||
callback: (event: any) => any) {
|
||||
switch (eventName) {
|
||||
case 'start':
|
||||
player.onStart(() => callback(event && copyAnimationEvent(event, 'start', player.totalTime)));
|
||||
break;
|
||||
case 'done':
|
||||
player.onDone(() => callback(event && copyAnimationEvent(event, 'done', player.totalTime)));
|
||||
break;
|
||||
case 'destroy':
|
||||
player.onDestroy(
|
||||
() => callback(event && copyAnimationEvent(event, 'destroy', player.totalTime)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export function copyAnimationEvent(
|
||||
e: AnimationEvent, phaseName?: string, totalTime?: number): AnimationEvent {
|
||||
return makeAnimationEvent(
|
||||
e.element, e.triggerName, e.fromState, e.toState, phaseName || e.phaseName,
|
||||
totalTime == undefined ? e.totalTime : totalTime);
|
||||
}
|
||||
|
||||
export function makeAnimationEvent(
|
||||
element: any, triggerName: string, fromState: string, toState: string, phaseName: string = '',
|
||||
totalTime: number = 0): AnimationEvent {
|
||||
return {element, triggerName, fromState, toState, phaseName, totalTime};
|
||||
}
|
||||
|
||||
export function getOrSetAsInMap(
|
||||
map: Map<any, any>| {[key: string]: any}, key: any, defaultValue: any) {
|
||||
let value: any;
|
||||
if (map instanceof Map) {
|
||||
value = map.get(key);
|
||||
if (!value) {
|
||||
map.set(key, value = defaultValue);
|
||||
}
|
||||
} else {
|
||||
value = map[key];
|
||||
if (!value) {
|
||||
value = map[key] = defaultValue;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function parseTimelineCommand(command: string): [string, string] {
|
||||
const separatorPos = command.indexOf(':');
|
||||
const id = command.substring(1, separatorPos);
|
||||
const action = command.substr(separatorPos + 1);
|
||||
return [id, action];
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
/**
|
||||
* @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 {AUTO_STYLE, AnimationMetadata, AnimationOptions, AnimationPlayer, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {Ast} from '../dsl/animation_ast';
|
||||
import {buildAnimationAst} from '../dsl/animation_ast_builder';
|
||||
import {buildAnimationTimelines} from '../dsl/animation_timeline_builder';
|
||||
import {AnimationTimelineInstruction} from '../dsl/animation_timeline_instruction';
|
||||
import {ElementInstructionMap} from '../dsl/element_instruction_map';
|
||||
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
|
||||
|
||||
import {AnimationDriver} from './animation_driver';
|
||||
import {getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from './shared';
|
||||
|
||||
const EMPTY_INSTRUCTION_MAP = new ElementInstructionMap();
|
||||
|
||||
export class TimelineAnimationEngine {
|
||||
private _animations: {[id: string]: Ast} = {};
|
||||
private _playersById: {[id: string]: AnimationPlayer} = {};
|
||||
public players: AnimationPlayer[] = [];
|
||||
|
||||
constructor(private _driver: AnimationDriver, private _normalizer: AnimationStyleNormalizer) {}
|
||||
|
||||
register(id: string, metadata: AnimationMetadata|AnimationMetadata[]) {
|
||||
const errors: any[] = [];
|
||||
const ast = buildAnimationAst(metadata, errors);
|
||||
if (errors.length) {
|
||||
throw new Error(
|
||||
`Unable to build the animation due to the following errors: ${errors.join("\n")}`);
|
||||
} else {
|
||||
this._animations[id] = ast;
|
||||
}
|
||||
}
|
||||
|
||||
private _buildPlayer(
|
||||
i: AnimationTimelineInstruction, preStyles: ɵStyleData,
|
||||
postStyles?: ɵStyleData): AnimationPlayer {
|
||||
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, []);
|
||||
}
|
||||
|
||||
create(id: string, element: any, options: AnimationOptions = {}): AnimationPlayer {
|
||||
const errors: any[] = [];
|
||||
const ast = this._animations[id];
|
||||
let instructions: AnimationTimelineInstruction[];
|
||||
|
||||
const autoStylesMap = new Map<any, ɵStyleData>();
|
||||
|
||||
if (ast) {
|
||||
instructions =
|
||||
buildAnimationTimelines(element, ast, {}, {}, options, EMPTY_INSTRUCTION_MAP, errors);
|
||||
instructions.forEach(inst => {
|
||||
const styles = getOrSetAsInMap(autoStylesMap, inst.element, {});
|
||||
inst.postStyleProps.forEach(prop => styles[prop] = null);
|
||||
});
|
||||
} else {
|
||||
errors.push('The requested animation doesn\'t exist or has already been destroyed');
|
||||
instructions = [];
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
throw new Error(
|
||||
`Unable to create the animation due to the following errors: ${errors.join("\n")}`);
|
||||
}
|
||||
|
||||
autoStylesMap.forEach((styles, element) => {
|
||||
Object.keys(styles).forEach(
|
||||
prop => { styles[prop] = this._driver.computeStyle(element, prop, AUTO_STYLE); });
|
||||
});
|
||||
|
||||
const players = instructions.map(i => {
|
||||
const styles = autoStylesMap.get(i.element);
|
||||
return this._buildPlayer(i, {}, styles);
|
||||
});
|
||||
const player = optimizeGroupPlayer(players);
|
||||
this._playersById[id] = player;
|
||||
player.onDestroy(() => this.destroy(id));
|
||||
|
||||
this.players.push(player);
|
||||
return player;
|
||||
}
|
||||
|
||||
destroy(id: string) {
|
||||
const player = this._getPlayer(id);
|
||||
player.destroy();
|
||||
delete this._playersById[id];
|
||||
const index = this.players.indexOf(player);
|
||||
if (index >= 0) {
|
||||
this.players.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private _getPlayer(id: string): AnimationPlayer {
|
||||
const player = this._playersById[id];
|
||||
if (!player) {
|
||||
throw new Error(`Unable to find the timeline player referenced by ${id}`);
|
||||
}
|
||||
return player;
|
||||
}
|
||||
|
||||
listen(id: string, element: string, eventName: string, callback: (event: any) => any):
|
||||
() => void {
|
||||
// triggerName, fromState, toState are all ignored for timeline animations
|
||||
const baseEvent = makeAnimationEvent(element, '', '', '');
|
||||
listenOnPlayer(this._getPlayer(id), eventName, baseEvent, callback);
|
||||
return () => {};
|
||||
}
|
||||
|
||||
command(id: string, element: any, command: string, args: any[]): void {
|
||||
if (command == 'register') {
|
||||
this.register(id, args[0] as AnimationMetadata | AnimationMetadata[]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command == 'create') {
|
||||
const options = (args[0] || {}) as AnimationOptions;
|
||||
this.create(id, element, options);
|
||||
return;
|
||||
}
|
||||
|
||||
const player = this._getPlayer(id);
|
||||
switch (command) {
|
||||
case 'play':
|
||||
player.play();
|
||||
break;
|
||||
case 'pause':
|
||||
player.pause();
|
||||
break;
|
||||
case 'reset':
|
||||
player.reset();
|
||||
break;
|
||||
case 'restart':
|
||||
player.restart();
|
||||
break;
|
||||
case 'finish':
|
||||
player.finish();
|
||||
break;
|
||||
case 'init':
|
||||
player.init();
|
||||
break;
|
||||
case 'setPosition':
|
||||
player.setPosition(parseFloat(args[0] as string));
|
||||
break;
|
||||
case 'destroy':
|
||||
this.destroy(id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -12,11 +12,15 @@ import {AnimationDriver} from '../animation_driver';
|
|||
import {WebAnimationsPlayer} from './web_animations_player';
|
||||
|
||||
export class WebAnimationsDriver implements AnimationDriver {
|
||||
computeStyle(element: any, prop: string, defaultValue?: string): string {
|
||||
return (window.getComputedStyle(element) as any)[prop] as string;
|
||||
}
|
||||
|
||||
animate(
|
||||
element: any, keyframes: ɵStyleData[], duration: number, delay: number, easing: string,
|
||||
previousPlayers: AnimationPlayer[] = []): WebAnimationsPlayer {
|
||||
const playerOptions: {[key: string]: string |
|
||||
number} = {'duration': duration, 'delay': delay, 'fill': 'forwards'};
|
||||
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)
|
||||
|
|
|
@ -5,7 +5,10 @@
|
|||
* 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 {AUTO_STYLE, AnimationPlayer} from '@angular/animations';
|
||||
import {AnimationPlayer} from '@angular/animations';
|
||||
|
||||
import {copyStyles, eraseStyles, setStyles} from '../../util';
|
||||
|
||||
import {DOMAnimation} from './dom_animation';
|
||||
|
||||
export class WebAnimationsPlayer implements AnimationPlayer {
|
||||
|
@ -24,18 +27,19 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
|||
|
||||
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},
|
||||
previousPlayers: WebAnimationsPlayer[] = []) {
|
||||
private previousPlayers: WebAnimationsPlayer[] = []) {
|
||||
this._duration = <number>options['duration'];
|
||||
this._delay = <number>options['delay'] || 0;
|
||||
this.time = this._duration + this._delay;
|
||||
|
||||
this.previousStyles = {};
|
||||
previousPlayers.forEach(player => {
|
||||
let styles = player._captureStyles();
|
||||
let styles = player.currentSnapshot;
|
||||
Object.keys(styles).forEach(prop => this.previousStyles[prop] = styles[prop]);
|
||||
});
|
||||
}
|
||||
|
@ -52,20 +56,7 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
|||
if (this._initialized) return;
|
||||
this._initialized = true;
|
||||
|
||||
const keyframes = this.keyframes.map(styles => {
|
||||
const formattedKeyframe: {[key: string]: string | number} = {};
|
||||
Object.keys(styles).forEach((prop, index) => {
|
||||
let value = styles[prop];
|
||||
if (value == AUTO_STYLE) {
|
||||
value = _computeStyle(this.element, prop);
|
||||
}
|
||||
if (value != undefined) {
|
||||
formattedKeyframe[prop] = value;
|
||||
}
|
||||
});
|
||||
return formattedKeyframe;
|
||||
});
|
||||
|
||||
const keyframes = this.keyframes.map(styles => copyStyles(styles, false));
|
||||
const previousStyleProps = Object.keys(this.previousStyles);
|
||||
if (previousStyleProps.length) {
|
||||
let startingKeyframe = keyframes[0];
|
||||
|
@ -90,11 +81,14 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
|||
}
|
||||
|
||||
this._player = this._triggerWebAnimation(this.element, keyframes, this.options);
|
||||
this._finalKeyframe =
|
||||
keyframes.length ? _copyKeyframeStyles(keyframes[keyframes.length - 1]) : {};
|
||||
this._finalKeyframe = keyframes.length ? keyframes[keyframes.length - 1] : {};
|
||||
|
||||
// this is required so that the player doesn't start to animate right away
|
||||
this._resetDomPlayerState();
|
||||
if (this._delay) {
|
||||
this._resetDomPlayerState();
|
||||
} else {
|
||||
this._player.pause();
|
||||
}
|
||||
this._player.addEventListener('finish', () => this._onFinish());
|
||||
}
|
||||
|
||||
|
@ -168,7 +162,9 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
|||
|
||||
getPosition(): number { return this._player.currentTime / this.time; }
|
||||
|
||||
private _captureStyles(): {[prop: string]: string | number} {
|
||||
get totalTime(): number { return this._delay + this._duration; }
|
||||
|
||||
beforeDestroy() {
|
||||
const styles: {[key: string]: string | number} = {};
|
||||
if (this.hasStarted()) {
|
||||
Object.keys(this._finalKeyframe).forEach(prop => {
|
||||
|
@ -178,22 +174,10 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
return styles;
|
||||
this.currentSnapshot = styles;
|
||||
}
|
||||
}
|
||||
|
||||
function _computeStyle(element: any, prop: string): string {
|
||||
return (<any>window.getComputedStyle(element))[prop];
|
||||
}
|
||||
|
||||
function _copyKeyframeStyles(styles: {[style: string]: string | number}):
|
||||
{[style: string]: string | number} {
|
||||
const newStyles: {[style: string]: string | number} = {};
|
||||
Object.keys(styles).forEach(prop => {
|
||||
if (prop != 'offset') {
|
||||
newStyles[prop] = styles[prop];
|
||||
}
|
||||
});
|
||||
return newStyles;
|
||||
}
|
||||
|
|
|
@ -5,37 +5,62 @@
|
|||
* 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 {AnimateTimings, ɵStyleData} from '@angular/animations';
|
||||
import {AnimateTimings, AnimationMetadata, AnimationOptions, sequence, ɵStyleData} from '@angular/animations';
|
||||
|
||||
export const ONE_SECOND = 1000;
|
||||
|
||||
export function parseTimeExpression(exp: string | number, errors: string[]): AnimateTimings {
|
||||
const regex = /^([\.\d]+)(m?s)(?:\s+([\.\d]+)(m?s))?(?:\s+([-a-z]+(?:\(.+?\))?))?$/i;
|
||||
export const ENTER_CLASSNAME = 'ng-enter';
|
||||
export const LEAVE_CLASSNAME = 'ng-leave';
|
||||
export const ENTER_SELECTOR = '.ng-enter';
|
||||
export const LEAVE_SELECTOR = '.ng-leave';
|
||||
export const NG_TRIGGER_CLASSNAME = 'ng-trigger';
|
||||
export const NG_TRIGGER_SELECTOR = '.ng-trigger';
|
||||
export const NG_ANIMATING_CLASSNAME = 'ng-animating';
|
||||
export const NG_ANIMATING_SELECTOR = '.ng-animating';
|
||||
|
||||
export function resolveTimingValue(value: string | number) {
|
||||
if (typeof value == 'number') return value;
|
||||
|
||||
const matches = (value as string).match(/^(-?[\.\d]+)(m?s)/);
|
||||
if (!matches || matches.length < 2) return 0;
|
||||
|
||||
return _convertTimeValueToMS(parseFloat(matches[1]), matches[2]);
|
||||
}
|
||||
|
||||
function _convertTimeValueToMS(value: number, unit: string): number {
|
||||
switch (unit) {
|
||||
case 's':
|
||||
return value * ONE_SECOND;
|
||||
default: // ms or something else
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveTiming(
|
||||
timings: string | number | AnimateTimings, errors: any[], allowNegativeValues?: boolean) {
|
||||
return timings.hasOwnProperty('duration') ?
|
||||
<AnimateTimings>timings :
|
||||
parseTimeExpression(<string|number>timings, errors, allowNegativeValues);
|
||||
}
|
||||
|
||||
function parseTimeExpression(
|
||||
exp: string | number, errors: string[], allowNegativeValues?: boolean): AnimateTimings {
|
||||
const regex = /^(-?[\.\d]+)(m?s)(?:\s+(-?[\.\d]+)(m?s))?(?:\s+([-a-z]+(?:\(.+?\))?))?$/i;
|
||||
let duration: number;
|
||||
let delay: number = 0;
|
||||
let easing: string|null = null;
|
||||
let easing: string = '';
|
||||
if (typeof exp === 'string') {
|
||||
const matches = exp.match(regex);
|
||||
if (matches === null) {
|
||||
errors.push(`The provided timing value "${exp}" is invalid.`);
|
||||
return {duration: 0, delay: 0, easing: null};
|
||||
return {duration: 0, delay: 0, easing: ''};
|
||||
}
|
||||
|
||||
let durationMatch = parseFloat(matches[1]);
|
||||
const durationUnit = matches[2];
|
||||
if (durationUnit == 's') {
|
||||
durationMatch *= ONE_SECOND;
|
||||
}
|
||||
duration = Math.floor(durationMatch);
|
||||
duration = _convertTimeValueToMS(parseFloat(matches[1]), matches[2]);
|
||||
|
||||
const delayMatch = matches[3];
|
||||
const delayUnit = matches[4];
|
||||
if (delayMatch != null) {
|
||||
let delayVal: number = parseFloat(delayMatch);
|
||||
if (delayUnit != null && delayUnit == 's') {
|
||||
delayVal *= ONE_SECOND;
|
||||
}
|
||||
delay = Math.floor(delayVal);
|
||||
delay = _convertTimeValueToMS(Math.floor(parseFloat(delayMatch)), matches[4]);
|
||||
}
|
||||
|
||||
const easingVal = matches[5];
|
||||
|
@ -46,9 +71,31 @@ export function parseTimeExpression(exp: string | number, errors: string[]): Ani
|
|||
duration = <number>exp;
|
||||
}
|
||||
|
||||
if (!allowNegativeValues) {
|
||||
let containsErrors = false;
|
||||
let startIndex = errors.length;
|
||||
if (duration < 0) {
|
||||
errors.push(`Duration values below 0 are not allowed for this animation step.`);
|
||||
containsErrors = true;
|
||||
}
|
||||
if (delay < 0) {
|
||||
errors.push(`Delay values below 0 are not allowed for this animation step.`);
|
||||
containsErrors = true;
|
||||
}
|
||||
if (containsErrors) {
|
||||
errors.splice(startIndex, 0, `The provided timing value "${exp}" is invalid.`);
|
||||
}
|
||||
}
|
||||
|
||||
return {duration, delay, easing};
|
||||
}
|
||||
|
||||
export function copyObj(
|
||||
obj: {[key: string]: any}, destination: {[key: string]: any} = {}): {[key: string]: any} {
|
||||
Object.keys(obj).forEach(prop => { destination[prop] = obj[prop]; });
|
||||
return destination;
|
||||
}
|
||||
|
||||
export function normalizeStyles(styles: ɵStyleData | ɵStyleData[]): ɵStyleData {
|
||||
const normalizedStyles: ɵStyleData = {};
|
||||
if (Array.isArray(styles)) {
|
||||
|
@ -69,7 +116,7 @@ export function copyStyles(
|
|||
destination[prop] = styles[prop];
|
||||
}
|
||||
} else {
|
||||
Object.keys(styles).forEach(prop => destination[prop] = styles[prop]);
|
||||
copyObj(styles, destination);
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
|
@ -89,3 +136,73 @@ export function eraseStyles(element: any, styles: ɵStyleData) {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeAnimationEntry(steps: AnimationMetadata | AnimationMetadata[]):
|
||||
AnimationMetadata {
|
||||
if (Array.isArray(steps)) {
|
||||
if (steps.length == 1) return steps[0];
|
||||
return sequence(steps);
|
||||
}
|
||||
return steps as AnimationMetadata;
|
||||
}
|
||||
|
||||
export function validateStyleParams(
|
||||
value: string | number, options: AnimationOptions, errors: any[]) {
|
||||
const params = options.params || {};
|
||||
if (typeof value !== 'string') return;
|
||||
|
||||
const matches = value.toString().match(PARAM_REGEX);
|
||||
if (matches) {
|
||||
matches.forEach(varName => {
|
||||
if (!params.hasOwnProperty(varName)) {
|
||||
errors.push(
|
||||
`Unable to resolve the local animation param ${varName} in the given list of values`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const PARAM_REGEX = /\{\{\s*(.+?)\s*\}\}/g;
|
||||
export function interpolateParams(
|
||||
value: string | number, params: {[name: string]: any}, errors: any[]): string|number {
|
||||
const original = value.toString();
|
||||
const str = original.replace(PARAM_REGEX, (_, varName) => {
|
||||
let localVal = params[varName];
|
||||
// this means that the value was never overidden by the data passed in by the user
|
||||
if (!params.hasOwnProperty(varName)) {
|
||||
errors.push(`Please provide a value for the animation param ${varName}`);
|
||||
localVal = '';
|
||||
}
|
||||
return localVal.toString();
|
||||
});
|
||||
|
||||
// we do this to assert that numeric values stay as they are
|
||||
return str == original ? value : str;
|
||||
}
|
||||
|
||||
export function iteratorToArray(iterator: any): any[] {
|
||||
const arr: any[] = [];
|
||||
let item = iterator.next();
|
||||
while (!item.done) {
|
||||
arr.push(item.value);
|
||||
item = iterator.next();
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
export function mergeAnimationOptions(
|
||||
source: AnimationOptions, destination: AnimationOptions): AnimationOptions {
|
||||
if (source.params) {
|
||||
const p0 = source.params;
|
||||
if (!destination.params) {
|
||||
destination.params = {};
|
||||
}
|
||||
const p1 = destination.params;
|
||||
Object.keys(p0).forEach(param => {
|
||||
if (!p1.hasOwnProperty(param)) {
|
||||
p1[param] = p0[param];
|
||||
}
|
||||
});
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
|
|
|
@ -5,14 +5,38 @@
|
|||
* 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 {AUTO_STYLE, AnimationMetadata, AnimationMetadataType, animate, group, keyframes, sequence, style, ɵStyleData} from '@angular/animations';
|
||||
import {AUTO_STYLE, AnimationMetadata, AnimationMetadataType, animate, group, keyframes, query, sequence, style, ɵStyleData} from '@angular/animations';
|
||||
import {AnimationOptions} from '@angular/core/src/animation/dsl';
|
||||
|
||||
import {Animation} from '../../src/dsl/animation';
|
||||
import {buildAnimationAst} from '../../src/dsl/animation_ast_builder';
|
||||
import {AnimationTimelineInstruction} from '../../src/dsl/animation_timeline_instruction';
|
||||
import {validateAnimationSequence} from '../../src/dsl/animation_validator_visitor';
|
||||
import {ElementInstructionMap} from '../../src/dsl/element_instruction_map';
|
||||
|
||||
function createDiv() {
|
||||
return document.createElement('div');
|
||||
}
|
||||
|
||||
export function main() {
|
||||
describe('Animation', () => {
|
||||
// these tests are only mean't to be run within the DOM (for now)
|
||||
if (typeof Element == 'undefined') return;
|
||||
|
||||
let rootElement: any;
|
||||
let subElement1: any;
|
||||
let subElement2: any;
|
||||
|
||||
beforeEach(() => {
|
||||
rootElement = createDiv();
|
||||
subElement1 = createDiv();
|
||||
subElement2 = createDiv();
|
||||
document.body.appendChild(rootElement);
|
||||
rootElement.appendChild(subElement1);
|
||||
rootElement.appendChild(subElement2);
|
||||
});
|
||||
|
||||
afterEach(() => { document.body.removeChild(rootElement); });
|
||||
|
||||
describe('validation', () => {
|
||||
it('should throw an error if one or more but not all keyframes() styles contain offsets',
|
||||
() => {
|
||||
|
@ -90,6 +114,48 @@ export function main() {
|
|||
validateAndThrowAnimationSequence(steps2);
|
||||
}).toThrowError(/The provided timing value "500ms 500ms 500ms ease-out" is invalid/);
|
||||
});
|
||||
|
||||
it('should throw if negative durations are used', () => {
|
||||
const steps = [animate(-1000, style({opacity: 1}))];
|
||||
|
||||
expect(() => {
|
||||
validateAndThrowAnimationSequence(steps);
|
||||
}).toThrowError(/Duration values below 0 are not allowed for this animation step/);
|
||||
|
||||
const steps2 = [animate('-1s', style({opacity: 1}))];
|
||||
|
||||
expect(() => {
|
||||
validateAndThrowAnimationSequence(steps2);
|
||||
}).toThrowError(/Duration values below 0 are not allowed for this animation step/);
|
||||
});
|
||||
|
||||
it('should throw if negative delays are used', () => {
|
||||
const steps = [animate('1s -500ms', style({opacity: 1}))];
|
||||
|
||||
expect(() => {
|
||||
validateAndThrowAnimationSequence(steps);
|
||||
}).toThrowError(/Delay values below 0 are not allowed for this animation step/);
|
||||
|
||||
const steps2 = [animate('1s -0.5s', style({opacity: 1}))];
|
||||
|
||||
expect(() => {
|
||||
validateAndThrowAnimationSequence(steps2);
|
||||
}).toThrowError(/Delay values below 0 are not allowed for this animation step/);
|
||||
});
|
||||
|
||||
it('should throw if keyframes() is not used inside of animate()', () => {
|
||||
const steps = [keyframes([])];
|
||||
|
||||
expect(() => {
|
||||
validateAndThrowAnimationSequence(steps);
|
||||
}).toThrowError(/keyframes\(\) must be placed inside of a call to animate\(\)/);
|
||||
|
||||
const steps2 = [group([keyframes([])])];
|
||||
|
||||
expect(() => {
|
||||
validateAndThrowAnimationSequence(steps2);
|
||||
}).toThrowError(/keyframes\(\) must be placed inside of a call to animate\(\)/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyframe building', () => {
|
||||
|
@ -102,7 +168,7 @@ export function main() {
|
|||
animate(1000, style({width: 200}))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
const players = invokeAnimationSequence(rootElement, steps);
|
||||
expect(players[0].keyframes).toEqual([
|
||||
{height: AUTO_STYLE, width: 0, offset: 0},
|
||||
{height: 50, width: 0, offset: .25},
|
||||
|
@ -116,7 +182,7 @@ export function main() {
|
|||
() => {
|
||||
const steps = [animate(1000, style({width: 999}))];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
const players = invokeAnimationSequence(rootElement, steps);
|
||||
expect(players[0].keyframes).toEqual([
|
||||
{width: AUTO_STYLE, offset: 0}, {width: 999, offset: 1}
|
||||
]);
|
||||
|
@ -128,7 +194,7 @@ export function main() {
|
|||
animate(1000, style({width: 100, height: 400, opacity: 1}))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
const players = invokeAnimationSequence(rootElement, steps);
|
||||
expect(players[0].keyframes).toEqual([
|
||||
{width: 200, height: 0, opacity: 0, offset: 0},
|
||||
{width: 100, height: 400, opacity: 1, offset: 1}
|
||||
|
@ -142,7 +208,7 @@ export function main() {
|
|||
animate(1000, style({opacity: 1}))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
const players = invokeAnimationSequence(rootElement, steps);
|
||||
const keyframes = humanizeOffsets(players[0].keyframes, 4);
|
||||
|
||||
expect(keyframes).toEqual([
|
||||
|
@ -159,7 +225,7 @@ export function main() {
|
|||
animate('1s cubic-bezier(.29, .55 ,.53 ,1.53)', style({opacity: 1}))
|
||||
];
|
||||
|
||||
const player = invokeAnimationSequence(steps)[0];
|
||||
const player = invokeAnimationSequence(rootElement, steps)[0];
|
||||
const firstKeyframe = player.keyframes[0];
|
||||
const firstKeyframeEasing = firstKeyframe['easing'] as string;
|
||||
expect(firstKeyframeEasing.replace(/\s+/g, '')).toEqual('cubic-bezier(.29,.55,.53,1.53)');
|
||||
|
@ -170,34 +236,30 @@ export function main() {
|
|||
it('should not produce extra timelines when multiple sequences are used within each other',
|
||||
() => {
|
||||
const steps = [
|
||||
style({width: 0}), animate(1000, style({width: 100})), sequence([
|
||||
style({width: 0}),
|
||||
animate(1000, style({width: 100})),
|
||||
sequence([
|
||||
animate(1000, style({width: 200})),
|
||||
sequence([animate(1000, style({width: 300}))])
|
||||
sequence([
|
||||
animate(1000, style({width: 300})),
|
||||
]),
|
||||
]),
|
||||
animate(1000, style({width: 400})),
|
||||
sequence([
|
||||
animate(1000, style({width: 500})),
|
||||
]),
|
||||
animate(1000, style({width: 400})), sequence([animate(1000, style({width: 500}))])
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
expect(players[0].keyframes).toEqual([
|
||||
const players = invokeAnimationSequence(rootElement, steps);
|
||||
expect(players.length).toEqual(1);
|
||||
|
||||
const player = players[0];
|
||||
expect(player.keyframes).toEqual([
|
||||
{width: 0, offset: 0}, {width: 100, offset: .2}, {width: 200, offset: .4},
|
||||
{width: 300, offset: .6}, {width: 400, offset: .8}, {width: 500, offset: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should produce a 1ms animation step if a style call exists before sequence within a call to animate()',
|
||||
() => {
|
||||
const steps = [
|
||||
style({width: 100}), sequence([
|
||||
animate(1000, style({width: 200})),
|
||||
])
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
expect(humanizeOffsets(players[0].keyframes, 4)).toEqual([
|
||||
{width: 100, offset: 0}, {width: 100, offset: .001}, {width: 200, offset: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should create a new timeline after a sequence if group() or keyframe() commands are used within',
|
||||
() => {
|
||||
const steps = [
|
||||
|
@ -211,7 +273,7 @@ export function main() {
|
|||
animate(1000, style({width: 500, height: 500}))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
const players = invokeAnimationSequence(rootElement, steps);
|
||||
expect(players.length).toEqual(4);
|
||||
|
||||
const finalPlayer = players[players.length - 1];
|
||||
|
@ -219,6 +281,76 @@ export function main() {
|
|||
{width: 200, height: 200, offset: 0}, {width: 500, height: 500, offset: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should push the start of a sequence if a delay option is provided', () => {
|
||||
const steps = [
|
||||
style({width: '0px'}), animate(1000, style({width: '100px'})),
|
||||
sequence(
|
||||
[
|
||||
animate(1000, style({width: '200px'})),
|
||||
],
|
||||
{delay: 500})
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(rootElement, steps);
|
||||
const finalPlayer = players[players.length - 1];
|
||||
expect(finalPlayer.keyframes).toEqual([
|
||||
{width: '100px', offset: 0},
|
||||
{width: '200px', offset: 1},
|
||||
]);
|
||||
expect(finalPlayer.delay).toEqual(1500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('subtitutions', () => {
|
||||
it('should substitute in timing values', () => {
|
||||
function makeAnimation(exp: string, values: {[key: string]: any}) {
|
||||
const steps = [style({opacity: 0}), animate(exp, style({opacity: 1}))];
|
||||
return invokeAnimationSequence(rootElement, steps, values);
|
||||
}
|
||||
|
||||
let players = makeAnimation('{{ duration }}', buildParams({duration: '1234ms'}));
|
||||
expect(players[0].duration).toEqual(1234);
|
||||
|
||||
players = makeAnimation('{{ duration }}', buildParams({duration: '9s 2s'}));
|
||||
expect(players[0].duration).toEqual(11000);
|
||||
|
||||
players = makeAnimation('{{ duration }} 1s', buildParams({duration: '1.5s'}));
|
||||
expect(players[0].duration).toEqual(2500);
|
||||
|
||||
players = makeAnimation(
|
||||
'{{ duration }} {{ delay }}', buildParams({duration: '1s', delay: '2s'}));
|
||||
expect(players[0].duration).toEqual(3000);
|
||||
});
|
||||
|
||||
it('should allow multiple substitutions to occur within the same style value', () => {
|
||||
const steps = [
|
||||
style({transform: ''}),
|
||||
animate(1000, style({transform: 'translateX({{ x }}) translateY({{ y }})'}))
|
||||
];
|
||||
const players =
|
||||
invokeAnimationSequence(rootElement, steps, buildParams({x: '200px', y: '400px'}));
|
||||
expect(players[0].keyframes).toEqual([
|
||||
{offset: 0, transform: ''},
|
||||
{offset: 1, transform: 'translateX(200px) translateY(400px)'}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw an error when an input variable is not provided when invoked and is not a default value',
|
||||
() => {
|
||||
expect(() => {invokeAnimationSequence(rootElement, [style({color: '{{ color }}'})])})
|
||||
.toThrowError(/Please provide a value for the animation param color/);
|
||||
|
||||
expect(
|
||||
() => {invokeAnimationSequence(
|
||||
rootElement,
|
||||
[
|
||||
style({color: '{{ start }}'}),
|
||||
animate('{{ time }}', style({color: '{{ end }}'})),
|
||||
],
|
||||
buildParams({start: 'blue', end: 'red'}))})
|
||||
.toThrowError(/Please provide a value for the animation param time/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyframes()', () => {
|
||||
|
@ -230,7 +362,7 @@ export function main() {
|
|||
animate(1000, style({height: 0, opacity: 0}))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
const players = invokeAnimationSequence(rootElement, steps);
|
||||
expect(players.length).toEqual(3);
|
||||
|
||||
const player0 = players[0];
|
||||
|
@ -267,7 +399,7 @@ export function main() {
|
|||
animate(1000, style({color: 'green', opacity: 0}))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
const players = invokeAnimationSequence(rootElement, steps);
|
||||
const finalPlayer = players[players.length - 1];
|
||||
expect(finalPlayer.keyframes).toEqual([
|
||||
{opacity: 1, color: 'blue', offset: 0}, {opacity: 0, color: 'green', offset: 1}
|
||||
|
@ -283,7 +415,7 @@ export function main() {
|
|||
]))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
const players = invokeAnimationSequence(rootElement, steps);
|
||||
expect(players.length).toEqual(2);
|
||||
|
||||
const topPlayer = players[0];
|
||||
|
@ -305,7 +437,7 @@ export function main() {
|
|||
keyframes([style({opacity: .8, offset: .5}), style({opacity: 1, offset: 1})]))
|
||||
];
|
||||
|
||||
const player = invokeAnimationSequence(steps)[1];
|
||||
const player = invokeAnimationSequence(rootElement, steps)[1];
|
||||
expect(player.easing).toEqual('ease-out');
|
||||
});
|
||||
|
||||
|
@ -318,7 +450,7 @@ export function main() {
|
|||
keyframes([style({opacity: .8, offset: .5}), style({opacity: 1, offset: 1})]))
|
||||
];
|
||||
|
||||
const player = invokeAnimationSequence(steps)[1];
|
||||
const player = invokeAnimationSequence(rootElement, steps)[1];
|
||||
expect(player.delay).toEqual(2500);
|
||||
});
|
||||
|
||||
|
@ -343,7 +475,7 @@ export function main() {
|
|||
group([animate('2s', style({height: '500px', width: '500px'}))])
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
const players = invokeAnimationSequence(rootElement, steps);
|
||||
expect(players.length).toEqual(5);
|
||||
|
||||
const firstPlayerKeyframes = players[0].keyframes;
|
||||
|
@ -381,7 +513,7 @@ export function main() {
|
|||
style({opacity: 1, offset: 1})
|
||||
]));
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
const players = invokeAnimationSequence(rootElement, steps);
|
||||
expect(players.length).toEqual(1);
|
||||
const player = players[0];
|
||||
|
||||
|
@ -398,7 +530,7 @@ export function main() {
|
|||
{type: AnimationMetadataType.Style, offset: 1, styles: {opacity: 1}},
|
||||
]));
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
const players = invokeAnimationSequence(rootElement, steps);
|
||||
expect(players.length).toEqual(1);
|
||||
const player = players[0];
|
||||
|
||||
|
@ -417,7 +549,7 @@ export function main() {
|
|||
animate(1000, style({width: 1000, height: 1000}))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
const players = invokeAnimationSequence(rootElement, steps);
|
||||
expect(players.length).toEqual(4);
|
||||
|
||||
const player0 = players[0];
|
||||
|
@ -460,7 +592,7 @@ export function main() {
|
|||
])
|
||||
])];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
const players = invokeAnimationSequence(rootElement, steps);
|
||||
expect(players.length).toEqual(2);
|
||||
|
||||
const gPlayer1 = players[0];
|
||||
|
@ -484,7 +616,7 @@ export function main() {
|
|||
animate('1s 1s', style({height: 200, width: 200}))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
const players = invokeAnimationSequence(rootElement, steps);
|
||||
expect(players.length).toEqual(4);
|
||||
|
||||
const finalPlayer = players[players.length - 1];
|
||||
|
@ -505,7 +637,7 @@ export function main() {
|
|||
animate(2000, style({width: 0, opacity: 0}))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
const players = invokeAnimationSequence(rootElement, steps);
|
||||
const middlePlayer = players[2];
|
||||
expect(middlePlayer.delay).toEqual(2000);
|
||||
expect(middlePlayer.duration).toEqual(2000);
|
||||
|
@ -514,6 +646,97 @@ export function main() {
|
|||
expect(finalPlayer.delay).toEqual(6000);
|
||||
expect(finalPlayer.duration).toEqual(2000);
|
||||
});
|
||||
|
||||
it('should push the start of a group if a delay option is provided', () => {
|
||||
const steps = [
|
||||
style({width: '0px', height: '0px'}),
|
||||
animate(1500, style({width: '100px', height: '100px'})),
|
||||
group(
|
||||
[
|
||||
animate(1000, style({width: '200px'})),
|
||||
animate(2000, style({height: '200px'})),
|
||||
],
|
||||
{delay: 300})
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(rootElement, steps);
|
||||
const finalWidthPlayer = players[players.length - 2];
|
||||
const finalHeightPlayer = players[players.length - 1];
|
||||
|
||||
expect(finalWidthPlayer.delay).toEqual(1800);
|
||||
expect(finalWidthPlayer.keyframes).toEqual([
|
||||
{width: '100px', offset: 0},
|
||||
{width: '200px', offset: 1},
|
||||
]);
|
||||
|
||||
expect(finalHeightPlayer.delay).toEqual(1800);
|
||||
expect(finalHeightPlayer.keyframes).toEqual([
|
||||
{height: '100px', offset: 0},
|
||||
{height: '200px', offset: 1},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('query()', () => {
|
||||
it('should delay the query operation if a delay option is provided', () => {
|
||||
const steps = [
|
||||
style({opacity: 0}), animate(1000, style({opacity: 1})),
|
||||
query(
|
||||
'div',
|
||||
[
|
||||
style({width: 0}),
|
||||
animate(500, style({width: 200})),
|
||||
],
|
||||
{delay: 200})
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(rootElement, steps);
|
||||
const finalPlayer = players[players.length - 1];
|
||||
expect(finalPlayer.delay).toEqual(1200);
|
||||
});
|
||||
|
||||
it('should throw an error when an animation query returns zero elements', () => {
|
||||
const steps =
|
||||
[query('somethingFake', [style({opacity: 0}), animate(1000, style({opacity: 1}))])];
|
||||
|
||||
expect(() => { invokeAnimationSequence(rootElement, steps); })
|
||||
.toThrowError(
|
||||
/`query\("somethingFake"\)` returned zero elements\. \(Use `query\("somethingFake", \{ optional: true \}\)` if you wish to allow this\.\)/);
|
||||
});
|
||||
|
||||
it('should allow a query to be skipped if it is set as optional and returns zero elements',
|
||||
() => {
|
||||
const steps = [query(
|
||||
'somethingFake', [style({opacity: 0}), animate(1000, style({opacity: 1}))],
|
||||
{optional: true})];
|
||||
|
||||
expect(() => { invokeAnimationSequence(rootElement, steps); }).not.toThrow();
|
||||
|
||||
const steps2 = [query(
|
||||
'fakeSomethings', [style({opacity: 0}), animate(1000, style({opacity: 1}))],
|
||||
{optional: true})];
|
||||
|
||||
expect(() => { invokeAnimationSequence(rootElement, steps2); }).not.toThrow();
|
||||
});
|
||||
|
||||
it('should delay the query operation if a delay option is provided', () => {
|
||||
const steps = [
|
||||
style({opacity: 0}), animate(1300, style({opacity: 1})),
|
||||
query(
|
||||
'div',
|
||||
[
|
||||
style({width: 0}),
|
||||
animate(500, style({width: 200})),
|
||||
],
|
||||
{delay: 300})
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(rootElement, steps);
|
||||
const fp1 = players[players.length - 2];
|
||||
const fp2 = players[players.length - 1];
|
||||
expect(fp1.delay).toEqual(1600);
|
||||
expect(fp2.delay).toEqual(1600);
|
||||
});
|
||||
});
|
||||
|
||||
describe('timing values', () => {
|
||||
|
@ -522,7 +745,7 @@ export function main() {
|
|||
const steps: AnimationMetadata[] =
|
||||
[style({opacity: 0}), animate('3s 1s ease-out', style({opacity: 1}))];
|
||||
|
||||
const player = invokeAnimationSequence(steps)[0];
|
||||
const player = invokeAnimationSequence(rootElement, steps)[0];
|
||||
expect(player.keyframes).toEqual([
|
||||
{opacity: 0, offset: 0}, {opacity: 0, offset: .25, easing: 'ease-out'},
|
||||
{opacity: 1, offset: 1}
|
||||
|
@ -535,7 +758,7 @@ export function main() {
|
|||
animate('2s ease-out', style({width: 20})), animate('1s ease-in', style({width: 30}))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
const players = invokeAnimationSequence(rootElement, steps);
|
||||
expect(players.length).toEqual(1);
|
||||
|
||||
const player = players[0];
|
||||
|
@ -560,7 +783,7 @@ export function main() {
|
|||
])
|
||||
];
|
||||
|
||||
const player = invokeAnimationSequence(steps)[0];
|
||||
const player = invokeAnimationSequence(rootElement, steps)[0];
|
||||
expect(player.duration).toEqual(1000);
|
||||
expect(player.delay).toEqual(0);
|
||||
});
|
||||
|
@ -579,7 +802,7 @@ export function main() {
|
|||
]))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
const players = invokeAnimationSequence(rootElement, steps);
|
||||
expect(players[0].delay).toEqual(0); // top-level animation
|
||||
expect(players[1].delay).toEqual(1500); // first entry in group()
|
||||
expect(players[2].delay).toEqual(1500); // second entry in group()
|
||||
|
@ -595,7 +818,7 @@ export function main() {
|
|||
|
||||
const toStyles: ɵStyleData[] = [{background: 'red'}];
|
||||
|
||||
const player = invokeAnimationSequence(steps, fromStyles, toStyles)[0];
|
||||
const player = invokeAnimationSequence(rootElement, steps, {}, fromStyles, toStyles)[0];
|
||||
expect(player.duration).toEqual(0);
|
||||
expect(player.keyframes).toEqual([]);
|
||||
});
|
||||
|
@ -608,7 +831,7 @@ export function main() {
|
|||
|
||||
const toStyles: ɵStyleData[] = [{background: 'red'}];
|
||||
|
||||
const players = invokeAnimationSequence(steps, fromStyles, toStyles);
|
||||
const players = invokeAnimationSequence(rootElement, steps, {}, fromStyles, toStyles);
|
||||
expect(players[0].keyframes).toEqual([
|
||||
{background: 'blue', height: 100, offset: 0},
|
||||
{background: 'red', height: AUTO_STYLE, offset: 1}
|
||||
|
@ -623,7 +846,7 @@ export function main() {
|
|||
|
||||
const toStyles: ɵStyleData[] = [{background: 'red'}];
|
||||
|
||||
const players = invokeAnimationSequence(steps, fromStyles, toStyles);
|
||||
const players = invokeAnimationSequence(rootElement, steps, {}, fromStyles, toStyles);
|
||||
expect(players[0].keyframes).toEqual([
|
||||
{background: 'blue', offset: 0, easing: 'ease-out'},
|
||||
{background: 'red', offset: 1}
|
||||
|
@ -642,16 +865,21 @@ function humanizeOffsets(keyframes: ɵStyleData[], digits: number = 3): ɵStyleD
|
|||
}
|
||||
|
||||
function invokeAnimationSequence(
|
||||
steps: AnimationMetadata | AnimationMetadata[], startingStyles: ɵStyleData[] = [],
|
||||
destinationStyles: ɵStyleData[] = []): AnimationTimelineInstruction[] {
|
||||
return new Animation(steps).buildTimelines(startingStyles, destinationStyles);
|
||||
element: any, steps: AnimationMetadata | AnimationMetadata[], locals: {[key: string]: any} = {},
|
||||
startingStyles: ɵStyleData[] = [], destinationStyles: ɵStyleData[] = [],
|
||||
subInstructions?: ElementInstructionMap): AnimationTimelineInstruction[] {
|
||||
return new Animation(steps).buildTimelines(
|
||||
element, startingStyles, destinationStyles, locals, subInstructions);
|
||||
}
|
||||
|
||||
function validateAndThrowAnimationSequence(steps: AnimationMetadata | AnimationMetadata[]) {
|
||||
const ast =
|
||||
Array.isArray(steps) ? sequence(<AnimationMetadata[]>steps) : <AnimationMetadata>steps;
|
||||
const errors = validateAnimationSequence(ast);
|
||||
const errors: any[] = [];
|
||||
const ast = buildAnimationAst(steps, errors);
|
||||
if (errors.length) {
|
||||
throw new Error(errors.join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
function buildParams(params: {[name: string]: any}): AnimationOptions {
|
||||
return <AnimationOptions>{params};
|
||||
}
|
||||
|
|
|
@ -6,17 +6,25 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {animate, state, style, transition, trigger} from '@angular/animations';
|
||||
import {buildTrigger} from '../../src/dsl/animation_trigger';
|
||||
import {AnimationOptions, animate, state, style, transition} from '@angular/animations';
|
||||
import {AnimationTransitionInstruction} from '@angular/animations/browser/src/dsl/animation_transition_instruction';
|
||||
import {AnimationTrigger} from '@angular/animations/browser/src/dsl/animation_trigger';
|
||||
|
||||
function makeTrigger(name: string, steps: any) {
|
||||
const triggerData = trigger(name, steps);
|
||||
const triggerInstance = buildTrigger(triggerData.name, triggerData.definitions);
|
||||
return triggerInstance;
|
||||
}
|
||||
import {makeTrigger} from '../shared';
|
||||
|
||||
export function main() {
|
||||
describe('AnimationTrigger', () => {
|
||||
// these tests are only mean't to be run within the DOM (for now)
|
||||
if (typeof Element == 'undefined') return;
|
||||
|
||||
let element: any;
|
||||
beforeEach(() => {
|
||||
element = document.createElement('div');
|
||||
document.body.appendChild(element);
|
||||
});
|
||||
|
||||
afterEach(() => { document.body.removeChild(element); });
|
||||
|
||||
describe('trigger validation', () => {
|
||||
it('should group errors together for an animation trigger', () => {
|
||||
expect(() => {
|
||||
|
@ -64,7 +72,7 @@ export function main() {
|
|||
const result = makeTrigger(
|
||||
'name', [transition('a => b', animate(1234)), transition('b => c', animate(5678))]);
|
||||
|
||||
const trans = result.matchTransition('b', 'c') !;
|
||||
const trans = buildTransition(result, element, 'b', 'c') !;
|
||||
expect(trans.timelines.length).toEqual(1);
|
||||
const timeline = trans.timelines[0];
|
||||
expect(timeline.duration).toEqual(5678);
|
||||
|
@ -76,99 +84,148 @@ export function main() {
|
|||
transition('* => *', animate(9999))
|
||||
]);
|
||||
|
||||
let trans = result.matchTransition('b', 'c') !;
|
||||
let trans = buildTransition(result, element, 'b', 'c') !;
|
||||
expect(trans.timelines[0].duration).toEqual(5678);
|
||||
|
||||
trans = result.matchTransition('a', 'b') !;
|
||||
trans = buildTransition(result, element, 'a', 'b') !;
|
||||
expect(trans.timelines[0].duration).toEqual(1234);
|
||||
|
||||
trans = result.matchTransition('c', 'c') !;
|
||||
trans = buildTransition(result, element, 'c', 'c') !;
|
||||
expect(trans.timelines[0].duration).toEqual(9999);
|
||||
});
|
||||
|
||||
it('should null when no results are found', () => {
|
||||
const result = makeTrigger('name', [transition('a => b', animate(1111))]);
|
||||
|
||||
const trans = result.matchTransition('b', 'a');
|
||||
expect(trans).toBeFalsy();
|
||||
const trigger = result.matchTransition('b', 'a');
|
||||
expect(trigger).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should allow a function to be used as a predicate for the transition', () => {
|
||||
let returnValue = false;
|
||||
|
||||
const result = makeTrigger('name', [transition((from, to) => returnValue, animate(1111))]);
|
||||
|
||||
expect(result.matchTransition('a', 'b')).toBeFalsy();
|
||||
expect(result.matchTransition('1', 2)).toBeFalsy();
|
||||
expect(result.matchTransition(false, true)).toBeFalsy();
|
||||
|
||||
returnValue = true;
|
||||
|
||||
expect(result.matchTransition('a', 'b')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call each transition predicate function until the first one that returns true',
|
||||
() => {
|
||||
let count = 0;
|
||||
|
||||
function countAndReturn(value: boolean) {
|
||||
return (fromState: any, toState: any) => {
|
||||
count++;
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
const result = makeTrigger('name', [
|
||||
transition(countAndReturn(false), animate(1111)),
|
||||
transition(countAndReturn(false), animate(2222)),
|
||||
transition(countAndReturn(true), animate(3333)),
|
||||
transition(countAndReturn(true), animate(3333))
|
||||
]);
|
||||
|
||||
const trans = result.matchTransition('a', 'b') !;
|
||||
expect(trans.timelines[0].duration).toEqual(3333);
|
||||
|
||||
expect(count).toEqual(3);
|
||||
});
|
||||
|
||||
it('should support bi-directional transition expressions', () => {
|
||||
const result = makeTrigger('name', [transition('a <=> b', animate(2222))]);
|
||||
|
||||
const t1 = result.matchTransition('a', 'b') !;
|
||||
const t1 = buildTransition(result, element, 'a', 'b') !;
|
||||
expect(t1.timelines[0].duration).toEqual(2222);
|
||||
|
||||
const t2 = result.matchTransition('b', 'a') !;
|
||||
const t2 = buildTransition(result, element, 'b', 'a') !;
|
||||
expect(t2.timelines[0].duration).toEqual(2222);
|
||||
});
|
||||
|
||||
it('should support multiple transition statements in one string', () => {
|
||||
const result = makeTrigger('name', [transition('a => b, b => a, c => *', animate(1234))]);
|
||||
|
||||
const t1 = result.matchTransition('a', 'b') !;
|
||||
const t1 = buildTransition(result, element, 'a', 'b') !;
|
||||
expect(t1.timelines[0].duration).toEqual(1234);
|
||||
|
||||
const t2 = result.matchTransition('b', 'a') !;
|
||||
const t2 = buildTransition(result, element, 'b', 'a') !;
|
||||
expect(t2.timelines[0].duration).toEqual(1234);
|
||||
|
||||
const t3 = result.matchTransition('c', 'a') !;
|
||||
const t3 = buildTransition(result, element, 'c', 'a') !;
|
||||
expect(t3.timelines[0].duration).toEqual(1234);
|
||||
});
|
||||
|
||||
describe('params', () => {
|
||||
it('should support transition-level animation variable params', () => {
|
||||
const result = makeTrigger(
|
||||
'name',
|
||||
[transition(
|
||||
'a => b', [style({height: '{{ a }}'}), animate(1000, style({height: '{{ b }}'}))],
|
||||
buildParams({a: '100px', b: '200px'}))]);
|
||||
|
||||
const trans = buildTransition(result, element, 'a', 'b') !;
|
||||
const keyframes = trans.timelines[0].keyframes;
|
||||
expect(keyframes).toEqual([{height: '100px', offset: 0}, {height: '200px', offset: 1}]);
|
||||
});
|
||||
|
||||
it('should subtitute variable params provided directly within the transition match', () => {
|
||||
const result = makeTrigger(
|
||||
'name',
|
||||
[transition(
|
||||
'a => b', [style({height: '{{ a }}'}), animate(1000, style({height: '{{ b }}'}))],
|
||||
buildParams({a: '100px', b: '200px'}))]);
|
||||
|
||||
const trans = buildTransition(result, element, 'a', 'b', buildParams({a: '300px'})) !;
|
||||
|
||||
const keyframes = trans.timelines[0].keyframes;
|
||||
expect(keyframes).toEqual([{height: '300px', offset: 0}, {height: '200px', offset: 1}]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should match `true` and `false` given boolean values', () => {
|
||||
const result = makeTrigger('name', [
|
||||
state('false', style({color: 'red'})), state('true', style({color: 'green'})),
|
||||
transition('true <=> false', animate(1234))
|
||||
]);
|
||||
|
||||
const trans = buildTransition(result, element, false, true) !;
|
||||
expect(trans.timelines[0].duration).toEqual(1234);
|
||||
});
|
||||
|
||||
it('should match `1` and `0` given boolean values', () => {
|
||||
const result = makeTrigger('name', [
|
||||
state('0', style({color: 'red'})), state('1', style({color: 'green'})),
|
||||
transition('1 <=> 0', animate(4567))
|
||||
]);
|
||||
|
||||
const trans = buildTransition(result, element, false, true) !;
|
||||
expect(trans.timelines[0].duration).toEqual(4567);
|
||||
});
|
||||
|
||||
it('should match `true` and `false` state styles on a `1 <=> 0` boolean transition given boolean values',
|
||||
() => {
|
||||
const result = makeTrigger('name', [
|
||||
state('false', style({color: 'red'})), state('true', style({color: 'green'})),
|
||||
transition('1 <=> 0', animate(4567))
|
||||
]);
|
||||
|
||||
const trans = buildTransition(result, element, false, true) !;
|
||||
expect(trans.timelines[0].keyframes).toEqual([
|
||||
{offset: 0, color: 'red'}, {offset: 1, color: 'green'}
|
||||
])
|
||||
});
|
||||
|
||||
it('should match `1` and `0` state styles on a `true <=> false` boolean transition given boolean values',
|
||||
() => {
|
||||
const result = makeTrigger('name', [
|
||||
state('0', style({color: 'orange'})), state('1', style({color: 'blue'})),
|
||||
transition('true <=> false', animate(4567))
|
||||
]);
|
||||
|
||||
const trans = buildTransition(result, element, false, true) !;
|
||||
expect(trans.timelines[0].keyframes).toEqual([
|
||||
{offset: 0, color: 'orange'}, {offset: 1, color: 'blue'}
|
||||
])
|
||||
});
|
||||
|
||||
describe('aliases', () => {
|
||||
it('should alias the :enter transition as void => *', () => {
|
||||
const result = makeTrigger('name', [transition(':enter', animate(3333))]);
|
||||
|
||||
const trans = result.matchTransition('void', 'something') !;
|
||||
const trans = buildTransition(result, element, 'void', 'something') !;
|
||||
expect(trans.timelines[0].duration).toEqual(3333);
|
||||
});
|
||||
|
||||
it('should alias the :leave transition as * => void', () => {
|
||||
const result = makeTrigger('name', [transition(':leave', animate(3333))]);
|
||||
|
||||
const trans = result.matchTransition('something', 'void') !;
|
||||
const trans = buildTransition(result, element, 'something', 'void') !;
|
||||
expect(trans.timelines[0].duration).toEqual(3333);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function buildTransition(
|
||||
trigger: AnimationTrigger, element: any, fromState: any, toState: any,
|
||||
params?: AnimationOptions): AnimationTransitionInstruction|null {
|
||||
const trans = trigger.matchTransition(fromState, toState) !;
|
||||
if (trans) {
|
||||
return trans.build(element, fromState, toState, params) !;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildParams(params: {[name: string]: any}): AnimationOptions {
|
||||
return <AnimationOptions>{params};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* @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 {AnimationMetadata, animate, style} from '@angular/animations';
|
||||
|
||||
import {AnimationStyleNormalizer, NoopAnimationStyleNormalizer} from '../../src/dsl/style_normalization/animation_style_normalizer';
|
||||
import {AnimationDriver} from '../../src/render/animation_driver';
|
||||
import {TimelineAnimationEngine} from '../../src/render/timeline_animation_engine';
|
||||
import {MockAnimationDriver, MockAnimationPlayer} from '../../testing/src/mock_animation_driver';
|
||||
|
||||
export function main() {
|
||||
const defaultDriver = new MockAnimationDriver();
|
||||
|
||||
function makeEngine(driver?: AnimationDriver, normalizer?: AnimationStyleNormalizer) {
|
||||
return new TimelineAnimationEngine(
|
||||
driver || defaultDriver, normalizer || new NoopAnimationStyleNormalizer());
|
||||
}
|
||||
|
||||
// these tests are only mean't to be run within the DOM
|
||||
if (typeof Element == 'undefined') return;
|
||||
|
||||
describe('TimelineAnimationEngine', () => {
|
||||
let element: any;
|
||||
|
||||
beforeEach(() => {
|
||||
MockAnimationDriver.log = [];
|
||||
element = document.createElement('div');
|
||||
document.body.appendChild(element);
|
||||
});
|
||||
|
||||
afterEach(() => document.body.removeChild(element));
|
||||
|
||||
it('should animate a timeline', () => {
|
||||
const engine = makeEngine();
|
||||
const steps = [style({height: 100}), animate(1000, style({height: 0}))];
|
||||
expect(MockAnimationDriver.log.length).toEqual(0);
|
||||
invokeAnimation(engine, element, steps);
|
||||
expect(MockAnimationDriver.log.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should not destroy timeline-based animations after they have finished', () => {
|
||||
const engine = makeEngine();
|
||||
|
||||
const log: string[] = [];
|
||||
function capture(value: string) {
|
||||
return () => { log.push(value); };
|
||||
}
|
||||
|
||||
const steps = [style({height: 0}), animate(1000, style({height: 500}))];
|
||||
|
||||
const player = invokeAnimation(engine, element, steps);
|
||||
player.onDone(capture('done'));
|
||||
player.onDestroy(capture('destroy'));
|
||||
expect(log).toEqual([]);
|
||||
|
||||
player.finish();
|
||||
expect(log).toEqual(['done']);
|
||||
|
||||
player.destroy();
|
||||
expect(log).toEqual(['done', 'destroy']);
|
||||
});
|
||||
|
||||
it('should normalize the style values that are animateTransitioned within an a timeline animation',
|
||||
() => {
|
||||
const engine = makeEngine(defaultDriver, new SuffixNormalizer('-normalized'));
|
||||
|
||||
const steps = [
|
||||
style({width: '333px'}),
|
||||
animate(1000, style({width: '999px'})),
|
||||
];
|
||||
|
||||
const player = invokeAnimation(engine, element, steps) as MockAnimationPlayer;
|
||||
expect(player.keyframes).toEqual([
|
||||
{'width-normalized': '333px-normalized', offset: 0},
|
||||
{'width-normalized': '999px-normalized', offset: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should normalize `*` values', () => {
|
||||
const driver = new SuperMockDriver();
|
||||
const engine = makeEngine(driver);
|
||||
|
||||
const steps = [
|
||||
style({width: '*'}),
|
||||
animate(1000, style({width: '999px'})),
|
||||
];
|
||||
|
||||
const player = invokeAnimation(engine, element, steps) as MockAnimationPlayer;
|
||||
expect(player.keyframes).toEqual([{width: '*star*', offset: 0}, {width: '999px', offset: 1}]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function invokeAnimation(
|
||||
engine: TimelineAnimationEngine, element: any, steps: AnimationMetadata | AnimationMetadata[],
|
||||
id: string = 'id') {
|
||||
engine.register(id, steps);
|
||||
return engine.create(id, element);
|
||||
}
|
||||
|
||||
class SuffixNormalizer extends AnimationStyleNormalizer {
|
||||
constructor(private _suffix: string) { super(); }
|
||||
|
||||
normalizePropertyName(propertyName: string, errors: string[]): string {
|
||||
return propertyName + this._suffix;
|
||||
}
|
||||
|
||||
normalizeStyleValue(
|
||||
userProvidedProperty: string, normalizedProperty: string, value: string|number,
|
||||
errors: string[]): string {
|
||||
return value + this._suffix;
|
||||
}
|
||||
}
|
||||
|
||||
class SuperMockDriver extends MockAnimationDriver {
|
||||
computeStyle(element: any, prop: string, defaultValue?: string): string { return '*star*'; }
|
||||
}
|
|
@ -5,20 +5,16 @@
|
|||
* 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 {AnimationEvent, NoopAnimationPlayer, animate, keyframes, state, style, transition, trigger} from '@angular/animations';
|
||||
import {el} from '@angular/platform-browser/testing/src/browser_util';
|
||||
import {AnimationEvent, AnimationMetadata, AnimationTriggerMetadata, NoopAnimationPlayer, animate, state, style, transition, trigger} from '@angular/animations';
|
||||
|
||||
import {buildAnimationKeyframes} from '../../src/dsl/animation_timeline_visitor';
|
||||
import {TriggerAst} from '../../src/dsl/animation_ast';
|
||||
import {buildAnimationAst} from '../../src/dsl/animation_ast_builder';
|
||||
import {buildTrigger} from '../../src/dsl/animation_trigger';
|
||||
import {AnimationStyleNormalizer, NoopAnimationStyleNormalizer} from '../../src/dsl/style_normalization/animation_style_normalizer';
|
||||
import {DomAnimationEngine} from '../../src/render/dom_animation_engine';
|
||||
import {TransitionAnimationEngine} from '../../src/render/transition_animation_engine';
|
||||
import {MockAnimationDriver, MockAnimationPlayer} from '../../testing/src/mock_animation_driver';
|
||||
|
||||
function makeTrigger(name: string, steps: any) {
|
||||
const triggerData = trigger(name, steps);
|
||||
const triggerInstance = buildTrigger(triggerData.name, triggerData.definitions);
|
||||
return triggerInstance;
|
||||
}
|
||||
const DEFAULT_NAMESPACE_ID = 'id';
|
||||
|
||||
export function main() {
|
||||
const driver = new MockAnimationDriver();
|
||||
|
@ -26,23 +22,30 @@ export function main() {
|
|||
// these tests are only mean't to be run within the DOM
|
||||
if (typeof Element == 'undefined') return;
|
||||
|
||||
describe('DomAnimationEngine', () => {
|
||||
describe('TransitionAnimationEngine', () => {
|
||||
let element: any;
|
||||
|
||||
beforeEach(() => {
|
||||
MockAnimationDriver.log = [];
|
||||
element = el('<div></div>');
|
||||
element = document.createElement('div');
|
||||
document.body.appendChild(element);
|
||||
});
|
||||
|
||||
afterEach(() => { document.body.removeChild(element); });
|
||||
|
||||
function makeEngine(normalizer?: AnimationStyleNormalizer) {
|
||||
return new DomAnimationEngine(driver, normalizer || new NoopAnimationStyleNormalizer());
|
||||
const engine =
|
||||
new TransitionAnimationEngine(driver, normalizer || new NoopAnimationStyleNormalizer());
|
||||
engine.createNamespace(DEFAULT_NAMESPACE_ID, element);
|
||||
return engine;
|
||||
}
|
||||
|
||||
describe('trigger registration', () => {
|
||||
it('should ignore and not throw an error if the same trigger is registered twice', () => {
|
||||
// TODO (matsko): ask why this is avoided
|
||||
const engine = makeEngine();
|
||||
engine.registerTrigger(trigger('trig', []));
|
||||
expect(() => { engine.registerTrigger(trigger('trig', [])); }).not.toThrow();
|
||||
registerTrigger(element, engine, trigger('trig', []));
|
||||
expect(() => { registerTrigger(element, engine, trigger('trig', [])); }).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -54,11 +57,10 @@ export function main() {
|
|||
transition('* => *', [style({height: '0px'}), animate(1000, style({height: '100px'}))])
|
||||
]);
|
||||
|
||||
engine.registerTrigger(trig);
|
||||
|
||||
expect(engine.queuedPlayers.length).toEqual(0);
|
||||
engine.setProperty(element, 'myTrigger', 'value');
|
||||
expect(engine.queuedPlayers.length).toEqual(1);
|
||||
registerTrigger(element, engine, trig);
|
||||
setProperty(element, engine, 'myTrigger', 'value');
|
||||
engine.flush();
|
||||
expect(engine.players.length).toEqual(1);
|
||||
|
||||
const player = MockAnimationDriver.log.pop() as MockAnimationPlayer;
|
||||
expect(player.keyframes).toEqual([
|
||||
|
@ -66,43 +68,6 @@ export function main() {
|
|||
]);
|
||||
});
|
||||
|
||||
it('should always invoke an animation even if the property change is not matched', () => {
|
||||
const engine = makeEngine();
|
||||
|
||||
const trig = trigger(
|
||||
'myTrigger',
|
||||
[transition(
|
||||
'yes => no', [style({height: '0px'}), animate(1000, style({height: '100px'}))])]);
|
||||
|
||||
engine.registerTrigger(trig);
|
||||
expect(engine.queuedPlayers.length).toEqual(0);
|
||||
|
||||
engine.setProperty(element, 'myTrigger', 'no');
|
||||
expect(engine.queuedPlayers.length).toEqual(1);
|
||||
expect(engine.queuedPlayers.pop() instanceof NoopAnimationPlayer).toBe(true);
|
||||
engine.flush();
|
||||
|
||||
engine.setProperty(element, 'myTrigger', 'yes');
|
||||
expect(engine.queuedPlayers.length).toEqual(1);
|
||||
expect(engine.queuedPlayers.pop() instanceof NoopAnimationPlayer).toBe(true);
|
||||
});
|
||||
|
||||
it('should not initialize the animation until the engine has been flushed', () => {
|
||||
const engine = makeEngine();
|
||||
engine.registerTrigger(trigger(
|
||||
'trig', [transition('* => something', [animate(1000, style({color: 'gold'}))])]));
|
||||
|
||||
engine.setProperty(element, 'trig', 'something');
|
||||
const player = engine.queuedPlayers.pop() as MockAnimationPlayer;
|
||||
|
||||
let initialized = false;
|
||||
player.onInit(() => initialized = true);
|
||||
|
||||
expect(initialized).toBe(false);
|
||||
engine.flush();
|
||||
expect(initialized).toBe(true);
|
||||
});
|
||||
|
||||
it('should not queue an animation if the property value has not changed at all', () => {
|
||||
const engine = makeEngine();
|
||||
|
||||
|
@ -110,25 +75,71 @@ export function main() {
|
|||
transition('* => *', [style({height: '0px'}), animate(1000, style({height: '100px'}))])
|
||||
]);
|
||||
|
||||
engine.registerTrigger(trig);
|
||||
expect(engine.queuedPlayers.length).toEqual(0);
|
||||
registerTrigger(element, engine, trig);
|
||||
engine.flush();
|
||||
expect(engine.players.length).toEqual(0);
|
||||
|
||||
engine.setProperty(element, 'myTrigger', 'abc');
|
||||
expect(engine.queuedPlayers.length).toEqual(1);
|
||||
setProperty(element, engine, 'myTrigger', 'abc');
|
||||
engine.flush();
|
||||
expect(engine.players.length).toEqual(1);
|
||||
|
||||
engine.setProperty(element, 'myTrigger', 'abc');
|
||||
expect(engine.queuedPlayers.length).toEqual(1);
|
||||
setProperty(element, engine, 'myTrigger', 'abc');
|
||||
engine.flush();
|
||||
expect(engine.players.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should throw an error if an animation property without a matching trigger is changed',
|
||||
() => {
|
||||
const engine = makeEngine();
|
||||
expect(() => {
|
||||
engine.setProperty(element, 'myTrigger', 'no');
|
||||
setProperty(element, engine, 'myTrigger', 'no');
|
||||
}).toThrowError(/The provided animation trigger "myTrigger" has not been registered!/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removal operations', () => {
|
||||
it('should cleanup all inner state that\'s tied to an element once removed', () => {
|
||||
const engine = makeEngine();
|
||||
|
||||
const trig = trigger('myTrigger', [
|
||||
transition(':leave', [style({height: '0px'}), animate(1000, style({height: '100px'}))])
|
||||
]);
|
||||
|
||||
registerTrigger(element, engine, trig);
|
||||
setProperty(element, engine, 'myTrigger', 'value');
|
||||
engine.flush();
|
||||
|
||||
expect(engine.elementContainsData(DEFAULT_NAMESPACE_ID, element)).toBeTruthy();
|
||||
|
||||
engine.removeNode(DEFAULT_NAMESPACE_ID, element, true);
|
||||
engine.flush();
|
||||
|
||||
expect(engine.elementContainsData(DEFAULT_NAMESPACE_ID, element)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should create and recreate a namespace for a host element with the same component source',
|
||||
() => {
|
||||
const engine = makeEngine();
|
||||
|
||||
const trig =
|
||||
trigger('myTrigger', [transition('* => *', animate(1234, style({color: 'red'})))]);
|
||||
|
||||
registerTrigger(element, engine, trig);
|
||||
setProperty(element, engine, 'myTrigger', 'value');
|
||||
engine.flush();
|
||||
expect((engine.players[0].getRealPlayer() as MockAnimationPlayer).duration)
|
||||
.toEqual(1234);
|
||||
|
||||
engine.destroy(DEFAULT_NAMESPACE_ID, null);
|
||||
|
||||
registerTrigger(element, engine, trig);
|
||||
setProperty(element, engine, 'myTrigger', 'value');
|
||||
engine.flush();
|
||||
expect((engine.players[0].getRealPlayer() as MockAnimationPlayer).duration)
|
||||
.toEqual(1234);
|
||||
});
|
||||
});
|
||||
|
||||
describe('event listeners', () => {
|
||||
it('should listen to the onStart operation for the animation', () => {
|
||||
const engine = makeEngine();
|
||||
|
@ -138,9 +149,9 @@ export function main() {
|
|||
]);
|
||||
|
||||
let count = 0;
|
||||
engine.registerTrigger(trig);
|
||||
engine.listen(element, 'myTrigger', 'start', () => count++);
|
||||
engine.setProperty(element, 'myTrigger', 'value');
|
||||
registerTrigger(element, engine, trig);
|
||||
listen(element, engine, 'myTrigger', 'start', () => count++);
|
||||
setProperty(element, engine, 'myTrigger', 'value');
|
||||
expect(count).toEqual(0);
|
||||
|
||||
engine.flush();
|
||||
|
@ -155,33 +166,31 @@ export function main() {
|
|||
]);
|
||||
|
||||
let count = 0;
|
||||
engine.registerTrigger(trig);
|
||||
engine.listen(element, 'myTrigger', 'done', () => count++);
|
||||
engine.setProperty(element, 'myTrigger', 'value');
|
||||
registerTrigger(element, engine, trig);
|
||||
listen(element, engine, 'myTrigger', 'done', () => count++);
|
||||
setProperty(element, engine, 'myTrigger', 'value');
|
||||
expect(count).toEqual(0);
|
||||
|
||||
engine.flush();
|
||||
expect(count).toEqual(0);
|
||||
|
||||
const player = engine.activePlayers.pop() !;
|
||||
player.finish();
|
||||
|
||||
engine.players[0].finish();
|
||||
expect(count).toEqual(1);
|
||||
});
|
||||
|
||||
it('should throw an error when an event is listened to that isn\'t supported', () => {
|
||||
const engine = makeEngine();
|
||||
const trig = trigger('myTrigger', []);
|
||||
engine.registerTrigger(trig);
|
||||
registerTrigger(element, engine, trig);
|
||||
|
||||
expect(() => { engine.listen(element, 'myTrigger', 'explode', () => {}); })
|
||||
expect(() => { listen(element, engine, 'myTrigger', 'explode', () => {}); })
|
||||
.toThrowError(
|
||||
/The provided animation trigger event "explode" for the animation trigger "myTrigger" is not supported!/);
|
||||
});
|
||||
|
||||
it('should throw an error when an event is listened for a trigger that doesn\'t exist', () => {
|
||||
const engine = makeEngine();
|
||||
expect(() => { engine.listen(element, 'myTrigger', 'explode', () => {}); })
|
||||
expect(() => { listen(element, engine, 'myTrigger', 'explode', () => {}); })
|
||||
.toThrowError(
|
||||
/Unable to listen on the animation trigger event "explode" because the animation trigger "myTrigger" doesn\'t exist!/);
|
||||
});
|
||||
|
@ -189,8 +198,8 @@ export function main() {
|
|||
it('should throw an error when an undefined event is listened for', () => {
|
||||
const engine = makeEngine();
|
||||
const trig = trigger('myTrigger', []);
|
||||
engine.registerTrigger(trig);
|
||||
expect(() => { engine.listen(element, 'myTrigger', '', () => {}); })
|
||||
registerTrigger(element, engine, trig);
|
||||
expect(() => { listen(element, engine, 'myTrigger', '', () => {}); })
|
||||
.toThrowError(
|
||||
/Unable to listen on the animation trigger "myTrigger" because the provided event is undefined!/);
|
||||
});
|
||||
|
@ -203,16 +212,16 @@ export function main() {
|
|||
[transition(
|
||||
'* => *', [style({height: '0px'}), animate(1000, style({height: '100px'}))])]);
|
||||
|
||||
engine.registerTrigger(trig);
|
||||
registerTrigger(element, engine, trig);
|
||||
|
||||
let count = 0;
|
||||
engine.listen(element, 'myTrigger', 'start', () => count++);
|
||||
listen(element, engine, 'myTrigger', 'start', () => count++);
|
||||
|
||||
engine.setProperty(element, 'myTrigger', '123');
|
||||
setProperty(element, engine, 'myTrigger', '123');
|
||||
engine.flush();
|
||||
expect(count).toEqual(1);
|
||||
|
||||
engine.setProperty(element, 'myTrigger', '456');
|
||||
setProperty(element, engine, 'myTrigger', '456');
|
||||
engine.flush();
|
||||
expect(count).toEqual(2);
|
||||
});
|
||||
|
@ -224,61 +233,64 @@ export function main() {
|
|||
'myTrigger1',
|
||||
[transition(
|
||||
'* => 123', [style({height: '0px'}), animate(1000, style({height: '100px'}))])]);
|
||||
engine.registerTrigger(trig1);
|
||||
registerTrigger(element, engine, trig1);
|
||||
|
||||
const trig2 = trigger(
|
||||
'myTrigger2',
|
||||
[transition(
|
||||
'* => 123', [style({width: '0px'}), animate(1000, style({width: '100px'}))])]);
|
||||
engine.registerTrigger(trig2);
|
||||
registerTrigger(element, engine, trig2);
|
||||
|
||||
let count = 0;
|
||||
engine.listen(element, 'myTrigger1', 'start', () => count++);
|
||||
listen(element, engine, 'myTrigger1', 'start', () => count++);
|
||||
|
||||
engine.setProperty(element, 'myTrigger1', '123');
|
||||
setProperty(element, engine, 'myTrigger1', '123');
|
||||
engine.flush();
|
||||
expect(count).toEqual(1);
|
||||
|
||||
engine.setProperty(element, 'myTrigger2', '123');
|
||||
setProperty(element, engine, 'myTrigger2', '123');
|
||||
engine.flush();
|
||||
expect(count).toEqual(1);
|
||||
});
|
||||
|
||||
it('should allow a listener to be deregistered', () => {
|
||||
it('should allow a listener to be deregistered, but only after a flush occurs', () => {
|
||||
const engine = makeEngine();
|
||||
const trig = trigger(
|
||||
'myTrigger',
|
||||
[transition(
|
||||
'* => 123', [style({height: '0px'}), animate(1000, style({height: '100px'}))])]);
|
||||
engine.registerTrigger(trig);
|
||||
registerTrigger(element, engine, trig);
|
||||
|
||||
let count = 0;
|
||||
const deregisterFn = engine.listen(element, 'myTrigger', 'start', () => count++);
|
||||
engine.setProperty(element, 'myTrigger', '123');
|
||||
const deregisterFn = listen(element, engine, 'myTrigger', 'start', () => count++);
|
||||
setProperty(element, engine, 'myTrigger', '123');
|
||||
engine.flush();
|
||||
expect(count).toEqual(1);
|
||||
|
||||
deregisterFn();
|
||||
engine.setProperty(element, 'myTrigger', '456');
|
||||
engine.flush();
|
||||
|
||||
setProperty(element, engine, 'myTrigger', '456');
|
||||
engine.flush();
|
||||
expect(count).toEqual(1);
|
||||
});
|
||||
|
||||
it('should trigger a listener callback with an AnimationEvent argument', () => {
|
||||
const engine = makeEngine();
|
||||
engine.registerTrigger(trigger(
|
||||
'myTrigger',
|
||||
[transition(
|
||||
'* => *', [style({height: '0px'}), animate(1234, style({height: '100px'}))])]));
|
||||
registerTrigger(
|
||||
element, engine, trigger('myTrigger', [
|
||||
transition(
|
||||
'* => *', [style({height: '0px'}), animate(1234, style({height: '100px'}))])
|
||||
]));
|
||||
|
||||
// we do this so that the next transition has a starting value that isnt null
|
||||
engine.setProperty(element, 'myTrigger', '123');
|
||||
setProperty(element, engine, 'myTrigger', '123');
|
||||
engine.flush();
|
||||
|
||||
let capture: AnimationEvent = null !;
|
||||
engine.listen(element, 'myTrigger', 'start', (e) => capture = e);
|
||||
engine.listen(element, 'myTrigger', 'done', (e) => capture = e);
|
||||
engine.setProperty(element, 'myTrigger', '456');
|
||||
listen(element, engine, 'myTrigger', 'start', e => capture = e);
|
||||
listen(element, engine, 'myTrigger', 'done', e => capture = e);
|
||||
setProperty(element, engine, 'myTrigger', '456');
|
||||
engine.flush();
|
||||
|
||||
expect(capture).toEqual({
|
||||
|
@ -291,7 +303,7 @@ export function main() {
|
|||
});
|
||||
|
||||
capture = null !;
|
||||
const player = engine.activePlayers.pop() !;
|
||||
const player = engine.players.pop() !;
|
||||
player.finish();
|
||||
|
||||
expect(capture).toEqual({
|
||||
|
@ -305,102 +317,46 @@ export function main() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('instructions', () => {
|
||||
it('should animate a transition instruction', () => {
|
||||
const engine = makeEngine();
|
||||
|
||||
const trig = makeTrigger('something', [
|
||||
state('on', style({height: 100})), state('off', style({height: 0})),
|
||||
transition('on => off', animate(9876))
|
||||
]);
|
||||
|
||||
const instruction = trig.matchTransition('on', 'off') !;
|
||||
|
||||
expect(MockAnimationDriver.log.length).toEqual(0);
|
||||
engine.animateTransition(element, instruction);
|
||||
expect(MockAnimationDriver.log.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should animate a timeline instruction', () => {
|
||||
const engine = makeEngine();
|
||||
const timelines =
|
||||
buildAnimationKeyframes([style({height: 100}), animate(1000, style({height: 0}))]);
|
||||
expect(MockAnimationDriver.log.length).toEqual(0);
|
||||
engine.animateTimeline(element, timelines);
|
||||
expect(MockAnimationDriver.log.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should animate an array of animation instructions', () => {
|
||||
const engine = makeEngine();
|
||||
|
||||
const instructions = buildAnimationKeyframes([
|
||||
style({height: 100}), animate(1000, style({height: 0})),
|
||||
animate(1000, keyframes([style({width: 0}), style({width: 1000})]))
|
||||
]);
|
||||
|
||||
expect(MockAnimationDriver.log.length).toEqual(0);
|
||||
engine.animateTimeline(element, instructions);
|
||||
expect(MockAnimationDriver.log.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removals / insertions', () => {
|
||||
it('should allow text nodes to be removed through the engine', () => {
|
||||
const engine = makeEngine();
|
||||
const node = document.createTextNode('hello');
|
||||
element.appendChild(node);
|
||||
|
||||
let called = false;
|
||||
engine.onRemove(node, () => called = true);
|
||||
|
||||
expect(called).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should allow text nodes to be inserted through the engine', () => {
|
||||
const engine = makeEngine();
|
||||
const node = document.createTextNode('hello');
|
||||
|
||||
let called = false;
|
||||
engine.onInsert(node, () => called = true);
|
||||
|
||||
expect(called).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('transition operations', () => {
|
||||
it('should persist the styles on the element as actual styles once the animation is complete',
|
||||
() => {
|
||||
const engine = makeEngine();
|
||||
const trig = makeTrigger('something', [
|
||||
const trig = trigger('something', [
|
||||
state('on', style({height: '100px'})), state('off', style({height: '0px'})),
|
||||
transition('on => off', animate(9876))
|
||||
]);
|
||||
|
||||
const instruction = trig.matchTransition('on', 'off') !;
|
||||
const player = engine.animateTransition(element, instruction);
|
||||
registerTrigger(element, engine, trig);
|
||||
setProperty(element, engine, trig.name, 'on');
|
||||
setProperty(element, engine, trig.name, 'off');
|
||||
engine.flush();
|
||||
|
||||
expect(element.style.height).not.toEqual('0px');
|
||||
player.finish();
|
||||
engine.players[0].finish();
|
||||
expect(element.style.height).toEqual('0px');
|
||||
});
|
||||
|
||||
it('should remove all existing state styling from an element when a follow-up transition occurs on the same trigger',
|
||||
() => {
|
||||
const engine = makeEngine();
|
||||
const trig = makeTrigger('something', [
|
||||
const trig = trigger('something', [
|
||||
state('a', style({height: '100px'})), state('b', style({height: '500px'})),
|
||||
state('c', style({width: '200px'})), transition('* => *', animate(9876))
|
||||
]);
|
||||
|
||||
const instruction1 = trig.matchTransition('a', 'b') !;
|
||||
const player1 = engine.animateTransition(element, instruction1);
|
||||
registerTrigger(element, engine, trig);
|
||||
setProperty(element, engine, trig.name, 'a');
|
||||
setProperty(element, engine, trig.name, 'b');
|
||||
engine.flush();
|
||||
|
||||
const player1 = engine.players[0];
|
||||
player1.finish();
|
||||
expect(element.style.height).toEqual('500px');
|
||||
|
||||
const instruction2 = trig.matchTransition('b', 'c') !;
|
||||
const player2 = engine.animateTransition(element, instruction2);
|
||||
setProperty(element, engine, trig.name, 'c');
|
||||
engine.flush();
|
||||
|
||||
const player2 = engine.players[0];
|
||||
expect(element.style.height).not.toEqual('500px');
|
||||
player2.finish();
|
||||
expect(element.style.width).toEqual('200px');
|
||||
|
@ -410,26 +366,33 @@ export function main() {
|
|||
it('should allow two animation transitions with different triggers to animate in parallel',
|
||||
() => {
|
||||
const engine = makeEngine();
|
||||
const trig1 = makeTrigger('something1', [
|
||||
const trig1 = trigger('something1', [
|
||||
state('a', style({width: '100px'})), state('b', style({width: '200px'})),
|
||||
transition('* => *', animate(1000))
|
||||
]);
|
||||
|
||||
const trig2 = makeTrigger('something2', [
|
||||
const trig2 = trigger('something2', [
|
||||
state('x', style({height: '500px'})), state('y', style({height: '1000px'})),
|
||||
transition('* => *', animate(2000))
|
||||
]);
|
||||
|
||||
registerTrigger(element, engine, trig1);
|
||||
registerTrigger(element, engine, trig2);
|
||||
|
||||
let doneCount = 0;
|
||||
function doneCallback() { doneCount++; }
|
||||
|
||||
const instruction1 = trig1.matchTransition('a', 'b') !;
|
||||
const instruction2 = trig2.matchTransition('x', 'y') !;
|
||||
const player1 = engine.animateTransition(element, instruction1);
|
||||
setProperty(element, engine, trig1.name, 'a');
|
||||
setProperty(element, engine, trig1.name, 'b');
|
||||
setProperty(element, engine, trig2.name, 'x');
|
||||
setProperty(element, engine, trig2.name, 'y');
|
||||
engine.flush();
|
||||
|
||||
const player1 = engine.players[0] !;
|
||||
player1.onDone(doneCallback);
|
||||
expect(doneCount).toEqual(0);
|
||||
|
||||
const player2 = engine.animateTransition(element, instruction2);
|
||||
const player2 = engine.players[1] !;
|
||||
player2.onDone(doneCallback);
|
||||
expect(doneCount).toEqual(0);
|
||||
|
||||
|
@ -446,18 +409,23 @@ export function main() {
|
|||
it('should cancel a previously running animation when a follow-up transition kicks off on the same trigger',
|
||||
() => {
|
||||
const engine = makeEngine();
|
||||
const trig = makeTrigger('something', [
|
||||
const trig = trigger('something', [
|
||||
state('x', style({opacity: 0})), state('y', style({opacity: .5})),
|
||||
state('z', style({opacity: 1})), transition('* => *', animate(1000))
|
||||
]);
|
||||
|
||||
const instruction1 = trig.matchTransition('x', 'y') !;
|
||||
const instruction2 = trig.matchTransition('y', 'z') !;
|
||||
registerTrigger(element, engine, trig);
|
||||
setProperty(element, engine, trig.name, 'x');
|
||||
setProperty(element, engine, trig.name, 'y');
|
||||
engine.flush();
|
||||
|
||||
expect(parseFloat(element.style.opacity)).not.toEqual(.5);
|
||||
|
||||
const player1 = engine.animateTransition(element, instruction1);
|
||||
const player2 = engine.animateTransition(element, instruction2);
|
||||
const player1 = engine.players[0];
|
||||
setProperty(element, engine, trig.name, 'z');
|
||||
engine.flush();
|
||||
|
||||
const player2 = engine.players[0];
|
||||
|
||||
expect(parseFloat(element.style.opacity)).toEqual(.5);
|
||||
|
||||
|
@ -471,65 +439,73 @@ export function main() {
|
|||
it('should pass in the previously running players into the follow-up transition player when cancelled',
|
||||
() => {
|
||||
const engine = makeEngine();
|
||||
const trig = makeTrigger('something', [
|
||||
const trig = trigger('something', [
|
||||
state('x', style({opacity: 0})), state('y', style({opacity: .5})),
|
||||
state('z', style({opacity: 1})), transition('* => *', animate(1000))
|
||||
]);
|
||||
|
||||
const instruction1 = trig.matchTransition('x', 'y') !;
|
||||
const instruction2 = trig.matchTransition('y', 'z') !;
|
||||
const instruction3 = trig.matchTransition('z', 'x') !;
|
||||
|
||||
const player1 = engine.animateTransition(element, instruction1);
|
||||
registerTrigger(element, engine, trig);
|
||||
setProperty(element, engine, trig.name, 'x');
|
||||
setProperty(element, engine, trig.name, 'y');
|
||||
engine.flush();
|
||||
|
||||
const player1 = MockAnimationDriver.log.pop() !as MockAnimationPlayer;
|
||||
player1.setPosition(0.5);
|
||||
|
||||
const player2 = <MockAnimationPlayer>engine.animateTransition(element, instruction2);
|
||||
setProperty(element, engine, trig.name, 'z');
|
||||
engine.flush();
|
||||
|
||||
const player2 = MockAnimationDriver.log.pop() !as MockAnimationPlayer;
|
||||
expect(player2.previousPlayers).toEqual([player1]);
|
||||
player2.finish();
|
||||
|
||||
const player3 = <MockAnimationPlayer>engine.animateTransition(element, instruction3);
|
||||
setProperty(element, engine, trig.name, 'x');
|
||||
engine.flush();
|
||||
|
||||
const player3 = MockAnimationDriver.log.pop() !as MockAnimationPlayer;
|
||||
expect(player3.previousPlayers).toEqual([]);
|
||||
});
|
||||
|
||||
it('should cancel all existing players if a removal animation is set to occur', () => {
|
||||
const engine = makeEngine();
|
||||
const trig = makeTrigger('something', [
|
||||
const trig = trigger('something', [
|
||||
state('m', style({opacity: 0})), state('n', style({opacity: 1})),
|
||||
transition('* => *', animate(1000))
|
||||
]);
|
||||
|
||||
registerTrigger(element, engine, trig);
|
||||
setProperty(element, engine, trig.name, 'm');
|
||||
setProperty(element, engine, trig.name, 'n');
|
||||
engine.flush();
|
||||
|
||||
let doneCount = 0;
|
||||
function doneCallback() { doneCount++; }
|
||||
|
||||
const instruction1 = trig.matchTransition('m', 'n') !;
|
||||
const instructions2 =
|
||||
buildAnimationKeyframes([style({height: 0}), animate(1000, style({height: 100}))]) !;
|
||||
const instruction3 = trig.matchTransition('n', 'void') !;
|
||||
|
||||
const player1 = engine.animateTransition(element, instruction1);
|
||||
const player1 = engine.players[0];
|
||||
player1.onDone(doneCallback);
|
||||
|
||||
const player2 = engine.animateTimeline(element, instructions2);
|
||||
player2.onDone(doneCallback);
|
||||
|
||||
engine.flush();
|
||||
expect(doneCount).toEqual(0);
|
||||
|
||||
const player3 = engine.animateTransition(element, instruction3);
|
||||
expect(doneCount).toEqual(2);
|
||||
setProperty(element, engine, trig.name, 'void');
|
||||
engine.flush();
|
||||
|
||||
expect(doneCount).toEqual(1);
|
||||
});
|
||||
|
||||
it('should only persist styles that exist in the final state styles and not the last keyframe',
|
||||
() => {
|
||||
const engine = makeEngine();
|
||||
const trig = makeTrigger('something', [
|
||||
const trig = trigger('something', [
|
||||
state('0', style({width: '0px'})), state('1', style({width: '100px'})),
|
||||
transition('* => *', [animate(1000, style({height: '200px'}))])
|
||||
]);
|
||||
|
||||
const instruction = trig.matchTransition('0', '1') !;
|
||||
const player = engine.animateTransition(element, instruction);
|
||||
registerTrigger(element, engine, trig);
|
||||
setProperty(element, engine, trig.name, '0');
|
||||
setProperty(element, engine, trig.name, '1');
|
||||
engine.flush();
|
||||
|
||||
const player = engine.players[0] !;
|
||||
expect(element.style.width).not.toEqual('100px');
|
||||
|
||||
player.finish();
|
||||
|
@ -540,104 +516,74 @@ export function main() {
|
|||
it('should default to using styling from the `*` state if a matching state is not found',
|
||||
() => {
|
||||
const engine = makeEngine();
|
||||
const trig = makeTrigger('something', [
|
||||
const trig = trigger('something', [
|
||||
state('a', style({opacity: 0})), state('*', style({opacity: .5})),
|
||||
transition('* => *', animate(1000))
|
||||
]);
|
||||
|
||||
const instruction = trig.matchTransition('a', 'z') !;
|
||||
engine.animateTransition(element, instruction).finish();
|
||||
registerTrigger(element, engine, trig);
|
||||
setProperty(element, engine, trig.name, 'a');
|
||||
setProperty(element, engine, trig.name, 'z');
|
||||
engine.flush();
|
||||
|
||||
engine.players[0].finish();
|
||||
expect(parseFloat(element.style.opacity)).toEqual(.5);
|
||||
});
|
||||
|
||||
it('should treat `void` as `void`', () => {
|
||||
const engine = makeEngine();
|
||||
const trig = makeTrigger('something', [
|
||||
const trig = trigger('something', [
|
||||
state('a', style({opacity: 0})), state('void', style({opacity: .8})),
|
||||
transition('* => *', animate(1000))
|
||||
]);
|
||||
|
||||
const instruction = trig.matchTransition('a', 'void') !;
|
||||
engine.animateTransition(element, instruction).finish();
|
||||
registerTrigger(element, engine, trig);
|
||||
setProperty(element, engine, trig.name, 'a');
|
||||
setProperty(element, engine, trig.name, 'void');
|
||||
engine.flush();
|
||||
|
||||
engine.players[0].finish();
|
||||
expect(parseFloat(element.style.opacity)).toEqual(.8);
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeline operations', () => {
|
||||
it('should not destroy timeline-based animations after they have finished', () => {
|
||||
const engine = makeEngine();
|
||||
|
||||
const log: string[] = [];
|
||||
function capture(value: string) {
|
||||
return () => { log.push(value); };
|
||||
}
|
||||
|
||||
const instructions =
|
||||
buildAnimationKeyframes([style({height: 0}), animate(1000, style({height: 500}))]);
|
||||
|
||||
const player = engine.animateTimeline(element, instructions);
|
||||
player.onDone(capture('done'));
|
||||
player.onDestroy(capture('destroy'));
|
||||
expect(log).toEqual([]);
|
||||
|
||||
player.finish();
|
||||
expect(log).toEqual(['done']);
|
||||
|
||||
player.destroy();
|
||||
expect(log).toEqual(['done', 'destroy']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('style normalizer', () => {
|
||||
it('should normalize the style values that are animateTransitioned within an a transition animation',
|
||||
() => {
|
||||
const engine = makeEngine(new SuffixNormalizer('-normalized'));
|
||||
|
||||
const trig = makeTrigger('something', [
|
||||
const trig = trigger('something', [
|
||||
state('on', style({height: 100})), state('off', style({height: 0})),
|
||||
transition('on => off', animate(9876))
|
||||
]);
|
||||
|
||||
const instruction = trig.matchTransition('on', 'off') !;
|
||||
const player = <MockAnimationPlayer>engine.animateTransition(element, instruction);
|
||||
registerTrigger(element, engine, trig);
|
||||
setProperty(element, engine, trig.name, 'on');
|
||||
setProperty(element, engine, trig.name, 'off');
|
||||
engine.flush();
|
||||
|
||||
const player = MockAnimationDriver.log.pop() as MockAnimationPlayer;
|
||||
expect(player.keyframes).toEqual([
|
||||
{'height-normalized': '100-normalized', offset: 0},
|
||||
{'height-normalized': '0-normalized', offset: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should normalize the style values that are animateTransitioned within an a timeline animation',
|
||||
() => {
|
||||
const engine = makeEngine(new SuffixNormalizer('-normalized'));
|
||||
|
||||
const instructions = buildAnimationKeyframes([
|
||||
style({width: '333px'}),
|
||||
animate(1000, style({width: '999px'})),
|
||||
]);
|
||||
|
||||
const player = <MockAnimationPlayer>engine.animateTimeline(element, instructions);
|
||||
expect(player.keyframes).toEqual([
|
||||
{'width-normalized': '333px-normalized', offset: 0},
|
||||
{'width-normalized': '999px-normalized', offset: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw an error when normalization fails within a transition animation', () => {
|
||||
const engine = makeEngine(new ExactCssValueNormalizer({left: '100px'}));
|
||||
|
||||
const trig = makeTrigger('something', [
|
||||
const trig = trigger('something', [
|
||||
state('a', style({left: '0px', width: '200px'})),
|
||||
state('b', style({left: '100px', width: '100px'})), transition('a => b', animate(9876))
|
||||
]);
|
||||
|
||||
const instruction = trig.matchTransition('a', 'b') !;
|
||||
registerTrigger(element, engine, trig);
|
||||
setProperty(element, engine, trig.name, 'a');
|
||||
setProperty(element, engine, trig.name, 'b');
|
||||
|
||||
let errorMessage = '';
|
||||
try {
|
||||
engine.animateTransition(element, instruction);
|
||||
engine.flush();
|
||||
} catch (e) {
|
||||
errorMessage = e.toString();
|
||||
}
|
||||
|
@ -652,15 +598,18 @@ export function main() {
|
|||
it('should perform insert operations immediately ', () => {
|
||||
const engine = makeEngine();
|
||||
|
||||
let container = <any>el('<div></div>');
|
||||
let child1 = <any>el('<div></div>');
|
||||
let child2 = <any>el('<div></div>');
|
||||
const child1 = document.createElement('div');
|
||||
const child2 = document.createElement('div');
|
||||
element.appendChild(child1);
|
||||
element.appendChild(child2);
|
||||
|
||||
engine.onInsert(container, () => container.appendChild(child1));
|
||||
engine.onInsert(container, () => container.appendChild(child2));
|
||||
element.appendChild(child1);
|
||||
engine.insertNode(DEFAULT_NAMESPACE_ID, child1, element, true);
|
||||
element.appendChild(child2);
|
||||
engine.insertNode(DEFAULT_NAMESPACE_ID, child2, element, true);
|
||||
|
||||
expect(container.contains(child1)).toBe(true);
|
||||
expect(container.contains(child2)).toBe(true);
|
||||
expect(element.contains(child1)).toBe(true);
|
||||
expect(element.contains(child2)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -700,3 +649,27 @@ class ExactCssValueNormalizer extends AnimationStyleNormalizer {
|
|||
return expectedValue;
|
||||
}
|
||||
}
|
||||
|
||||
function registerTrigger(
|
||||
element: any, engine: TransitionAnimationEngine, metadata: AnimationTriggerMetadata,
|
||||
id: string = DEFAULT_NAMESPACE_ID) {
|
||||
const errors: any[] = [];
|
||||
const name = metadata.name;
|
||||
const ast = buildAnimationAst(metadata as AnimationMetadata, errors) as TriggerAst;
|
||||
if (errors.length) {
|
||||
}
|
||||
const trigger = buildTrigger(name, ast);
|
||||
engine.register(id, element, name, trigger)
|
||||
}
|
||||
|
||||
function setProperty(
|
||||
element: any, engine: TransitionAnimationEngine, property: string, value: any,
|
||||
id: string = DEFAULT_NAMESPACE_ID) {
|
||||
engine.trigger(id, element, property, value);
|
||||
}
|
||||
|
||||
function listen(
|
||||
element: any, engine: TransitionAnimationEngine, eventName: string, phaseName: string,
|
||||
callback: (event: any) => any, id: string = DEFAULT_NAMESPACE_ID) {
|
||||
return engine.listen(id, element, eventName, phaseName, callback);
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
/**
|
||||
* @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 {DOMAnimation} from '../../src/render/web_animations/dom_animation';
|
||||
import {WebAnimationsPlayer} from '../../src/render/web_animations/web_animations_player';
|
||||
|
||||
export function main() {
|
||||
describe('WebAnimationsPlayer', function() {
|
||||
// these tests are only mean't to be run within the DOM
|
||||
if (typeof Element == 'undefined') return;
|
||||
|
||||
let element: any;
|
||||
beforeEach(() => {
|
||||
element = document.createElement('div');
|
||||
document.body.appendChild(element);
|
||||
});
|
||||
|
||||
afterEach(() => { document.body.removeChild(element); });
|
||||
|
||||
it('should properly balance any previous player styles into the animation keyframes', () => {
|
||||
element.style.height = '666px';
|
||||
element.style.width = '333px';
|
||||
|
||||
const prevPlayer1 = new MockWebAnimationsPlayer(
|
||||
element, [{width: '0px', offset: 0}, {width: '200px', offset: 1}], {});
|
||||
prevPlayer1.play();
|
||||
prevPlayer1.finish();
|
||||
|
||||
const prevPlayer2 = new MockWebAnimationsPlayer(
|
||||
element, [{height: '0px', offset: 0}, {height: '200px', offset: 1}], {});
|
||||
prevPlayer2.play();
|
||||
prevPlayer2.finish();
|
||||
|
||||
// what needs to happen here is the player below should
|
||||
// examine which styles are present in the provided previous
|
||||
// players and use them as input data for the keyframes of
|
||||
// the new player. Given that the players are in their finished
|
||||
// state, the styles are copied over as the starting keyframe
|
||||
// for the animation and if the styles are missing in later keyframes
|
||||
// then the styling is resolved by computing the styles
|
||||
const player = new MockWebAnimationsPlayer(
|
||||
element, [{width: '100px', offset: 0}, {width: '500px', offset: 1}], {},
|
||||
[prevPlayer1, prevPlayer2]);
|
||||
|
||||
player.init();
|
||||
expect(player.capturedKeyframes).toEqual([
|
||||
{height: '200px', width: '200px', offset: 0},
|
||||
{height: '666px', width: '500px', offset: 1}
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class MockWebAnimationsPlayer extends WebAnimationsPlayer {
|
||||
capturedKeyframes: any[];
|
||||
|
||||
_triggerWebAnimation(element: any, keyframes: any[], options: any): any {
|
||||
this.capturedKeyframes = keyframes;
|
||||
return new MockDOMAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
class MockDOMAnimation implements DOMAnimation {
|
||||
onfinish = (callback: (e: any) => any) => {};
|
||||
position = 0;
|
||||
currentTime = 0;
|
||||
|
||||
cancel(): void {}
|
||||
play(): void {}
|
||||
pause(): void {}
|
||||
finish(): void {}
|
||||
addEventListener(eventName: string, handler: (event: any) => any): any { return null; }
|
||||
dispatchEvent(eventName: string): any { return null; }
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* @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 {trigger} from '@angular/animations';
|
||||
|
||||
import {TriggerAst} from '../src/dsl/animation_ast';
|
||||
import {buildAnimationAst} from '../src/dsl/animation_ast_builder';
|
||||
import {AnimationTrigger, buildTrigger} from '../src/dsl/animation_trigger';
|
||||
|
||||
export function makeTrigger(
|
||||
name: string, steps: any, skipErrors: boolean = false): AnimationTrigger {
|
||||
const errors: any[] = [];
|
||||
const triggerData = trigger(name, steps);
|
||||
const triggerAst = buildAnimationAst(triggerData, errors) as TriggerAst;
|
||||
if (!skipErrors && errors.length) {
|
||||
const LINE_START = '\n - ';
|
||||
throw new Error(
|
||||
`Animation parsing for the ${name} trigger have failed:${LINE_START}${errors.join(LINE_START)}`);
|
||||
}
|
||||
return buildTrigger(name, triggerAst);
|
||||
}
|
|
@ -15,6 +15,10 @@ import {AnimationDriver} from '../../src/render/animation_driver';
|
|||
export class MockAnimationDriver implements AnimationDriver {
|
||||
static log: AnimationPlayer[] = [];
|
||||
|
||||
computeStyle(element: any, prop: string, defaultValue?: string): string {
|
||||
return defaultValue || '';
|
||||
}
|
||||
|
||||
animate(
|
||||
element: any, keyframes: {[key: string]: string | number}[], duration: number, delay: number,
|
||||
easing: string, previousPlayers: any[] = []): MockAnimationPlayer {
|
||||
|
@ -30,8 +34,10 @@ export class MockAnimationDriver implements AnimationDriver {
|
|||
*/
|
||||
export class MockAnimationPlayer extends NoopAnimationPlayer {
|
||||
private __finished = false;
|
||||
private __started = false;
|
||||
public previousStyles: {[key: string]: string | number} = {};
|
||||
private _onInitFns: (() => any)[] = [];
|
||||
public currentSnapshot: ɵStyleData = {};
|
||||
|
||||
constructor(
|
||||
public element: any, public keyframes: {[key: string]: string | number}[],
|
||||
|
@ -40,10 +46,12 @@ export class MockAnimationPlayer extends NoopAnimationPlayer {
|
|||
super();
|
||||
previousPlayers.forEach(player => {
|
||||
if (player instanceof MockAnimationPlayer) {
|
||||
const styles = player._captureStyles();
|
||||
Object.keys(styles).forEach(prop => { this.previousStyles[prop] = styles[prop]; });
|
||||
const styles = player.currentSnapshot;
|
||||
Object.keys(styles).forEach(prop => this.previousStyles[prop] = styles[prop]);
|
||||
}
|
||||
});
|
||||
|
||||
this.totalTime = delay + duration;
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
|
@ -66,7 +74,17 @@ export class MockAnimationPlayer extends NoopAnimationPlayer {
|
|||
this.__finished = true;
|
||||
}
|
||||
|
||||
private _captureStyles(): {[styleName: string]: string | number} {
|
||||
/* @internal */
|
||||
triggerMicrotask() {}
|
||||
|
||||
play(): void {
|
||||
super.play();
|
||||
this.__started = true;
|
||||
}
|
||||
|
||||
hasStarted() { return this.__started; }
|
||||
|
||||
beforeDestroy() {
|
||||
const captures: ɵStyleData = {};
|
||||
|
||||
Object.keys(this.previousStyles).forEach(prop => {
|
||||
|
@ -86,6 +104,6 @@ export class MockAnimationPlayer extends NoopAnimationPlayer {
|
|||
});
|
||||
}
|
||||
|
||||
return captures;
|
||||
this.currentSnapshot = captures;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* @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 {AnimationMetadata, AnimationOptions} from './animation_metadata';
|
||||
import {AnimationPlayer} from './players/animation_player';
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export abstract class AnimationBuilder {
|
||||
abstract build(animation: AnimationMetadata|AnimationMetadata[]): Animation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export abstract class Animation {
|
||||
abstract create(element: any, options?: AnimationOptions): AnimationPlayer;
|
||||
}
|
|
@ -16,17 +16,32 @@ export declare type AnimateTimings = {
|
|||
easing: string | null
|
||||
};
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export declare interface AnimationOptions {
|
||||
delay?: number|string;
|
||||
duration?: number|string;
|
||||
params?: {[name: string]: any};
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export const enum AnimationMetadataType {
|
||||
State,
|
||||
Transition,
|
||||
Sequence,
|
||||
Group,
|
||||
Animate,
|
||||
KeyframeSequence,
|
||||
Style
|
||||
State = 0,
|
||||
Transition = 1,
|
||||
Sequence = 2,
|
||||
Group = 3,
|
||||
Animate = 4,
|
||||
Keyframes = 5,
|
||||
Style = 6,
|
||||
Trigger = 7,
|
||||
Reference = 8,
|
||||
AnimateChild = 9,
|
||||
AnimateRef = 10,
|
||||
Query = 11,
|
||||
Stagger = 12
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -42,9 +57,10 @@ export interface AnimationMetadata { type: AnimationMetadataType; }
|
|||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export interface AnimationTriggerMetadata {
|
||||
export interface AnimationTriggerMetadata extends AnimationMetadata {
|
||||
name: string;
|
||||
definitions: AnimationMetadata[];
|
||||
options: {params?: {[name: string]: any}}|null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -65,8 +81,26 @@ export interface AnimationStateMetadata extends AnimationMetadata {
|
|||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export interface AnimationTransitionMetadata extends AnimationMetadata {
|
||||
expr: string|((fromState: string, toState: string) => boolean);
|
||||
expr: string;
|
||||
animation: AnimationMetadata|AnimationMetadata[];
|
||||
options: AnimationOptions|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export interface AnimationReferenceMetadata extends AnimationMetadata {
|
||||
animation: AnimationMetadata|AnimationMetadata[];
|
||||
options: AnimationOptions|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export interface AnimationQueryMetadata extends AnimationMetadata {
|
||||
selector: string;
|
||||
animation: AnimationMetadata|AnimationMetadata[];
|
||||
options: AnimationQueryOptions|null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -86,8 +120,8 @@ export interface AnimationKeyframesSequenceMetadata extends AnimationMetadata {
|
|||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export interface AnimationStyleMetadata extends AnimationMetadata {
|
||||
styles: {[key: string]: string | number}|{[key: string]: string | number}[];
|
||||
offset?: number;
|
||||
styles: '*'|{[key: string]: string | number}|Array<{[key: string]: string | number}|'*'>;
|
||||
offset: number|null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -101,13 +135,31 @@ export interface AnimationAnimateMetadata extends AnimationMetadata {
|
|||
styles: AnimationStyleMetadata|AnimationKeyframesSequenceMetadata|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export interface AnimationAnimateChildMetadata extends AnimationMetadata {
|
||||
options: AnimationOptions|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export interface AnimationAnimateRefMetadata extends AnimationMetadata {
|
||||
animation: AnimationReferenceMetadata;
|
||||
options: AnimationOptions|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata representing the entry of animations. Instances of this class are provided via the
|
||||
* animation DSL when the {@link sequence sequence animation function} is called.
|
||||
*
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export interface AnimationSequenceMetadata extends AnimationMetadata { steps: AnimationMetadata[]; }
|
||||
export interface AnimationSequenceMetadata extends AnimationMetadata {
|
||||
steps: AnimationMetadata[];
|
||||
options: AnimationOptions|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata representing the entry of animations. Instances of this class are provided via the
|
||||
|
@ -115,7 +167,18 @@ export interface AnimationSequenceMetadata extends AnimationMetadata { steps: An
|
|||
*
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export interface AnimationGroupMetadata extends AnimationMetadata { steps: AnimationMetadata[]; }
|
||||
export interface AnimationGroupMetadata extends AnimationMetadata {
|
||||
steps: AnimationMetadata[];
|
||||
options: AnimationOptions|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export interface AnimationStaggerMetadata extends AnimationMetadata {
|
||||
timings: string|number;
|
||||
animation: AnimationMetadata|AnimationMetadata[];
|
||||
}
|
||||
|
||||
/**
|
||||
* `trigger` is an animation-specific function that is designed to be used inside of Angular's
|
||||
|
@ -169,7 +232,7 @@ export interface AnimationGroupMetadata extends AnimationMetadata { steps: Anima
|
|||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export function trigger(name: string, definitions: AnimationMetadata[]): AnimationTriggerMetadata {
|
||||
return {name, definitions};
|
||||
return {type: AnimationMetadataType.Trigger, name, definitions, options: {}};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -220,7 +283,7 @@ export function trigger(name: string, definitions: AnimationMetadata[]): Animati
|
|||
export function animate(
|
||||
timings: string | number, styles: AnimationStyleMetadata | AnimationKeyframesSequenceMetadata |
|
||||
null = null): AnimationAnimateMetadata {
|
||||
return {type: AnimationMetadataType.Animate, styles: styles, timings: timings};
|
||||
return {type: AnimationMetadataType.Animate, styles, timings};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -254,8 +317,9 @@ export function animate(
|
|||
*
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export function group(steps: AnimationMetadata[]): AnimationGroupMetadata {
|
||||
return {type: AnimationMetadataType.Group, steps: steps};
|
||||
export function group(
|
||||
steps: AnimationMetadata[], options: AnimationOptions | null = null): AnimationGroupMetadata {
|
||||
return {type: AnimationMetadataType.Group, steps, options};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -292,8 +356,9 @@ export function group(steps: AnimationMetadata[]): AnimationGroupMetadata {
|
|||
*
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export function sequence(steps: AnimationMetadata[]): AnimationSequenceMetadata {
|
||||
return {type: AnimationMetadataType.Sequence, steps: steps};
|
||||
export function sequence(steps: AnimationMetadata[], options: AnimationOptions | null = null):
|
||||
AnimationSequenceMetadata {
|
||||
return {type: AnimationMetadataType.Sequence, steps, options};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -339,9 +404,9 @@ export function sequence(steps: AnimationMetadata[]): AnimationSequenceMetadata
|
|||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export function style(
|
||||
tokens: {[key: string]: string | number} |
|
||||
Array<{[key: string]: string | number}>): AnimationStyleMetadata {
|
||||
return {type: AnimationMetadataType.Style, styles: tokens};
|
||||
tokens: '*' | {[key: string]: string | number} |
|
||||
Array<'*'|{[key: string]: string | number}>): AnimationStyleMetadata {
|
||||
return {type: AnimationMetadataType.Style, styles: tokens, offset: null};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -393,7 +458,7 @@ export function style(
|
|||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export function state(name: string, styles: AnimationStyleMetadata): AnimationStateMetadata {
|
||||
return {type: AnimationMetadataType.State, name: name, styles: styles};
|
||||
return {type: AnimationMetadataType.State, name, styles};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -442,7 +507,7 @@ export function state(name: string, styles: AnimationStyleMetadata): AnimationSt
|
|||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export function keyframes(steps: AnimationStyleMetadata[]): AnimationKeyframesSequenceMetadata {
|
||||
return {type: AnimationMetadataType.KeyframeSequence, steps: steps};
|
||||
return {type: AnimationMetadataType.Keyframes, steps};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -553,7 +618,59 @@ export function keyframes(steps: AnimationStyleMetadata[]): AnimationKeyframesSe
|
|||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export function transition(
|
||||
stateChangeExpr: string | ((fromState: string, toState: string) => boolean),
|
||||
steps: AnimationMetadata | AnimationMetadata[]): AnimationTransitionMetadata {
|
||||
return {type: AnimationMetadataType.Transition, expr: stateChangeExpr, animation: steps};
|
||||
stateChangeExpr: string, steps: AnimationMetadata | AnimationMetadata[],
|
||||
options: AnimationOptions | null = null): AnimationTransitionMetadata {
|
||||
return {type: AnimationMetadataType.Transition, expr: stateChangeExpr, animation: steps, options};
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export function animation(
|
||||
steps: AnimationMetadata | AnimationMetadata[],
|
||||
options: AnimationOptions | null = null): AnimationReferenceMetadata {
|
||||
return {type: AnimationMetadataType.Reference, animation: steps, options};
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export function animateChild(options: AnimationOptions | null = null):
|
||||
AnimationAnimateChildMetadata {
|
||||
return {type: AnimationMetadataType.AnimateChild, options};
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export function useAnimation(
|
||||
animation: AnimationReferenceMetadata,
|
||||
options: AnimationOptions | null = null): AnimationAnimateRefMetadata {
|
||||
return {type: AnimationMetadataType.AnimateRef, animation, options};
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export declare interface AnimationQueryOptions extends AnimationOptions {
|
||||
optional?: boolean;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export function query(
|
||||
selector: string, animation: AnimationMetadata | AnimationMetadata[],
|
||||
options: AnimationQueryOptions | null = null): AnimationQueryMetadata {
|
||||
return {type: AnimationMetadataType.Query, selector, animation, options};
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export function stagger(
|
||||
timings: string | number,
|
||||
animation: AnimationMetadata | AnimationMetadata[]): AnimationStaggerMetadata {
|
||||
return {type: AnimationMetadataType.Stagger, timings, animation};
|
||||
}
|
||||
|
|
|
@ -11,8 +11,9 @@
|
|||
* @description
|
||||
* Entry point for all animation APIs of the animation package.
|
||||
*/
|
||||
export {Animation, AnimationBuilder} from './animation_builder';
|
||||
export {AnimationEvent} from './animation_event';
|
||||
export {AUTO_STYLE, AnimateTimings, AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, AnimationTriggerMetadata, animate, group, keyframes, sequence, state, style, transition, trigger, ɵStyleData} from './animation_metadata';
|
||||
export {AUTO_STYLE, AnimateTimings, AnimationAnimateChildMetadata, AnimationAnimateMetadata, AnimationAnimateRefMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationOptions, AnimationQueryMetadata, AnimationQueryOptions, AnimationReferenceMetadata, AnimationSequenceMetadata, AnimationStaggerMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, AnimationTriggerMetadata, animate, animateChild, animation, group, keyframes, query, sequence, stagger, state, style, transition, trigger, useAnimation, ɵStyleData} from './animation_metadata';
|
||||
export {AnimationPlayer, NoopAnimationPlayer} from './players/animation_player';
|
||||
|
||||
export * from './private_export';
|
||||
|
|
|
@ -18,22 +18,38 @@ export class AnimationGroupPlayer implements AnimationPlayer {
|
|||
private _onDestroyFns: Function[] = [];
|
||||
|
||||
public parentPlayer: AnimationPlayer|null = null;
|
||||
public totalTime: number = 0;
|
||||
|
||||
constructor(private _players: AnimationPlayer[]) {
|
||||
let count = 0;
|
||||
let doneCount = 0;
|
||||
let destroyCount = 0;
|
||||
let startCount = 0;
|
||||
const total = this._players.length;
|
||||
|
||||
if (total == 0) {
|
||||
scheduleMicroTask(() => this._onFinish());
|
||||
} else {
|
||||
this._players.forEach(player => {
|
||||
player.parentPlayer = this;
|
||||
player.onDone(() => {
|
||||
if (++count >= total) {
|
||||
if (++doneCount >= total) {
|
||||
this._onFinish();
|
||||
}
|
||||
});
|
||||
player.onDestroy(() => {
|
||||
if (++destroyCount >= total) {
|
||||
this._onDestroy();
|
||||
}
|
||||
});
|
||||
player.onStart(() => {
|
||||
if (++startCount >= total) {
|
||||
this._onStart();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.totalTime = this._players.reduce((time, player) => Math.max(time, player.totalTime), 0);
|
||||
}
|
||||
|
||||
private _onFinish() {
|
||||
|
@ -48,6 +64,14 @@ export class AnimationGroupPlayer implements AnimationPlayer {
|
|||
|
||||
onStart(fn: () => void): void { this._onStartFns.push(fn); }
|
||||
|
||||
private _onStart() {
|
||||
if (!this.hasStarted()) {
|
||||
this._onStartFns.forEach(fn => fn());
|
||||
this._onStartFns = [];
|
||||
this._started = true;
|
||||
}
|
||||
}
|
||||
|
||||
onDone(fn: () => void): void { this._onDoneFns.push(fn); }
|
||||
|
||||
onDestroy(fn: () => void): void { this._onDestroyFns.push(fn); }
|
||||
|
@ -58,11 +82,7 @@ export class AnimationGroupPlayer implements AnimationPlayer {
|
|||
if (!this.parentPlayer) {
|
||||
this.init();
|
||||
}
|
||||
if (!this.hasStarted()) {
|
||||
this._onStartFns.forEach(fn => fn());
|
||||
this._onStartFns = [];
|
||||
this._started = true;
|
||||
}
|
||||
this._onStart();
|
||||
this._players.forEach(player => player.play());
|
||||
}
|
||||
|
||||
|
@ -75,11 +95,13 @@ export class AnimationGroupPlayer implements AnimationPlayer {
|
|||
this._players.forEach(player => player.finish());
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
destroy(): void { this._onDestroy(); }
|
||||
|
||||
private _onDestroy() {
|
||||
if (!this._destroyed) {
|
||||
this._destroyed = true;
|
||||
this._onFinish();
|
||||
this._players.forEach(player => player.destroy());
|
||||
this._destroyed = true;
|
||||
this._onDestroyFns.forEach(fn => fn());
|
||||
this._onDestroyFns = [];
|
||||
}
|
||||
|
@ -93,7 +115,11 @@ export class AnimationGroupPlayer implements AnimationPlayer {
|
|||
}
|
||||
|
||||
setPosition(p: number): void {
|
||||
this._players.forEach(player => { player.setPosition(p); });
|
||||
const timeAtPosition = p * this.totalTime;
|
||||
this._players.forEach(player => {
|
||||
const position = player.totalTime ? Math.min(1, timeAtPosition / player.totalTime) : 1;
|
||||
player.setPosition(position);
|
||||
});
|
||||
}
|
||||
|
||||
getPosition(): number {
|
||||
|
|
|
@ -26,6 +26,8 @@ export abstract class AnimationPlayer {
|
|||
abstract getPosition(): number;
|
||||
get parentPlayer(): AnimationPlayer|null { throw new Error('NOT IMPLEMENTED: Base Class'); }
|
||||
set parentPlayer(player: AnimationPlayer|null) { throw new Error('NOT IMPLEMENTED: Base Class'); }
|
||||
get totalTime(): number { throw new Error('NOT IMPLEMENTED: Base Class'); }
|
||||
beforeDestroy?: () => any;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -39,6 +41,7 @@ export class NoopAnimationPlayer implements AnimationPlayer {
|
|||
private _destroyed = false;
|
||||
private _finished = false;
|
||||
public parentPlayer: AnimationPlayer|null = null;
|
||||
public totalTime = 0;
|
||||
constructor() {}
|
||||
private _onFinish() {
|
||||
if (!this._finished) {
|
||||
|
@ -54,15 +57,20 @@ export class NoopAnimationPlayer implements AnimationPlayer {
|
|||
init(): void {}
|
||||
play(): void {
|
||||
if (!this.hasStarted()) {
|
||||
scheduleMicroTask(() => this._onFinish());
|
||||
this.triggerMicrotask();
|
||||
this._onStart();
|
||||
}
|
||||
this._started = true;
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
triggerMicrotask() { scheduleMicroTask(() => this._onFinish()); }
|
||||
|
||||
private _onStart() {
|
||||
this._onStartFns.forEach(fn => fn());
|
||||
this._onStartFns = [];
|
||||
}
|
||||
|
||||
pause(): void {}
|
||||
restart(): void {}
|
||||
finish(): void { this._onFinish(); }
|
||||
|
|
|
@ -6,3 +6,4 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
export {AnimationGroupPlayer as ɵAnimationGroupPlayer} from './players/animation_group_player';
|
||||
export const ɵPRE_STYLE = '!';
|
||||
|
|
|
@ -53,8 +53,8 @@ export interface AnimationKeyframesSequenceMetadata extends AnimationMetadata {
|
|||
* @deprecated This symbol has moved. Please Import from @angular/animations instead!
|
||||
*/
|
||||
export interface AnimationStyleMetadata extends AnimationMetadata {
|
||||
styles: {[key: string]: string | number}|{[key: string]: string | number}[];
|
||||
offset?: number;
|
||||
styles: '*'|{[key: string]: string | number}|Array<{[key: string]: string | number}|'*'>;
|
||||
offset: number|null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -131,9 +131,8 @@ export function keyframes(steps: AnimationStyleMetadata[]): AnimationKeyframesSe
|
|||
/**
|
||||
* @deprecated This symbol has moved. Please Import from @angular/animations instead!
|
||||
*/
|
||||
export function transition(
|
||||
stateChangeExpr: string | ((fromState: string, toState: string) => boolean),
|
||||
steps: AnimationMetadata | AnimationMetadata[]): AnimationTransitionMetadata {
|
||||
export function transition(stateChangeExpr: string, steps: AnimationMetadata | AnimationMetadata[]):
|
||||
AnimationTransitionMetadata {
|
||||
return _transition(stateChangeExpr, steps);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 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 {AUTO_STYLE, AnimationEvent, animate, group, keyframes, state, style, transition, trigger} from '@angular/animations';
|
||||
import {AUTO_STYLE, AnimationEvent, AnimationOptions, animate, animateChild, group, keyframes, query, state, style, transition, trigger} from '@angular/animations';
|
||||
import {AnimationDriver, ɵAnimationEngine, ɵNoopAnimationDriver} from '@angular/animations/browser';
|
||||
import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/browser/testing';
|
||||
import {Component, HostBinding, HostListener, RendererFactory2, ViewChild} from '@angular/core';
|
||||
|
@ -15,6 +15,9 @@ import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
|||
|
||||
import {TestBed, fakeAsync, flushMicrotasks} from '../../testing';
|
||||
|
||||
const DEFAULT_NAMESPACE_ID = 'id';
|
||||
const DEFAULT_COMPONENT_ID = '1';
|
||||
|
||||
export function main() {
|
||||
// these tests are only mean't to be run within the DOM (for now)
|
||||
if (typeof Element == 'undefined') return;
|
||||
|
@ -65,6 +68,85 @@ export function main() {
|
|||
]);
|
||||
});
|
||||
|
||||
it('should not cancel the previous transition if a follow-up transition is not matched',
|
||||
fakeAsync(() => {
|
||||
@Component({
|
||||
selector: 'if-cmp',
|
||||
template: `
|
||||
<div [@myAnimation]="exp" (@myAnimation.start)="callback($event)" (@myAnimation.done)="callback($event)"></div>
|
||||
`,
|
||||
animations: [trigger(
|
||||
'myAnimation',
|
||||
[transition(
|
||||
'a => b', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
|
||||
})
|
||||
class Cmp {
|
||||
exp: any;
|
||||
startEvent: any;
|
||||
doneEvent: any;
|
||||
|
||||
callback(event: any) {
|
||||
if (event.phaseName == 'done') {
|
||||
this.doneEvent = event;
|
||||
} else {
|
||||
this.startEvent = event;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
|
||||
cmp.exp = 'a';
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
expect(getLog().length).toEqual(0);
|
||||
expect(engine.players.length).toEqual(0);
|
||||
|
||||
flushMicrotasks();
|
||||
expect(cmp.startEvent.toState).toEqual('a');
|
||||
expect(cmp.startEvent.totalTime).toEqual(0);
|
||||
expect(cmp.startEvent.toState).toEqual('a');
|
||||
expect(cmp.startEvent.totalTime).toEqual(0);
|
||||
resetLog();
|
||||
|
||||
cmp.exp = 'b';
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
|
||||
const players = getLog();
|
||||
expect(players.length).toEqual(1);
|
||||
expect(engine.players.length).toEqual(1);
|
||||
|
||||
flushMicrotasks();
|
||||
expect(cmp.startEvent.toState).toEqual('b');
|
||||
expect(cmp.startEvent.totalTime).toEqual(500);
|
||||
expect(cmp.startEvent.toState).toEqual('b');
|
||||
expect(cmp.startEvent.totalTime).toEqual(500);
|
||||
resetLog();
|
||||
|
||||
let completed = false;
|
||||
players[0].onDone(() => completed = true);
|
||||
|
||||
cmp.exp = 'c';
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
|
||||
expect(engine.players.length).toEqual(1);
|
||||
expect(getLog().length).toEqual(0);
|
||||
|
||||
flushMicrotasks();
|
||||
expect(cmp.startEvent.toState).toEqual('c');
|
||||
expect(cmp.startEvent.totalTime).toEqual(0);
|
||||
expect(cmp.startEvent.toState).toEqual('c');
|
||||
expect(cmp.startEvent.totalTime).toEqual(0);
|
||||
|
||||
expect(completed).toBe(false);
|
||||
}));
|
||||
|
||||
it('should only turn a view removal as into `void` state transition', () => {
|
||||
@Component({
|
||||
selector: 'if-cmp',
|
||||
|
@ -285,7 +367,7 @@ export function main() {
|
|||
{opacity: '0', offset: 1},
|
||||
]);
|
||||
|
||||
flushMicrotasks();
|
||||
player.finish();
|
||||
expect(fixture.debugElement.nativeElement.children.length).toBe(0);
|
||||
}));
|
||||
|
||||
|
@ -335,6 +417,7 @@ export function main() {
|
|||
{opacity: '0', offset: 1},
|
||||
]);
|
||||
|
||||
player.finish();
|
||||
flushMicrotasks();
|
||||
expect(fixture.debugElement.nativeElement.children.length).toBe(0);
|
||||
}));
|
||||
|
@ -387,15 +470,17 @@ export function main() {
|
|||
|
||||
const [p1, p2] = getLog();
|
||||
expect(p1.keyframes).toEqual([
|
||||
{height: '100px', offset: 0},
|
||||
{height: '0px', offset: 1},
|
||||
]);
|
||||
|
||||
expect(p2.keyframes).toEqual([
|
||||
{width: '100px', offset: 0},
|
||||
{width: '0px', offset: 1},
|
||||
]);
|
||||
|
||||
expect(p2.keyframes).toEqual([
|
||||
{height: '100px', offset: 0},
|
||||
{height: '0px', offset: 1},
|
||||
]);
|
||||
|
||||
p1.finish();
|
||||
p2.finish();
|
||||
flushMicrotasks();
|
||||
expect(fixture.debugElement.nativeElement.children.length).toBe(0);
|
||||
}));
|
||||
|
@ -621,7 +706,11 @@ export function main() {
|
|||
template: `
|
||||
<div #green @green></div>
|
||||
`,
|
||||
animations: [trigger('green', [state('*', style({backgroundColor: 'green'}))])]
|
||||
animations: [trigger(
|
||||
'green',
|
||||
[
|
||||
state('*', style({backgroundColor: 'green'})), transition('* => *', animate(500))
|
||||
])]
|
||||
})
|
||||
class Cmp {
|
||||
@ViewChild('green') public element: any;
|
||||
|
@ -635,7 +724,7 @@ export function main() {
|
|||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
|
||||
const player = engine.activePlayers.pop();
|
||||
const player = engine.players.pop();
|
||||
player.finish();
|
||||
|
||||
expect(getDOM().hasStyle(cmp.element.nativeElement, 'background-color', 'green'))
|
||||
|
@ -774,13 +863,219 @@ export function main() {
|
|||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
expect(getLog().length).toEqual(0);
|
||||
resetLog();
|
||||
});
|
||||
|
||||
it('should cancel all active inner child animations when a parent removal animation is set to go',
|
||||
() => {
|
||||
@Component({
|
||||
selector: 'ani-cmp',
|
||||
template: `
|
||||
<div *ngIf="exp1" @parent>
|
||||
<div [@child]="exp2" class="child1"></div>
|
||||
<div [@child]="exp2" class="child2"></div>
|
||||
</div>
|
||||
`,
|
||||
animations: [
|
||||
trigger('parent', [transition(
|
||||
':leave',
|
||||
[style({opacity: 0}), animate(1000, style({opacity: 1}))])]),
|
||||
trigger('child', [transition(
|
||||
'a => b',
|
||||
[style({opacity: 0}), animate(1000, style({opacity: 1}))])])
|
||||
]
|
||||
})
|
||||
class Cmp {
|
||||
public exp1: any;
|
||||
public exp2: any;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
|
||||
cmp.exp1 = true;
|
||||
cmp.exp2 = 'a';
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
resetLog();
|
||||
|
||||
cmp.exp2 = 'b';
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
|
||||
let players = getLog();
|
||||
expect(players.length).toEqual(2);
|
||||
const [p1, p2] = players;
|
||||
|
||||
let count = 0;
|
||||
p1.onDone(() => count++);
|
||||
p2.onDone(() => count++);
|
||||
|
||||
cmp.exp1 = false;
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
|
||||
expect(count).toEqual(2);
|
||||
});
|
||||
|
||||
it('should destroy inner animations when a parent node is set for removal', () => {
|
||||
@Component({
|
||||
selector: 'ani-cmp',
|
||||
template: `
|
||||
<div #parent class="parent">
|
||||
<div [@child]="exp" class="child1"></div>
|
||||
<div [@child]="exp" class="child2"></div>
|
||||
</div>
|
||||
`,
|
||||
animations: [trigger(
|
||||
'child',
|
||||
[transition('a => b', [style({opacity: 0}), animate(1000, style({opacity: 1}))])])]
|
||||
})
|
||||
class Cmp {
|
||||
public exp: any;
|
||||
|
||||
@ViewChild('parent') public parentElement: any;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
|
||||
const engine = TestBed.get(ɵAnimationEngine) as ɵAnimationEngine;
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
|
||||
const someTrigger = trigger('someTrigger', []);
|
||||
engine.registerTrigger(
|
||||
DEFAULT_COMPONENT_ID, DEFAULT_NAMESPACE_ID, fixture.nativeElement, someTrigger.name,
|
||||
someTrigger);
|
||||
|
||||
cmp.exp = 'a';
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
resetLog();
|
||||
|
||||
cmp.exp = 'b';
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
|
||||
const players = getLog();
|
||||
expect(players.length).toEqual(2);
|
||||
const [p1, p2] = players;
|
||||
|
||||
let count = 0;
|
||||
p1.onDone(() => count++);
|
||||
p2.onDone(() => count++);
|
||||
|
||||
engine.onRemove(DEFAULT_NAMESPACE_ID, cmp.parentElement.nativeElement, null);
|
||||
expect(count).toEqual(2);
|
||||
});
|
||||
|
||||
it('should always make children wait for the parent animation to finish before any removals occur',
|
||||
() => {
|
||||
@Component({
|
||||
selector: 'ani-cmp',
|
||||
template: `
|
||||
<div #parent [@parent]="exp1" class="parent">
|
||||
<div #child1 *ngIf="exp2" class="child1"></div>
|
||||
<div #child2 *ngIf="exp2" class="child2"></div>
|
||||
</div>
|
||||
`,
|
||||
animations: [trigger(
|
||||
'parent',
|
||||
[transition(
|
||||
'a => b', [style({opacity: 0}), animate(1000, style({opacity: 1}))])])]
|
||||
})
|
||||
class Cmp {
|
||||
public exp1: any;
|
||||
public exp2: any;
|
||||
|
||||
@ViewChild('parent') public parent: any;
|
||||
|
||||
@ViewChild('child1') public child1Elm: any;
|
||||
|
||||
@ViewChild('child2') public child2Elm: any;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
|
||||
cmp.exp1 = 'a';
|
||||
cmp.exp2 = true;
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
resetLog();
|
||||
|
||||
cmp.exp1 = 'b';
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
|
||||
const player = getLog()[0];
|
||||
const p = cmp.parent.nativeElement;
|
||||
const c1 = cmp.child1Elm.nativeElement;
|
||||
const c2 = cmp.child2Elm.nativeElement;
|
||||
|
||||
expect(p.contains(c1)).toBeTruthy();
|
||||
expect(p.contains(c2)).toBeTruthy();
|
||||
|
||||
cmp.exp2 = false;
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
|
||||
expect(p.contains(c1)).toBeTruthy();
|
||||
expect(p.contains(c2)).toBeTruthy();
|
||||
|
||||
player.finish();
|
||||
|
||||
expect(p.contains(c1)).toBeFalsy();
|
||||
expect(p.contains(c2)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should substitute in values if the provided state match is an object with values', () => {
|
||||
@Component({
|
||||
selector: 'ani-cmp',
|
||||
template: `
|
||||
<div [@myAnimation]="exp"></div>
|
||||
`,
|
||||
animations: [trigger(
|
||||
'myAnimation',
|
||||
[transition(
|
||||
'a => b',
|
||||
[style({opacity: '{{ start }}'}), animate(1000, style({opacity: '{{ end }}'}))],
|
||||
buildParams({start: '0', end: '1'}))])]
|
||||
})
|
||||
class Cmp {
|
||||
public exp: any;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
|
||||
cmp.exp = {value: 'a'};
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
resetLog();
|
||||
|
||||
cmp.exp = {value: 'b', params: {start: .3, end: .6}};
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
const player = getLog().pop() !;
|
||||
expect(player.keyframes).toEqual([
|
||||
{opacity: '0.3', offset: 0}, {opacity: '0.6', offset: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('animation listeners', () => {
|
||||
it('should trigger a `start` state change listener for when the animation changes state from void => state',
|
||||
() => {
|
||||
fakeAsync(() => {
|
||||
@Component({
|
||||
selector: 'if-cmp',
|
||||
template: `
|
||||
|
@ -806,17 +1101,17 @@ export function main() {
|
|||
const cmp = fixture.componentInstance;
|
||||
cmp.exp = 'true';
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
flushMicrotasks();
|
||||
|
||||
expect(cmp.event.triggerName).toEqual('myAnimation');
|
||||
expect(cmp.event.phaseName).toEqual('start');
|
||||
expect(cmp.event.totalTime).toEqual(500);
|
||||
expect(cmp.event.fromState).toEqual('void');
|
||||
expect(cmp.event.toState).toEqual('true');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should trigger a `done` state change listener for when the animation changes state from a => b',
|
||||
() => {
|
||||
fakeAsync(() => {
|
||||
@Component({
|
||||
selector: 'if-cmp',
|
||||
template: `
|
||||
|
@ -846,75 +1141,79 @@ export function main() {
|
|||
|
||||
expect(cmp.event).toBeFalsy();
|
||||
|
||||
const player = engine.activePlayers.pop();
|
||||
const player = engine.players.pop();
|
||||
player.finish();
|
||||
flushMicrotasks();
|
||||
|
||||
expect(cmp.event.triggerName).toEqual('myAnimation123');
|
||||
expect(cmp.event.phaseName).toEqual('done');
|
||||
expect(cmp.event.totalTime).toEqual(999);
|
||||
expect(cmp.event.fromState).toEqual('void');
|
||||
expect(cmp.event.toState).toEqual('b');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should handle callbacks for multiple triggers running simultaneously', () => {
|
||||
@Component({
|
||||
selector: 'if-cmp',
|
||||
template: `
|
||||
it('should handle callbacks for multiple triggers running simultaneously', fakeAsync(() => {
|
||||
@Component({
|
||||
selector: 'if-cmp',
|
||||
template: `
|
||||
<div [@ani1]="exp1" (@ani1.done)="callback1($event)"></div>
|
||||
<div [@ani2]="exp2" (@ani2.done)="callback2($event)"></div>
|
||||
`,
|
||||
animations: [
|
||||
trigger(
|
||||
'ani1',
|
||||
[
|
||||
transition(
|
||||
'* => a', [style({'opacity': '0'}), animate(999, style({'opacity': '1'}))]),
|
||||
]),
|
||||
trigger(
|
||||
'ani2',
|
||||
[
|
||||
transition(
|
||||
'* => b', [style({'width': '0px'}), animate(999, style({'width': '100px'}))]),
|
||||
])
|
||||
],
|
||||
})
|
||||
class Cmp {
|
||||
exp1: any = false;
|
||||
exp2: any = false;
|
||||
event1: AnimationEvent;
|
||||
event2: AnimationEvent;
|
||||
callback1 = (event: any) => { this.event1 = event; };
|
||||
callback2 = (event: any) => { this.event2 = event; };
|
||||
}
|
||||
animations: [
|
||||
trigger(
|
||||
'ani1',
|
||||
[
|
||||
transition(
|
||||
'* => a',
|
||||
[style({'opacity': '0'}), animate(999, style({'opacity': '1'}))]),
|
||||
]),
|
||||
trigger(
|
||||
'ani2',
|
||||
[
|
||||
transition(
|
||||
'* => b',
|
||||
[style({'width': '0px'}), animate(999, style({'width': '100px'}))]),
|
||||
])
|
||||
],
|
||||
})
|
||||
class Cmp {
|
||||
exp1: any = false;
|
||||
exp2: any = false;
|
||||
event1: AnimationEvent;
|
||||
event2: AnimationEvent;
|
||||
callback1 = (event: any) => { this.event1 = event; };
|
||||
callback2 = (event: any) => { this.event2 = event; };
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
|
||||
cmp.exp1 = 'a';
|
||||
cmp.exp2 = 'b';
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
cmp.exp1 = 'a';
|
||||
cmp.exp2 = 'b';
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
|
||||
expect(cmp.event1).toBeFalsy();
|
||||
expect(cmp.event2).toBeFalsy();
|
||||
expect(cmp.event1).toBeFalsy();
|
||||
expect(cmp.event2).toBeFalsy();
|
||||
|
||||
const player1 = engine.activePlayers[0];
|
||||
const player2 = engine.activePlayers[1];
|
||||
const player1 = engine.players[0];
|
||||
const player2 = engine.players[1];
|
||||
|
||||
player1.finish();
|
||||
expect(cmp.event1.triggerName).toBeTruthy('ani1');
|
||||
expect(cmp.event2).toBeFalsy();
|
||||
player1.finish();
|
||||
player2.finish();
|
||||
expect(cmp.event1).toBeFalsy();
|
||||
expect(cmp.event2).toBeFalsy();
|
||||
|
||||
player2.finish();
|
||||
expect(cmp.event1.triggerName).toBeTruthy('ani1');
|
||||
expect(cmp.event2.triggerName).toBeTruthy('ani2');
|
||||
});
|
||||
flushMicrotasks();
|
||||
expect(cmp.event1.triggerName).toBeTruthy('ani1');
|
||||
expect(cmp.event2.triggerName).toBeTruthy('ani2');
|
||||
}));
|
||||
|
||||
it('should handle callbacks for multiple triggers running simultaneously on the same element',
|
||||
() => {
|
||||
fakeAsync(() => {
|
||||
@Component({
|
||||
selector: 'if-cmp',
|
||||
template: `
|
||||
|
@ -960,20 +1259,21 @@ export function main() {
|
|||
expect(cmp.event1).toBeFalsy();
|
||||
expect(cmp.event2).toBeFalsy();
|
||||
|
||||
const player1 = engine.activePlayers[0];
|
||||
const player2 = engine.activePlayers[1];
|
||||
const player1 = engine.players[0];
|
||||
const player2 = engine.players[1];
|
||||
|
||||
player1.finish();
|
||||
expect(cmp.event1.triggerName).toBeTruthy('ani1');
|
||||
player2.finish();
|
||||
expect(cmp.event1).toBeFalsy();
|
||||
expect(cmp.event2).toBeFalsy();
|
||||
|
||||
player2.finish();
|
||||
flushMicrotasks();
|
||||
expect(cmp.event1.triggerName).toBeTruthy('ani1');
|
||||
expect(cmp.event2.triggerName).toBeTruthy('ani2');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should trigger a state change listener for when the animation changes state from void => state on the host element',
|
||||
() => {
|
||||
fakeAsync(() => {
|
||||
@Component({
|
||||
selector: 'my-cmp',
|
||||
template: `...`,
|
||||
|
@ -1000,14 +1300,14 @@ export function main() {
|
|||
const cmp = fixture.componentInstance;
|
||||
cmp.exp = 'TRUE';
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
flushMicrotasks();
|
||||
|
||||
expect(cmp.event.triggerName).toEqual('myAnimation2');
|
||||
expect(cmp.event.phaseName).toEqual('start');
|
||||
expect(cmp.event.totalTime).toEqual(1000);
|
||||
expect(cmp.event.fromState).toEqual('void');
|
||||
expect(cmp.event.toState).toEqual('TRUE');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should always fire callbacks even when a transition is not detected', fakeAsync(() => {
|
||||
@Component({
|
||||
|
@ -1044,7 +1344,8 @@ export function main() {
|
|||
expect(cmp.log).toEqual(['start => b', 'done => b']);
|
||||
}));
|
||||
|
||||
it('should fire callback events for leave animations', fakeAsync(() => {
|
||||
it('should fire callback events for leave animations even if there is no leave transition',
|
||||
fakeAsync(() => {
|
||||
@Component({
|
||||
selector: 'my-cmp',
|
||||
template: `
|
||||
|
@ -1082,6 +1383,233 @@ export function main() {
|
|||
|
||||
expect(cmp.log).toEqual(['start => void', 'done => void']);
|
||||
}));
|
||||
|
||||
it('should fire callbacks on a sub animation once it starts and finishes', fakeAsync(() => {
|
||||
@Component({
|
||||
selector: 'my-cmp',
|
||||
template: `
|
||||
<div class="parent"
|
||||
[@parent]="exp1"
|
||||
(@parent.start)="cb('parent-start',$event)"
|
||||
(@parent.done)="cb('parent-done', $event)">
|
||||
<div class="child"
|
||||
[@child]="exp2"
|
||||
(@child.start)="cb('child-start',$event)"
|
||||
(@child.done)="cb('child-done', $event)"></div>
|
||||
</div>
|
||||
`,
|
||||
animations: [
|
||||
trigger(
|
||||
'parent',
|
||||
[
|
||||
transition(
|
||||
'* => go',
|
||||
[
|
||||
style({width: '0px'}),
|
||||
animate(1000, style({width: '100px'})),
|
||||
query(
|
||||
'.child',
|
||||
[
|
||||
animateChild({duration: '1s'}),
|
||||
]),
|
||||
animate(1000, style({width: '0px'})),
|
||||
]),
|
||||
]),
|
||||
trigger(
|
||||
'child',
|
||||
[
|
||||
transition(
|
||||
'* => go',
|
||||
[
|
||||
style({height: '0px'}),
|
||||
animate(1000, style({height: '100px'})),
|
||||
]),
|
||||
])
|
||||
]
|
||||
})
|
||||
class Cmp {
|
||||
log: string[] = [];
|
||||
exp1: string;
|
||||
exp2: string;
|
||||
|
||||
cb(name: string, event: AnimationEvent) { this.log.push(name); }
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
cmp.exp1 = 'go';
|
||||
cmp.exp2 = 'go';
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
flushMicrotasks();
|
||||
|
||||
expect(cmp.log).toEqual(['parent-start', 'child-start']);
|
||||
cmp.log = [];
|
||||
|
||||
const players = getLog();
|
||||
expect(players.length).toEqual(3);
|
||||
const [p1, p2, p3] = players;
|
||||
|
||||
p1.finish();
|
||||
flushMicrotasks();
|
||||
expect(cmp.log).toEqual([]);
|
||||
|
||||
p2.finish();
|
||||
flushMicrotasks();
|
||||
expect(cmp.log).toEqual([]);
|
||||
|
||||
p3.finish();
|
||||
flushMicrotasks();
|
||||
expect(cmp.log).toEqual(['parent-done', 'child-done']);
|
||||
}));
|
||||
|
||||
it('should fire callbacks and collect the correct the totalTime and element details for any queried sub animations',
|
||||
fakeAsync(
|
||||
() => {
|
||||
@Component({
|
||||
selector: 'my-cmp',
|
||||
template: `
|
||||
<div class="parent" [@parent]="exp" (@parent.done)="cb('all','done', $event)">
|
||||
<div *ngFor="let item of items"
|
||||
class="item item-{{ item }}"
|
||||
@child
|
||||
(@child.start)="cb('c-' + item, 'start', $event)"
|
||||
(@child.done)="cb('c-' + item, 'done', $event)">
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
animations: [
|
||||
trigger('parent', [
|
||||
transition('* => go', [
|
||||
style({ opacity: 0 }),
|
||||
animate('1s', style({ opacity: 1 })),
|
||||
query('.item', [
|
||||
style({ opacity: 0 }),
|
||||
animate(1000, style({ opacity: 1 }))
|
||||
]),
|
||||
query('.item', [
|
||||
animateChild({ duration: '1.8s', delay: '300ms' })
|
||||
])
|
||||
])
|
||||
]),
|
||||
trigger('child', [
|
||||
transition(':enter', [
|
||||
style({ opacity: 0 }),
|
||||
animate(1500, style({ opactiy: 1 }))
|
||||
])
|
||||
])
|
||||
]
|
||||
})
|
||||
class Cmp {
|
||||
log: string[] = [];
|
||||
events: {[name: string]: any} = {};
|
||||
exp: string;
|
||||
items: any = [0, 1, 2, 3];
|
||||
|
||||
cb(name: string, phase: string, event: AnimationEvent) {
|
||||
this.log.push(name + '-' + phase);
|
||||
this.events[name] = event;
|
||||
}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
cmp.exp = 'go';
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
flushMicrotasks();
|
||||
|
||||
expect(cmp.log).toEqual(['c-0-start', 'c-1-start', 'c-2-start', 'c-3-start']);
|
||||
cmp.log = [];
|
||||
|
||||
const players = getLog();
|
||||
// 1 + 4 + 4 = 9 players
|
||||
expect(players.length).toEqual(9);
|
||||
|
||||
const [pA, pq1a, pq1b, pq1c, pq1d, pq2a, pq2b, pq2c, pq2d] = getLog();
|
||||
pA.finish();
|
||||
pq1a.finish();
|
||||
pq1b.finish();
|
||||
pq1c.finish();
|
||||
pq1d.finish();
|
||||
flushMicrotasks();
|
||||
|
||||
expect(cmp.log).toEqual([]);
|
||||
pq2a.finish();
|
||||
pq2b.finish();
|
||||
pq2c.finish();
|
||||
pq2d.finish();
|
||||
flushMicrotasks();
|
||||
|
||||
expect(cmp.log).toEqual(
|
||||
['all-done', 'c-0-done', 'c-1-done', 'c-2-done', 'c-3-done']);
|
||||
|
||||
expect(cmp.events['c-0'].totalTime).toEqual(4100); // 1000 + 1000 + 1800 + 300
|
||||
expect(cmp.events['c-0'].element.innerText.trim()).toEqual('0');
|
||||
expect(cmp.events['c-1'].totalTime).toEqual(4100);
|
||||
expect(cmp.events['c-1'].element.innerText.trim()).toEqual('1');
|
||||
expect(cmp.events['c-2'].totalTime).toEqual(4100);
|
||||
expect(cmp.events['c-2'].element.innerText.trim()).toEqual('2');
|
||||
expect(cmp.events['c-3'].totalTime).toEqual(4100);
|
||||
expect(cmp.events['c-3'].element.innerText.trim()).toEqual('3');
|
||||
}));
|
||||
});
|
||||
|
||||
it('should throw neither state() or transition() are used inside of trigger()', () => {
|
||||
@Component({
|
||||
selector: 'if-cmp',
|
||||
template: `
|
||||
<div [@myAnimation]="exp"></div>
|
||||
`,
|
||||
animations: [trigger('myAnimation', [animate(1000, style({width: '100px'}))])]
|
||||
})
|
||||
class Cmp {
|
||||
exp: any = false;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
|
||||
expect(() => { TestBed.createComponent(Cmp); })
|
||||
.toThrowError(
|
||||
/only state\(\) and transition\(\) definitions can sit inside of a trigger\(\)/);
|
||||
});
|
||||
|
||||
it('should not throw an error if styles overlap in separate transitions', () => {
|
||||
@Component({
|
||||
selector: 'if-cmp',
|
||||
template: `
|
||||
<div [@myAnimation]="exp"></div>
|
||||
`,
|
||||
animations: [
|
||||
trigger(
|
||||
'myAnimation',
|
||||
[
|
||||
transition(
|
||||
'void => *',
|
||||
[
|
||||
style({opacity: 0}),
|
||||
animate('0.5s 1s', style({opacity: 1})),
|
||||
]),
|
||||
transition(
|
||||
'* => void',
|
||||
[animate(1000, style({height: 0})), animate(1000, style({opacity: 0}))]),
|
||||
]),
|
||||
]
|
||||
})
|
||||
class Cmp {
|
||||
exp: any = false;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
|
||||
expect(() => { TestBed.createComponent(Cmp); }).not.toThrowError();
|
||||
});
|
||||
|
||||
describe('errors for not using the animation module', () => {
|
||||
|
@ -1128,3 +1656,7 @@ function assertHasParent(element: any, yes: boolean) {
|
|||
expect(parent).toBeFalsy();
|
||||
}
|
||||
}
|
||||
|
||||
function buildParams(params: {[name: string]: any}): AnimationOptions {
|
||||
return {params};
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -26,25 +26,23 @@ export function main() {
|
|||
});
|
||||
});
|
||||
|
||||
it('should animate a component that captures height during an animation', () => {
|
||||
it('should compute pre (!) and post (*) animation styles with different dom states', () => {
|
||||
@Component({
|
||||
selector: 'if-cmp',
|
||||
selector: 'ani-cmp',
|
||||
template: `
|
||||
<div *ngIf="exp" #element [@myAnimation]="exp">
|
||||
hello {{ text }}
|
||||
</div>
|
||||
`,
|
||||
<div [@myAnimation]="exp" #parent>
|
||||
<div *ngFor="let item of items" class="child" style="line-height:20px">
|
||||
- {{ item }}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
animations: [trigger(
|
||||
'myAnimation',
|
||||
[
|
||||
transition('* => *', [style({height: '0px'}), animate(1000, style({height: '*'}))]),
|
||||
])]
|
||||
[transition('* => *', [style({height: '!'}), animate(1000, style({height: '*'}))])])]
|
||||
})
|
||||
class Cmp {
|
||||
exp: any = false;
|
||||
text: string;
|
||||
|
||||
@ViewChild('element') public element: any;
|
||||
public exp: number;
|
||||
public items = [0, 1, 2, 3, 4];
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
|
@ -52,33 +50,36 @@ export function main() {
|
|||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
|
||||
cmp.exp = 1;
|
||||
cmp.text = '';
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
|
||||
const element = cmp.element.nativeElement;
|
||||
element.style.lineHeight = '20px';
|
||||
element.style.width = '50px';
|
||||
expect(engine.players.length).toEqual(1);
|
||||
let player = engine.players[0];
|
||||
let webPlayer = player.getRealPlayer() as ɵWebAnimationsPlayer;
|
||||
|
||||
expect(webPlayer.keyframes).toEqual([
|
||||
{height: '0px', offset: 0}, {height: '100px', offset: 1}
|
||||
]);
|
||||
|
||||
// we destroy the player because since it has started and is
|
||||
// at 0ms duration a height value of `0px` will be extracted
|
||||
// from the element and passed into the follow-up animation.
|
||||
player.destroy();
|
||||
|
||||
cmp.exp = 2;
|
||||
cmp.text = '12345';
|
||||
cmp.items = [0, 1, 2, 6];
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
|
||||
let player = engine.activePlayers.pop() as ɵWebAnimationsPlayer;
|
||||
player.setPosition(1);
|
||||
expect(engine.players.length).toEqual(1);
|
||||
player = engine.players[0];
|
||||
webPlayer = player.getRealPlayer() as ɵWebAnimationsPlayer;
|
||||
|
||||
assertStyleBetween(element, 'height', 15, 25);
|
||||
|
||||
cmp.exp = 3;
|
||||
cmp.text = '12345-12345-12345-12345';
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
|
||||
player = engine.activePlayers.pop() as ɵWebAnimationsPlayer;
|
||||
player.setPosition(1);
|
||||
assertStyleBetween(element, 'height', 35, 45);
|
||||
expect(webPlayer.keyframes).toEqual([
|
||||
{height: '100px', offset: 0}, {height: '80px', offset: 1}
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* @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 {Animation, AnimationBuilder, AnimationMetadata, AnimationOptions, AnimationPlayer, NoopAnimationPlayer, sequence} from '@angular/animations';
|
||||
import {Injectable, RendererFactory2, RendererType2, ViewEncapsulation} from '@angular/core';
|
||||
|
||||
import {AnimationRenderer} from './animation_renderer';
|
||||
|
||||
@Injectable()
|
||||
export class BrowserAnimationBuilder extends AnimationBuilder {
|
||||
private _nextAnimationId = 0;
|
||||
private _renderer: AnimationRenderer;
|
||||
|
||||
constructor(rootRenderer: RendererFactory2) {
|
||||
super();
|
||||
const typeData = {
|
||||
id: '0',
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
styles: [],
|
||||
data: {animation: []}
|
||||
} as RendererType2;
|
||||
this._renderer = rootRenderer.createRenderer(document.body, typeData) as AnimationRenderer;
|
||||
}
|
||||
|
||||
build(animation: AnimationMetadata|AnimationMetadata[]): Animation {
|
||||
const id = this._nextAnimationId.toString();
|
||||
this._nextAnimationId++;
|
||||
const entry = Array.isArray(animation) ? sequence(animation) : animation;
|
||||
issueAnimationCommand(this._renderer, null, id, 'register', [entry]);
|
||||
return new BrowserAnimation(id, this._renderer);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class NoopAnimationBuilder extends BrowserAnimationBuilder {
|
||||
build(animation: AnimationMetadata|AnimationMetadata[]): Animation { return new NoopAnimation(); }
|
||||
}
|
||||
|
||||
export class BrowserAnimation extends Animation {
|
||||
constructor(private _id: string, private _renderer: AnimationRenderer) { super(); }
|
||||
|
||||
create(element: any, options?: AnimationOptions): AnimationPlayer {
|
||||
return new RendererAnimationPlayer(this._id, element, options || {}, this._renderer);
|
||||
}
|
||||
}
|
||||
|
||||
export class NoopAnimation extends Animation {
|
||||
constructor() { super(); }
|
||||
|
||||
create(element: any, options?: AnimationOptions): AnimationPlayer {
|
||||
return new NoopAnimationPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
export class RendererAnimationPlayer implements AnimationPlayer {
|
||||
public parentPlayer: AnimationPlayer|null = null;
|
||||
private _started = false;
|
||||
|
||||
constructor(
|
||||
public id: string, public element: any, options: AnimationOptions,
|
||||
private _renderer: AnimationRenderer) {
|
||||
this._command('create', options);
|
||||
}
|
||||
|
||||
private _listen(eventName: string, callback: (event: any) => any): () => void {
|
||||
return this._renderer.listen(this.element, `@@${this.id}:${eventName}`, callback);
|
||||
}
|
||||
|
||||
private _command(command: string, ...args: any[]) {
|
||||
return issueAnimationCommand(this._renderer, this.element, this.id, command, args);
|
||||
}
|
||||
|
||||
onDone(fn: () => void): void { this._listen('done', fn); }
|
||||
|
||||
onStart(fn: () => void): void { this._listen('start', fn); }
|
||||
|
||||
onDestroy(fn: () => void): void { this._listen('destroy', fn); }
|
||||
|
||||
init(): void { this._command('init'); }
|
||||
|
||||
hasStarted(): boolean { return this._started; }
|
||||
|
||||
play(): void {
|
||||
this._command('play');
|
||||
this._started = true;
|
||||
}
|
||||
|
||||
pause(): void { this._command('pause'); }
|
||||
|
||||
restart(): void { this._command('restart'); }
|
||||
|
||||
finish(): void { this._command('finish'); }
|
||||
|
||||
destroy(): void { this._command('destroy'); }
|
||||
|
||||
reset(): void { this._command('reset'); }
|
||||
|
||||
setPosition(p: number): void { this._command('setPosition', p); }
|
||||
|
||||
getPosition(): number { return 0; }
|
||||
|
||||
public totalTime = 0;
|
||||
}
|
||||
|
||||
function issueAnimationCommand(
|
||||
renderer: AnimationRenderer, element: any, id: string, command: string, args: any[]): any {
|
||||
return renderer.setProperty(element, `@@${id}:${command}`, args);
|
||||
}
|
|
@ -11,18 +11,33 @@ import {Injectable, NgZone, Renderer2, RendererFactory2, RendererStyleFlags2, Re
|
|||
|
||||
@Injectable()
|
||||
export class AnimationRendererFactory implements RendererFactory2 {
|
||||
private _currentId: number = 0;
|
||||
|
||||
constructor(
|
||||
private delegate: RendererFactory2, private _engine: AnimationEngine, private _zone: NgZone) {
|
||||
_engine.onRemovalComplete = (element: any, delegate: any) => {
|
||||
// Note: if an component element has a leave animation, and the component
|
||||
// a host leave animation, the view engine will call `removeChild` for the parent
|
||||
// component renderer as well as for the child component renderer.
|
||||
// Therefore, we need to check if we already removed the element.
|
||||
if (delegate && delegate.parentNode(element)) {
|
||||
delegate.removeChild(element.parentNode, element);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
createRenderer(hostElement: any, type: RendererType2): Renderer2 {
|
||||
let delegate = this.delegate.createRenderer(hostElement, type);
|
||||
if (!hostElement || !type || !type.data || !type.data['animation']) return delegate;
|
||||
|
||||
const namespaceId = type.id;
|
||||
const componentId = type.id;
|
||||
const namespaceId = type.id + '-' + this._currentId;
|
||||
this._currentId++;
|
||||
|
||||
const animationTriggers = type.data['animation'] as AnimationTriggerMetadata[];
|
||||
animationTriggers.forEach(
|
||||
trigger => this._engine.registerTrigger(trigger, namespaceify(namespaceId, trigger.name)));
|
||||
trigger => this._engine.registerTrigger(
|
||||
componentId, namespaceId, hostElement, trigger.name, trigger));
|
||||
return new AnimationRenderer(delegate, this._engine, this._zone, namespaceId);
|
||||
}
|
||||
|
||||
|
@ -31,7 +46,9 @@ export class AnimationRendererFactory implements RendererFactory2 {
|
|||
this.delegate.begin();
|
||||
}
|
||||
}
|
||||
|
||||
end() {
|
||||
this._zone.runOutsideAngular(() => this._engine.flush());
|
||||
if (this.delegate.end) {
|
||||
this.delegate.end();
|
||||
}
|
||||
|
@ -40,7 +57,7 @@ export class AnimationRendererFactory implements RendererFactory2 {
|
|||
|
||||
export class AnimationRenderer implements Renderer2 {
|
||||
public destroyNode: ((node: any) => any)|null = null;
|
||||
private _flushPromise: Promise<any>|null = null;
|
||||
private _animationCallbacksBuffer: [(e: any) => any, any][] = [];
|
||||
|
||||
constructor(
|
||||
public delegate: Renderer2, private _engine: AnimationEngine, private _zone: NgZone,
|
||||
|
@ -50,7 +67,10 @@ export class AnimationRenderer implements Renderer2 {
|
|||
|
||||
get data() { return this.delegate.data; }
|
||||
|
||||
destroy(): void { this.delegate.destroy(); }
|
||||
destroy(): void {
|
||||
this._engine.destroy(this._namespaceId, this.delegate);
|
||||
this.delegate.destroy();
|
||||
}
|
||||
|
||||
createElement(name: string, namespace?: string): any {
|
||||
return this.delegate.createElement(name, namespace);
|
||||
|
@ -91,32 +111,23 @@ export class AnimationRenderer implements Renderer2 {
|
|||
setValue(node: any, value: string): void { this.delegate.setValue(node, value); }
|
||||
|
||||
appendChild(parent: any, newChild: any): void {
|
||||
this._engine.onInsert(newChild, () => this.delegate.appendChild(parent, newChild));
|
||||
this._queueFlush();
|
||||
this.delegate.appendChild(parent, newChild);
|
||||
this._engine.onInsert(this._namespaceId, newChild, parent, false);
|
||||
}
|
||||
|
||||
insertBefore(parent: any, newChild: any, refChild: any): void {
|
||||
this._engine.onInsert(newChild, () => this.delegate.insertBefore(parent, newChild, refChild));
|
||||
this._queueFlush();
|
||||
this.delegate.insertBefore(parent, newChild, refChild);
|
||||
this._engine.onInsert(this._namespaceId, newChild, parent, true);
|
||||
}
|
||||
|
||||
removeChild(parent: any, oldChild: any): void {
|
||||
this._engine.onRemove(oldChild, () => {
|
||||
// Note: if an component element has a leave animation, and the component
|
||||
// a host leave animation, the view engine will call `removeChild` for the parent
|
||||
// component renderer as well as for the child component renderer.
|
||||
// Therefore, we need to check if we already removed the element.
|
||||
if (this.delegate.parentNode(oldChild)) {
|
||||
this.delegate.removeChild(parent, oldChild);
|
||||
}
|
||||
});
|
||||
this._queueFlush();
|
||||
this._engine.onRemove(this._namespaceId, oldChild, this.delegate);
|
||||
}
|
||||
|
||||
setProperty(el: any, name: string, value: any): void {
|
||||
if (name.charAt(0) == '@') {
|
||||
this._engine.setProperty(el, namespaceify(this._namespaceId, name.substr(1)), value);
|
||||
this._queueFlush();
|
||||
name = name.substr(1);
|
||||
this._engine.setProperty(this._namespaceId, el, name, value);
|
||||
} else {
|
||||
this.delegate.setProperty(el, name, value);
|
||||
}
|
||||
|
@ -126,28 +137,32 @@ export class AnimationRenderer implements Renderer2 {
|
|||
() => void {
|
||||
if (eventName.charAt(0) == '@') {
|
||||
const element = resolveElementFromTarget(target);
|
||||
const [name, phase] = parseTriggerCallbackName(eventName.substr(1));
|
||||
return this._engine.listen(
|
||||
element, namespaceify(this._namespaceId, name), phase, (event: any) => {
|
||||
const e = event as any;
|
||||
if (e.triggerName) {
|
||||
e.triggerName = deNamespaceify(this._namespaceId, e.triggerName);
|
||||
}
|
||||
this._zone.run(() => callback(event));
|
||||
});
|
||||
let name = eventName.substr(1);
|
||||
let phase = '';
|
||||
if (name.charAt(0) != '@') { // transition-specific
|
||||
[name, phase] = parseTriggerCallbackName(name);
|
||||
}
|
||||
return this._engine.listen(this._namespaceId, element, name, phase, event => {
|
||||
this._bufferMicrotaskIntoZone(callback, event);
|
||||
});
|
||||
}
|
||||
return this.delegate.listen(target, eventName, callback);
|
||||
}
|
||||
|
||||
private _queueFlush() {
|
||||
if (!this._flushPromise) {
|
||||
this._zone.runOutsideAngular(() => {
|
||||
this._flushPromise = Promise.resolve(null).then(() => {
|
||||
this._flushPromise = null !;
|
||||
this._engine.flush();
|
||||
private _bufferMicrotaskIntoZone(fn: (e: any) => any, data: any) {
|
||||
if (this._animationCallbacksBuffer.length == 0) {
|
||||
Promise.resolve(null).then(() => {
|
||||
this._zone.run(() => {
|
||||
this._animationCallbacksBuffer.forEach(tuple => {
|
||||
const [fn, data] = tuple;
|
||||
fn(data);
|
||||
});
|
||||
this._animationCallbacksBuffer = [];
|
||||
});
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
this._animationCallbacksBuffer.push([fn, data]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -170,11 +185,3 @@ function parseTriggerCallbackName(triggerName: string) {
|
|||
const phase = triggerName.substr(dotIndex + 1);
|
||||
return [trigger, phase];
|
||||
}
|
||||
|
||||
function namespaceify(namespaceId: string, value: string): string {
|
||||
return `${namespaceId}#${value}`;
|
||||
}
|
||||
|
||||
function deNamespaceify(namespaceId: string, value: string): string {
|
||||
return value.replace(namespaceId + '#', '');
|
||||
}
|
||||
|
|
|
@ -12,4 +12,5 @@
|
|||
* Entry point for all animation APIs of the animation browser package.
|
||||
*/
|
||||
export {BrowserAnimationsModule, NoopAnimationsModule} from './module';
|
||||
|
||||
export * from './private_export';
|
||||
|
|
|
@ -5,4 +5,6 @@
|
|||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
export {NoopAnimation as ɵNoopAnimation, NoopAnimationBuilder as ɵNoopAnimationBuilder} from './animation_builder';
|
||||
export {BrowserAnimation as ɵBrowserAnimation, BrowserAnimationBuilder as ɵBrowserAnimationBuilder} from './animation_builder';
|
||||
export {AnimationRenderer as ɵAnimationRenderer, AnimationRendererFactory as ɵAnimationRendererFactory} from './animation_renderer';
|
||||
|
|
|
@ -6,10 +6,12 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AnimationDriver, ɵAnimationEngine as AnimationEngine, ɵAnimationStyleNormalizer as AnimationStyleNormalizer, ɵDomAnimationEngine as DomAnimationEngine, ɵNoopAnimationDriver as NoopAnimationDriver, ɵNoopAnimationEngine as NoopAnimationEngine, ɵWebAnimationsDriver as WebAnimationsDriver, ɵWebAnimationsStyleNormalizer as WebAnimationsStyleNormalizer, ɵsupportsWebAnimations as supportsWebAnimations} from '@angular/animations/browser';
|
||||
import {AnimationBuilder} from '@angular/animations';
|
||||
import {AnimationDriver, ɵAnimationEngine as AnimationEngine, ɵAnimationStyleNormalizer as AnimationStyleNormalizer, ɵDomAnimationEngine as DomAnimationEngine, ɵNoopAnimationDriver as NoopAnimationDriver, ɵNoopAnimationEngine as NoopAnimationEngine, ɵNoopAnimationStyleNormalizer as NoopAnimationStyleNormalizer, ɵWebAnimationsDriver as WebAnimationsDriver, ɵWebAnimationsStyleNormalizer as WebAnimationsStyleNormalizer, ɵsupportsWebAnimations as supportsWebAnimations} from '@angular/animations/browser';
|
||||
import {Injectable, NgZone, Provider, RendererFactory2} from '@angular/core';
|
||||
import {ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser';
|
||||
|
||||
import {BrowserAnimationBuilder, NoopAnimationBuilder} from './animation_builder';
|
||||
import {AnimationRendererFactory} from './animation_renderer';
|
||||
|
||||
@Injectable()
|
||||
|
@ -19,6 +21,13 @@ export class InjectableAnimationEngine extends DomAnimationEngine {
|
|||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class InjectableNoopAnimationEngine extends NoopAnimationEngine {
|
||||
constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) {
|
||||
super(driver, normalizer);
|
||||
}
|
||||
}
|
||||
|
||||
export function instantiateSupportedAnimationDriver() {
|
||||
if (supportsWebAnimations()) {
|
||||
return new WebAnimationsDriver();
|
||||
|
@ -40,6 +49,7 @@ export function instantiateRendererFactory(
|
|||
* include them in the BrowserModule.
|
||||
*/
|
||||
export const BROWSER_ANIMATIONS_PROVIDERS: Provider[] = [
|
||||
{provide: AnimationBuilder, useClass: NoopAnimationBuilder},
|
||||
{provide: AnimationDriver, useFactory: instantiateSupportedAnimationDriver},
|
||||
{provide: AnimationStyleNormalizer, useFactory: instantiateDefaultStyleNormalizer},
|
||||
{provide: AnimationEngine, useClass: InjectableAnimationEngine}, {
|
||||
|
@ -54,7 +64,14 @@ export const BROWSER_ANIMATIONS_PROVIDERS: Provider[] = [
|
|||
* include them in the BrowserTestingModule.
|
||||
*/
|
||||
export const BROWSER_NOOP_ANIMATIONS_PROVIDERS: Provider[] = [
|
||||
{provide: AnimationEngine, useClass: NoopAnimationEngine}, {
|
||||
{provide: AnimationBuilder, useClass: BrowserAnimationBuilder},
|
||||
{provide: AnimationDriver, useClass: NoopAnimationDriver},
|
||||
{provide: AnimationStyleNormalizer, useFactory: instantiateDefaultStyleNormalizer}, {
|
||||
provide: AnimationEngine,
|
||||
useClass: NoopAnimationEngine,
|
||||
deps: [AnimationDriver, AnimationStyleNormalizer]
|
||||
},
|
||||
{
|
||||
provide: RendererFactory2,
|
||||
useFactory: instantiateRendererFactory,
|
||||
deps: [DomRendererFactory2, AnimationEngine, NgZone]
|
||||
|
|
|
@ -5,10 +5,19 @@
|
|||
* 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 {state, style, trigger} from '@angular/animations';
|
||||
import {AnimationMetadata, AnimationTriggerMetadata, state, style, trigger} from '@angular/animations';
|
||||
import {ɵNoopAnimationEngine as NoopAnimationEngine} from '@angular/animations/browser';
|
||||
import {NoopAnimationStyleNormalizer} from '@angular/animations/browser/src/dsl/style_normalization/animation_style_normalizer';
|
||||
import {MockAnimationDriver} from '@angular/animations/browser/testing';
|
||||
import {el} from '@angular/platform-browser/testing/src/browser_util';
|
||||
|
||||
import {TriggerAst} from '../../../animations/browser/src/dsl/animation_ast';
|
||||
import {buildAnimationAst} from '../../../animations/browser/src/dsl/animation_ast_builder';
|
||||
import {buildTrigger} from '../../../animations/browser/src/dsl/animation_trigger';
|
||||
|
||||
const DEFAULT_NAMESPACE_ID = 'id';
|
||||
const DEFAULT_COMPONENT_ID = '1';
|
||||
|
||||
export function main() {
|
||||
describe('NoopAnimationEngine', () => {
|
||||
let captures: string[] = [];
|
||||
|
@ -16,19 +25,37 @@ export function main() {
|
|||
|
||||
beforeEach(() => { captures = []; });
|
||||
|
||||
function makeEngine() {
|
||||
const driver = new MockAnimationDriver();
|
||||
const normalizer = new NoopAnimationStyleNormalizer();
|
||||
return new NoopAnimationEngine(driver, normalizer);
|
||||
}
|
||||
|
||||
it('should immediately issue DOM removals during remove animations and then fire the animation callbacks after flush',
|
||||
() => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
const engine = makeEngine();
|
||||
const capture1 = capture('1');
|
||||
const capture2 = capture('2');
|
||||
engine.onRemovalComplete = (element: any, context: any) => {
|
||||
switch (context as string) {
|
||||
case '1':
|
||||
capture1();
|
||||
break;
|
||||
case '2':
|
||||
capture2();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const elm1 = {nodeType: 1};
|
||||
const elm2 = {nodeType: 1};
|
||||
engine.onRemove(elm1, capture('1'));
|
||||
engine.onRemove(elm2, capture('2'));
|
||||
engine.onRemove(DEFAULT_NAMESPACE_ID, elm1, '1');
|
||||
engine.onRemove(DEFAULT_NAMESPACE_ID, elm2, '2');
|
||||
|
||||
engine.listen(elm1, 'trig', 'start', capture('1-start'));
|
||||
engine.listen(elm2, 'trig', 'start', capture('2-start'));
|
||||
engine.listen(elm1, 'trig', 'done', capture('1-done'));
|
||||
engine.listen(elm2, 'trig', 'done', capture('2-done'));
|
||||
listen(elm1, engine, 'trig', 'start', capture('1-start'));
|
||||
listen(elm2, engine, 'trig', 'start', capture('2-start'));
|
||||
listen(elm1, engine, 'trig', 'done', capture('1-done'));
|
||||
listen(elm2, engine, 'trig', 'done', capture('2-done'));
|
||||
|
||||
expect(captures).toEqual(['1', '2']);
|
||||
engine.flush();
|
||||
|
@ -37,17 +64,17 @@ export function main() {
|
|||
});
|
||||
|
||||
it('should only fire the `start` listener for a trigger that has had a property change', () => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
const engine = makeEngine();
|
||||
|
||||
const elm1 = {};
|
||||
const elm2 = {};
|
||||
const elm3 = {};
|
||||
|
||||
engine.listen(elm1, 'trig1', 'start', capture());
|
||||
engine.setProperty(elm1, 'trig1', 'cool');
|
||||
engine.setProperty(elm2, 'trig2', 'sweet');
|
||||
engine.listen(elm2, 'trig2', 'start', capture());
|
||||
engine.listen(elm3, 'trig3', 'start', capture());
|
||||
listen(elm1, engine, 'trig1', 'start', capture());
|
||||
setProperty(elm1, engine, 'trig1', 'cool');
|
||||
setProperty(elm2, engine, 'trig2', 'sweet');
|
||||
listen(elm2, engine, 'trig2', 'start', capture());
|
||||
listen(elm3, engine, 'trig3', 'start', capture());
|
||||
|
||||
expect(captures).toEqual([]);
|
||||
engine.flush();
|
||||
|
@ -79,17 +106,17 @@ export function main() {
|
|||
});
|
||||
|
||||
it('should only fire the `done` listener for a trigger that has had a property change', () => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
const engine = makeEngine();
|
||||
|
||||
const elm1 = {};
|
||||
const elm2 = {};
|
||||
const elm3 = {};
|
||||
|
||||
engine.listen(elm1, 'trig1', 'done', capture());
|
||||
engine.setProperty(elm1, 'trig1', 'awesome');
|
||||
engine.setProperty(elm2, 'trig2', 'amazing');
|
||||
engine.listen(elm2, 'trig2', 'done', capture());
|
||||
engine.listen(elm3, 'trig3', 'done', capture());
|
||||
listen(elm1, engine, 'trig1', 'done', capture());
|
||||
setProperty(elm1, engine, 'trig1', 'awesome');
|
||||
setProperty(elm2, engine, 'trig2', 'amazing');
|
||||
listen(elm2, engine, 'trig2', 'done', capture());
|
||||
listen(elm3, engine, 'trig3', 'done', capture());
|
||||
|
||||
expect(captures).toEqual([]);
|
||||
engine.flush();
|
||||
|
@ -122,48 +149,49 @@ export function main() {
|
|||
|
||||
it('should deregister a listener when the return function is called, but only after flush',
|
||||
() => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
const engine = makeEngine();
|
||||
const elm = {};
|
||||
|
||||
const fn1 = engine.listen(elm, 'trig1', 'start', capture('trig1-start'));
|
||||
const fn2 = engine.listen(elm, 'trig2', 'done', capture('trig2-done'));
|
||||
const fn1 = listen(elm, engine, 'trig1', 'start', capture('trig1-start'));
|
||||
const fn2 = listen(elm, engine, 'trig2', 'done', capture('trig2-done'));
|
||||
|
||||
engine.setProperty(elm, 'trig1', 'value1');
|
||||
engine.setProperty(elm, 'trig2', 'value2');
|
||||
setProperty(elm, engine, 'trig1', 'value1');
|
||||
setProperty(elm, engine, 'trig2', 'value2');
|
||||
engine.flush();
|
||||
expect(captures).toEqual(['trig1-start', 'trig2-done']);
|
||||
|
||||
captures = [];
|
||||
engine.setProperty(elm, 'trig1', 'value3');
|
||||
engine.setProperty(elm, 'trig2', 'value4');
|
||||
setProperty(elm, engine, 'trig1', 'value3');
|
||||
setProperty(elm, engine, 'trig2', 'value4');
|
||||
|
||||
fn1();
|
||||
engine.flush();
|
||||
expect(captures).toEqual(['trig1-start', 'trig2-done']);
|
||||
|
||||
captures = [];
|
||||
engine.setProperty(elm, 'trig1', 'value5');
|
||||
engine.setProperty(elm, 'trig2', 'value6');
|
||||
setProperty(elm, engine, 'trig1', 'value5');
|
||||
setProperty(elm, engine, 'trig2', 'value6');
|
||||
|
||||
fn2();
|
||||
engine.flush();
|
||||
expect(captures).toEqual(['trig2-done']);
|
||||
|
||||
captures = [];
|
||||
engine.setProperty(elm, 'trig1', 'value7');
|
||||
engine.setProperty(elm, 'trig2', 'value8');
|
||||
setProperty(elm, engine, 'trig1', 'value7');
|
||||
setProperty(elm, engine, 'trig2', 'value8');
|
||||
engine.flush();
|
||||
expect(captures).toEqual([]);
|
||||
});
|
||||
|
||||
it('should fire a removal listener even if the listener is deregistered prior to flush', () => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
const engine = makeEngine();
|
||||
const elm = {nodeType: 1};
|
||||
engine.onRemovalComplete = (element: any, context: string) => { capture(context)(); };
|
||||
|
||||
const fn = engine.listen(elm, 'trig', 'start', capture('removal listener'));
|
||||
const fn = listen(elm, engine, 'trig', 'start', capture('removal listener'));
|
||||
fn();
|
||||
|
||||
engine.onRemove(elm, capture('dom removal'));
|
||||
engine.onRemove(DEFAULT_NAMESPACE_ID, elm, 'dom removal');
|
||||
engine.flush();
|
||||
|
||||
expect(captures).toEqual(['dom removal', 'removal listener']);
|
||||
|
@ -174,15 +202,15 @@ export function main() {
|
|||
if (typeof Element == 'undefined') return;
|
||||
|
||||
it('should persist the styles on the element when the animation is complete', () => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
engine.registerTrigger(trigger('matias', [
|
||||
state('a', style({width: '100px'})),
|
||||
]));
|
||||
|
||||
const engine = makeEngine();
|
||||
const element = el('<div></div>');
|
||||
registerTrigger(element, engine, trigger('matias', [
|
||||
state('a', style({width: '100px'})),
|
||||
]));
|
||||
|
||||
expect(element.style.width).not.toEqual('100px');
|
||||
|
||||
engine.setProperty(element, 'matias', 'a');
|
||||
setProperty(element, engine, 'matias', 'a');
|
||||
expect(element.style.width).not.toEqual('100px');
|
||||
|
||||
engine.flush();
|
||||
|
@ -191,19 +219,19 @@ export function main() {
|
|||
|
||||
it('should remove previously persist styles off of the element when a follow-up animation starts',
|
||||
() => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
engine.registerTrigger(trigger('matias', [
|
||||
state('a', style({width: '100px'})),
|
||||
state('b', style({height: '100px'})),
|
||||
]));
|
||||
|
||||
const engine = makeEngine();
|
||||
const element = el('<div></div>');
|
||||
|
||||
engine.setProperty(element, 'matias', 'a');
|
||||
registerTrigger(element, engine, trigger('matias', [
|
||||
state('a', style({width: '100px'})),
|
||||
state('b', style({height: '100px'})),
|
||||
]));
|
||||
|
||||
setProperty(element, engine, 'matias', 'a');
|
||||
engine.flush();
|
||||
expect(element.style.width).toEqual('100px');
|
||||
|
||||
engine.setProperty(element, 'matias', 'b');
|
||||
setProperty(element, engine, 'matias', 'b');
|
||||
expect(element.style.width).not.toEqual('100px');
|
||||
expect(element.style.height).not.toEqual('100px');
|
||||
|
||||
|
@ -212,17 +240,35 @@ export function main() {
|
|||
});
|
||||
|
||||
it('should fall back to `*` styles incase the target state styles are not found', () => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
engine.registerTrigger(trigger('matias', [
|
||||
state('*', style({opacity: '0.5'})),
|
||||
]));
|
||||
|
||||
const engine = makeEngine();
|
||||
const element = el('<div></div>');
|
||||
|
||||
engine.setProperty(element, 'matias', 'xyz');
|
||||
registerTrigger(element, engine, trigger('matias', [
|
||||
state('*', style({opacity: '0.5'})),
|
||||
]));
|
||||
|
||||
setProperty(element, engine, 'matias', 'xyz');
|
||||
engine.flush();
|
||||
expect(element.style.opacity).toEqual('0.5');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function registerTrigger(
|
||||
element: any, engine: NoopAnimationEngine, metadata: AnimationTriggerMetadata,
|
||||
namespaceId: string = DEFAULT_NAMESPACE_ID, componentId: string = DEFAULT_COMPONENT_ID) {
|
||||
engine.registerTrigger(componentId, namespaceId, element, name, metadata)
|
||||
}
|
||||
|
||||
function setProperty(
|
||||
element: any, engine: NoopAnimationEngine, property: string, value: any,
|
||||
id: string = DEFAULT_NAMESPACE_ID) {
|
||||
engine.setProperty(id, element, property, value);
|
||||
}
|
||||
|
||||
function listen(
|
||||
element: any, engine: NoopAnimationEngine, eventName: string, phaseName: string,
|
||||
callback: (event: any) => any, id: string = DEFAULT_NAMESPACE_ID) {
|
||||
return engine.listen(id, element, eventName, phaseName, callback);
|
||||
}
|
||||
|
|
|
@ -6,22 +6,23 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {AnimationPlayer, AnimationTriggerMetadata, animate, state, style, transition, trigger} from '@angular/animations';
|
||||
import {ɵAnimationEngine} from '@angular/animations/browser';
|
||||
import {Component, Injectable, RendererFactory2, RendererType2, ViewChild} from '@angular/core';
|
||||
import {ɵAnimationEngine as AnimationEngine} from '@angular/animations/browser';
|
||||
import {Component, Injectable, NgZone, RendererFactory2, RendererType2, ViewChild} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {BrowserAnimationsModule, ɵAnimationRendererFactory} from '@angular/platform-browser/animations';
|
||||
import {BrowserAnimationsModule, ɵAnimationRendererFactory as AnimationRendererFactory} from '@angular/platform-browser/animations';
|
||||
import {DomRendererFactory2} from '@angular/platform-browser/src/dom/dom_renderer';
|
||||
|
||||
import {InjectableAnimationEngine} from '../../animations/src/providers';
|
||||
import {el} from '../../testing/src/browser_util';
|
||||
|
||||
export function main() {
|
||||
describe('ɵAnimationRenderer', () => {
|
||||
describe('AnimationRenderer', () => {
|
||||
let element: any;
|
||||
beforeEach(() => {
|
||||
element = el('<div></div>');
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{provide: ɵAnimationEngine, useClass: MockAnimationEngine}],
|
||||
providers: [{provide: AnimationEngine, useClass: MockAnimationEngine}],
|
||||
imports: [BrowserAnimationsModule]
|
||||
});
|
||||
});
|
||||
|
@ -33,20 +34,13 @@ export function main() {
|
|||
styles: [],
|
||||
data: {'animation': animationTriggers}
|
||||
};
|
||||
return (TestBed.get(RendererFactory2) as ɵAnimationRendererFactory)
|
||||
return (TestBed.get(RendererFactory2) as AnimationRendererFactory)
|
||||
.createRenderer(element, type);
|
||||
}
|
||||
|
||||
it('should register the provided triggers with the view engine when created', () => {
|
||||
const renderer = makeRenderer([trigger('trig1', []), trigger('trig2', [])]);
|
||||
|
||||
const engine = TestBed.get(ɵAnimationEngine) as MockAnimationEngine;
|
||||
expect(engine.triggers.map(t => t.name)).toEqual(['trig1', 'trig2']);
|
||||
});
|
||||
|
||||
it('should hook into the engine\'s insert operations when appending children', () => {
|
||||
const renderer = makeRenderer();
|
||||
const engine = TestBed.get(ɵAnimationEngine) as MockAnimationEngine;
|
||||
const engine = TestBed.get(AnimationEngine) as MockAnimationEngine;
|
||||
const container = el('<div></div>');
|
||||
|
||||
renderer.appendChild(container, element);
|
||||
|
@ -56,7 +50,7 @@ export function main() {
|
|||
it('should hook into the engine\'s insert operations when inserting a child before another',
|
||||
() => {
|
||||
const renderer = makeRenderer();
|
||||
const engine = TestBed.get(ɵAnimationEngine) as MockAnimationEngine;
|
||||
const engine = TestBed.get(AnimationEngine) as MockAnimationEngine;
|
||||
const container = el('<div></div>');
|
||||
const element2 = el('<div></div>');
|
||||
container.appendChild(element2);
|
||||
|
@ -67,7 +61,7 @@ export function main() {
|
|||
|
||||
it('should hook into the engine\'s insert operations when removing children', () => {
|
||||
const renderer = makeRenderer();
|
||||
const engine = TestBed.get(ɵAnimationEngine) as MockAnimationEngine;
|
||||
const engine = TestBed.get(AnimationEngine) as MockAnimationEngine;
|
||||
const container = el('<div></div>');
|
||||
|
||||
renderer.removeChild(container, element);
|
||||
|
@ -76,19 +70,19 @@ export function main() {
|
|||
|
||||
it('should hook into the engine\'s setProperty call if the property begins with `@`', () => {
|
||||
const renderer = makeRenderer();
|
||||
const engine = TestBed.get(ɵAnimationEngine) as MockAnimationEngine;
|
||||
const engine = TestBed.get(AnimationEngine) as MockAnimationEngine;
|
||||
|
||||
renderer.setProperty(element, 'prop', 'value');
|
||||
expect(engine.captures['setProperty']).toBeFalsy();
|
||||
|
||||
renderer.setProperty(element, '@prop', 'value');
|
||||
expect(engine.captures['setProperty'].pop()).toEqual([element, 'id#prop', 'value']);
|
||||
expect(engine.captures['setProperty'].pop()).toEqual([element, 'prop', 'value']);
|
||||
});
|
||||
|
||||
describe('listen', () => {
|
||||
it('should hook into the engine\'s listen call if the property begins with `@`', () => {
|
||||
const renderer = makeRenderer();
|
||||
const engine = TestBed.get(ɵAnimationEngine) as MockAnimationEngine;
|
||||
const engine = TestBed.get(AnimationEngine) as MockAnimationEngine;
|
||||
|
||||
const cb = (event: any): boolean => { return true; };
|
||||
|
||||
|
@ -96,13 +90,13 @@ export function main() {
|
|||
expect(engine.captures['listen']).toBeFalsy();
|
||||
|
||||
renderer.listen(element, '@event.phase', cb);
|
||||
expect(engine.captures['listen'].pop()).toEqual([element, 'id#event', 'phase']);
|
||||
expect(engine.captures['listen'].pop()).toEqual([element, 'event', 'phase']);
|
||||
});
|
||||
|
||||
it('should resolve the body|document|window nodes given their values as strings as input',
|
||||
() => {
|
||||
const renderer = makeRenderer();
|
||||
const engine = TestBed.get(ɵAnimationEngine) as MockAnimationEngine;
|
||||
const engine = TestBed.get(AnimationEngine) as MockAnimationEngine;
|
||||
|
||||
const cb = (event: any): boolean => { return true; };
|
||||
|
||||
|
@ -117,6 +111,10 @@ export function main() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('registering animations', () => {
|
||||
it('should only create a trigger definition once even if the registered multiple times');
|
||||
});
|
||||
|
||||
describe('flushing animations', () => {
|
||||
// these tests are only mean't to be run within the DOM
|
||||
if (typeof Element == 'undefined') return;
|
||||
|
@ -138,11 +136,11 @@ export function main() {
|
|||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{provide: ɵAnimationEngine, useClass: InjectableAnimationEngine}],
|
||||
providers: [{provide: AnimationEngine, useClass: InjectableAnimationEngine}],
|
||||
declarations: [Cmp]
|
||||
});
|
||||
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const engine = TestBed.get(AnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
cmp.exp = 'state';
|
||||
|
@ -174,7 +172,7 @@ export function main() {
|
|||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{provide: ɵAnimationEngine, useClass: InjectableAnimationEngine}],
|
||||
providers: [{provide: AnimationEngine, useClass: InjectableAnimationEngine}],
|
||||
declarations: [Cmp]
|
||||
});
|
||||
|
||||
|
@ -223,11 +221,11 @@ export function main() {
|
|||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{provide: ɵAnimationEngine, useClass: InjectableAnimationEngine}],
|
||||
providers: [{provide: AnimationEngine, useClass: InjectableAnimationEngine}],
|
||||
declarations: [Cmp]
|
||||
});
|
||||
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const engine = TestBed.get(AnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
|
||||
|
@ -239,7 +237,7 @@ export function main() {
|
|||
assertHasParent(elm2);
|
||||
assertHasParent(elm3);
|
||||
engine.flush();
|
||||
finishPlayers(engine.activePlayers);
|
||||
finishPlayers(engine.players);
|
||||
|
||||
cmp.exp1 = false;
|
||||
fixture.detectChanges();
|
||||
|
@ -247,7 +245,7 @@ export function main() {
|
|||
assertHasParent(elm2);
|
||||
assertHasParent(elm3);
|
||||
engine.flush();
|
||||
expect(engine.activePlayers.length).toEqual(0);
|
||||
expect(engine.players.length).toEqual(0);
|
||||
|
||||
cmp.exp2 = false;
|
||||
fixture.detectChanges();
|
||||
|
@ -255,7 +253,7 @@ export function main() {
|
|||
assertHasParent(elm2, false);
|
||||
assertHasParent(elm3);
|
||||
engine.flush();
|
||||
expect(engine.activePlayers.length).toEqual(0);
|
||||
expect(engine.players.length).toEqual(0);
|
||||
|
||||
cmp.exp3 = false;
|
||||
fixture.detectChanges();
|
||||
|
@ -263,14 +261,57 @@ export function main() {
|
|||
assertHasParent(elm2, false);
|
||||
assertHasParent(elm3);
|
||||
engine.flush();
|
||||
expect(engine.activePlayers.length).toEqual(1);
|
||||
expect(engine.players.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AnimationRendererFactory', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{
|
||||
provide: RendererFactory2,
|
||||
useClass: ExtendedAnimationRendererFactory,
|
||||
deps: [DomRendererFactory2, AnimationEngine, NgZone]
|
||||
}],
|
||||
imports: [BrowserAnimationsModule]
|
||||
});
|
||||
});
|
||||
|
||||
it('should provide hooks at the start and end of change detection', () => {
|
||||
@Component({
|
||||
selector: 'my-cmp',
|
||||
template: `
|
||||
<div [@myAnimation]="exp"></div>
|
||||
`,
|
||||
animations: [trigger('myAnimation', [])]
|
||||
})
|
||||
class Cmp {
|
||||
public exp: any;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{provide: AnimationEngine, useClass: InjectableAnimationEngine}],
|
||||
declarations: [Cmp]
|
||||
});
|
||||
|
||||
const renderer = TestBed.get(RendererFactory2) as ExtendedAnimationRendererFactory;
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
|
||||
renderer.log = [];
|
||||
fixture.detectChanges();
|
||||
expect(renderer.log).toEqual(['begin', 'end']);
|
||||
|
||||
renderer.log = [];
|
||||
fixture.detectChanges();
|
||||
expect(renderer.log).toEqual(['begin', 'end']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class MockAnimationEngine extends ɵAnimationEngine {
|
||||
class MockAnimationEngine extends AnimationEngine {
|
||||
captures: {[method: string]: any[]} = {};
|
||||
triggers: AnimationTriggerMetadata[] = [];
|
||||
|
||||
|
@ -279,24 +320,46 @@ class MockAnimationEngine extends ɵAnimationEngine {
|
|||
data.push(args);
|
||||
}
|
||||
|
||||
registerTrigger(trigger: AnimationTriggerMetadata) { this.triggers.push(trigger); }
|
||||
registerTrigger(componentId: string, namespaceId: string, trigger: AnimationTriggerMetadata) {
|
||||
this.triggers.push(trigger);
|
||||
}
|
||||
|
||||
onInsert(element: any, domFn: () => any): void { this._capture('onInsert', [element]); }
|
||||
onInsert(namespaceId: string, element: any): void { this._capture('onInsert', [element]); }
|
||||
|
||||
onRemove(element: any, domFn: () => any): void { this._capture('onRemove', [element]); }
|
||||
onRemove(namespaceId: string, element: any, domFn: () => any): void {
|
||||
this._capture('onRemove', [element]);
|
||||
}
|
||||
|
||||
setProperty(element: any, property: string, value: any): void {
|
||||
setProperty(namespaceId: string, element: any, property: string, value: any): void {
|
||||
this._capture('setProperty', [element, property, value]);
|
||||
}
|
||||
|
||||
listen(element: any, eventName: string, eventPhase: string, callback: (event: any) => any):
|
||||
() => void {
|
||||
listen(
|
||||
namespaceId: string, element: any, eventName: string, eventPhase: string,
|
||||
callback: (event: any) => any): () => void {
|
||||
// we don't capture the callback here since the renderer wraps it in a zone
|
||||
this._capture('listen', [element, eventName, eventPhase]);
|
||||
return () => {};
|
||||
}
|
||||
|
||||
flush() {}
|
||||
|
||||
destroy(namespaceId: string) {}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class ExtendedAnimationRendererFactory extends AnimationRendererFactory {
|
||||
public log: string[] = [];
|
||||
|
||||
begin() {
|
||||
super.begin();
|
||||
this.log.push('begin');
|
||||
}
|
||||
|
||||
end() {
|
||||
super.end();
|
||||
this.log.push('end');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* @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 {AnimationBuilder, animate, style} from '@angular/animations';
|
||||
import {AnimationDriver} from '@angular/animations/browser';
|
||||
import {MockAnimationDriver} from '@angular/animations/browser/testing';
|
||||
import {Component, ViewChild} from '@angular/core';
|
||||
import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing';
|
||||
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||
|
||||
import {BrowserAnimationBuilder} from '../../animations/src/animation_builder';
|
||||
import {el} from '../../testing/src/browser_util';
|
||||
|
||||
export function main() {
|
||||
describe('BrowserAnimationBuilder', () => {
|
||||
let element: any;
|
||||
beforeEach(() => {
|
||||
element = el('<div></div>');
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule],
|
||||
providers: [{provide: AnimationDriver, useClass: MockAnimationDriver}]
|
||||
});
|
||||
});
|
||||
|
||||
it('should inject AnimationBuilder into a component', () => {
|
||||
@Component({
|
||||
selector: 'ani-cmp',
|
||||
template: '...',
|
||||
})
|
||||
class Cmp {
|
||||
constructor(public builder: AnimationBuilder) {}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(cmp.builder instanceof BrowserAnimationBuilder).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should listen on start and done on the animation builder\'s player', fakeAsync(() => {
|
||||
@Component({
|
||||
selector: 'ani-cmp',
|
||||
template: '...',
|
||||
})
|
||||
class Cmp {
|
||||
@ViewChild('target') public target: any;
|
||||
|
||||
constructor(public builder: AnimationBuilder) {}
|
||||
|
||||
build() {
|
||||
const definition =
|
||||
this.builder.build([style({opacity: 0}), animate(1000, style({opacity: 1}))]);
|
||||
|
||||
return definition.create(this.target);
|
||||
}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
const player = cmp.build();
|
||||
|
||||
let started = false;
|
||||
player.onStart(() => started = true);
|
||||
|
||||
let finished = false;
|
||||
player.onDone(() => finished = true);
|
||||
|
||||
let destroyed = false;
|
||||
player.onDestroy(() => destroyed = true);
|
||||
|
||||
player.init();
|
||||
flushMicrotasks();
|
||||
expect(started).toBeFalsy();
|
||||
expect(finished).toBeFalsy();
|
||||
expect(destroyed).toBeFalsy();
|
||||
|
||||
player.play();
|
||||
flushMicrotasks();
|
||||
expect(started).toBeTruthy();
|
||||
expect(finished).toBeFalsy();
|
||||
expect(destroyed).toBeFalsy();
|
||||
|
||||
player.finish();
|
||||
flushMicrotasks();
|
||||
expect(started).toBeTruthy();
|
||||
expect(finished).toBeTruthy();
|
||||
expect(destroyed).toBeFalsy();
|
||||
|
||||
player.destroy();
|
||||
flushMicrotasks();
|
||||
expect(started).toBeTruthy();
|
||||
expect(finished).toBeTruthy();
|
||||
expect(destroyed).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
/** @experimental */
|
||||
export declare function animate(timings: string | number, styles?: AnimationStyleMetadata | AnimationKeyframesSequenceMetadata | null): AnimationAnimateMetadata;
|
||||
|
||||
/** @experimental */
|
||||
export declare function animateChild(options?: AnimationOptions | null): AnimationAnimateChildMetadata;
|
||||
|
||||
/** @experimental */
|
||||
export declare type AnimateTimings = {
|
||||
duration: number;
|
||||
|
@ -8,12 +11,36 @@ export declare type AnimateTimings = {
|
|||
easing: string | null;
|
||||
};
|
||||
|
||||
/** @experimental */
|
||||
export declare function animation(steps: AnimationMetadata | AnimationMetadata[], options?: AnimationOptions | null): AnimationReferenceMetadata;
|
||||
|
||||
/** @experimental */
|
||||
export declare abstract class Animation {
|
||||
abstract create(element: any, options?: AnimationOptions): AnimationPlayer;
|
||||
}
|
||||
|
||||
/** @experimental */
|
||||
export interface AnimationAnimateChildMetadata extends AnimationMetadata {
|
||||
options: AnimationOptions | null;
|
||||
}
|
||||
|
||||
/** @experimental */
|
||||
export interface AnimationAnimateMetadata extends AnimationMetadata {
|
||||
styles: AnimationStyleMetadata | AnimationKeyframesSequenceMetadata | null;
|
||||
timings: string | number | AnimateTimings;
|
||||
}
|
||||
|
||||
/** @experimental */
|
||||
export interface AnimationAnimateRefMetadata extends AnimationMetadata {
|
||||
animation: AnimationReferenceMetadata;
|
||||
options: AnimationOptions | null;
|
||||
}
|
||||
|
||||
/** @experimental */
|
||||
export declare abstract class AnimationBuilder {
|
||||
abstract build(animation: AnimationMetadata | AnimationMetadata[]): Animation;
|
||||
}
|
||||
|
||||
/** @experimental */
|
||||
export interface AnimationEvent {
|
||||
element: any;
|
||||
|
@ -26,6 +53,7 @@ export interface AnimationEvent {
|
|||
|
||||
/** @experimental */
|
||||
export interface AnimationGroupMetadata extends AnimationMetadata {
|
||||
options: AnimationOptions | null;
|
||||
steps: AnimationMetadata[];
|
||||
}
|
||||
|
||||
|
@ -46,13 +74,30 @@ export declare const enum AnimationMetadataType {
|
|||
Sequence = 2,
|
||||
Group = 3,
|
||||
Animate = 4,
|
||||
KeyframeSequence = 5,
|
||||
Keyframes = 5,
|
||||
Style = 6,
|
||||
Trigger = 7,
|
||||
Reference = 8,
|
||||
AnimateChild = 9,
|
||||
AnimateRef = 10,
|
||||
Query = 11,
|
||||
Stagger = 12,
|
||||
}
|
||||
|
||||
/** @experimental */
|
||||
export interface AnimationOptions {
|
||||
delay?: number | string;
|
||||
duration?: number | string;
|
||||
params?: {
|
||||
[name: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
/** @experimental */
|
||||
export declare abstract class AnimationPlayer {
|
||||
beforeDestroy?: () => any;
|
||||
parentPlayer: AnimationPlayer | null;
|
||||
readonly totalTime: number;
|
||||
abstract destroy(): void;
|
||||
abstract finish(): void;
|
||||
abstract getPosition(): number;
|
||||
|
@ -68,11 +113,37 @@ export declare abstract class AnimationPlayer {
|
|||
abstract setPosition(p: any): void;
|
||||
}
|
||||
|
||||
/** @experimental */
|
||||
export interface AnimationQueryMetadata extends AnimationMetadata {
|
||||
animation: AnimationMetadata | AnimationMetadata[];
|
||||
options: AnimationQueryOptions | null;
|
||||
selector: string;
|
||||
}
|
||||
|
||||
/** @experimental */
|
||||
export interface AnimationQueryOptions extends AnimationOptions {
|
||||
limit?: number;
|
||||
optional?: boolean;
|
||||
}
|
||||
|
||||
/** @experimental */
|
||||
export interface AnimationReferenceMetadata extends AnimationMetadata {
|
||||
animation: AnimationMetadata | AnimationMetadata[];
|
||||
options: AnimationOptions | null;
|
||||
}
|
||||
|
||||
/** @experimental */
|
||||
export interface AnimationSequenceMetadata extends AnimationMetadata {
|
||||
options: AnimationOptions | null;
|
||||
steps: AnimationMetadata[];
|
||||
}
|
||||
|
||||
/** @experimental */
|
||||
export interface AnimationStaggerMetadata extends AnimationMetadata {
|
||||
animation: AnimationMetadata | AnimationMetadata[];
|
||||
timings: string | number;
|
||||
}
|
||||
|
||||
/** @experimental */
|
||||
export interface AnimationStateMetadata extends AnimationMetadata {
|
||||
name: string;
|
||||
|
@ -81,31 +152,37 @@ export interface AnimationStateMetadata extends AnimationMetadata {
|
|||
|
||||
/** @experimental */
|
||||
export interface AnimationStyleMetadata extends AnimationMetadata {
|
||||
offset?: number;
|
||||
styles: {
|
||||
offset: number | null;
|
||||
styles: '*' | {
|
||||
[key: string]: string | number;
|
||||
} | {
|
||||
} | Array<{
|
||||
[key: string]: string | number;
|
||||
}[];
|
||||
} | '*'>;
|
||||
}
|
||||
|
||||
/** @experimental */
|
||||
export interface AnimationTransitionMetadata extends AnimationMetadata {
|
||||
animation: AnimationMetadata | AnimationMetadata[];
|
||||
expr: string | ((fromState: string, toState: string) => boolean);
|
||||
expr: string;
|
||||
options: AnimationOptions | null;
|
||||
}
|
||||
|
||||
/** @experimental */
|
||||
export interface AnimationTriggerMetadata {
|
||||
export interface AnimationTriggerMetadata extends AnimationMetadata {
|
||||
definitions: AnimationMetadata[];
|
||||
name: string;
|
||||
options: {
|
||||
params?: {
|
||||
[name: string]: any;
|
||||
};
|
||||
} | null;
|
||||
}
|
||||
|
||||
/** @experimental */
|
||||
export declare const AUTO_STYLE = "*";
|
||||
|
||||
/** @experimental */
|
||||
export declare function group(steps: AnimationMetadata[]): AnimationGroupMetadata;
|
||||
export declare function group(steps: AnimationMetadata[], options?: AnimationOptions | null): AnimationGroupMetadata;
|
||||
|
||||
/** @experimental */
|
||||
export declare function keyframes(steps: AnimationStyleMetadata[]): AnimationKeyframesSequenceMetadata;
|
||||
|
@ -113,6 +190,7 @@ export declare function keyframes(steps: AnimationStyleMetadata[]): AnimationKey
|
|||
/** @experimental */
|
||||
export declare class NoopAnimationPlayer implements AnimationPlayer {
|
||||
parentPlayer: AnimationPlayer | null;
|
||||
totalTime: number;
|
||||
constructor();
|
||||
destroy(): void;
|
||||
finish(): void;
|
||||
|
@ -130,20 +208,29 @@ export declare class NoopAnimationPlayer implements AnimationPlayer {
|
|||
}
|
||||
|
||||
/** @experimental */
|
||||
export declare function sequence(steps: AnimationMetadata[]): AnimationSequenceMetadata;
|
||||
export declare function query(selector: string, animation: AnimationMetadata | AnimationMetadata[], options?: AnimationQueryOptions | null): AnimationQueryMetadata;
|
||||
|
||||
/** @experimental */
|
||||
export declare function sequence(steps: AnimationMetadata[], options?: AnimationOptions | null): AnimationSequenceMetadata;
|
||||
|
||||
/** @experimental */
|
||||
export declare function stagger(timings: string | number, animation: AnimationMetadata | AnimationMetadata[]): AnimationStaggerMetadata;
|
||||
|
||||
/** @experimental */
|
||||
export declare function state(name: string, styles: AnimationStyleMetadata): AnimationStateMetadata;
|
||||
|
||||
/** @experimental */
|
||||
export declare function style(tokens: {
|
||||
export declare function style(tokens: '*' | {
|
||||
[key: string]: string | number;
|
||||
} | Array<{
|
||||
} | Array<'*' | {
|
||||
[key: string]: string | number;
|
||||
}>): AnimationStyleMetadata;
|
||||
|
||||
/** @experimental */
|
||||
export declare function transition(stateChangeExpr: string | ((fromState: string, toState: string) => boolean), steps: AnimationMetadata | AnimationMetadata[]): AnimationTransitionMetadata;
|
||||
export declare function transition(stateChangeExpr: string, steps: AnimationMetadata | AnimationMetadata[], options?: AnimationOptions | null): AnimationTransitionMetadata;
|
||||
|
||||
/** @experimental */
|
||||
export declare function trigger(name: string, definitions: AnimationMetadata[]): AnimationTriggerMetadata;
|
||||
|
||||
/** @experimental */
|
||||
export declare function useAnimation(animation: AnimationReferenceMetadata, options?: AnimationOptions | null): AnimationAnimateRefMetadata;
|
||||
|
|
|
@ -3,5 +3,6 @@ export declare abstract class AnimationDriver {
|
|||
abstract animate(element: any, keyframes: {
|
||||
[key: string]: string | number;
|
||||
}[], duration: number, delay: number, easing?: string | null, previousPlayers?: any[]): any;
|
||||
abstract computeStyle(element: any, prop: string, defaultValue?: string): string;
|
||||
static NOOP: AnimationDriver;
|
||||
}
|
||||
|
|
|
@ -3,11 +3,13 @@ export declare class MockAnimationDriver implements AnimationDriver {
|
|||
animate(element: any, keyframes: {
|
||||
[key: string]: string | number;
|
||||
}[], duration: number, delay: number, easing: string, previousPlayers?: any[]): MockAnimationPlayer;
|
||||
computeStyle(element: any, prop: string, defaultValue?: string): string;
|
||||
static log: AnimationPlayer[];
|
||||
}
|
||||
|
||||
/** @experimental */
|
||||
export declare class MockAnimationPlayer extends NoopAnimationPlayer {
|
||||
currentSnapshot: ɵStyleData;
|
||||
delay: number;
|
||||
duration: number;
|
||||
easing: string;
|
||||
|
@ -22,6 +24,9 @@ export declare class MockAnimationPlayer extends NoopAnimationPlayer {
|
|||
constructor(element: any, keyframes: {
|
||||
[key: string]: string | number;
|
||||
}[], duration: number, delay: number, easing: string, previousPlayers: any[]);
|
||||
beforeDestroy(): void;
|
||||
destroy(): void;
|
||||
finish(): void;
|
||||
hasStarted(): boolean;
|
||||
play(): void;
|
||||
}
|
||||
|
|
|
@ -70,12 +70,12 @@ export declare type AnimationStateTransitionMetadata = any;
|
|||
|
||||
/** @deprecated */
|
||||
export interface AnimationStyleMetadata extends AnimationMetadata {
|
||||
offset?: number;
|
||||
styles: {
|
||||
offset: number | null;
|
||||
styles: '*' | {
|
||||
[key: string]: string | number;
|
||||
} | {
|
||||
} | Array<{
|
||||
[key: string]: string | number;
|
||||
}[];
|
||||
} | '*'>;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
|
@ -1007,7 +1007,7 @@ export interface TrackByFunction<T> {
|
|||
}
|
||||
|
||||
/** @deprecated */
|
||||
export declare function transition(stateChangeExpr: string | ((fromState: string, toState: string) => boolean), steps: AnimationMetadata | AnimationMetadata[]): AnimationTransitionMetadata;
|
||||
export declare function transition(stateChangeExpr: string, steps: AnimationMetadata | AnimationMetadata[]): AnimationTransitionMetadata;
|
||||
|
||||
/** @experimental */
|
||||
export declare const TRANSLATIONS: InjectionToken<string>;
|
||||
|
|
|
@ -1,27 +1,7 @@
|
|||
/** @experimental */
|
||||
export declare class MockAnimationDriver implements AnimationDriver {
|
||||
animate(element: any, keyframes: {
|
||||
[key: string]: string | number;
|
||||
}[], duration: number, delay: number, easing: string, previousPlayers?: any[]): MockAnimationPlayer;
|
||||
static log: AnimationPlayer[];
|
||||
export declare class BrowserAnimationsTestingModule {
|
||||
}
|
||||
|
||||
/** @experimental */
|
||||
export declare class MockAnimationPlayer extends NoopAnimationPlayer {
|
||||
delay: number;
|
||||
duration: number;
|
||||
easing: string;
|
||||
element: any;
|
||||
keyframes: {
|
||||
[key: string]: string | number;
|
||||
}[];
|
||||
previousPlayers: any[];
|
||||
previousStyles: {
|
||||
[key: string]: string | number;
|
||||
};
|
||||
constructor(element: any, keyframes: {
|
||||
[key: string]: string | number;
|
||||
}[], duration: number, delay: number, easing: string, previousPlayers: any[]);
|
||||
destroy(): void;
|
||||
finish(): void;
|
||||
export declare class NoopAnimationsTestingModule {
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue