@angular/animations
Version:
Angular - animations integration with web-animations
1,305 lines (1,296 loc) • 187 kB
JavaScript
/**
* @license Angular v10.0.6
* (c) 2010-2020 Google LLC. https://angular.io/
* License: MIT
*/
import { ɵAnimationGroupPlayer, NoopAnimationPlayer, AUTO_STYLE, ɵPRE_STYLE, sequence, style } from '@angular/animations';
import { Injectable } from '@angular/core';
/**
* @license
* Copyright Google LLC 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
*/
function isBrowser() {
return (typeof window !== 'undefined' && typeof window.document !== 'undefined');
}
function isNode() {
// Checking only for `process` isn't enough to identify whether or not we're in a Node
// environment, because Webpack by default will polyfill the `process`. While we can discern
// that Webpack polyfilled it by looking at `process.browser`, it's very Webpack-specific and
// might not be future-proof. Instead we look at the stringified version of `process` which
// is `[object process]` in Node and `[object Object]` when polyfilled.
return typeof process !== 'undefined' && {}.toString.call(process) === '[object process]';
}
function optimizeGroupPlayer(players) {
switch (players.length) {
case 0:
return new NoopAnimationPlayer();
case 1:
return players[0];
default:
return new ɵAnimationGroupPlayer(players);
}
}
function normalizeKeyframes(driver, normalizer, element, keyframes, preStyles = {}, postStyles = {}) {
const errors = [];
const normalizedKeyframes = [];
let previousOffset = -1;
let previousKeyframe = null;
keyframes.forEach(kf => {
const offset = kf['offset'];
const isSameOffset = offset == previousOffset;
const normalizedKeyframe = (isSameOffset && previousKeyframe) || {};
Object.keys(kf).forEach(prop => {
let normalizedProp = prop;
let normalizedValue = kf[prop];
if (prop !== 'offset') {
normalizedProp = normalizer.normalizePropertyName(normalizedProp, errors);
switch (normalizedValue) {
case ɵPRE_STYLE:
normalizedValue = preStyles[prop];
break;
case AUTO_STYLE:
normalizedValue = postStyles[prop];
break;
default:
normalizedValue =
normalizer.normalizeStyleValue(prop, normalizedProp, normalizedValue, errors);
break;
}
}
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;
}
function listenOnPlayer(player, eventName, event, callback) {
switch (eventName) {
case 'start':
player.onStart(() => callback(event && copyAnimationEvent(event, 'start', player)));
break;
case 'done':
player.onDone(() => callback(event && copyAnimationEvent(event, 'done', player)));
break;
case 'destroy':
player.onDestroy(() => callback(event && copyAnimationEvent(event, 'destroy', player)));
break;
}
}
function copyAnimationEvent(e, phaseName, player) {
const totalTime = player.totalTime;
const disabled = player.disabled ? true : false;
const event = makeAnimationEvent(e.element, e.triggerName, e.fromState, e.toState, phaseName || e.phaseName, totalTime == undefined ? e.totalTime : totalTime, disabled);
const data = e['_data'];
if (data != null) {
event['_data'] = data;
}
return event;
}
function makeAnimationEvent(element, triggerName, fromState, toState, phaseName = '', totalTime = 0, disabled) {
return { element, triggerName, fromState, toState, phaseName, totalTime, disabled: !!disabled };
}
function getOrSetAsInMap(map, key, defaultValue) {
let value;
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;
}
function parseTimelineCommand(command) {
const separatorPos = command.indexOf(':');
const id = command.substring(1, separatorPos);
const action = command.substr(separatorPos + 1);
return [id, action];
}
let _contains = (elm1, elm2) => false;
const ɵ0 = _contains;
let _matches = (element, selector) => false;
const ɵ1 = _matches;
let _query = (element, selector, multi) => {
return [];
};
const ɵ2 = _query;
// Define utility methods for browsers and platform-server(domino) where Element
// and utility methods exist.
const _isNode = isNode();
if (_isNode || typeof Element !== 'undefined') {
// this is well supported in all browsers
_contains = (elm1, elm2) => {
return elm1.contains(elm2);
};
_matches = (() => {
if (_isNode || Element.prototype.matches) {
return (element, selector) => element.matches(selector);
}
else {
const proto = Element.prototype;
const fn = proto.matchesSelector || proto.mozMatchesSelector || proto.msMatchesSelector ||
proto.oMatchesSelector || proto.webkitMatchesSelector;
if (fn) {
return (element, selector) => fn.apply(element, [selector]);
}
else {
return _matches;
}
}
})();
_query = (element, selector, multi) => {
let results = [];
if (multi) {
results.push(...element.querySelectorAll(selector));
}
else {
const elm = element.querySelector(selector);
if (elm) {
results.push(elm);
}
}
return results;
};
}
function containsVendorPrefix(prop) {
// Webkit is the only real popular vendor prefix nowadays
// cc: http://shouldiprefix.com/
return prop.substring(1, 6) == 'ebkit'; // webkit or Webkit
}
let _CACHED_BODY = null;
let _IS_WEBKIT = false;
function validateStyleProperty(prop) {
if (!_CACHED_BODY) {
_CACHED_BODY = getBodyNode() || {};
_IS_WEBKIT = _CACHED_BODY.style ? ('WebkitAppearance' in _CACHED_BODY.style) : false;
}
let result = true;
if (_CACHED_BODY.style && !containsVendorPrefix(prop)) {
result = prop in _CACHED_BODY.style;
if (!result && _IS_WEBKIT) {
const camelProp = 'Webkit' + prop.charAt(0).toUpperCase() + prop.substr(1);
result = camelProp in _CACHED_BODY.style;
}
}
return result;
}
function getBodyNode() {
if (typeof document != 'undefined') {
return document.body;
}
return null;
}
const matchesElement = _matches;
const containsElement = _contains;
const invokeQuery = _query;
function hypenatePropsObject(object) {
const newObj = {};
Object.keys(object).forEach(prop => {
const newProp = prop.replace(/([a-z])([A-Z])/g, '$1-$2');
newObj[newProp] = object[prop];
});
return newObj;
}
/**
* @license
* Copyright Google LLC 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
*/
/**
* @publicApi
*/
class NoopAnimationDriver {
validateStyleProperty(prop) {
return validateStyleProperty(prop);
}
matchesElement(element, selector) {
return matchesElement(element, selector);
}
containsElement(elm1, elm2) {
return containsElement(elm1, elm2);
}
query(element, selector, multi) {
return invokeQuery(element, selector, multi);
}
computeStyle(element, prop, defaultValue) {
return defaultValue || '';
}
animate(element, keyframes, duration, delay, easing, previousPlayers = [], scrubberAccessRequested) {
return new NoopAnimationPlayer(duration, delay);
}
}
NoopAnimationDriver.decorators = [
{ type: Injectable }
];
/**
* @publicApi
*/
class AnimationDriver {
}
AnimationDriver.NOOP = new NoopAnimationDriver();
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
const ONE_SECOND = 1000;
const SUBSTITUTION_EXPR_START = '{{';
const SUBSTITUTION_EXPR_END = '}}';
const ENTER_CLASSNAME = 'ng-enter';
const LEAVE_CLASSNAME = 'ng-leave';
const ENTER_SELECTOR = '.ng-enter';
const LEAVE_SELECTOR = '.ng-leave';
const NG_TRIGGER_CLASSNAME = 'ng-trigger';
const NG_TRIGGER_SELECTOR = '.ng-trigger';
const NG_ANIMATING_CLASSNAME = 'ng-animating';
const NG_ANIMATING_SELECTOR = '.ng-animating';
function resolveTimingValue(value) {
if (typeof value == 'number')
return value;
const matches = value.match(/^(-?[\.\d]+)(m?s)/);
if (!matches || matches.length < 2)
return 0;
return _convertTimeValueToMS(parseFloat(matches[1]), matches[2]);
}
function _convertTimeValueToMS(value, unit) {
switch (unit) {
case 's':
return value * ONE_SECOND;
default: // ms or something else
return value;
}
}
function resolveTiming(timings, errors, allowNegativeValues) {
return timings.hasOwnProperty('duration') ?
timings :
parseTimeExpression(timings, errors, allowNegativeValues);
}
function parseTimeExpression(exp, errors, allowNegativeValues) {
const regex = /^(-?[\.\d]+)(m?s)(?:\s+(-?[\.\d]+)(m?s))?(?:\s+([-a-z]+(?:\(.+?\))?))?$/i;
let duration;
let delay = 0;
let easing = '';
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: '' };
}
duration = _convertTimeValueToMS(parseFloat(matches[1]), matches[2]);
const delayMatch = matches[3];
if (delayMatch != null) {
delay = _convertTimeValueToMS(parseFloat(delayMatch), matches[4]);
}
const easingVal = matches[5];
if (easingVal) {
easing = easingVal;
}
}
else {
duration = 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 };
}
function copyObj(obj, destination = {}) {
Object.keys(obj).forEach(prop => {
destination[prop] = obj[prop];
});
return destination;
}
function normalizeStyles(styles) {
const normalizedStyles = {};
if (Array.isArray(styles)) {
styles.forEach(data => copyStyles(data, false, normalizedStyles));
}
else {
copyStyles(styles, false, normalizedStyles);
}
return normalizedStyles;
}
function copyStyles(styles, readPrototype, destination = {}) {
if (readPrototype) {
// we make use of a for-in loop so that the
// prototypically inherited properties are
// revealed from the backFill map
for (let prop in styles) {
destination[prop] = styles[prop];
}
}
else {
copyObj(styles, destination);
}
return destination;
}
function getStyleAttributeString(element, key, value) {
// Return the key-value pair string to be added to the style attribute for the
// given CSS style key.
if (value) {
return key + ':' + value + ';';
}
else {
return '';
}
}
function writeStyleAttribute(element) {
// Read the style property of the element and manually reflect it to the
// style attribute. This is needed because Domino on platform-server doesn't
// understand the full set of allowed CSS properties and doesn't reflect some
// of them automatically.
let styleAttrValue = '';
for (let i = 0; i < element.style.length; i++) {
const key = element.style.item(i);
styleAttrValue += getStyleAttributeString(element, key, element.style.getPropertyValue(key));
}
for (const key in element.style) {
// Skip internal Domino properties that don't need to be reflected.
if (!element.style.hasOwnProperty(key) || key.startsWith('_')) {
continue;
}
const dashKey = camelCaseToDashCase(key);
styleAttrValue += getStyleAttributeString(element, dashKey, element.style[key]);
}
element.setAttribute('style', styleAttrValue);
}
function setStyles(element, styles, formerStyles) {
if (element['style']) {
Object.keys(styles).forEach(prop => {
const camelProp = dashCaseToCamelCase(prop);
if (formerStyles && !formerStyles.hasOwnProperty(prop)) {
formerStyles[prop] = element.style[camelProp];
}
element.style[camelProp] = styles[prop];
});
// On the server set the 'style' attribute since it's not automatically reflected.
if (isNode()) {
writeStyleAttribute(element);
}
}
}
function eraseStyles(element, styles) {
if (element['style']) {
Object.keys(styles).forEach(prop => {
const camelProp = dashCaseToCamelCase(prop);
element.style[camelProp] = '';
});
// On the server set the 'style' attribute since it's not automatically reflected.
if (isNode()) {
writeStyleAttribute(element);
}
}
}
function normalizeAnimationEntry(steps) {
if (Array.isArray(steps)) {
if (steps.length == 1)
return steps[0];
return sequence(steps);
}
return steps;
}
function validateStyleParams(value, options, errors) {
const params = options.params || {};
const matches = extractStyleParams(value);
if (matches.length) {
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 = new RegExp(`${SUBSTITUTION_EXPR_START}\\s*(.+?)\\s*${SUBSTITUTION_EXPR_END}`, 'g');
function extractStyleParams(value) {
let params = [];
if (typeof value === 'string') {
let match;
while (match = PARAM_REGEX.exec(value)) {
params.push(match[1]);
}
PARAM_REGEX.lastIndex = 0;
}
return params;
}
function interpolateParams(value, params, errors) {
const original = value.toString();
const str = original.replace(PARAM_REGEX, (_, varName) => {
let localVal = params[varName];
// this means that the value was never overridden 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;
}
function iteratorToArray(iterator) {
const arr = [];
let item = iterator.next();
while (!item.done) {
arr.push(item.value);
item = iterator.next();
}
return arr;
}
function mergeAnimationOptions(source, destination) {
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;
}
const DASH_CASE_REGEXP = /-+([a-z0-9])/g;
function dashCaseToCamelCase(input) {
return input.replace(DASH_CASE_REGEXP, (...m) => m[1].toUpperCase());
}
function camelCaseToDashCase(input) {
return input.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
function allowPreviousPlayerStylesMerge(duration, delay) {
return duration === 0 || delay === 0;
}
function balancePreviousStylesIntoKeyframes(element, keyframes, previousStyles) {
const previousStyleProps = Object.keys(previousStyles);
if (previousStyleProps.length && keyframes.length) {
let startingKeyframe = keyframes[0];
let missingStyleProps = [];
previousStyleProps.forEach(prop => {
if (!startingKeyframe.hasOwnProperty(prop)) {
missingStyleProps.push(prop);
}
startingKeyframe[prop] = previousStyles[prop];
});
if (missingStyleProps.length) {
// tslint:disable-next-line
for (var i = 1; i < keyframes.length; i++) {
let kf = keyframes[i];
missingStyleProps.forEach(function (prop) {
kf[prop] = computeStyle(element, prop);
});
}
}
}
return keyframes;
}
function visitDslNode(visitor, node, context) {
switch (node.type) {
case 7 /* Trigger */:
return visitor.visitTrigger(node, context);
case 0 /* State */:
return visitor.visitState(node, context);
case 1 /* Transition */:
return visitor.visitTransition(node, context);
case 2 /* Sequence */:
return visitor.visitSequence(node, context);
case 3 /* Group */:
return visitor.visitGroup(node, context);
case 4 /* Animate */:
return visitor.visitAnimate(node, context);
case 5 /* Keyframes */:
return visitor.visitKeyframes(node, context);
case 6 /* Style */:
return visitor.visitStyle(node, context);
case 8 /* Reference */:
return visitor.visitReference(node, context);
case 9 /* AnimateChild */:
return visitor.visitAnimateChild(node, context);
case 10 /* AnimateRef */:
return visitor.visitAnimateRef(node, context);
case 11 /* Query */:
return visitor.visitQuery(node, context);
case 12 /* Stagger */:
return visitor.visitStagger(node, context);
default:
throw new Error(`Unable to resolve animation metadata node #${node.type}`);
}
}
function computeStyle(element, prop) {
return window.getComputedStyle(element)[prop];
}
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
const ANY_STATE = '*';
function parseTransitionExpr(transitionValue, errors) {
const expressions = [];
if (typeof transitionValue == 'string') {
transitionValue.split(/\s*,\s*/).forEach(str => parseInnerTransitionStr(str, expressions, errors));
}
else {
expressions.push(transitionValue);
}
return expressions;
}
function parseInnerTransitionStr(eventStr, expressions, errors) {
if (eventStr[0] == ':') {
const result = parseAnimationAlias(eventStr, errors);
if (typeof result == 'function') {
expressions.push(result);
return;
}
eventStr = result;
}
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, errors) {
switch (alias) {
case ':enter':
return 'void => *';
case ':leave':
return '* => void';
case ':increment':
return (fromState, toState) => parseFloat(toState) > parseFloat(fromState);
case ':decrement':
return (fromState, toState) => parseFloat(toState) < parseFloat(fromState);
default:
errors.push(`The transition alias value "${alias}" is not supported`);
return '* => *';
}
}
// DO NOT REFACTOR ... keep the follow set instantiations
// with the values intact (closure compiler for some reason
// removes follow-up lines that add the values outside of
// the constructor...
const TRUE_BOOLEAN_VALUES = new Set(['true', '1']);
const FALSE_BOOLEAN_VALUES = new Set(['false', '0']);
function makeLambdaFromStates(lhs, rhs) {
const LHS_MATCH_BOOLEAN = TRUE_BOOLEAN_VALUES.has(lhs) || FALSE_BOOLEAN_VALUES.has(lhs);
const RHS_MATCH_BOOLEAN = TRUE_BOOLEAN_VALUES.has(rhs) || FALSE_BOOLEAN_VALUES.has(rhs);
return (fromState, toState) => {
let lhsMatch = lhs == ANY_STATE || lhs == fromState;
let rhsMatch = rhs == ANY_STATE || rhs == toState;
if (!lhsMatch && LHS_MATCH_BOOLEAN && typeof fromState === 'boolean') {
lhsMatch = fromState ? TRUE_BOOLEAN_VALUES.has(lhs) : FALSE_BOOLEAN_VALUES.has(lhs);
}
if (!rhsMatch && RHS_MATCH_BOOLEAN && typeof toState === 'boolean') {
rhsMatch = toState ? TRUE_BOOLEAN_VALUES.has(rhs) : FALSE_BOOLEAN_VALUES.has(rhs);
}
return lhsMatch && rhsMatch;
};
}
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
const 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 behavior 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.
*/
function buildAnimationAst(driver, metadata, errors) {
return new AnimationAstBuilderVisitor(driver).build(metadata, errors);
}
const ROOT_SELECTOR = '';
class AnimationAstBuilderVisitor {
constructor(_driver) {
this._driver = _driver;
}
build(metadata, errors) {
const context = new AnimationAstBuilderContext(errors);
this._resetContextStyleTimingState(context);
return visitDslNode(this, normalizeAnimationEntry(metadata), context);
}
_resetContextStyleTimingState(context) {
context.currentQuerySelector = ROOT_SELECTOR;
context.collectedStyles = {};
context.collectedStyles[ROOT_SELECTOR] = {};
context.currentTime = 0;
}
visitTrigger(metadata, context) {
let queryCount = context.queryCount = 0;
let depCount = context.depCount = 0;
const states = [];
const transitions = [];
if (metadata.name.charAt(0) == '@') {
context.errors.push('animation triggers cannot be prefixed with an `@` sign (e.g. trigger(\'@foo\', [...]))');
}
metadata.definitions.forEach(def => {
this._resetContextStyleTimingState(context);
if (def.type == 0 /* State */) {
const stateDef = def;
const name = stateDef.name;
name.toString().split(/\s*,\s*/).forEach(n => {
stateDef.name = n;
states.push(this.visitState(stateDef, context));
});
stateDef.name = name;
}
else if (def.type == 1 /* Transition */) {
const transition = this.visitTransition(def, 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()');
}
});
return {
type: 7 /* Trigger */,
name: metadata.name,
states,
transitions,
queryCount,
depCount,
options: null
};
}
visitState(metadata, context) {
const styleAst = this.visitStyle(metadata.styles, context);
const astParams = (metadata.options && metadata.options.params) || null;
if (styleAst.containsDynamicStyles) {
const missingSubs = new Set();
const params = astParams || {};
styleAst.styles.forEach(value => {
if (isObject(value)) {
const stylesObj = value;
Object.keys(stylesObj).forEach(prop => {
extractStyleParams(stylesObj[prop]).forEach(sub => {
if (!params.hasOwnProperty(sub)) {
missingSubs.add(sub);
}
});
});
}
});
if (missingSubs.size) {
const missingSubsArr = iteratorToArray(missingSubs.values());
context.errors.push(`state("${metadata
.name}", ...) must define default values for all the following style substitutions: ${missingSubsArr.join(', ')}`);
}
}
return {
type: 0 /* State */,
name: metadata.name,
style: styleAst,
options: astParams ? { params: astParams } : null
};
}
visitTransition(metadata, context) {
context.queryCount = 0;
context.depCount = 0;
const animation = visitDslNode(this, normalizeAnimationEntry(metadata.animation), context);
const matchers = parseTransitionExpr(metadata.expr, context.errors);
return {
type: 1 /* Transition */,
matchers,
animation,
queryCount: context.queryCount,
depCount: context.depCount,
options: normalizeAnimationOptions(metadata.options)
};
}
visitSequence(metadata, context) {
return {
type: 2 /* Sequence */,
steps: metadata.steps.map(s => visitDslNode(this, s, context)),
options: normalizeAnimationOptions(metadata.options)
};
}
visitGroup(metadata, context) {
const currentTime = context.currentTime;
let furthestTime = 0;
const steps = metadata.steps.map(step => {
context.currentTime = currentTime;
const innerAst = visitDslNode(this, step, context);
furthestTime = Math.max(furthestTime, context.currentTime);
return innerAst;
});
context.currentTime = furthestTime;
return {
type: 3 /* Group */,
steps,
options: normalizeAnimationOptions(metadata.options)
};
}
visitAnimate(metadata, context) {
const timingAst = constructTimingAst(metadata.timings, context.errors);
context.currentAnimateTimings = timingAst;
let styleAst;
let styleMetadata = metadata.styles ? metadata.styles : style({});
if (styleMetadata.type == 5 /* Keyframes */) {
styleAst = this.visitKeyframes(styleMetadata, context);
}
else {
let styleMetadata = metadata.styles;
let isEmpty = false;
if (!styleMetadata) {
isEmpty = true;
const newStyleData = {};
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;
styleAst = _styleAst;
}
context.currentAnimateTimings = null;
return {
type: 4 /* Animate */,
timings: timingAst,
style: styleAst,
options: null
};
}
visitStyle(metadata, context) {
const ast = this._makeStyleAst(metadata, context);
this._validateStyleAst(ast, context);
return ast;
}
_makeStyleAst(metadata, context) {
const styles = [];
if (Array.isArray(metadata.styles)) {
metadata.styles.forEach(styleTuple => {
if (typeof styleTuple == 'string') {
if (styleTuple == AUTO_STYLE) {
styles.push(styleTuple);
}
else {
context.errors.push(`The provided style string value ${styleTuple} is not allowed.`);
}
}
else {
styles.push(styleTuple);
}
});
}
else {
styles.push(metadata.styles);
}
let containsDynamicStyles = false;
let collectedEasing = null;
styles.forEach(styleData => {
if (isObject(styleData)) {
const styleMap = styleData;
const easing = styleMap['easing'];
if (easing) {
collectedEasing = easing;
delete styleMap['easing'];
}
if (!containsDynamicStyles) {
for (let prop in styleMap) {
const value = styleMap[prop];
if (value.toString().indexOf(SUBSTITUTION_EXPR_START) >= 0) {
containsDynamicStyles = true;
break;
}
}
}
}
});
return {
type: 6 /* Style */,
styles,
easing: collectedEasing,
offset: metadata.offset,
containsDynamicStyles,
options: null
};
}
_validateStyleAst(ast, context) {
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 => {
if (!this._driver.validateStyleProperty(prop)) {
context.errors.push(`The provided animation property "${prop}" is not a supported CSS property for animations`);
return;
}
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, context) {
const ast = { type: 5 /* Keyframes */, styles: [], options: null };
if (!context.currentAnimateTimings) {
context.errors.push(`keyframes() must be placed inside of a call to animate()`);
return ast;
}
const MAX_KEYFRAME_OFFSET = 1;
let totalKeyframesWithOffsets = 0;
const offsets = [];
let offsetsOutOfOrder = false;
let keyframesOutOfRange = false;
let previousOffset = 0;
const keyframes = metadata.steps.map(styles => {
const style = this._makeStyleAst(styles, context);
let offsetVal = style.offset != null ? style.offset : consumeOffset(style.styles);
let offset = 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;
ast.styles.push(kf);
});
return ast;
}
visitReference(metadata, context) {
return {
type: 8 /* Reference */,
animation: visitDslNode(this, normalizeAnimationEntry(metadata.animation), context),
options: normalizeAnimationOptions(metadata.options)
};
}
visitAnimateChild(metadata, context) {
context.depCount++;
return {
type: 9 /* AnimateChild */,
options: normalizeAnimationOptions(metadata.options)
};
}
visitAnimateRef(metadata, context) {
return {
type: 10 /* AnimateRef */,
animation: this.visitReference(metadata.animation, context),
options: normalizeAnimationOptions(metadata.options)
};
}
visitQuery(metadata, context) {
const parentSelector = context.currentQuerySelector;
const options = (metadata.options || {});
context.queryCount++;
context.currentQuery = metadata;
const [selector, includeSelf] = normalizeSelector(metadata.selector);
context.currentQuerySelector =
parentSelector.length ? (parentSelector + ' ' + selector) : selector;
getOrSetAsInMap(context.collectedStyles, context.currentQuerySelector, {});
const animation = visitDslNode(this, normalizeAnimationEntry(metadata.animation), context);
context.currentQuery = null;
context.currentQuerySelector = parentSelector;
return {
type: 11 /* Query */,
selector,
limit: options.limit || 0,
optional: !!options.optional,
includeSelf,
animation,
originalSelector: metadata.selector,
options: normalizeAnimationOptions(metadata.options)
};
}
visitStagger(metadata, context) {
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);
return {
type: 12 /* Stagger */,
animation: visitDslNode(this, normalizeAnimationEntry(metadata.animation), context),
timings,
options: null
};
}
}
function normalizeSelector(selector) {
const hasAmpersand = selector.split(/\s*,\s*/).find(token => token == SELF_TOKEN) ? true : false;
if (hasAmpersand) {
selector = selector.replace(SELF_TOKEN_REGEX, '');
}
// the :enter and :leave selectors are filled in at runtime during timeline building
selector = 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) {
return obj ? copyObj(obj) : null;
}
class AnimationAstBuilderContext {
constructor(errors) {
this.errors = errors;
this.queryCount = 0;
this.depCount = 0;
this.currentTransition = null;
this.currentQuery = null;
this.currentQuerySelector = null;
this.currentAnimateTimings = null;
this.currentTime = 0;
this.collectedStyles = {};
this.options = null;
}
}
function consumeOffset(styles) {
if (typeof styles == 'string')
return null;
let offset = null;
if (Array.isArray(styles)) {
styles.forEach(styleTuple => {
if (isObject(styleTuple) && styleTuple.hasOwnProperty('offset')) {
const obj = styleTuple;
offset = parseFloat(obj['offset']);
delete obj['offset'];
}
});
}
else if (isObject(styles) && styles.hasOwnProperty('offset')) {
const obj = styles;
offset = parseFloat(obj['offset']);
delete obj['offset'];
}
return offset;
}
function isObject(value) {
return !Array.isArray(value) && typeof value == 'object';
}
function constructTimingAst(value, errors) {
let timings = null;
if (value.hasOwnProperty('duration')) {
timings = value;
}
else if (typeof value == 'number') {
const duration = resolveTiming(value, errors).duration;
return makeTimingAst(duration, 0, '');
}
const strValue = value;
const isDynamic = strValue.split(/\s+/).some(v => v.charAt(0) == '{' && v.charAt(1) == '{');
if (isDynamic) {
const ast = makeTimingAst(0, 0, '');
ast.dynamic = true;
ast.strValue = strValue;
return ast;
}
timings = timings || resolveTiming(strValue, errors);
return makeTimingAst(timings.duration, timings.delay, timings.easing);
}
function normalizeAnimationOptions(options) {
if (options) {
options = copyObj(options);
if (options['params']) {
options['params'] = normalizeParams(options['params']);
}
}
else {
options = {};
}
return options;
}
function makeTimingAst(duration, delay, easing) {
return { duration, delay, easing };
}
function createTimelineInstruction(element, keyframes, preStyleProps, postStyleProps, duration, delay, easing = null, subTimeline = false) {
return {
type: 1 /* TimelineAnimation */,
element,
keyframes,
preStyleProps,
postStyleProps,
duration,
delay,
totalTime: duration + delay,
easing,
subTimeline
};
}
class ElementInstructionMap {
constructor() {
this._map = new Map();
}
consume(element) {
let instructions = this._map.get(element);
if (instructions) {
this._map.delete(element);
}
else {
instructions = [];
}
return instructions;
}
append(element, instructions) {
let existingInstructions = this._map.get(element);
if (!existingInstructions) {
this._map.set(element, existingInstructions = []);
}
existingInstructions.push(...instructions);
}
has(element) {
return this._map.has(element);
}
clear() {
this._map.clear();
}
}
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
const ONE_FRAME_IN_MILLISECONDS = 1;
const ENTER_TOKEN = ':enter';
const ENTER_TOKEN_REGEX = new RegExp(ENTER_TOKEN, 'g');
const LEAVE_TOKEN = ':leave';
const LEAVE_TOKEN_REGEX = new RegExp(LEAVE_TOKEN, 'g');
/*
* 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.
*/
function buildAnimationTimelines(driver, rootElement, ast, enterClassName, leaveClassName, startingStyles = {}, finalStyles = {}, options, subInstructions, errors = []) {
return new AnimationTimelineBuilderVisitor().buildKeyframes(driver, rootElement, ast, enterClassName, leaveClassName, startingStyles, finalStyles, options, subInstructions, errors);
}
class AnimationTimelineBuilderVisitor {
buildKeyframes(driver, rootElement, ast, enterClassName, leaveClassName, startingStyles, finalStyles, options, subInstructions, errors = []) {
subInstructions = subInstructions || new ElementInstructionMap();
const context = new AnimationTimelineContext(driver, rootElement, subInstructions, enterClassName, leaveClassName, errors, []);
context.options = options;
context.currentTimeline.setStyles([startingStyles], null, context.errors, options);
visitDslNode(this, ast, 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, context) {
// these values are not visited in this AST
}
visitState(ast, context) {
// these values are not visited in this AST
}
visitTransition(ast, context) {
// these values are not visited in this AST
}
visitAnimateChild(ast, context) {
const elementInstructions = context.subInstructions.