UNPKG

@angular/animations

Version:

Angular - animations integration with web-animations

1,366 lines (1,354 loc) 125 kB
/** * @license Angular v21.0.6 * (c) 2010-2025 Google LLC. https://angular.dev/ * License: MIT */ import * as i0 from '@angular/core'; import { Injectable } from '@angular/core'; import { validateStyleProperty, containsElement, getParentElement, invokeQuery, dashCaseToCamelCase, invalidCssUnitValue, invalidExpression, invalidTransitionAlias, visitDslNode, invalidTrigger, invalidDefinition, extractStyleParams, invalidState, invalidStyleValue, SUBSTITUTION_EXPR_START, invalidParallelAnimation, validateStyleParams, invalidKeyframes, invalidOffset, keyframeOffsetsOutOfOrder, keyframesMissingOffsets, getOrSetDefaultValue, invalidStagger, resolveTiming, NG_TRIGGER_SELECTOR, NG_ANIMATING_SELECTOR, normalizeAnimationEntry, resolveTimingValue, interpolateParams, invalidQuery, registerFailed, normalizeKeyframes, LEAVE_CLASSNAME, ENTER_CLASSNAME, missingOrDestroyedAnimation, createAnimationFailed, optimizeGroupPlayer, missingPlayer, listenOnPlayer, makeAnimationEvent, triggerTransitionsFailed, eraseStyles, setStyles, transitionFailed, missingTrigger, missingEvent, unsupportedTriggerEvent, NG_TRIGGER_CLASSNAME, unregisteredTrigger, NG_ANIMATING_CLASSNAME, triggerBuildFailed, parseTimelineCommand, computeStyle, camelCaseToDashCase, validateWebAnimatableStyleProperty, allowPreviousPlayerStylesMerge, normalizeKeyframes$1, balancePreviousStylesIntoKeyframes, validationFailed, normalizeStyles, buildingFailed } from './_util-chunk.mjs'; import { NoopAnimationPlayer, AnimationMetadataType, style, AUTO_STYLE, ɵPRE_STYLE as _PRE_STYLE, AnimationGroupPlayer } from './_private_export-chunk.mjs'; class NoopAnimationDriver { validateStyleProperty(prop) { return validateStyleProperty(prop); } containsElement(elm1, elm2) { return containsElement(elm1, elm2); } getParentElement(element) { return getParentElement(element); } 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); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NoopAnimationDriver, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NoopAnimationDriver }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NoopAnimationDriver, decorators: [{ type: Injectable }] }); class AnimationDriver { static NOOP = new NoopAnimationDriver(); } class AnimationStyleNormalizer {} class NoopAnimationStyleNormalizer { normalizePropertyName(propertyName, errors) { return propertyName; } normalizeStyleValue(userProvidedProperty, normalizedProperty, value, errors) { return value; } } const DIMENSIONAL_PROP_SET = new Set(['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', 'perspective']); class WebAnimationsStyleNormalizer extends AnimationStyleNormalizer { normalizePropertyName(propertyName, errors) { return dashCaseToCamelCase(propertyName); } normalizeStyleValue(userProvidedProperty, normalizedProperty, value, errors) { let unit = ''; const strVal = value.toString().trim(); if (DIMENSIONAL_PROP_SET.has(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(invalidCssUnitValue(userProvidedProperty, value)); } } } return strVal + unit; } } function createListOfWarnings(warnings) { const LINE_START = '\n - '; return `${LINE_START}${warnings.filter(Boolean).map(warning => warning).join(LINE_START)}`; } function warnValidation(warnings) { console.warn(`animation validation warnings:${createListOfWarnings(warnings)}`); } function warnTriggerBuild(name, warnings) { console.warn(`The animation trigger "${name}" has built with the following warnings:${createListOfWarnings(warnings)}`); } function warnRegister(warnings) { console.warn(`Animation built with the following warnings:${createListOfWarnings(warnings)}`); } function pushUnrecognizedPropertiesWarning(warnings, props) { if (props.length) { warnings.push(`The following provided properties are not recognized: ${props.join(', ')}`); } } 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(invalidExpression(eventStr)); 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)); } return; } 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(invalidTransitionAlias(alias)); return '* => *'; } } 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; }; } const SELF_TOKEN = ':self'; const SELF_TOKEN_REGEX = /* @__PURE__ */new RegExp(`s*${SELF_TOKEN}s*,?`, 'g'); function buildAnimationAst(driver, metadata, errors, warnings) { return new AnimationAstBuilderVisitor(driver).build(metadata, errors, warnings); } const ROOT_SELECTOR = ''; class AnimationAstBuilderVisitor { _driver; constructor(_driver) { this._driver = _driver; } build(metadata, errors, warnings) { const context = new AnimationAstBuilderContext(errors); this._resetContextStyleTimingState(context); const ast = visitDslNode(this, normalizeAnimationEntry(metadata), context); if (typeof ngDevMode === 'undefined' || ngDevMode) { if (context.unsupportedCSSPropertiesFound.size) { pushUnrecognizedPropertiesWarning(warnings, [...context.unsupportedCSSPropertiesFound.keys()]); } } return ast; } _resetContextStyleTimingState(context) { context.currentQuerySelector = ROOT_SELECTOR; context.collectedStyles = new Map(); context.collectedStyles.set(ROOT_SELECTOR, new Map()); 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(invalidTrigger()); } metadata.definitions.forEach(def => { this._resetContextStyleTimingState(context); if (def.type == AnimationMetadataType.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 == AnimationMetadataType.Transition) { const transition = this.visitTransition(def, context); queryCount += transition.queryCount; depCount += transition.depCount; transitions.push(transition); } else { context.errors.push(invalidDefinition()); } }); return { type: AnimationMetadataType.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(style => { if (style instanceof Map) { style.forEach(value => { extractStyleParams(value).forEach(sub => { if (!params.hasOwnProperty(sub)) { missingSubs.add(sub); } }); }); } }); if (missingSubs.size) { context.errors.push(invalidState(metadata.name, [...missingSubs.values()])); } } return { type: AnimationMetadataType.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: AnimationMetadataType.Transition, matchers, animation, queryCount: context.queryCount, depCount: context.depCount, options: normalizeAnimationOptions(metadata.options) }; } visitSequence(metadata, context) { return { type: AnimationMetadataType.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: AnimationMetadataType.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 == AnimationMetadataType.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: AnimationMetadataType.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 = []; const metadataStyles = Array.isArray(metadata.styles) ? metadata.styles : [metadata.styles]; for (let styleTuple of metadataStyles) { if (typeof styleTuple === 'string') { if (styleTuple === AUTO_STYLE) { styles.push(styleTuple); } else { context.errors.push(invalidStyleValue(styleTuple)); } } else { styles.push(new Map(Object.entries(styleTuple))); } } let containsDynamicStyles = false; let collectedEasing = null; styles.forEach(styleData => { if (styleData instanceof Map) { if (styleData.has('easing')) { collectedEasing = styleData.get('easing'); styleData.delete('easing'); } if (!containsDynamicStyles) { for (let value of styleData.values()) { if (value.toString().indexOf(SUBSTITUTION_EXPR_START) >= 0) { containsDynamicStyles = true; break; } } } } }); return { type: AnimationMetadataType.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; tuple.forEach((value, prop) => { if (typeof ngDevMode === 'undefined' || ngDevMode) { if (!this._driver.validateStyleProperty(prop)) { tuple.delete(prop); context.unsupportedCSSPropertiesFound.add(prop); return; } } const collectedStyles = context.collectedStyles.get(context.currentQuerySelector); const collectedEntry = collectedStyles.get(prop); let updateCollectedStyle = true; if (collectedEntry) { if (startTime != endTime && startTime >= collectedEntry.startTime && endTime <= collectedEntry.endTime) { context.errors.push(invalidParallelAnimation(prop, collectedEntry.startTime, collectedEntry.endTime, startTime, endTime)); updateCollectedStyle = false; } startTime = collectedEntry.startTime; } if (updateCollectedStyle) { collectedStyles.set(prop, { startTime, endTime }); } if (context.options) { validateStyleParams(value, context.options, context.errors); } }); }); } visitKeyframes(metadata, context) { const ast = { type: AnimationMetadataType.Keyframes, styles: [], options: null }; if (!context.currentAnimateTimings) { context.errors.push(invalidKeyframes()); 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(invalidOffset()); } if (offsetsOutOfOrder) { context.errors.push(keyframeOffsetsOutOfOrder()); } const length = metadata.steps.length; let generatedOffset = 0; if (totalKeyframesWithOffsets > 0 && totalKeyframesWithOffsets < length) { context.errors.push(keyframesMissingOffsets()); } 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: AnimationMetadataType.Reference, animation: visitDslNode(this, normalizeAnimationEntry(metadata.animation), context), options: normalizeAnimationOptions(metadata.options) }; } visitAnimateChild(metadata, context) { context.depCount++; return { type: AnimationMetadataType.AnimateChild, options: normalizeAnimationOptions(metadata.options) }; } visitAnimateRef(metadata, context) { return { type: AnimationMetadataType.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; getOrSetDefaultValue(context.collectedStyles, context.currentQuerySelector, new Map()); const animation = visitDslNode(this, normalizeAnimationEntry(metadata.animation), context); context.currentQuery = null; context.currentQuerySelector = parentSelector; return { type: AnimationMetadataType.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(invalidStagger()); } const timings = metadata.timings === 'full' ? { duration: 0, delay: 0, easing: 'full' } : resolveTiming(metadata.timings, context.errors, true); return { type: AnimationMetadataType.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, ''); } selector = selector.replace(/@\*/g, NG_TRIGGER_SELECTOR).replace(/@\w+/g, match => NG_TRIGGER_SELECTOR + '-' + match.slice(1)).replace(/:animating/g, NG_ANIMATING_SELECTOR); return [selector, hasAmpersand]; } function normalizeParams(obj) { return obj ? { ...obj } : null; } class AnimationAstBuilderContext { errors; queryCount = 0; depCount = 0; currentTransition = null; currentQuery = null; currentQuerySelector = null; currentAnimateTimings = null; currentTime = 0; collectedStyles = new Map(); options = null; unsupportedCSSPropertiesFound = new Set(); constructor(errors) { this.errors = errors; } } function consumeOffset(styles) { if (typeof styles == 'string') return null; let offset = null; if (Array.isArray(styles)) { styles.forEach(styleTuple => { if (styleTuple instanceof Map && styleTuple.has('offset')) { const obj = styleTuple; offset = parseFloat(obj.get('offset')); obj.delete('offset'); } }); } else if (styles instanceof Map && styles.has('offset')) { const obj = styles; offset = parseFloat(obj.get('offset')); obj.delete('offset'); } return offset; } function constructTimingAst(value, errors) { if (value.hasOwnProperty('duration')) { return value; } 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; } const timings = resolveTiming(strValue, errors); return makeTimingAst(timings.duration, timings.delay, timings.easing); } function normalizeAnimationOptions(options) { if (options) { options = { ...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, element, keyframes, preStyleProps, postStyleProps, duration, delay, totalTime: duration + delay, easing, subTimeline }; } class ElementInstructionMap { _map = new Map(); get(element) { return this._map.get(element) || []; } 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(); } } const ONE_FRAME_IN_MILLISECONDS = 1; const ENTER_TOKEN = ':enter'; const ENTER_TOKEN_REGEX = /* @__PURE__ */new RegExp(ENTER_TOKEN, 'g'); const LEAVE_TOKEN = ':leave'; const LEAVE_TOKEN_REGEX = /* @__PURE__ */new RegExp(LEAVE_TOKEN, 'g'); function buildAnimationTimelines(driver, rootElement, ast, enterClassName, leaveClassName, startingStyles = new Map(), finalStyles = new Map(), 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; const delay = options.delay ? resolveTimingValue(options.delay) : 0; context.currentTimeline.delayNextStep(delay); context.currentTimeline.setStyles([startingStyles], null, context.errors, options); visitDslNode(this, ast, context); const timelines = context.timelines.filter(timeline => timeline.containsAnimation()); if (timelines.length && finalStyles.size) { let lastRootTimeline; for (let i = timelines.length - 1; i >= 0; i--) { const timeline = timelines[i]; if (timeline.element === rootElement) { lastRootTimeline = timeline; break; } } if (lastRootTimeline && !lastRootTimeline.allowOnlyTimelineStyles()) { lastRootTimeline.setStyles([finalStyles], null, context.errors, options); } } return timelines.length ? timelines.map(timeline => timeline.buildKeyframes()) : [createTimelineInstruction(rootElement, [], [], [], 0, delay, '', false)]; } visitTrigger(ast, context) {} visitState(ast, context) {} visitTransition(ast, context) {} visitAnimateChild(ast, context) { const elementInstructions = context.subInstructions.get(context.element); if (elementInstructions) { const innerContext = context.createSubContext(ast.options); const startTime = context.currentTimeline.currentTime; const endTime = this._visitSubInstructions(elementInstructions, innerContext, innerContext.options); if (startTime != endTime) { context.transformIntoNewTimeline(endTime); } } context.previousNode = ast; } visitAnimateRef(ast, context) { const innerContext = context.createSubContext(ast.options); innerContext.transformIntoNewTimeline(); this._applyAnimationRefDelays([ast.options, ast.animation.options], context, innerContext); this.visitReference(ast.animation, innerContext); context.transformIntoNewTimeline(innerContext.currentTimeline.currentTime); context.previousNode = ast; } _applyAnimationRefDelays(animationsRefsOptions, context, innerContext) { for (const animationRefOptions of animationsRefsOptions) { const animationDelay = animationRefOptions?.delay; if (animationDelay) { const animationDelayValue = typeof animationDelay === 'number' ? animationDelay : resolveTimingValue(interpolateParams(animationDelay, animationRefOptions?.params ?? {}, context.errors)); innerContext.delayNextStep(animationDelayValue); } } } _visitSubInstructions(instructions, context, options) { const startTime = context.currentTimeline.currentTime; let furthestTime = startTime; const duration = options.duration != null ? resolveTimingValue(options.duration) : null; const delay = options.delay != null ? resolveTimingValue(options.delay) : null; if (duration !== 0) { instructions.forEach(instruction => { const instructionTimings = context.appendInstructionToTimeline(instruction, duration, delay); furthestTime = Math.max(furthestTime, instructionTimings.duration + instructionTimings.delay); }); } return furthestTime; } visitReference(ast, context) { context.updateOptions(ast.options, true); visitDslNode(this, ast.animation, context); context.previousNode = ast; } visitSequence(ast, context) { const subContextCount = context.subContextCount; let ctx = context; const options = ast.options; if (options && (options.params || options.delay)) { ctx = context.createSubContext(options); ctx.transformIntoNewTimeline(); if (options.delay != null) { if (ctx.previousNode.type == AnimationMetadataType.Style) { ctx.currentTimeline.snapshotCurrentStyles(); ctx.previousNode = DEFAULT_NOOP_PREVIOUS_NODE; } const delay = resolveTimingValue(options.delay); ctx.delayNextStep(delay); } } if (ast.steps.length) { ast.steps.forEach(s => visitDslNode(this, s, ctx)); ctx.currentTimeline.applyStylesToKeyframe(); if (ctx.subContextCount > subContextCount) { ctx.transformIntoNewTimeline(); } } context.previousNode = ast; } visitGroup(ast, context) { const innerTimelines = []; let furthestTime = context.currentTimeline.currentTime; const delay = ast.options && ast.options.delay ? resolveTimingValue(ast.options.delay) : 0; ast.steps.forEach(s => { const innerContext = context.createSubContext(ast.options); if (delay) { innerContext.delayNextStep(delay); } visitDslNode(this, s, innerContext); furthestTime = Math.max(furthestTime, innerContext.currentTimeline.currentTime); innerTimelines.push(innerContext.currentTimeline); }); innerTimelines.forEach(timeline => context.currentTimeline.mergeTimelineCollectedStyles(timeline)); context.transformIntoNewTimeline(furthestTime); context.previousNode = ast; } _visitTiming(ast, context) { if (ast.dynamic) { const strValue = ast.strValue; const timingValue = context.params ? interpolateParams(strValue, context.params, context.errors) : strValue; return resolveTiming(timingValue, context.errors); } else { return { duration: ast.duration, delay: ast.delay, easing: ast.easing }; } } visitAnimate(ast, context) { const timings = context.currentAnimateTimings = this._visitTiming(ast.timings, context); const timeline = context.currentTimeline; if (timings.delay) { context.incrementTime(timings.delay); timeline.snapshotCurrentStyles(); } const style = ast.style; if (style.type == AnimationMetadataType.Keyframes) { this.visitKeyframes(style, context); } else { context.incrementTime(timings.duration); this.visitStyle(style, context); timeline.applyStylesToKeyframe(); } context.currentAnimateTimings = null; context.previousNode = ast; } visitStyle(ast, context) { const timeline = context.currentTimeline; const timings = context.currentAnimateTimings; if (!timings && timeline.hasCurrentStyleProperties()) { timeline.forwardFrame(); } const easing = timings && timings.easing || ast.easing; if (ast.isEmptyStep) { timeline.applyEmptyStep(easing); } else { timeline.setStyles(ast.styles, easing, context.errors, context.options); } context.previousNode = ast; } visitKeyframes(ast, context) { const currentAnimateTimings = context.currentAnimateTimings; const startTime = context.currentTimeline.duration; const duration = currentAnimateTimings.duration; const innerContext = context.createSubContext(); const innerTimeline = innerContext.currentTimeline; innerTimeline.easing = currentAnimateTimings.easing; ast.styles.forEach(step => { const offset = step.offset || 0; innerTimeline.forwardTime(offset * duration); innerTimeline.setStyles(step.styles, step.easing, context.errors, context.options); innerTimeline.applyStylesToKeyframe(); }); context.currentTimeline.mergeTimelineCollectedStyles(innerTimeline); context.transformIntoNewTimeline(startTime + duration); context.previousNode = ast; } visitQuery(ast, context) { const startTime = context.currentTimeline.currentTime; const options = ast.options || {}; const delay = options.delay ? resolveTimingValue(options.delay) : 0; if (delay && (context.previousNode.type === AnimationMetadataType.Style || startTime == 0 && context.currentTimeline.hasCurrentStyleProperties())) { context.currentTimeline.snapshotCurrentStyles(); context.previousNode = DEFAULT_NOOP_PREVIOUS_NODE; } let furthestTime = startTime; const elms = context.invokeQuery(ast.selector, ast.originalSelector, ast.limit, ast.includeSelf, options.optional ? true : false, context.errors); context.currentQueryTotal = elms.length; let sameElementTimeline = null; elms.forEach((element, i) => { context.currentQueryIndex = i; const innerContext = context.createSubContext(ast.options, element); if (delay) { innerContext.delayNextStep(delay); } if (element === context.element) { sameElementTimeline = innerContext.currentTimeline; } visitDslNode(this, ast.animation, innerContext); innerContext.currentTimeline.applyStylesToKeyframe(); const endTime = innerContext.currentTimeline.currentTime; furthestTime = Math.max(furthestTime, endTime); }); context.currentQueryIndex = 0; context.currentQueryTotal = 0; context.transformIntoNewTimeline(furthestTime); if (sameElementTimeline) { context.currentTimeline.mergeTimelineCollectedStyles(sameElementTimeline); context.currentTimeline.snapshotCurrentStyles(); } context.previousNode = ast; } visitStagger(ast, context) { const parentContext = context.parentContext; const tl = context.currentTimeline; const timings = ast.timings; const duration = Math.abs(timings.duration); const maxTime = duration * (context.currentQueryTotal - 1); let delay = duration * context.currentQueryIndex; let staggerTransformer = timings.duration < 0 ? 'reverse' : timings.easing; switch (staggerTransformer) { case 'reverse': delay = maxTime - delay; break; case 'full': delay = parentContext.currentStaggerTime; break; } const timeline = context.currentTimeline; if (delay) { timeline.delayNextStep(delay); } const startingTime = timeline.currentTime; visitDslNode(this, ast.animation, context); context.previousNode = ast; parentContext.currentStaggerTime = tl.currentTime - startingTime + (tl.startTime - parentContext.currentTimeline.startTime); } } const DEFAULT_NOOP_PREVIOUS_NODE = {}; class AnimationTimelineContext { _driver; element; subInstructions; _enterClassName; _leaveClassName; errors; timelines; parentContext = null; currentTimeline; currentAnimateTimings = null; previousNode = DEFAULT_NOOP_PREVIOUS_NODE; subContextCount = 0; options = {}; currentQueryIndex = 0; currentQueryTotal = 0; currentStaggerTime = 0; constructor(_driver, element, subInstructions, _enterClassName, _leaveClassName, errors, timelines, initialTimeline) { this._driver = _driver; this.element = element; this.subInstructions = subInstructions; this._enterClassName = _enterClassName; this._leaveClassName = _leaveClassName; this.errors = errors; this.timelines = timelines; this.currentTimeline = initialTimeline || new TimelineBuilder(this._driver, element, 0); timelines.push(this.currentTimeline); } get params() { return this.options.params; } updateOptions(options, skipIfExists) { if (!options) return; const newOptions = options; let optionsToUpdate = this.options; if (newOptions.duration != null) { optionsToUpdate.duration = resolveTimingValue(newOptions.duration); } if (newOptions.delay != null) { optionsToUpdate.delay = resolveTimingValue(newOptions.delay); } const newParams = newOptions.params; if (newParams) { let paramsToUpdate = optionsToUpdate.params; if (!paramsToUpdate) { paramsToUpdate = this.options.params = {}; } Object.keys(newParams).forEach(name => { if (!skipIfExists || !paramsToUpdate.hasOwnProperty(name)) { paramsToUpdate[name] = interpolateParams(newParams[name], paramsToUpdate, this.errors); } }); } } _copyOptions() { const options = {}; if (this.options) { const oldParams = this.options.params; if (oldParams) { const params = options['params'] = {}; Object.keys(oldParams).forEach(name => { params[name] = oldParams[name]; }); } } return options; } createSubContext(options = null, element, newTime) { const target = element || this.element; const context = new AnimationTimelineContext(this._driver, target, this.subInstructions, this._enterClassName, this._leaveClassName, this.errors, this.timelines, this.currentTimeline.fork(target, newTime || 0)); context.previousNode = this.previousNode; context.currentAnimateTimings = this.currentAnimateTimings; context.options = this._copyOptions(); context.updateOptions(options); context.currentQueryIndex = this.currentQueryIndex; context.currentQueryTotal = this.currentQueryTotal; context.parentContext = this; this.subContextCount++; return context; } transformIntoNewTimeline(newTime) { this.previousNode = DEFAULT_NOOP_PREVIOUS_NODE; this.currentTimeline = this.currentTimeline.fork(this.element, newTime); this.timelines.push(this.currentTimeline); return this.currentTimeline; } appendInstructionToTimeline(instruction, duration, delay) { const updatedTimings = { duration: duration != null ? duration : instruction.duration, delay: this.currentTimeline.currentTime + (delay != null ? delay : 0) + instruction.delay, easing: '' }; const builder = new SubTimelineBuilder(this._driver, instruction.element, instruction.keyframes, instruction.preStyleProps, instruction.postStyleProps, updatedTimings, instruction.stretchStartingKeyframe); this.timelines.push(builder); return updatedTimings; } incrementTime(time) { this.currentTimeline.forwardTime(this.currentTimeline.duration + time); } delayNextStep(delay) { if (delay > 0) { this.currentTimeline.delayNextStep(delay); } } invokeQuery(selector, originalSelector, limit, includeSelf, optional, errors) { let results = []; if (includeSelf) { results.push(this.element); } if (selector.length > 0) { selector = selector.replace(ENTER_TOKEN_REGEX, '.' + this._enterClassName); selector = selector.replace(LEAVE_TOKEN_REGEX, '.' + this._leaveClassName); const multi = limit != 1; let elements = this._driver.query(this.element, selector, multi); if (limit !== 0) { elements = limit < 0 ? elements.slice(elements.length + limit, elements.length) : elements.slice(0, limit); } results.push(...elements); } if (!optional && results.length == 0) { errors.push(invalidQuery(originalSelector)); } return results; } } class TimelineBuilder { _driver; element; startTime; _elementTimelineStylesLookup; duration = 0; easing = null; _previousKeyframe = new Map(); _currentKeyframe = new Map(); _keyframes = new Map(); _styleSummary = new Map(); _localTimelineStyles = new Map(); _globalTimelineStyles; _pendingStyles = new Map(); _backFill = new Map(); _currentEmptyStepKeyframe = null; constructor(_driver, element, startTime, _elementTimelineStylesLookup) { this._driver = _driver; this.element = element; this.startTime = startTime; this._elementTimelineStylesLookup = _elementTimelineStylesLookup; if (!this._elementTimelineStylesLookup) { this._elementTimelineStylesLookup = new Map(); } this._globalTimelineStyles = this._elementTimelineStylesLookup.get(element); if (!this._globalTimelineStyles) { this._globalTimelineStyles = this._localTimelineStyles; this._elementTimelineStylesLookup.set(element, this._localTimelineStyles); } this._loadKeyframe(); } containsAnimation() { switch (this._keyframes.size) { case 0: return false; case 1: return this.hasCurrentStyleProperties(); default: return true; } } hasCurrentStyleProperties() { return this._currentKeyframe.size > 0; } get currentTime() { return this.startTime + this.duration; } delayNextStep(delay) { const hasPreStyleStep = this._keyframes.size === 1 && this._pendingStyles.size; if (this.duration || hasPreStyleStep) { this.forwardTime(this.currentTime + delay); if (hasPreStyleStep) { this.snapshotCurrentStyles(); } } else { this.startTime += delay; } } fork(element, currentTime) { this.applyStylesToKeyframe(); return new TimelineBuilder(this._driver, element, currentTime || this.currentTime, this._elementTimelineStylesLookup); } _loadKeyframe() { if (this._currentKeyframe) { this._previousKeyframe = this._currentKeyframe; } this._currentKeyframe = this._keyframes.get(this.duration); if (!this._currentKeyframe) { this._currentKeyframe = new Map(); this._keyframes.set(this.duration, this._currentKeyframe); } } forwardFrame() { this.duration += ONE_FRAME_IN_MILLISECONDS; this._loadKeyframe(); } forwardTime(time) { this.applyStylesToKeyframe(); this.duration = time; this._loadKeyframe(); } _updateStyle(prop, value) { this._localTimelineStyles.set(prop, value); this._globalTimelineStyles.set(prop, value); this._styleSummary.set(prop, { time: this.currentTime, value }); } allowOnlyTimelineStyles() { return this._currentEmptyStepKeyframe !== this._currentKeyframe; } applyEmptyStep(easing) { if (easing) { this._previousKeyframe.set('easing', easing); } for (let [prop, value] of this._globalTimelineStyles) { this._backFill.set(prop, value || AUTO_STYLE); this._currentKeyframe.set(prop, AUTO_STYLE); } this._currentEmptyStepKeyframe = this._currentKeyframe; } setStyles(input, easing, errors, options) { if (easing) { this._previousKeyframe.set('easing', easing); } const params = options && options.params || {}; const styles = flattenStyles(input, this._globalTimelineStyles); for (let [prop, value] of styles) { const val = interpolateParams(value, params, errors); this._pendingStyles.set(prop, val); if (!this._localTimelineStyles.has(prop)) { this._backFill.set(prop, this._globalTimelineStyles.get(prop) ?? AUTO_STYLE); } this._updateStyle(prop, val); } } applyStylesToKeyframe() { if (this._pendingStyles.size == 0) return; this._pendingStyles.forEach((val, prop) => { this._currentKeyframe.set(prop, val); }); this._pendingStyles.clear(); this._localTimelineStyles.forEach((val, prop) => { if (!this._currentKeyframe.has(prop)) { this._currentKeyframe.set(prop, val); } }); } snapshotCurrentStyles() { for (let [prop, val] of this._localTimelineStyles) { this._pendingStyles.set(prop, val); this._updateStyle(prop, val); } } getFinalKeyframe() { return this._keyframes.get(this.duration); } get properties() { const properties = []; for (let prop in this._currentKeyframe) { properties.push(prop); } return properties; } mergeTimelineCollectedStyles(timeline) { timeline._styleSummary.forEach((details1, prop) => { const details0 = this._styleSummary.get(prop); if (!details0 || details1.time > details0.time) { this._updateStyle(prop, details1.value); } }); } buildKeyframes() { this.applyStylesToKeyframe(); const preStyleProps = new Set(); const postStyleProps = new Set(); const isEmpty = this._keyframes.size === 1 && this.duration === 0; let finalKeyframes = []; this._keyframes.forEach((keyframe, time) => { const finalKeyframe = new Map([...this._backFill, ...keyframe]); finalKeyframe.forEach((value, prop) => { if (value === _PRE_STYLE) { preStyleProps.add(prop); } else if (value === AUTO_STYLE) { postStyleProps.add(prop); } }); if (!isEmpty) { finalKeyframe.set('offset', time / this.duration); } finalKeyframes.push(finalKeyframe); }); const preProps = [...preStyleProps.values()]; const postProps = [...postStyleProps.values()]; if (isEmpty) { const kf0 = finalKeyframes[0]; const kf1 = new Map(kf0); kf0.set('offset', 0); kf1.set('offset', 1); finalKeyframes = [kf0, kf1]; } return createTimelineInstruction(this.element, finalKeyframes, preProps, postProps, this.duration, this.startTime, this.easing, false); } } class SubTimelineBuilder extends TimelineBuilder { keyframes; preStyleProps; postStyleProps; _stretchStartingKeyframe; timings; constructor(driver, element, keyframes, preStyleProps, postStyleProps, timings, _stretchStartingKeyframe = false) { super(driver, element, timings.delay); this.keyframes = keyframes; this.preStyleProps = preStyleProps; this.postStyleProps = postStyleProps; this._stretchStartingKeyframe = _stretchStartingKeyframe; this.timings = { duration: timings.duration, delay: timings.delay, easing: timings.easing }; } containsAnimation() { return this.keyframes.length > 1; } buildKeyframes() { let keyframes = this.keyframes; let { delay, duration, easing } = this.timings; if (this._stretchStartingKeyframe && delay) { const newKeyframes = []; const totalTime = duration + delay; const startingGap = delay / totalTime; const newFirstKeyframe = new Map(keyframes[0]); newFirstKeyframe.set('offset', 0); newKeyframes.push(newFirstKeyframe); const oldFirstKeyframe = new Map(keyframes[0]); oldFirstKeyframe.set('offset', roundOffset(startingGap)); newKeyframes.push(oldFirstKeyframe); const limit = keyframes.length - 1; for (let i = 1; i <= limit; i++) { let kf = new Map(keyframes[i]); const oldOffset = kf.get('offset'); const timeAtKeyframe = delay + oldOffset * duration; kf.set('offset', roundOffset(timeAtKeyframe / totalTime)); newKeyframes.push(kf); } duration = totalTime; delay = 0; easing = ''; keyframes = newKeyframes; } return createTimelineInstruction(this.element, keyframes, this.preStyleProps, this.postStyleProps, duration, delay, easing, true); } } function roundOffset(offset, decimalPoints = 3) { const mult = Math.pow(10, decimalPoints - 1); return Math.round(offset * mult) / mult; } function flattenStyles(input, allStyles) { const styles = new Map(); let allProperties; input.forEach(token => { if (token === '*') { allProperties ??= allStyles.keys(); for (let prop of allProperties) { styles.set(prop, AUTO_STYLE); } } else { for (let [prop, val] of token) { styles.set(prop, val); } } }); return styles; } function createTransitionInstruction(element, triggerName, fromState, toState, isRemovalTransition, fromStyles, toStyles, timelines, queriedElements, preStyleProps, postStyleProps, totalTime, errors) { return { type: 0, element, triggerName, isRemovalTransition, fromState, fromStyles, toState, toStyles, timelines, queriedElements, preStyleProps, postStyleProps, totalTime, errors }; } const EMPTY_OBJECT = {}; class AnimationTransitionFactory { _triggerName; ast; _stateStyles; constructor(_triggerName, ast, _stateStyles) { this._triggerName = _triggerName; this.ast = ast; this._stateStyles = _stateStyles; } match(currentState, nextState, element, params) { return oneOrMoreTransitionsMatch(this.ast.matchers, currentState, nextState, element, params); } buildStyles(stateName, params, errors) { let styler = this._stateStyles.get('*'); if (stateName !== undefined) { styler = this._stateStyles.get(stateName?.toString()) || styler; } return styler ? styler.buildStyles(params, errors) : new Map(); } build(driver, element, currentState, nextState, enterClassName, leaveClassName, currentOptions, nextOptions, subInstructions, skipAstBuild) { const errors = []; const transitionAnimationParams = this.ast.options && this.ast.options.params || EMPTY_OBJECT; const currentAnimationParams = currentOptions && currentOptions.params || EMPTY_OBJECT; const currentStateStyles = this.buildStyles(currentState, currentAnimationParams, errors); const nextAnimationParams = nextOptions && nextOptions.params || EMPTY_OBJECT; const nextStateStyles = this.buildStyles(nextState, nextAnimationParams, errors); const queriedElements = new Set(); const preStyleMap = new Map(); const postStyleMap = new Map(); const isRemoval = nextState === 'void'; const animationOptions = { params: applyParamDefaults(nextAnimationParams, transitionAnimationParams), delay: this.ast.options?.delay }; const timelines = skipAstBuild ? [] : buildAnimationTimelines(driver, element, this.ast.animation, enterClassName, leaveClassName, currentStateStyles, nextStateStyles, animationOptions, subInstructions, errors); let totalTime = 0; timelines.forEach(tl => { totalTime = Math.max(tl.duration + tl.delay, totalTime); }); if (errors.length) { return createTransitionInstruction(element, this._triggerName, currentState, nextState, isRemoval, currentStateStyles, nextStateStyles, [], [], preStyleMap, postStyleMap, totalTime, errors); } timelines.forEach(tl => { const elm = tl.element; const preProps = getOrSetDefaultValue(preStyleMap, elm, new Set()); tl.preStyleProps.forEach(prop => preProps.add(prop)); const postProps = getOrSetDefaultValue(postStyleMap, elm, new S