90 lines
3.3 KiB
JavaScript
90 lines
3.3 KiB
JavaScript
|
import { invariant } from '../../../utils/errors.mjs';
|
||
|
import { isNumericalString } from '../../../utils/is-numerical-string.mjs';
|
||
|
import { isCSSVariableToken } from './is-css-variable.mjs';
|
||
|
|
||
|
/**
|
||
|
* Parse Framer's special CSS variable format into a CSS token and a fallback.
|
||
|
*
|
||
|
* ```
|
||
|
* `var(--foo, #fff)` => [`--foo`, '#fff']
|
||
|
* ```
|
||
|
*
|
||
|
* @param current
|
||
|
*/
|
||
|
const splitCSSVariableRegex = /var\((--[a-zA-Z0-9-_]+),? ?([a-zA-Z0-9 ()%#.,-]+)?\)/;
|
||
|
function parseCSSVariable(current) {
|
||
|
const match = splitCSSVariableRegex.exec(current);
|
||
|
if (!match)
|
||
|
return [,];
|
||
|
const [, token, fallback] = match;
|
||
|
return [token, fallback];
|
||
|
}
|
||
|
const maxDepth = 4;
|
||
|
function getVariableValue(current, element, depth = 1) {
|
||
|
invariant(depth <= maxDepth, `Max CSS variable fallback depth detected in property "${current}". This may indicate a circular fallback dependency.`);
|
||
|
const [token, fallback] = parseCSSVariable(current);
|
||
|
// No CSS variable detected
|
||
|
if (!token)
|
||
|
return;
|
||
|
// Attempt to read this CSS variable off the element
|
||
|
const resolved = window.getComputedStyle(element).getPropertyValue(token);
|
||
|
if (resolved) {
|
||
|
const trimmed = resolved.trim();
|
||
|
return isNumericalString(trimmed) ? parseFloat(trimmed) : trimmed;
|
||
|
}
|
||
|
else if (isCSSVariableToken(fallback)) {
|
||
|
// The fallback might itself be a CSS variable, in which case we attempt to resolve it too.
|
||
|
return getVariableValue(fallback, element, depth + 1);
|
||
|
}
|
||
|
else {
|
||
|
return fallback;
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Resolve CSS variables from
|
||
|
*
|
||
|
* @internal
|
||
|
*/
|
||
|
function resolveCSSVariables(visualElement, { ...target }, transitionEnd) {
|
||
|
const element = visualElement.current;
|
||
|
if (!(element instanceof Element))
|
||
|
return { target, transitionEnd };
|
||
|
// If `transitionEnd` isn't `undefined`, clone it. We could clone `target` and `transitionEnd`
|
||
|
// only if they change but I think this reads clearer and this isn't a performance-critical path.
|
||
|
if (transitionEnd) {
|
||
|
transitionEnd = { ...transitionEnd };
|
||
|
}
|
||
|
// Go through existing `MotionValue`s and ensure any existing CSS variables are resolved
|
||
|
visualElement.values.forEach((value) => {
|
||
|
const current = value.get();
|
||
|
if (!isCSSVariableToken(current))
|
||
|
return;
|
||
|
const resolved = getVariableValue(current, element);
|
||
|
if (resolved)
|
||
|
value.set(resolved);
|
||
|
});
|
||
|
// Cycle through every target property and resolve CSS variables. Currently
|
||
|
// we only read single-var properties like `var(--foo)`, not `calc(var(--foo) + 20px)`
|
||
|
for (const key in target) {
|
||
|
const current = target[key];
|
||
|
if (!isCSSVariableToken(current))
|
||
|
continue;
|
||
|
const resolved = getVariableValue(current, element);
|
||
|
if (!resolved)
|
||
|
continue;
|
||
|
// Clone target if it hasn't already been
|
||
|
target[key] = resolved;
|
||
|
if (!transitionEnd)
|
||
|
transitionEnd = {};
|
||
|
// If the user hasn't already set this key on `transitionEnd`, set it to the unresolved
|
||
|
// CSS variable. This will ensure that after the animation the component will reflect
|
||
|
// changes in the value of the CSS variable.
|
||
|
if (transitionEnd[key] === undefined) {
|
||
|
transitionEnd[key] = current;
|
||
|
}
|
||
|
}
|
||
|
return { target, transitionEnd };
|
||
|
}
|
||
|
|
||
|
export { parseCSSVariable, resolveCSSVariables };
|