fix(core): CSS animation parsing (#10245)

This commit is contained in:
Igor Randjelovic
2023-03-22 22:16:15 +01:00
committed by GitHub
parent 5f96ffe7d9
commit ab436dbfe6
6 changed files with 924 additions and 123 deletions

View File

@@ -7,13 +7,13 @@ import { transformConverter } from '../styling/style-properties';
import { cleanupImportantFlags } from './css-utils';
const ANIMATION_PROPERTY_HANDLERS = Object.freeze({
'animation-name': (info: any, value: any) => (info.name = value),
'animation-name': (info: any, value: any) => (info.name = value.replace(/['"]/g, '')),
'animation-duration': (info: any, value: any) => (info.duration = timeConverter(value)),
'animation-delay': (info: any, value: any) => (info.delay = timeConverter(value)),
'animation-timing-function': (info: any, value: any) => (info.curve = animationTimingFunctionConverter(value)),
'animation-iteration-count': (info: any, value: any) => (info.iterations = value === 'infinite' ? Number.POSITIVE_INFINITY : parseFloat(value)),
'animation-direction': (info: any, value: any) => (info.isReverse = value === 'reverse'),
'animation-fill-mode': (info: any, value: any) => (info.isForwards = value === 'forwards'),
'animation-fill-mode': (info: any, value: any) => (info.isForwards = value === 'forwards' || value === 'both'),
});
export class CssAnimationParser {
@@ -65,6 +65,7 @@ export class CssAnimationParser {
if (current === undefined) {
current = <KeyframeInfo>{};
current.duration = time;
current.declarations = [];
parsedKeyframes[time] = current;
}
for (const declaration of keyframe.declarations) {
@@ -72,7 +73,7 @@ export class CssAnimationParser {
current.curve = animationTimingFunctionConverter(declaration.value);
}
}
current.declarations = declarations;
current.declarations = current.declarations.concat(declarations);
}
}
const array = [];
@@ -87,39 +88,97 @@ export class CssAnimationParser {
}
}
function keyframeAnimationsFromCSSProperty(value: any, animations: KeyframeAnimationInfo[]) {
if (typeof value === 'string') {
const values = value.split(/[,]+/);
for (const parsedValue of values) {
const animationInfo = new KeyframeAnimationInfo();
const arr = (<string>parsedValue).trim().split(/[ ]+/);
/**
* @see https://w3c.github.io/csswg-drafts/css-animations/#propdef-animation
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/animation
* @internal - exported for testing
* @param value
* @param animations
*/
export function keyframeAnimationsFromCSSProperty(value: any, animations: KeyframeAnimationInfo[]) {
if (typeof value !== 'string') {
return;
}
if (arr.length > 0) {
animationInfo.name = arr[0];
}
if (arr.length > 1) {
animationInfo.duration = timeConverter(arr[1]);
}
if (arr.length > 2) {
animationInfo.curve = animationTimingFunctionConverter(arr[2]);
}
if (arr.length > 3) {
animationInfo.delay = timeConverter(arr[3]);
}
if (arr.length > 4) {
animationInfo.iterations = parseInt(arr[4]);
}
if (arr.length > 5) {
animationInfo.isReverse = arr[4] === 'reverse';
}
if (arr.length > 6) {
animationInfo.isForwards = arr[5] === 'forwards';
}
if (arr.length > 7) {
throw new Error('Invalid value for animation: ' + value);
}
animations.push(animationInfo);
if (value.trim().length === 0) {
return;
}
/**
* Matches whitespace except if the whitespace is contained in parenthesis - ex. cubic-bezier(1, 1, 1, 1).
*/
const VALUE_SPLIT_RE = /\s(?![^(]*\))/;
/**
* Matches commas except if the comma is contained in parenthesis - ex. cubic-bezier(1, 1, 1, 1).
*/
const MULTIPLE_SPLIT_RE = /,(?![^(]*\))/;
const isTime = (v: string) => !!v.match(/\dm?s$/g);
const isTimingFunction = (v: string) => !!v.match(/ease|linear|ease-in|ease-out|ease-in-out|spring|cubic-bezier/g);
const isIterationCount = (v: string) => !!v.match(/infinite|[\d.]+$/g);
const isDirection = (v: string) => !!v.match(/normal|reverse|alternate|alternate-reverse/g);
const isFillMode = (v: string) => !!v.match(/none|forwards|backwards|both/g);
const isPlayState = (v: string) => !!v.match(/running|paused/g);
const values = value.split(MULTIPLE_SPLIT_RE);
for (const parsedValue of values) {
const animationInfo = new KeyframeAnimationInfo();
const parts = (<string>parsedValue).trim().split(VALUE_SPLIT_RE);
const [duration, delay] = parts.filter(isTime);
const [timing] = parts.filter(isTimingFunction);
const [iterationCount] = parts.filter(isIterationCount);
const [direction] = parts.filter(isDirection);
const [fillMode] = parts.filter(isFillMode);
const [playState] = parts.filter(isPlayState);
const [name] = parts.filter((v) => {
// filter out "consumed" values
return ![duration, delay, timing, iterationCount, direction, fillMode, playState].filter(Boolean).includes(v);
});
// console.log({
// duration,
// delay,
// timing,
// iterationCount,
// direction,
// fillMode,
// playState,
// name,
// });
if (duration) {
ANIMATION_PROPERTY_HANDLERS['animation-duration'](animationInfo, duration);
}
if (delay) {
ANIMATION_PROPERTY_HANDLERS['animation-delay'](animationInfo, delay);
}
if (timing) {
ANIMATION_PROPERTY_HANDLERS['animation-timing-function'](animationInfo, timing);
}
if (iterationCount) {
ANIMATION_PROPERTY_HANDLERS['animation-iteration-count'](animationInfo, iterationCount);
}
if (direction) {
ANIMATION_PROPERTY_HANDLERS['animation-direction'](animationInfo, direction);
}
if (fillMode) {
ANIMATION_PROPERTY_HANDLERS['animation-fill-mode'](animationInfo, fillMode);
}
if (playState) {
// TODO: implement play state? Currently not supported...
}
if (name) {
ANIMATION_PROPERTY_HANDLERS['animation-name'](animationInfo, name);
} else {
// based on the SPEC we should set the name to 'none' if no name is provided
// however we just don't set the name at all.
// perhaps we should set it to 'none' and handle it accordingly.
// animationInfo.name = 'none'
}
animations.push(animationInfo);
}
}
@@ -128,9 +187,9 @@ export function parseKeyframeDeclarations(unparsedKeyframeDeclarations: Keyframe
const property = CssAnimationProperty._getByCssName(unparsedProperty);
unparsedValue = cleanupImportantFlags(unparsedValue, property?.cssLocalName);
if (typeof unparsedProperty === 'string' && property && property._valueConverter) {
if (typeof unparsedProperty === 'string' && property?._valueConverter) {
declarations[property.name] = property._valueConverter(<string>unparsedValue);
} else if (typeof unparsedValue === 'string' && unparsedProperty === 'transform') {
} else if (unparsedProperty === 'transform') {
const transformations = transformConverter(unparsedValue);
Object.assign(declarations, transformations);
}