569 lines
22 KiB
TypeScript

/**
* @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 {CompileAnimationAnimateMetadata, CompileAnimationEntryMetadata, CompileAnimationGroupMetadata, CompileAnimationKeyframesSequenceMetadata, CompileAnimationMetadata, CompileAnimationSequenceMetadata, CompileAnimationStateDeclarationMetadata, CompileAnimationStateTransitionMetadata, CompileAnimationStyleMetadata, CompileAnimationWithStepsMetadata} from '../compile_metadata';
import {ListWrapper, StringMapWrapper} from '../facade/collection';
import {NumberWrapper, isArray, isBlank, isPresent, isString, isStringMap} from '../facade/lang';
import {Math} from '../facade/math';
import {ParseError} from '../parse_util';
import {ANY_STATE, AnimationOutput, FILL_STYLE_FLAG} from '../private_import_core';
import {AnimationAst, AnimationEntryAst, AnimationGroupAst, AnimationKeyframeAst, AnimationSequenceAst, AnimationStateDeclarationAst, AnimationStateTransitionAst, AnimationStateTransitionExpression, AnimationStepAst, AnimationStylesAst, AnimationWithStepsAst} from './animation_ast';
import {StylesCollection} from './styles_collection';
const _INITIAL_KEYFRAME = 0;
const _TERMINAL_KEYFRAME = 1;
const _ONE_SECOND = 1000;
export class AnimationParseError extends ParseError {
constructor(message: any /** TODO #9100 */) { super(null, message); }
toString(): string { return `${this.msg}`; }
}
export class ParsedAnimationResult {
constructor(public ast: AnimationEntryAst, public errors: AnimationParseError[]) {}
}
export function parseAnimationEntry(entry: CompileAnimationEntryMetadata): ParsedAnimationResult {
var errors: AnimationParseError[] = [];
var stateStyles: {[key: string]: AnimationStylesAst} = {};
var transitions: CompileAnimationStateTransitionMetadata[] = [];
var stateDeclarationAsts: any[] /** TODO #9100 */ = [];
entry.definitions.forEach(def => {
if (def instanceof CompileAnimationStateDeclarationMetadata) {
_parseAnimationDeclarationStates(def, errors).forEach(ast => {
stateDeclarationAsts.push(ast);
stateStyles[ast.stateName] = ast.styles;
});
} else {
transitions.push(<CompileAnimationStateTransitionMetadata>def);
}
});
var stateTransitionAsts =
transitions.map(transDef => _parseAnimationStateTransition(transDef, stateStyles, errors));
var ast = new AnimationEntryAst(entry.name, stateDeclarationAsts, stateTransitionAsts);
return new ParsedAnimationResult(ast, errors);
}
export function parseAnimationOutputName(
outputName: string, errors: AnimationParseError[]): AnimationOutput {
var values = outputName.split('.');
var name: string;
var phase: string = '';
if (values.length > 1) {
name = values[0];
let parsedPhase = values[1];
switch (parsedPhase) {
case 'start':
case 'done':
phase = parsedPhase;
break;
default:
errors.push(new AnimationParseError(
`The provided animation output phase value "${parsedPhase}" for "@${name}" is not supported (use start or done)`));
}
} else {
name = outputName;
errors.push(new AnimationParseError(
`The animation trigger output event (@${name}) is missing its phase value name (start or done are currently supported)`));
}
return new AnimationOutput(name, phase, outputName);
}
function _parseAnimationDeclarationStates(
stateMetadata: CompileAnimationStateDeclarationMetadata,
errors: AnimationParseError[]): AnimationStateDeclarationAst[] {
var styleValues: {[key: string]: string | number}[] = [];
stateMetadata.styles.styles.forEach(stylesEntry => {
// TODO (matsko): change this when we get CSS class integration support
if (isStringMap(stylesEntry)) {
styleValues.push(<{[key: string]: string | number}>stylesEntry);
} else {
errors.push(new AnimationParseError(
`State based animations cannot contain references to other states`));
}
});
var defStyles = new AnimationStylesAst(styleValues);
var states = stateMetadata.stateNameExpr.split(/\s*,\s*/);
return states.map(state => new AnimationStateDeclarationAst(state, defStyles));
}
function _parseAnimationStateTransition(
transitionStateMetadata: CompileAnimationStateTransitionMetadata,
stateStyles: {[key: string]: AnimationStylesAst},
errors: AnimationParseError[]): AnimationStateTransitionAst {
var styles = new StylesCollection();
var transitionExprs: any[] /** TODO #9100 */ = [];
var transitionStates = transitionStateMetadata.stateChangeExpr.split(/\s*,\s*/);
transitionStates.forEach(expr => {
_parseAnimationTransitionExpr(expr, errors).forEach(transExpr => {
transitionExprs.push(transExpr);
});
});
var entry = _normalizeAnimationEntry(transitionStateMetadata.steps);
var animation = _normalizeStyleSteps(entry, stateStyles, errors);
var animationAst = _parseTransitionAnimation(animation, 0, styles, stateStyles, errors);
if (errors.length == 0) {
_fillAnimationAstStartingKeyframes(animationAst, styles, errors);
}
var stepsAst: AnimationWithStepsAst = (animationAst instanceof AnimationWithStepsAst) ?
animationAst :
new AnimationSequenceAst([animationAst]);
return new AnimationStateTransitionAst(transitionExprs, stepsAst);
}
function _parseAnimationTransitionExpr(
eventStr: string, errors: AnimationParseError[]): AnimationStateTransitionExpression[] {
var expressions: any[] /** TODO #9100 */ = [];
var match = eventStr.match(/^(\*|[-\w]+)\s*(<?[=-]>)\s*(\*|[-\w]+)$/);
if (!isPresent(match) || match.length < 4) {
errors.push(new AnimationParseError(`the provided ${eventStr} is not of a supported format`));
return expressions;
}
var fromState = match[1];
var separator = match[2];
var toState = match[3];
expressions.push(new AnimationStateTransitionExpression(fromState, toState));
var isFullAnyStateExpr = fromState == ANY_STATE && toState == ANY_STATE;
if (separator[0] == '<' && !isFullAnyStateExpr) {
expressions.push(new AnimationStateTransitionExpression(toState, fromState));
}
return expressions;
}
function _fetchSylesFromState(stateName: string, stateStyles: {[key: string]: AnimationStylesAst}):
CompileAnimationStyleMetadata {
var entry = stateStyles[stateName];
if (isPresent(entry)) {
var styles = <{[key: string]: string | number}[]>entry.styles;
return new CompileAnimationStyleMetadata(0, styles);
}
return null;
}
function _normalizeAnimationEntry(entry: CompileAnimationMetadata | CompileAnimationMetadata[]):
CompileAnimationMetadata {
return isArray(entry) ? new CompileAnimationSequenceMetadata(<CompileAnimationMetadata[]>entry) :
<CompileAnimationMetadata>entry;
}
function _normalizeStyleMetadata(
entry: CompileAnimationStyleMetadata, stateStyles: {[key: string]: AnimationStylesAst},
errors: AnimationParseError[]): Array<{[key: string]: string | number}> {
var normalizedStyles: any[] /** TODO #9100 */ = [];
entry.styles.forEach(styleEntry => {
if (isString(styleEntry)) {
ListWrapper.addAll(
normalizedStyles, _resolveStylesFromState(<string>styleEntry, stateStyles, errors));
} else {
normalizedStyles.push(<{[key: string]: string | number}>styleEntry);
}
});
return normalizedStyles;
}
function _normalizeStyleSteps(
entry: CompileAnimationMetadata, stateStyles: {[key: string]: AnimationStylesAst},
errors: AnimationParseError[]): CompileAnimationMetadata {
var steps = _normalizeStyleStepEntry(entry, stateStyles, errors);
return (entry instanceof CompileAnimationGroupMetadata) ?
new CompileAnimationGroupMetadata(steps) :
new CompileAnimationSequenceMetadata(steps);
}
function _mergeAnimationStyles(
stylesList: any[], newItem: {[key: string]: string | number} | string) {
if (isStringMap(newItem) && stylesList.length > 0) {
var lastIndex = stylesList.length - 1;
var lastItem = stylesList[lastIndex];
if (isStringMap(lastItem)) {
stylesList[lastIndex] = StringMapWrapper.merge(
<{[key: string]: string | number}>lastItem, <{[key: string]: string | number}>newItem);
return;
}
}
stylesList.push(newItem);
}
function _normalizeStyleStepEntry(
entry: CompileAnimationMetadata, stateStyles: {[key: string]: AnimationStylesAst},
errors: AnimationParseError[]): CompileAnimationMetadata[] {
var steps: CompileAnimationMetadata[];
if (entry instanceof CompileAnimationWithStepsMetadata) {
steps = entry.steps;
} else {
return [entry];
}
var newSteps: CompileAnimationMetadata[] = [];
var combinedStyles: {[key: string]: string | number}[];
steps.forEach(step => {
if (step instanceof CompileAnimationStyleMetadata) {
// this occurs when a style step is followed by a previous style step
// or when the first style step is run. We want to concatenate all subsequent
// style steps together into a single style step such that we have the correct
// starting keyframe data to pass into the animation player.
if (!isPresent(combinedStyles)) {
combinedStyles = [];
}
_normalizeStyleMetadata(<CompileAnimationStyleMetadata>step, stateStyles, errors)
.forEach(entry => { _mergeAnimationStyles(combinedStyles, entry); });
} else {
// it is important that we create a metadata entry of the combined styles
// before we go on an process the animate, sequence or group metadata steps.
// This will ensure that the AST will have the previous styles painted on
// screen before any further animations that use the styles take place.
if (isPresent(combinedStyles)) {
newSteps.push(new CompileAnimationStyleMetadata(0, combinedStyles));
combinedStyles = null;
}
if (step instanceof CompileAnimationAnimateMetadata) {
// we do not recurse into CompileAnimationAnimateMetadata since
// those style steps are not going to be squashed
var animateStyleValue = (<CompileAnimationAnimateMetadata>step).styles;
if (animateStyleValue instanceof CompileAnimationStyleMetadata) {
animateStyleValue.styles =
_normalizeStyleMetadata(animateStyleValue, stateStyles, errors);
} else if (animateStyleValue instanceof CompileAnimationKeyframesSequenceMetadata) {
animateStyleValue.steps.forEach(
step => { step.styles = _normalizeStyleMetadata(step, stateStyles, errors); });
}
} else if (step instanceof CompileAnimationWithStepsMetadata) {
let innerSteps = _normalizeStyleStepEntry(step, stateStyles, errors);
step = step instanceof CompileAnimationGroupMetadata ?
new CompileAnimationGroupMetadata(innerSteps) :
new CompileAnimationSequenceMetadata(innerSteps);
}
newSteps.push(step);
}
});
// this happens when only styles were animated within the sequence
if (isPresent(combinedStyles)) {
newSteps.push(new CompileAnimationStyleMetadata(0, combinedStyles));
}
return newSteps;
}
function _resolveStylesFromState(
stateName: string, stateStyles: {[key: string]: AnimationStylesAst},
errors: AnimationParseError[]) {
var styles: {[key: string]: string | number}[] = [];
if (stateName[0] != ':') {
errors.push(new AnimationParseError(`Animation states via styles must be prefixed with a ":"`));
} else {
var normalizedStateName = stateName.substring(1);
var value = stateStyles[normalizedStateName];
if (!isPresent(value)) {
errors.push(new AnimationParseError(
`Unable to apply styles due to missing a state: "${normalizedStateName}"`));
} else {
value.styles.forEach(stylesEntry => {
if (isStringMap(stylesEntry)) {
styles.push(<{[key: string]: string | number}>stylesEntry);
}
});
}
}
return styles;
}
class _AnimationTimings {
constructor(public duration: number, public delay: number, public easing: string) {}
}
function _parseAnimationKeyframes(
keyframeSequence: CompileAnimationKeyframesSequenceMetadata, currentTime: number,
collectedStyles: StylesCollection, stateStyles: {[key: string]: AnimationStylesAst},
errors: AnimationParseError[]): AnimationKeyframeAst[] {
var totalEntries = keyframeSequence.steps.length;
var totalOffsets = 0;
keyframeSequence.steps.forEach(step => totalOffsets += (isPresent(step.offset) ? 1 : 0));
if (totalOffsets > 0 && totalOffsets < totalEntries) {
errors.push(new AnimationParseError(
`Not all style() entries contain an offset for the provided keyframe()`));
totalOffsets = totalEntries;
}
var limit = totalEntries - 1;
var margin = totalOffsets == 0 ? (1 / limit) : 0;
var rawKeyframes: any[] /** TODO #9100 */ = [];
var index = 0;
var doSortKeyframes = false;
var lastOffset = 0;
keyframeSequence.steps.forEach(styleMetadata => {
var offset = styleMetadata.offset;
var keyframeStyles: {[key: string]: string | number} = {};
styleMetadata.styles.forEach(entry => {
StringMapWrapper.forEach(
<{[key: string]: string | number}>entry,
(value: any /** TODO #9100 */, prop: any /** TODO #9100 */) => {
if (prop != 'offset') {
keyframeStyles[prop] = value;
}
});
});
if (isPresent(offset)) {
doSortKeyframes = doSortKeyframes || (offset < lastOffset);
} else {
offset = index == limit ? _TERMINAL_KEYFRAME : (margin * index);
}
rawKeyframes.push([offset, keyframeStyles]);
lastOffset = offset;
index++;
});
if (doSortKeyframes) {
ListWrapper.sort(rawKeyframes, (a, b) => a[0] <= b[0] ? -1 : 1);
}
var i: any /** TODO #9100 */;
var firstKeyframe = rawKeyframes[0];
if (firstKeyframe[0] != _INITIAL_KEYFRAME) {
ListWrapper.insert(rawKeyframes, 0, firstKeyframe = [_INITIAL_KEYFRAME, {}]);
}
var firstKeyframeStyles = firstKeyframe[1];
limit = rawKeyframes.length - 1;
var lastKeyframe = rawKeyframes[limit];
if (lastKeyframe[0] != _TERMINAL_KEYFRAME) {
rawKeyframes.push(lastKeyframe = [_TERMINAL_KEYFRAME, {}]);
limit++;
}
var lastKeyframeStyles = lastKeyframe[1];
for (i = 1; i <= limit; i++) {
let entry = rawKeyframes[i];
let styles = entry[1];
StringMapWrapper.forEach(
styles, (value: any /** TODO #9100 */, prop: any /** TODO #9100 */) => {
if (!isPresent(firstKeyframeStyles[prop])) {
firstKeyframeStyles[prop] = FILL_STYLE_FLAG;
}
});
}
for (i = limit - 1; i >= 0; i--) {
let entry = rawKeyframes[i];
let styles = entry[1];
StringMapWrapper.forEach(
styles, (value: any /** TODO #9100 */, prop: any /** TODO #9100 */) => {
if (!isPresent(lastKeyframeStyles[prop])) {
lastKeyframeStyles[prop] = value;
}
});
}
return rawKeyframes.map(
entry => new AnimationKeyframeAst(entry[0], new AnimationStylesAst([entry[1]])));
}
function _parseTransitionAnimation(
entry: CompileAnimationMetadata, currentTime: number, collectedStyles: StylesCollection,
stateStyles: {[key: string]: AnimationStylesAst}, errors: AnimationParseError[]): AnimationAst {
var ast: any /** TODO #9100 */;
var playTime = 0;
var startingTime = currentTime;
if (entry instanceof CompileAnimationWithStepsMetadata) {
var maxDuration = 0;
var steps: any[] /** TODO #9100 */ = [];
var isGroup = entry instanceof CompileAnimationGroupMetadata;
var previousStyles: any /** TODO #9100 */;
entry.steps.forEach(entry => {
// these will get picked up by the next step...
var time = isGroup ? startingTime : currentTime;
if (entry instanceof CompileAnimationStyleMetadata) {
entry.styles.forEach(stylesEntry => {
// by this point we know that we only have stringmap values
var map = <{[key: string]: string | number}>stylesEntry;
StringMapWrapper.forEach(
map, (value: any /** TODO #9100 */, prop: any /** TODO #9100 */) => {
collectedStyles.insertAtTime(prop, time, value);
});
});
previousStyles = entry.styles;
return;
}
var innerAst = _parseTransitionAnimation(entry, time, collectedStyles, stateStyles, errors);
if (isPresent(previousStyles)) {
if (entry instanceof CompileAnimationWithStepsMetadata) {
let startingStyles = new AnimationStylesAst(previousStyles);
steps.push(new AnimationStepAst(startingStyles, [], 0, 0, ''));
} else {
var innerStep = <AnimationStepAst>innerAst;
ListWrapper.addAll(innerStep.startingStyles.styles, previousStyles);
}
previousStyles = null;
}
var astDuration = innerAst.playTime;
currentTime += astDuration;
playTime += astDuration;
maxDuration = Math.max(astDuration, maxDuration);
steps.push(innerAst);
});
if (isPresent(previousStyles)) {
let startingStyles = new AnimationStylesAst(previousStyles);
steps.push(new AnimationStepAst(startingStyles, [], 0, 0, ''));
}
if (isGroup) {
ast = new AnimationGroupAst(steps);
playTime = maxDuration;
currentTime = startingTime + playTime;
} else {
ast = new AnimationSequenceAst(steps);
}
} else if (entry instanceof CompileAnimationAnimateMetadata) {
var timings = _parseTimeExpression(entry.timings, errors);
var styles = entry.styles;
var keyframes: any /** TODO #9100 */;
if (styles instanceof CompileAnimationKeyframesSequenceMetadata) {
keyframes =
_parseAnimationKeyframes(styles, currentTime, collectedStyles, stateStyles, errors);
} else {
let styleData = <CompileAnimationStyleMetadata>styles;
let offset = _TERMINAL_KEYFRAME;
let styleAst = new AnimationStylesAst(<{[key: string]: string | number}[]>styleData.styles);
var keyframe = new AnimationKeyframeAst(offset, styleAst);
keyframes = [keyframe];
}
ast = new AnimationStepAst(
new AnimationStylesAst([]), keyframes, timings.duration, timings.delay, timings.easing);
playTime = timings.duration + timings.delay;
currentTime += playTime;
keyframes.forEach(
(keyframe: any /** TODO #9100 */) => keyframe.styles.styles.forEach(
(entry: any /** TODO #9100 */) => StringMapWrapper.forEach(
entry, (value: any /** TODO #9100 */, prop: any /** TODO #9100 */) =>
collectedStyles.insertAtTime(prop, currentTime, value))));
} else {
// if the code reaches this stage then an error
// has already been populated within the _normalizeStyleSteps()
// operation...
ast = new AnimationStepAst(null, [], 0, 0, '');
}
ast.playTime = playTime;
ast.startTime = startingTime;
return ast;
}
function _fillAnimationAstStartingKeyframes(
ast: AnimationAst, collectedStyles: StylesCollection, errors: AnimationParseError[]): void {
// steps that only contain style will not be filled
if ((ast instanceof AnimationStepAst) && ast.keyframes.length > 0) {
var keyframes = ast.keyframes;
if (keyframes.length == 1) {
var endKeyframe = keyframes[0];
var startKeyframe = _createStartKeyframeFromEndKeyframe(
endKeyframe, ast.startTime, ast.playTime, collectedStyles, errors);
ast.keyframes = [startKeyframe, endKeyframe];
}
} else if (ast instanceof AnimationWithStepsAst) {
ast.steps.forEach(entry => _fillAnimationAstStartingKeyframes(entry, collectedStyles, errors));
}
}
function _parseTimeExpression(
exp: string | number, errors: AnimationParseError[]): _AnimationTimings {
var regex = /^([\.\d]+)(m?s)(?:\s+([\.\d]+)(m?s))?(?:\s+([-a-z]+(?:\(.+?\))?))?/i;
var duration: number;
var delay: number = 0;
var easing: string = null;
if (isString(exp)) {
const matches = exp.match(regex);
if (matches === null) {
errors.push(new AnimationParseError(`The provided timing value "${exp}" is invalid.`));
return new _AnimationTimings(0, 0, null);
}
var durationMatch = NumberWrapper.parseFloat(matches[1]);
var durationUnit = matches[2];
if (durationUnit == 's') {
durationMatch *= _ONE_SECOND;
}
duration = Math.floor(durationMatch);
var delayMatch = matches[3];
var delayUnit = matches[4];
if (isPresent(delayMatch)) {
var delayVal: number = NumberWrapper.parseFloat(delayMatch);
if (isPresent(delayUnit) && delayUnit == 's') {
delayVal *= _ONE_SECOND;
}
delay = Math.floor(delayVal);
}
var easingVal = matches[5];
if (!isBlank(easingVal)) {
easing = easingVal;
}
} else {
duration = <number>exp;
}
return new _AnimationTimings(duration, delay, easing);
}
function _createStartKeyframeFromEndKeyframe(
endKeyframe: AnimationKeyframeAst, startTime: number, duration: number,
collectedStyles: StylesCollection, errors: AnimationParseError[]): AnimationKeyframeAst {
var values: {[key: string]: string | number} = {};
var endTime = startTime + duration;
endKeyframe.styles.styles.forEach((styleData: {[key: string]: string | number}) => {
StringMapWrapper.forEach(styleData, (val: any /** TODO #9100 */, prop: any /** TODO #9100 */) => {
if (prop == 'offset') return;
var resultIndex = collectedStyles.indexOfAtOrBeforeTime(prop, startTime);
var resultEntry: any /** TODO #9100 */, nextEntry: any /** TODO #9100 */,
value: any /** TODO #9100 */;
if (isPresent(resultIndex)) {
resultEntry = collectedStyles.getByIndex(prop, resultIndex);
value = resultEntry.value;
nextEntry = collectedStyles.getByIndex(prop, resultIndex + 1);
} else {
// this is a flag that the runtime code uses to pass
// in a value either from the state declaration styles
// or using the AUTO_STYLE value (e.g. getComputedStyle)
value = FILL_STYLE_FLAG;
}
if (isPresent(nextEntry) && !nextEntry.matches(endTime, val)) {
errors.push(new AnimationParseError(
`The animated CSS property "${prop}" unexpectedly changes between steps "${resultEntry.time}ms" and "${endTime}ms" at "${nextEntry.time}ms"`));
}
values[prop] = value;
});
});
return new AnimationKeyframeAst(_INITIAL_KEYFRAME, new AnimationStylesAst([values]));
}