Revert "refactor(platform-browser): move platform-browser/animations to animations/browser (#15043)"
This reverts commit 195b863ea4
.
This commit is contained in:
@ -1,55 +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 {AnimationMetadata, AnimationPlayer, AnimationStyleMetadata, sequence, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {AnimationDriver} from '../render/animation_driver';
|
||||
import {DomAnimationEngine} from '../render/dom_animation_engine';
|
||||
import {normalizeStyles} from '../util';
|
||||
|
||||
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';
|
||||
|
||||
export class Animation {
|
||||
private _animationAst: AnimationMetadata;
|
||||
constructor(input: AnimationMetadata|AnimationMetadata[]) {
|
||||
const ast =
|
||||
Array.isArray(input) ? sequence(<AnimationMetadata[]>input) : <AnimationMetadata>input;
|
||||
const errors = validateAnimationSequence(ast);
|
||||
if (errors.length) {
|
||||
const errorMessage = `animation validation failed:\n${errors.join("\n")}`;
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
this._animationAst = ast;
|
||||
}
|
||||
|
||||
buildTimelines(
|
||||
startingStyles: ɵStyleData|ɵStyleData[],
|
||||
destinationStyles: ɵStyleData|ɵStyleData[]): 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);
|
||||
}
|
||||
}
|
@ -1,40 +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 {AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata} from '@angular/animations';
|
||||
|
||||
export interface AnimationDslVisitor {
|
||||
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;
|
||||
}
|
||||
|
||||
export function visitAnimationNode(
|
||||
visitor: AnimationDslVisitor, node: AnimationMetadata, context: any) {
|
||||
switch (node.type) {
|
||||
case AnimationMetadataType.State:
|
||||
return visitor.visitState(<AnimationStateMetadata>node, context);
|
||||
case AnimationMetadataType.Transition:
|
||||
return visitor.visitTransition(<AnimationTransitionMetadata>node, context);
|
||||
case AnimationMetadataType.Sequence:
|
||||
return visitor.visitSequence(<AnimationSequenceMetadata>node, context);
|
||||
case AnimationMetadataType.Group:
|
||||
return visitor.visitGroup(<AnimationGroupMetadata>node, context);
|
||||
case AnimationMetadataType.Animate:
|
||||
return visitor.visitAnimate(<AnimationAnimateMetadata>node, context);
|
||||
case AnimationMetadataType.KeyframeSequence:
|
||||
return visitor.visitKeyframeSequence(<AnimationKeyframesSequenceMetadata>node, context);
|
||||
case AnimationMetadataType.Style:
|
||||
return visitor.visitStyle(<AnimationStyleMetadata>node, context);
|
||||
default:
|
||||
throw new Error(`Unable to resolve animation metadata node #${node.type}`);
|
||||
}
|
||||
}
|
@ -1,29 +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 {ɵStyleData} from '@angular/animations';
|
||||
import {AnimationEngineInstruction, AnimationTransitionInstructionType} from '../render/animation_engine_instruction';
|
||||
|
||||
export interface AnimationTimelineInstruction extends AnimationEngineInstruction {
|
||||
keyframes: ɵStyleData[];
|
||||
duration: number;
|
||||
delay: number;
|
||||
totalTime: number;
|
||||
easing: string;
|
||||
}
|
||||
|
||||
export function createTimelineInstruction(
|
||||
keyframes: ɵStyleData[], duration: number, delay: number,
|
||||
easing: string): AnimationTimelineInstruction {
|
||||
return {
|
||||
type: AnimationTransitionInstructionType.TimelineAnimation,
|
||||
keyframes,
|
||||
duration,
|
||||
delay,
|
||||
totalTime: duration + delay, easing
|
||||
};
|
||||
}
|
@ -1,446 +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, ɵ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 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 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;
|
||||
previousNode: AnimationMetadata = <AnimationMetadata>{};
|
||||
subContextCount = 0;
|
||||
|
||||
constructor(
|
||||
public errors: any[], public timelines: TimelineBuilder[],
|
||||
initialTimeline: TimelineBuilder = null) {
|
||||
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);
|
||||
const normalizedFinalStyles = copyStyles(finalStyles, true);
|
||||
|
||||
// this is a special case for when animate(TIME) is used (without any styles)
|
||||
// thus indicating to create an animation arc between the final keyframe and
|
||||
// the destination styles. When this occurs we need to ensure that the styles
|
||||
// that are missing on the finalStyles map are set to AUTO
|
||||
if (Object.keys(context.currentTimeline.getFinalKeyframe()).length == 0) {
|
||||
context.currentTimeline.properties.forEach(prop => {
|
||||
const val = normalizedFinalStyles[prop];
|
||||
if (val == null) {
|
||||
normalizedFinalStyles[prop] = AUTO_STYLE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
context.currentTimeline.setStyles(normalizedFinalStyles);
|
||||
const timelineInstructions: AnimationTimelineInstruction[] = [];
|
||||
context.timelines.forEach(timeline => {
|
||||
// this checks to see if an actual animation happened
|
||||
if (timeline.hasStyling()) {
|
||||
timelineInstructions.push(timeline.buildKeyframes());
|
||||
}
|
||||
});
|
||||
|
||||
if (timelineInstructions.length == 0) {
|
||||
timelineInstructions.push(createTimelineInstruction([], 0, 0, ''));
|
||||
}
|
||||
return timelineInstructions;
|
||||
}
|
||||
|
||||
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 {
|
||||
context.incrementTime(timings.duration);
|
||||
if (astType == AnimationMetadataType.Style) {
|
||||
this.visitStyle(<AnimationStyleMetadata>ast.styles, 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;
|
||||
if (easing) {
|
||||
normalizedStyles['easing'] = easing;
|
||||
}
|
||||
|
||||
context.currentTimeline.setStyles(normalizedStyles);
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
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);
|
||||
innerTimeline.setStyles(normalizedStyles);
|
||||
});
|
||||
|
||||
// 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 = '';
|
||||
private _currentKeyframe: ɵStyleData;
|
||||
private _keyframes = new Map<number, ɵStyleData>();
|
||||
private _styleSummary: {[prop: string]: StyleAtTime} = {};
|
||||
private _localTimelineStyles: ɵStyleData;
|
||||
private _backFill: ɵStyleData = {};
|
||||
|
||||
constructor(public startTime: number, private _globalTimelineStyles: ɵStyleData = null) {
|
||||
this._localTimelineStyles = Object.create(this._backFill, {});
|
||||
if (!this._globalTimelineStyles) {
|
||||
this._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() {
|
||||
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) {
|
||||
if (prop != 'easing') {
|
||||
this._localTimelineStyles[prop] = value;
|
||||
this._globalTimelineStyles[prop] = value;
|
||||
this._styleSummary[prop] = {time: this.currentTime, value};
|
||||
}
|
||||
}
|
||||
|
||||
setStyles(styles: ɵStyleData) {
|
||||
Object.keys(styles).forEach(prop => {
|
||||
if (prop !== 'offset') {
|
||||
const val = styles[prop];
|
||||
this._currentKeyframe[prop] = val;
|
||||
if (prop !== 'easing' && !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() { 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;
|
||||
}
|
@ -1,64 +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
|
||||
*/
|
||||
export const ANY_STATE = '*';
|
||||
export declare type TransitionMatcherFn = (fromState: any, toState: any) => boolean;
|
||||
|
||||
export function parseTransitionExpr(
|
||||
transitionValue: string | TransitionMatcherFn, errors: string[]): TransitionMatcherFn[] {
|
||||
const expressions: TransitionMatcherFn[] = [];
|
||||
if (typeof transitionValue == 'string') {
|
||||
(<string>transitionValue)
|
||||
.split(/\s*,\s*/)
|
||||
.forEach(str => parseInnerTransitionStr(str, expressions, errors));
|
||||
} else {
|
||||
expressions.push(<TransitionMatcherFn>transitionValue);
|
||||
}
|
||||
return expressions;
|
||||
}
|
||||
|
||||
function parseInnerTransitionStr(
|
||||
eventStr: string, expressions: TransitionMatcherFn[], errors: string[]) {
|
||||
if (eventStr[0] == ':') {
|
||||
eventStr = parseAnimationAlias(eventStr, errors);
|
||||
}
|
||||
const match = eventStr.match(/^(\*|[-\w]+)\s*(<?[=-]>)\s*(\*|[-\w]+)$/);
|
||||
if (match == null || match.length < 4) {
|
||||
errors.push(`The provided transition expression "${eventStr}" is not supported`);
|
||||
return expressions;
|
||||
}
|
||||
|
||||
const fromState = match[1];
|
||||
const separator = match[2];
|
||||
const toState = match[3];
|
||||
expressions.push(makeLambdaFromStates(fromState, toState));
|
||||
|
||||
const isFullAnyStateExpr = fromState == ANY_STATE && toState == ANY_STATE;
|
||||
if (separator[0] == '<' && !isFullAnyStateExpr) {
|
||||
expressions.push(makeLambdaFromStates(toState, fromState));
|
||||
}
|
||||
}
|
||||
|
||||
function parseAnimationAlias(alias: string, errors: string[]): string {
|
||||
switch (alias) {
|
||||
case ':enter':
|
||||
return 'void => *';
|
||||
case ':leave':
|
||||
return '* => void';
|
||||
default:
|
||||
errors.push(`The transition alias value "${alias}" is not supported`);
|
||||
return '* => *';
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
return lhsMatch && rhsMatch;
|
||||
};
|
||||
}
|
@ -1,46 +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 {AnimationMetadata, AnimationTransitionMetadata, sequence, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {buildAnimationKeyframes} from './animation_timeline_visitor';
|
||||
import {TransitionMatcherFn} from './animation_transition_expr';
|
||||
import {AnimationTransitionInstruction, createTransitionInstruction} from './animation_transition_instruction';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
match(currentState: any, nextState: any): AnimationTransitionInstruction {
|
||||
if (!oneOrMoreTransitionsMatch(this.matchFns, currentState, nextState)) return;
|
||||
|
||||
const backupStateStyles = this._stateStyles['*'] || {};
|
||||
const currentStateStyles = this._stateStyles[currentState] || backupStateStyles;
|
||||
const nextStateStyles = this._stateStyles[nextState] || backupStateStyles;
|
||||
|
||||
const timelines =
|
||||
buildAnimationKeyframes(this._animationAst, currentStateStyles, nextStateStyles);
|
||||
|
||||
return createTransitionInstruction(
|
||||
this._triggerName, currentState, nextState, nextState === 'void', currentStateStyles,
|
||||
nextStateStyles, timelines);
|
||||
}
|
||||
}
|
||||
|
||||
function oneOrMoreTransitionsMatch(
|
||||
matchFns: TransitionMatcherFn[], currentState: any, nextState: any): boolean {
|
||||
return matchFns.some(fn => fn(currentState, nextState));
|
||||
}
|
@ -1,36 +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 {ɵStyleData} from '@angular/animations';
|
||||
import {AnimationEngineInstruction, AnimationTransitionInstructionType} from '../render/animation_engine_instruction';
|
||||
import {AnimationTimelineInstruction} from './animation_timeline_instruction';
|
||||
|
||||
export interface AnimationTransitionInstruction extends AnimationEngineInstruction {
|
||||
triggerName: string;
|
||||
isRemovalTransition: boolean;
|
||||
fromState: string;
|
||||
fromStyles: ɵStyleData;
|
||||
toState: string;
|
||||
toStyles: ɵStyleData;
|
||||
timelines: AnimationTimelineInstruction[];
|
||||
}
|
||||
|
||||
export function createTransitionInstruction(
|
||||
triggerName: string, fromState: string, toState: string, isRemovalTransition: boolean,
|
||||
fromStyles: ɵStyleData, toStyles: ɵStyleData,
|
||||
timelines: AnimationTimelineInstruction[]): AnimationTransitionInstruction {
|
||||
return {
|
||||
type: AnimationTransitionInstructionType.TransitionAnimation,
|
||||
triggerName,
|
||||
isRemovalTransition,
|
||||
fromState,
|
||||
fromStyles,
|
||||
toState,
|
||||
toStyles,
|
||||
timelines
|
||||
};
|
||||
}
|
@ -1,115 +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 {AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {copyStyles, normalizeStyles} from '../util';
|
||||
|
||||
import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor';
|
||||
import {parseTransitionExpr} from './animation_transition_expr';
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export class AnimationTrigger {
|
||||
public transitionFactories: 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));
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.length) {
|
||||
const LINE_START = '\n - ';
|
||||
throw new Error(
|
||||
`Animation parsing for the ${name} trigger have failed:${LINE_START}${errors.join(LINE_START)}`);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
for (let i = 0; i < this.transitionFactories.length; i++) {
|
||||
let result = this.transitionFactories[i].match(currentState, nextState);
|
||||
if (result) return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
context.states[ast.name] = normalizeStyles(ast.styles.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
|
||||
}
|
||||
}
|
@ -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;
|
||||
public collectedStyles: {[propName: string]: StyleTimeTuple} = {};
|
||||
}
|
@ -1,30 +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
|
||||
*/
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export abstract class AnimationStyleNormalizer {
|
||||
abstract normalizePropertyName(propertyName: string, errors: string[]): string;
|
||||
abstract normalizeStyleValue(
|
||||
userProvidedProperty: string, normalizedProperty: string, value: string|number,
|
||||
errors: string[]): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export class NoopAnimationStyleNormalizer {
|
||||
normalizePropertyName(propertyName: string, errors: string[]): string { return propertyName; }
|
||||
|
||||
normalizeStyleValue(
|
||||
userProvidedProperty: string, normalizedProperty: string, value: string|number,
|
||||
errors: string[]): string {
|
||||
return <any>value;
|
||||
}
|
||||
}
|
@ -1,48 +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 {AnimationStyleNormalizer} from './animation_style_normalizer';
|
||||
|
||||
export class WebAnimationsStyleNormalizer extends AnimationStyleNormalizer {
|
||||
normalizePropertyName(propertyName: string, errors: string[]): string {
|
||||
return dashCaseToCamelCase(propertyName);
|
||||
}
|
||||
|
||||
normalizeStyleValue(
|
||||
userProvidedProperty: string, normalizedProperty: string, value: string|number,
|
||||
errors: string[]): string {
|
||||
let unit: string = '';
|
||||
const strVal = value.toString().trim();
|
||||
|
||||
if (DIMENSIONAL_PROP_MAP[normalizedProperty] && value !== 0 && value !== '0') {
|
||||
if (typeof value === 'number') {
|
||||
unit = 'px';
|
||||
} else {
|
||||
const valAndSuffixMatch = value.match(/^[+-]?[\d\.]+([a-z]*)$/);
|
||||
if (valAndSuffixMatch && valAndSuffixMatch[1].length == 0) {
|
||||
errors.push(`Please provide a CSS unit value for ${userProvidedProperty}:${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return strVal + unit;
|
||||
}
|
||||
}
|
||||
|
||||
const DIMENSIONAL_PROP_MAP = makeBooleanMap(
|
||||
'width,height,minWidth,minHeight,maxWidth,maxHeight,left,top,bottom,right,fontSize,outlineWidth,outlineOffset,paddingTop,paddingLeft,paddingBottom,paddingRight,marginTop,marginLeft,marginBottom,marginRight,borderRadius,borderWidth,borderTopWidth,borderLeftWidth,borderRightWidth,borderBottomWidth,textIndent'
|
||||
.split(','));
|
||||
|
||||
function makeBooleanMap(keys: string[]): {[key: string]: boolean} {
|
||||
const map: {[key: string]: boolean} = {};
|
||||
keys.forEach(key => map[key] = true);
|
||||
return map;
|
||||
}
|
||||
|
||||
const DASH_CASE_REGEXP = /-+([a-z0-9])/g;
|
||||
export function dashCaseToCamelCase(input: string): string {
|
||||
return input.replace(DASH_CASE_REGEXP, (...m: any[]) => m[1].toUpperCase());
|
||||
}
|
Reference in New Issue
Block a user