mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-10 14:35:56 +09:00
refactor: use longhand properties to save transition values in style-panel (#3584)
## Description fixes #3567 Use long-hand properties to save `transition` values in the style-panel. ## Steps for reproduction - Add some layers in transition panel in UI. - Delete some layers - Swap some layers ## TODO - [ ] Allow `show/hide` for each layer. - [ ] Add tests for all the layer handlers for transition. ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview @TrySound i made some refactor to use individual properties as long-hands instead of short-hand property. I am still yet to add tests for the new handlers, since they need to be handled in a different way now for transitions. But you can still play around with the current setup. ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 5de6) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests
This commit is contained in:
1 parent
9a39c24502
commit
ce2dad417d
11 files changed
+847
-117
No files matched your search
+29
-17
@@ -25,6 +25,7 @@ import {
|
||||
type ExtractedTransitionProperties,
|
||||
} from "@webstudio-is/css-data";
|
||||
import type {
|
||||
CreateBatchUpdate,
|
||||
DeleteProperty,
|
||||
StyleUpdateOptions,
|
||||
} from "../../shared/use-style-data";
|
||||
@@ -32,6 +33,14 @@ import { type IntermediateStyleValue } from "../../shared/css-value-input";
|
||||
import { TransitionProperty } from "./transition-property";
|
||||
import { TransitionTiming } from "./transition-timing";
|
||||
import { CssValueInputContainer } from "../../shared/css-value-input";
|
||||
import {
|
||||
defaultTransitionProperty,
|
||||
defaultTransitionDuration,
|
||||
defaultTransitionTimingFunction,
|
||||
defaultTransitionDelay,
|
||||
deleteTransitionLayer,
|
||||
} from "./transition-utils";
|
||||
import type { StyleInfo } from "../../shared/style-info";
|
||||
|
||||
type TransitionContentProps = {
|
||||
index: number;
|
||||
@@ -45,16 +54,18 @@ type TransitionContentProps = {
|
||||
options: StyleUpdateOptions
|
||||
) => void;
|
||||
deleteProperty: DeleteProperty;
|
||||
currentStyle: StyleInfo;
|
||||
createBatchUpdate: CreateBatchUpdate;
|
||||
};
|
||||
|
||||
export const TransitionContent = ({
|
||||
layer,
|
||||
index,
|
||||
tooltip,
|
||||
property: transitionPropertyKey,
|
||||
onEditLayer,
|
||||
propertyValue,
|
||||
deleteProperty,
|
||||
createBatchUpdate,
|
||||
currentStyle,
|
||||
}: TransitionContentProps) => {
|
||||
const [intermediateValue, setIntermediateValue] = useState<
|
||||
IntermediateStyleValue | InvalidValue | undefined
|
||||
@@ -73,7 +84,7 @@ export const TransitionContent = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleComplete = () => {
|
||||
const handleComplete = (options: StyleUpdateOptions) => {
|
||||
if (intermediateValue === undefined) {
|
||||
return;
|
||||
}
|
||||
@@ -87,7 +98,7 @@ export const TransitionContent = ({
|
||||
return;
|
||||
}
|
||||
|
||||
onEditLayer(index, layers, { isEphemeral: false });
|
||||
onEditLayer(index, layers, options);
|
||||
};
|
||||
|
||||
const handlePropertyUpdate = (
|
||||
@@ -129,8 +140,7 @@ export const TransitionContent = ({
|
||||
}}
|
||||
>
|
||||
<TransitionProperty
|
||||
/* Browser defaults for transition-property - all */
|
||||
property={property ?? { type: "keyword" as const, value: "all" }}
|
||||
property={property ?? defaultTransitionProperty}
|
||||
onPropertySelection={handlePropertyUpdate}
|
||||
/>
|
||||
|
||||
@@ -157,14 +167,13 @@ export const TransitionContent = ({
|
||||
key={"transitionDuration"}
|
||||
property={"transitionDuration"}
|
||||
styleSource="local"
|
||||
/* Browser default for transition-duration */
|
||||
value={duration ?? { type: "unit", value: 0, unit: "ms" }}
|
||||
value={duration ?? defaultTransitionDuration}
|
||||
keywords={[]}
|
||||
deleteProperty={() => {
|
||||
handlePropertyUpdate({ duration });
|
||||
}}
|
||||
setValue={(value, options) => {
|
||||
if (value === undefined) {
|
||||
if (value === undefined || value.type !== "unit") {
|
||||
return;
|
||||
}
|
||||
handlePropertyUpdate({ duration: value }, options);
|
||||
@@ -193,12 +202,11 @@ export const TransitionContent = ({
|
||||
property={"transitionDelay"}
|
||||
key={"transitionDelay"}
|
||||
styleSource="local"
|
||||
/* Browser default for transition-delay */
|
||||
value={delay ?? { type: "unit", value: 0, unit: "ms" }}
|
||||
value={delay ?? defaultTransitionDelay}
|
||||
keywords={[]}
|
||||
deleteProperty={() => handlePropertyUpdate({ delay })}
|
||||
setValue={(value, options) => {
|
||||
if (value === undefined) {
|
||||
if (value === undefined || value.type !== "unit") {
|
||||
return;
|
||||
}
|
||||
handlePropertyUpdate({ delay: value }, options);
|
||||
@@ -206,8 +214,7 @@ export const TransitionContent = ({
|
||||
/>
|
||||
|
||||
<TransitionTiming
|
||||
/* Browser defaults for transition-property - ease */
|
||||
timing={timing ?? { type: "keyword", value: "ease" }}
|
||||
timing={timing ?? defaultTransitionTimingFunction}
|
||||
onTimingSelection={handlePropertyUpdate}
|
||||
/>
|
||||
</Grid>
|
||||
@@ -235,12 +242,12 @@ export const TransitionContent = ({
|
||||
color={intermediateValue?.type === "invalid" ? "error" : undefined}
|
||||
value={intermediateValue?.value ?? ""}
|
||||
onChange={handleChange}
|
||||
onBlur={handleComplete}
|
||||
onBlur={() => handleComplete({ isEphemeral: true })}
|
||||
onKeyDown={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (event.key === "Enter") {
|
||||
handleComplete();
|
||||
handleComplete({ isEphemeral: false });
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
@@ -249,7 +256,12 @@ export const TransitionContent = ({
|
||||
return;
|
||||
}
|
||||
|
||||
deleteProperty(transitionPropertyKey, { isEphemeral: true });
|
||||
deleteTransitionLayer({
|
||||
currentStyle,
|
||||
createBatchUpdate,
|
||||
index,
|
||||
options: { isEphemeral: true },
|
||||
});
|
||||
setIntermediateValue(undefined);
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
+2
-11
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
animatableProperties,
|
||||
commonTransitionProperties,
|
||||
isAnimatableProperty,
|
||||
} from "@webstudio-is/css-data";
|
||||
import {
|
||||
@@ -31,17 +32,7 @@ type TransitionPropertyProps = {
|
||||
onPropertySelection: (params: { property: KeywordValue }) => void;
|
||||
};
|
||||
|
||||
const commonPropertiesSet = new Set<string>([
|
||||
"all",
|
||||
"opacity",
|
||||
"margin",
|
||||
"padding",
|
||||
"border",
|
||||
"transform",
|
||||
"filter",
|
||||
"flex",
|
||||
"background-color",
|
||||
]);
|
||||
const commonPropertiesSet = new Set(commonTransitionProperties);
|
||||
|
||||
const properties = Array.from(
|
||||
setUnion(commonPropertiesSet, new Set(animatableProperties))
|
||||
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
import { afterEach, describe, test, expect } from "@jest/globals";
|
||||
import type { CreateBatchUpdate } from "../../shared/use-style-data";
|
||||
import {
|
||||
toValue,
|
||||
type StyleProperty,
|
||||
type StyleValue,
|
||||
} from "@webstudio-is/css-engine";
|
||||
import type { StyleInfo } from "../../shared/style-info";
|
||||
import {
|
||||
addDefaultTransitionLayer,
|
||||
convertIndividualTransitionToLayers,
|
||||
deleteTransitionLayer,
|
||||
editTransitionLayer,
|
||||
getTransitionProperties,
|
||||
hideTransitionLayer,
|
||||
swapTransitionLayers,
|
||||
} from "./transition-utils";
|
||||
import type { TransitionProperties } from "./transition-utils";
|
||||
import type { LayersValue } from "@webstudio-is/css-engine";
|
||||
|
||||
let styleInfo: StyleInfo = {};
|
||||
let published = false;
|
||||
const deletedProperties = new Set<string>();
|
||||
|
||||
afterEach(() => {
|
||||
styleInfo = {};
|
||||
published = false;
|
||||
deletedProperties.clear();
|
||||
});
|
||||
|
||||
const createBatchUpdate: CreateBatchUpdate = () => ({
|
||||
setProperty: (propertyName: StyleProperty) => (newValue: StyleValue) => {
|
||||
styleInfo[propertyName] = { value: newValue, local: newValue };
|
||||
},
|
||||
deleteProperty: (propertyName: string) => {
|
||||
deletedProperties.add(propertyName);
|
||||
},
|
||||
publish: (options?: unknown) => {
|
||||
published = true;
|
||||
},
|
||||
});
|
||||
|
||||
describe("transition-utils", () => {
|
||||
test("convert individual transition properties into short-hand transition layers", () => {
|
||||
const properties: Record<TransitionProperties, LayersValue> = {
|
||||
transitionProperty: {
|
||||
type: "layers",
|
||||
value: [
|
||||
{ type: "keyword", value: "opacity" },
|
||||
{ type: "keyword", value: "width" },
|
||||
{ type: "keyword", value: "all" },
|
||||
{ type: "keyword", value: "height" },
|
||||
],
|
||||
},
|
||||
transitionDuration: {
|
||||
type: "layers",
|
||||
value: [
|
||||
{ type: "unit", value: 50, unit: "ms" },
|
||||
{ type: "unit", value: 0, unit: "ms" },
|
||||
],
|
||||
},
|
||||
transitionTimingFunction: {
|
||||
type: "layers",
|
||||
value: [
|
||||
{ type: "keyword", value: "ease" },
|
||||
{ type: "keyword", value: "ease-in-out" },
|
||||
{ type: "keyword", value: "cubic-bezier(0.25,1,0.5,1)" },
|
||||
],
|
||||
},
|
||||
transitionDelay: {
|
||||
type: "layers",
|
||||
value: [
|
||||
{ type: "unit", value: 100, unit: "ms" },
|
||||
{ type: "unit", value: 0, unit: "ms" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const layers = convertIndividualTransitionToLayers(properties);
|
||||
expect(toValue(layers)).toMatchInlineSnapshot(
|
||||
`"opacity 50ms ease 100ms, width 0ms ease-in-out 0ms, all 50ms cubic-bezier(0.25,1,0.5,1) 100ms, height 0ms ease 0ms"`
|
||||
);
|
||||
});
|
||||
|
||||
test("add a layer to transition long hand proeprties", () => {
|
||||
addDefaultTransitionLayer({
|
||||
createBatchUpdate,
|
||||
currentStyle: styleInfo,
|
||||
});
|
||||
|
||||
const properties = getTransitionProperties(styleInfo);
|
||||
const layers = convertIndividualTransitionToLayers(properties);
|
||||
|
||||
expect(published).toBe(true);
|
||||
expect(toValue(layers)).toMatchInlineSnapshot(`"opacity 200ms ease 0s"`);
|
||||
});
|
||||
|
||||
test("update individual transition properties", () => {
|
||||
addDefaultTransitionLayer({
|
||||
createBatchUpdate,
|
||||
currentStyle: styleInfo,
|
||||
});
|
||||
|
||||
addDefaultTransitionLayer({
|
||||
createBatchUpdate,
|
||||
currentStyle: styleInfo,
|
||||
});
|
||||
|
||||
addDefaultTransitionLayer({
|
||||
createBatchUpdate,
|
||||
currentStyle: styleInfo,
|
||||
});
|
||||
|
||||
editTransitionLayer({
|
||||
currentStyle: styleInfo,
|
||||
index: 1,
|
||||
createBatchUpdate,
|
||||
layers: {
|
||||
type: "layers",
|
||||
value: [
|
||||
{ type: "tuple", value: [{ type: "keyword", value: "width" }] },
|
||||
],
|
||||
},
|
||||
options: { isEphemeral: false },
|
||||
});
|
||||
|
||||
const layers = convertIndividualTransitionToLayers(
|
||||
getTransitionProperties(styleInfo)
|
||||
);
|
||||
|
||||
expect(published).toBe(true);
|
||||
expect(layers.value.length).toBe(3);
|
||||
expect(toValue(layers.value[1])).toMatchInlineSnapshot(
|
||||
`"width 0ms ease 0ms"`
|
||||
);
|
||||
expect(toValue(layers)).toMatchInlineSnapshot(
|
||||
`"opacity 200ms ease 0s, width 0ms ease 0ms, opacity 200ms ease 0s"`
|
||||
);
|
||||
});
|
||||
|
||||
test("delete a layer from transition properties", () => {
|
||||
addDefaultTransitionLayer({
|
||||
createBatchUpdate,
|
||||
currentStyle: styleInfo,
|
||||
});
|
||||
|
||||
addDefaultTransitionLayer({
|
||||
createBatchUpdate,
|
||||
currentStyle: styleInfo,
|
||||
});
|
||||
|
||||
addDefaultTransitionLayer({
|
||||
createBatchUpdate,
|
||||
currentStyle: styleInfo,
|
||||
});
|
||||
|
||||
editTransitionLayer({
|
||||
currentStyle: styleInfo,
|
||||
index: 2,
|
||||
createBatchUpdate,
|
||||
layers: {
|
||||
type: "layers",
|
||||
value: [
|
||||
{
|
||||
type: "tuple",
|
||||
value: [
|
||||
{ type: "keyword", value: "width" },
|
||||
{ type: "keyword", value: "ease-in-out" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
options: { isEphemeral: false },
|
||||
});
|
||||
|
||||
deleteTransitionLayer({
|
||||
currentStyle: styleInfo,
|
||||
createBatchUpdate,
|
||||
index: 1,
|
||||
});
|
||||
|
||||
const layers = convertIndividualTransitionToLayers(
|
||||
getTransitionProperties(styleInfo)
|
||||
);
|
||||
|
||||
expect(published).toBe(true);
|
||||
expect(toValue(layers)).toMatchInlineSnapshot(
|
||||
`"opacity 200ms ease 0s, width 0ms ease-in-out 0ms"`
|
||||
);
|
||||
});
|
||||
|
||||
test("hide a layer from transition properties", () => {
|
||||
addDefaultTransitionLayer({
|
||||
createBatchUpdate,
|
||||
currentStyle: styleInfo,
|
||||
});
|
||||
|
||||
addDefaultTransitionLayer({
|
||||
createBatchUpdate,
|
||||
currentStyle: styleInfo,
|
||||
});
|
||||
|
||||
hideTransitionLayer({
|
||||
index: 1,
|
||||
createBatchUpdate,
|
||||
currentStyle: styleInfo,
|
||||
});
|
||||
|
||||
const layers = convertIndividualTransitionToLayers(
|
||||
getTransitionProperties(styleInfo)
|
||||
);
|
||||
|
||||
expect(published).toBe(true);
|
||||
expect(layers.value.length).toBe(2);
|
||||
expect(toValue(layers)).toMatchInlineSnapshot(`"opacity 200ms ease 0s"`);
|
||||
});
|
||||
|
||||
test("swap layer positions in transitions", () => {
|
||||
addDefaultTransitionLayer({ currentStyle: styleInfo, createBatchUpdate });
|
||||
addDefaultTransitionLayer({ currentStyle: styleInfo, createBatchUpdate });
|
||||
editTransitionLayer({
|
||||
currentStyle: styleInfo,
|
||||
index: 1,
|
||||
createBatchUpdate,
|
||||
layers: {
|
||||
type: "layers",
|
||||
value: [
|
||||
{
|
||||
type: "tuple",
|
||||
value: [
|
||||
{ type: "keyword", value: "width" },
|
||||
{ type: "keyword", value: "ease-in-out" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
options: { isEphemeral: false },
|
||||
});
|
||||
swapTransitionLayers({
|
||||
oldIndex: 1,
|
||||
newIndex: 0,
|
||||
currentStyle: styleInfo,
|
||||
createBatchUpdate,
|
||||
});
|
||||
|
||||
const layers = convertIndividualTransitionToLayers(
|
||||
getTransitionProperties(styleInfo)
|
||||
);
|
||||
|
||||
expect(published).toBe(true);
|
||||
expect(layers.value.length).toBe(2);
|
||||
expect(toValue(layers)).toMatchInlineSnapshot(
|
||||
`"width 0ms ease-in-out 0ms, opacity 200ms ease 0s"`
|
||||
);
|
||||
});
|
||||
});
|
||||
+324
@@ -1,3 +1,41 @@
|
||||
import {
|
||||
type KeywordValue,
|
||||
type LayersValue,
|
||||
type TupleValue,
|
||||
type UnitValue,
|
||||
} from "@webstudio-is/css-engine";
|
||||
import type { StyleInfo } from "../../shared/style-info";
|
||||
import type {
|
||||
CreateBatchUpdate,
|
||||
StyleUpdateOptions,
|
||||
} from "../../shared/use-style-data";
|
||||
import {
|
||||
extractTransitionProperties,
|
||||
isValidTransitionValue,
|
||||
parseTransition,
|
||||
transitionLongHandProperties,
|
||||
} from "@webstudio-is/css-data";
|
||||
|
||||
export const initialTransition = "opacity 200ms ease 0s";
|
||||
export const defaultTransitionProperty: KeywordValue = {
|
||||
type: "keyword",
|
||||
value: "opacity",
|
||||
};
|
||||
export const defaultTransitionDuration: UnitValue = {
|
||||
type: "unit",
|
||||
value: 0,
|
||||
unit: "ms",
|
||||
};
|
||||
export const defaultTransitionDelay: UnitValue = {
|
||||
type: "unit",
|
||||
value: 0,
|
||||
unit: "ms",
|
||||
};
|
||||
export const defaultTransitionTimingFunction: KeywordValue = {
|
||||
type: "keyword",
|
||||
value: "ease",
|
||||
};
|
||||
|
||||
export const defaultFunctions = {
|
||||
linear: "linear",
|
||||
ease: "ease",
|
||||
@@ -63,3 +101,289 @@ export const findTimingFunctionFromValue = (
|
||||
(key: TimingFunctions) => timingFunctions[key] === timingFunction
|
||||
);
|
||||
};
|
||||
|
||||
export type TransitionProperties =
|
||||
(typeof transitionLongHandProperties)[number];
|
||||
|
||||
export const getTransitionProperties = (
|
||||
currentyStyle: StyleInfo
|
||||
): Record<TransitionProperties, LayersValue> => {
|
||||
const properties: Record<TransitionProperties, LayersValue> = {
|
||||
transitionProperty: { type: "layers", value: [] },
|
||||
transitionTimingFunction: { type: "layers", value: [] },
|
||||
transitionDelay: { type: "layers", value: [] },
|
||||
transitionDuration: { type: "layers", value: [] },
|
||||
};
|
||||
for (const property of transitionLongHandProperties) {
|
||||
const value = currentyStyle[property];
|
||||
|
||||
if (value !== undefined && value.value.type === "layers") {
|
||||
properties[property] = value.value;
|
||||
}
|
||||
}
|
||||
|
||||
return properties;
|
||||
};
|
||||
|
||||
const getValueOrRepeatLast = (arr: LayersValue["value"], index: number) => {
|
||||
return arr[index % arr.length];
|
||||
};
|
||||
|
||||
export const convertIndividualTransitionToLayers = (
|
||||
properties: Record<TransitionProperties, LayersValue>
|
||||
) => {
|
||||
const layers: { type: "layers"; value: Array<TupleValue> } = {
|
||||
type: "layers",
|
||||
value: [],
|
||||
};
|
||||
|
||||
const {
|
||||
transitionProperty,
|
||||
transitionDuration,
|
||||
transitionDelay,
|
||||
transitionTimingFunction,
|
||||
} = properties;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/transition-timing-function
|
||||
// You may specify multiple easing, duration and delay values;
|
||||
// each one will be applied to the corresponding property as specified by the transition-property property,
|
||||
// which acts as a transition-property list. If there are fewer easing, duration and transition values specified than in the transition-property list,
|
||||
// the user agent must calculate which value is used by repeating the list of values until there is one for each transition property.
|
||||
// If there are more values, the list is truncated to the right size. In both cases, the CSS declaration stays valid.
|
||||
// And so, we take the lenght of the transiton-property as the base length of the layers
|
||||
|
||||
for (let index = 0; index < transitionProperty.value.length; index++) {
|
||||
const property = getValueOrRepeatLast(transitionProperty.value, index);
|
||||
const duration =
|
||||
getValueOrRepeatLast(transitionDuration.value, index) ??
|
||||
defaultTransitionDuration;
|
||||
const timingFunction =
|
||||
getValueOrRepeatLast(transitionTimingFunction.value, index) ??
|
||||
defaultTransitionTimingFunction;
|
||||
const delay =
|
||||
getValueOrRepeatLast(transitionDelay.value, index) ??
|
||||
defaultTransitionDelay;
|
||||
|
||||
if (
|
||||
isValidTransitionValue(property) &&
|
||||
isValidTransitionValue(duration) &&
|
||||
isValidTransitionValue(timingFunction) &&
|
||||
isValidTransitionValue(delay)
|
||||
) {
|
||||
const layer: TupleValue = {
|
||||
type: "tuple",
|
||||
value: [property, duration, timingFunction, delay],
|
||||
hidden: property.hidden ?? false,
|
||||
};
|
||||
layers.value.push(layer);
|
||||
}
|
||||
}
|
||||
|
||||
return layers;
|
||||
};
|
||||
|
||||
export const deleteTransitionProperties = (props: {
|
||||
createBatchUpdate: CreateBatchUpdate;
|
||||
}) => {
|
||||
const batch = props.createBatchUpdate();
|
||||
transitionLongHandProperties.forEach((property) => {
|
||||
batch.deleteProperty(property);
|
||||
});
|
||||
batch.publish();
|
||||
};
|
||||
|
||||
export const addDefaultTransitionLayer = (props: {
|
||||
createBatchUpdate: CreateBatchUpdate;
|
||||
currentStyle: StyleInfo;
|
||||
}) => {
|
||||
const { createBatchUpdate, currentStyle } = props;
|
||||
const properties = getTransitionProperties(currentStyle);
|
||||
const { timing, property, delay, duration } = extractTransitionProperties(
|
||||
parseTransition(initialTransition).value[0] as TupleValue
|
||||
);
|
||||
const batch = createBatchUpdate();
|
||||
|
||||
if (property) {
|
||||
batch.setProperty("transitionProperty")({
|
||||
type: "layers",
|
||||
value: [...properties.transitionProperty.value, property],
|
||||
});
|
||||
}
|
||||
|
||||
if (timing) {
|
||||
batch.setProperty("transitionTimingFunction")({
|
||||
type: "layers",
|
||||
value: [...properties.transitionTimingFunction.value, timing],
|
||||
});
|
||||
}
|
||||
|
||||
if (duration) {
|
||||
batch.setProperty("transitionDuration")({
|
||||
type: "layers",
|
||||
value: [...properties.transitionDuration.value, duration],
|
||||
});
|
||||
}
|
||||
|
||||
if (delay) {
|
||||
batch.setProperty("transitionDelay")({
|
||||
type: "layers",
|
||||
value: [...properties.transitionDelay.value, delay],
|
||||
});
|
||||
}
|
||||
|
||||
batch.publish();
|
||||
};
|
||||
|
||||
export const deleteTransitionLayer = (props: {
|
||||
index: number;
|
||||
createBatchUpdate: CreateBatchUpdate;
|
||||
currentStyle: StyleInfo;
|
||||
options?: StyleUpdateOptions;
|
||||
}) => {
|
||||
const {
|
||||
index,
|
||||
createBatchUpdate,
|
||||
currentStyle,
|
||||
options = { isEphemeral: false },
|
||||
} = props;
|
||||
|
||||
const properties = getTransitionProperties(currentStyle);
|
||||
const batch = createBatchUpdate();
|
||||
transitionLongHandProperties.forEach((property) => {
|
||||
const value = properties[property].value;
|
||||
batch.setProperty(property)({
|
||||
type: "layers",
|
||||
value: value.filter((_, i) => i !== index),
|
||||
});
|
||||
});
|
||||
batch.publish(options);
|
||||
};
|
||||
|
||||
export const editTransitionLayer = (props: {
|
||||
index: number;
|
||||
layers: LayersValue;
|
||||
options: StyleUpdateOptions;
|
||||
currentStyle: StyleInfo;
|
||||
createBatchUpdate: CreateBatchUpdate;
|
||||
}) => {
|
||||
const { index, layers, createBatchUpdate, options, currentStyle } = props;
|
||||
const batch = createBatchUpdate();
|
||||
|
||||
const properties = getTransitionProperties(currentStyle);
|
||||
const {
|
||||
transitionProperty,
|
||||
transitionDelay,
|
||||
transitionDuration,
|
||||
transitionTimingFunction,
|
||||
} = properties;
|
||||
|
||||
const newTransitionProperties: KeywordValue[] = [];
|
||||
const newTransitionDurations: UnitValue[] = [];
|
||||
const newTransitionDelays: UnitValue[] = [];
|
||||
const newTransitionTimingFunctions: KeywordValue[] = [];
|
||||
|
||||
for (const layer of layers.value) {
|
||||
if (layer.type !== "tuple") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const {
|
||||
property,
|
||||
duration = defaultTransitionDuration,
|
||||
timing = defaultTransitionTimingFunction,
|
||||
delay = defaultTransitionDelay,
|
||||
} = extractTransitionProperties(layer);
|
||||
|
||||
// transition-property can't be undefined
|
||||
if (property === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
newTransitionProperties.push(property);
|
||||
newTransitionDurations.push(duration);
|
||||
newTransitionDelays.push(delay);
|
||||
newTransitionTimingFunctions.push(timing);
|
||||
}
|
||||
|
||||
const newProperty = [...transitionProperty.value];
|
||||
newProperty.splice(index, 1, ...newTransitionProperties);
|
||||
batch.setProperty("transitionProperty")({
|
||||
type: "layers",
|
||||
value: newProperty,
|
||||
});
|
||||
|
||||
const newDuration = [...transitionDuration.value];
|
||||
newDuration.splice(index, 1, ...newTransitionDurations);
|
||||
batch.setProperty("transitionDuration")({
|
||||
type: "layers",
|
||||
value: newDuration,
|
||||
});
|
||||
|
||||
const newTiming = [...transitionTimingFunction.value];
|
||||
newTiming.splice(index, 1, ...newTransitionTimingFunctions);
|
||||
batch.setProperty("transitionTimingFunction")({
|
||||
type: "layers",
|
||||
value: newTiming,
|
||||
});
|
||||
|
||||
const newDelay = [...transitionDelay.value];
|
||||
newDelay.splice(index, 1, ...newTransitionDelays);
|
||||
batch.setProperty("transitionDelay")({
|
||||
type: "layers",
|
||||
value: newDelay,
|
||||
});
|
||||
|
||||
batch.publish(options);
|
||||
};
|
||||
|
||||
export const swapTransitionLayers = (props: {
|
||||
oldIndex: number;
|
||||
newIndex: number;
|
||||
createBatchUpdate: CreateBatchUpdate;
|
||||
currentStyle: StyleInfo;
|
||||
}) => {
|
||||
const { oldIndex, newIndex, createBatchUpdate, currentStyle } = props;
|
||||
const properties = getTransitionProperties(currentStyle);
|
||||
const batch = createBatchUpdate();
|
||||
|
||||
for (const property of transitionLongHandProperties) {
|
||||
const layer = properties[property];
|
||||
const layervalue = [...layer.value];
|
||||
layervalue.splice(oldIndex, 1);
|
||||
layervalue.splice(newIndex, 0, layer.value[oldIndex]);
|
||||
batch.setProperty(property)({
|
||||
...layer,
|
||||
value: layervalue,
|
||||
});
|
||||
}
|
||||
|
||||
batch.publish();
|
||||
};
|
||||
|
||||
export const hideTransitionLayer = (props: {
|
||||
index: number;
|
||||
createBatchUpdate: CreateBatchUpdate;
|
||||
currentStyle: StyleInfo;
|
||||
}) => {
|
||||
const { createBatchUpdate, index, currentStyle } = props;
|
||||
const properties = getTransitionProperties(currentStyle);
|
||||
const batch = createBatchUpdate();
|
||||
|
||||
for (const property of transitionLongHandProperties) {
|
||||
const propertyLayer = properties[property];
|
||||
batch.setProperty(property)({
|
||||
type: "layers",
|
||||
value: propertyLayer.value.map((layer, i) => {
|
||||
if (layer.type !== "keyword" && layer.type !== "unit") {
|
||||
return layer;
|
||||
}
|
||||
|
||||
return i === index
|
||||
? { ...layer, hidden: layer.hidden ? false : true }
|
||||
: layer;
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
batch.publish();
|
||||
};
|
||||
+80
-38
@@ -1,39 +1,47 @@
|
||||
import type { StyleProperty } from "@webstudio-is/css-engine";
|
||||
import { CollapsibleSectionRoot } from "~/builder/shared/collapsible-section";
|
||||
import type { SectionProps } from "../shared/section";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useStore } from "@nanostores/react";
|
||||
import { InfoCircleIcon, PlusIcon } from "@webstudio-is/icons";
|
||||
import {
|
||||
SectionTitle,
|
||||
SectionTitleButton,
|
||||
SectionTitleLabel,
|
||||
Tooltip,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from "@webstudio-is/design-system";
|
||||
import { useState } from "react";
|
||||
import { transitionLongHandProperties } from "@webstudio-is/css-data";
|
||||
|
||||
import { CollapsibleSectionRoot } from "~/builder/shared/collapsible-section";
|
||||
import type { SectionProps } from "../shared/section";
|
||||
import { getDots } from "../../shared/collapsible-section";
|
||||
import { getStyleSource } from "../../shared/style-info";
|
||||
import { InfoCircleIcon, PlusIcon } from "@webstudio-is/icons";
|
||||
import { PropertyName } from "../../shared/property-name";
|
||||
import { addLayer } from "../../style-layer-utils";
|
||||
import { parseTransition } from "@webstudio-is/css-data";
|
||||
import { LayersList } from "../../style-layers-list";
|
||||
import { $selectedOrLastStyleSourceSelector } from "~/shared/nano-states";
|
||||
import { useStore } from "@nanostores/react";
|
||||
import { TransitionContent } from "./transition-content";
|
||||
import {
|
||||
initialTransition,
|
||||
getTransitionProperties,
|
||||
deleteTransitionProperties,
|
||||
addDefaultTransitionLayer,
|
||||
deleteTransitionLayer,
|
||||
editTransitionLayer,
|
||||
swapTransitionLayers,
|
||||
hideTransitionLayer,
|
||||
convertIndividualTransitionToLayers,
|
||||
} from "./transition-utils";
|
||||
import type { StyleProperty } from "@webstudio-is/css-engine";
|
||||
|
||||
export const properties = ["transition"] satisfies Array<StyleProperty>;
|
||||
|
||||
const property: StyleProperty = properties[0];
|
||||
const label = "Transitions";
|
||||
const initialTransition = "opacity 200ms ease";
|
||||
export const properties =
|
||||
transitionLongHandProperties as unknown as Array<StyleProperty>;
|
||||
|
||||
export const Section = (props: SectionProps) => {
|
||||
const { currentStyle, deleteProperty } = props;
|
||||
const { currentStyle, createBatchUpdate } = props;
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
const value = currentStyle[property]?.value;
|
||||
const sectionStyleSource =
|
||||
value?.type === "unparsed" || value?.type === "guaranteedInvalid"
|
||||
? undefined
|
||||
: getStyleSource(currentStyle[property]);
|
||||
const extractedProperties = getTransitionProperties(currentStyle);
|
||||
const layers = useMemo(
|
||||
() => convertIndividualTransitionToLayers(extractedProperties),
|
||||
[extractedProperties]
|
||||
);
|
||||
|
||||
const selectedOrLastStyleSourceSelector = useStore(
|
||||
$selectedOrLastStyleSourceSelector
|
||||
@@ -51,7 +59,7 @@ export const Section = (props: SectionProps) => {
|
||||
onOpenChange={setIsOpen}
|
||||
trigger={
|
||||
<SectionTitle
|
||||
dots={getDots(currentStyle, [property])}
|
||||
dots={getDots(currentStyle, properties)}
|
||||
suffix={
|
||||
<Tooltip
|
||||
content={
|
||||
@@ -63,15 +71,12 @@ export const Section = (props: SectionProps) => {
|
||||
<SectionTitleButton
|
||||
disabled={isStyleInLocalState === false}
|
||||
prefix={<PlusIcon />}
|
||||
onClick={() => {
|
||||
addLayer(
|
||||
property,
|
||||
parseTransition(initialTransition),
|
||||
onClick={() =>
|
||||
addDefaultTransitionLayer({
|
||||
createBatchUpdate,
|
||||
currentStyle,
|
||||
props.createBatchUpdate
|
||||
);
|
||||
setIsOpen(true);
|
||||
}}
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
@@ -81,23 +86,49 @@ export const Section = (props: SectionProps) => {
|
||||
style={currentStyle}
|
||||
description="Animate the transition between states on this instance."
|
||||
properties={properties}
|
||||
label={
|
||||
<SectionTitleLabel color={sectionStyleSource}>
|
||||
{label}
|
||||
</SectionTitleLabel>
|
||||
label={<SectionTitleLabel>{label}</SectionTitleLabel>}
|
||||
onReset={() =>
|
||||
deleteTransitionProperties({
|
||||
createBatchUpdate,
|
||||
})
|
||||
}
|
||||
onReset={() => deleteProperty(property)}
|
||||
/>
|
||||
</SectionTitle>
|
||||
}
|
||||
>
|
||||
{value?.type === "layers" && value.value.length > 0 && (
|
||||
{layers.value.length > 0 && (
|
||||
<LayersList
|
||||
{...props}
|
||||
property={property}
|
||||
value={value}
|
||||
property={properties[0]}
|
||||
value={layers}
|
||||
label={label}
|
||||
deleteProperty={deleteProperty}
|
||||
deleteProperty={() =>
|
||||
deleteTransitionProperties({
|
||||
createBatchUpdate,
|
||||
})
|
||||
}
|
||||
deleteLayer={(index) =>
|
||||
deleteTransitionLayer({
|
||||
index,
|
||||
createBatchUpdate,
|
||||
currentStyle,
|
||||
})
|
||||
}
|
||||
swapLayers={(oldIndex, newIndex) =>
|
||||
swapTransitionLayers({
|
||||
oldIndex,
|
||||
newIndex,
|
||||
createBatchUpdate,
|
||||
currentStyle,
|
||||
})
|
||||
}
|
||||
hideLayer={(index) => {
|
||||
hideTransitionLayer({
|
||||
index,
|
||||
createBatchUpdate,
|
||||
currentStyle,
|
||||
});
|
||||
}}
|
||||
renderContent={(layerProps) => {
|
||||
if (layerProps.layer.type !== "tuple") {
|
||||
return <></>;
|
||||
@@ -106,7 +137,18 @@ export const Section = (props: SectionProps) => {
|
||||
return (
|
||||
<TransitionContent
|
||||
{...layerProps}
|
||||
currentStyle={currentStyle}
|
||||
layer={layerProps.layer}
|
||||
createBatchUpdate={createBatchUpdate}
|
||||
onEditLayer={(index, layers, options) =>
|
||||
editTransitionLayer({
|
||||
index,
|
||||
layers,
|
||||
options,
|
||||
createBatchUpdate,
|
||||
currentStyle,
|
||||
})
|
||||
}
|
||||
tooltip={
|
||||
<Tooltip
|
||||
variant="wrapped"
|
||||
|
||||
@@ -41,6 +41,10 @@ type LayerListProps = SectionProps & {
|
||||
label: string;
|
||||
property: StyleProperty;
|
||||
value: TupleValue | LayersValue;
|
||||
deleteProperty: DeleteProperty;
|
||||
deleteLayer?: (index: number) => boolean | void;
|
||||
swapLayers?: (oldIndex: number, newIndex: number) => void;
|
||||
hideLayer?: (index: number) => void;
|
||||
renderContent: (props: {
|
||||
index: number;
|
||||
layer: TupleValue | FunctionValue;
|
||||
@@ -90,15 +94,16 @@ const extractPropertiesFromLayer = (layer: TupleValue | FunctionValue) => {
|
||||
return { name: name.join(" "), value: shadow.join(" "), color };
|
||||
};
|
||||
|
||||
export const LayersList = ({
|
||||
label,
|
||||
property,
|
||||
value,
|
||||
currentStyle,
|
||||
createBatchUpdate,
|
||||
renderContent,
|
||||
deleteProperty,
|
||||
}: LayerListProps) => {
|
||||
export const LayersList = (props: LayerListProps) => {
|
||||
const {
|
||||
label,
|
||||
property,
|
||||
value,
|
||||
currentStyle,
|
||||
createBatchUpdate,
|
||||
renderContent,
|
||||
deleteProperty,
|
||||
} = props;
|
||||
const layersCount = getLayerCount(property, currentStyle);
|
||||
|
||||
const sortableItems = useMemo(
|
||||
@@ -112,17 +117,28 @@ export const LayersList = ({
|
||||
|
||||
const { dragItemId, placementIndicator, sortableRefCallback } = useSortable({
|
||||
items: sortableItems,
|
||||
onSort: (newIndex, oldIndex) => {
|
||||
swapLayers(property, newIndex, oldIndex, currentStyle, createBatchUpdate);
|
||||
},
|
||||
onSort: (newIndex, oldIndex) =>
|
||||
props.swapLayers
|
||||
? props.swapLayers(oldIndex, newIndex)
|
||||
: swapLayers(
|
||||
property,
|
||||
newIndex,
|
||||
oldIndex,
|
||||
currentStyle,
|
||||
createBatchUpdate
|
||||
),
|
||||
});
|
||||
|
||||
const handleDeleteLayer = (index: number) => {
|
||||
return deleteLayer(property, index, value, createBatchUpdate);
|
||||
return props?.deleteLayer
|
||||
? props.deleteLayer(index)
|
||||
: deleteLayer(property, index, value, createBatchUpdate);
|
||||
};
|
||||
|
||||
const handleHideLayer = (index: number) => {
|
||||
return hideLayer(property, index, value, createBatchUpdate);
|
||||
return props?.hideLayer
|
||||
? props.hideLayer(index)
|
||||
: hideLayer(property, index, value, createBatchUpdate);
|
||||
};
|
||||
|
||||
const onEditLayer = (
|
||||
|
||||
@@ -10,7 +10,13 @@ import {
|
||||
} from "@webstudio-is/css-engine";
|
||||
import { keywordValues } from "./__generated__/keyword-values";
|
||||
import { units } from "./__generated__/units";
|
||||
import { parseFilter, parseShadow, parseTransition } from "./property-parsers";
|
||||
import {
|
||||
isTransitionLongHandProperty,
|
||||
parseFilter,
|
||||
parseShadow,
|
||||
parseTransition,
|
||||
parseTransitionLonghands,
|
||||
} from "./property-parsers";
|
||||
import { camelCase } from "change-case";
|
||||
|
||||
export const cssTryParseValue = (input: string) => {
|
||||
@@ -92,6 +98,10 @@ export const parseCssValue = (
|
||||
return parseTransition(input);
|
||||
}
|
||||
|
||||
if (isTransitionLongHandProperty(property)) {
|
||||
return parseTransitionLonghands(property, input);
|
||||
}
|
||||
|
||||
if (
|
||||
ast != null &&
|
||||
ast.type === "Value" &&
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type {
|
||||
KeywordValue,
|
||||
StyleValue,
|
||||
TupleValue,
|
||||
UnitValue,
|
||||
} from "@webstudio-is/css-engine";
|
||||
@@ -12,19 +11,17 @@ export const isTimingFunction = (timing: string) => {
|
||||
};
|
||||
|
||||
export type ExtractedTransitionProperties = {
|
||||
property?: KeywordValue | null;
|
||||
timing?: KeywordValue | null;
|
||||
delay?: StyleValue | null;
|
||||
duration?: StyleValue | null;
|
||||
property?: KeywordValue;
|
||||
timing?: KeywordValue;
|
||||
delay?: UnitValue;
|
||||
duration?: UnitValue;
|
||||
};
|
||||
|
||||
export const extractTransitionProperties = (
|
||||
transition: TupleValue
|
||||
): ExtractedTransitionProperties => {
|
||||
let property: KeywordValue | null = null;
|
||||
let timing: KeywordValue | null = null;
|
||||
let duration: UnitValue | null = null;
|
||||
let delay: UnitValue | null = null;
|
||||
let property: KeywordValue | undefined;
|
||||
let timing: KeywordValue | undefined;
|
||||
|
||||
const keywordValues: KeywordValue[] = [];
|
||||
const unitValues: UnitValue[] = [];
|
||||
@@ -43,11 +40,11 @@ export const extractTransitionProperties = (
|
||||
timing = keywordValues[0];
|
||||
} else {
|
||||
property = keywordValues[0];
|
||||
timing = keywordValues[1] ?? null;
|
||||
timing = keywordValues[1] ?? undefined;
|
||||
}
|
||||
|
||||
duration = unitValues[0] ?? null;
|
||||
delay = unitValues[1] ?? null;
|
||||
const duration: UnitValue | undefined = unitValues[0] ?? undefined;
|
||||
const delay: UnitValue | undefined = unitValues[1] ?? undefined;
|
||||
|
||||
return {
|
||||
property,
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import { describe, expect, test } from "@jest/globals";
|
||||
import { expect, test } from "@jest/globals";
|
||||
|
||||
import { parseTransition } from "./transition";
|
||||
|
||||
describe("parseTransition", () => {
|
||||
test("parses value and returns invalid when used a invalid color", () => {
|
||||
expect(parseTransition(`10px 10px 5px foo`)).toMatchInlineSnapshot(`
|
||||
test("parses value and returns invalid when used a invalid color", () => {
|
||||
expect(parseTransition(`10px 10px 5px foo`)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"type": "invalid",
|
||||
"value": "10px 10px 5px foo",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
test("parse value and returns multiple valid layers", () => {
|
||||
expect(parseTransition("color .2s, text-shadow .2s"))
|
||||
.toMatchInlineSnapshot(`
|
||||
test("parse value and returns multiple valid layers", () => {
|
||||
expect(parseTransition("color .2s, text-shadow .2s")).toMatchInlineSnapshot(`
|
||||
{
|
||||
"type": "layers",
|
||||
"value": [
|
||||
@@ -49,10 +47,10 @@ describe("parseTransition", () => {
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
test("parse value and returns valid layer", () => {
|
||||
expect(parseTransition("opacity 200ms ease 0s")).toMatchInlineSnapshot(`
|
||||
test("parse value and returns valid layer", () => {
|
||||
expect(parseTransition("opacity 200ms ease 0s")).toMatchInlineSnapshot(`
|
||||
{
|
||||
"type": "layers",
|
||||
"value": [
|
||||
@@ -82,27 +80,24 @@ describe("parseTransition", () => {
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
test("throws error if any custom transition proeprty is used", () => {
|
||||
const transition = parseTransition(
|
||||
"all 200ms ease 0s, --foo 200ms ease 0s"
|
||||
);
|
||||
expect(transition).toMatchInlineSnapshot(`
|
||||
test("throws error if any custom transition proeprty is used", () => {
|
||||
const transition = parseTransition("all 200ms ease 0s, --foo 200ms ease 0s");
|
||||
expect(transition).toMatchInlineSnapshot(`
|
||||
{
|
||||
"type": "invalid",
|
||||
"value": "all 200ms ease 0s, --foo 200ms ease 0s",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
test("throws error if any custom transition timing function is used", () => {
|
||||
const transition = parseTransition("all 200ms custom-function 0s");
|
||||
expect(transition).toMatchInlineSnapshot(`
|
||||
test("throws error if any custom transition timing function is used", () => {
|
||||
const transition = parseTransition("all 200ms custom-function 0s");
|
||||
expect(transition).toMatchInlineSnapshot(`
|
||||
{
|
||||
"type": "invalid",
|
||||
"value": "all 200ms custom-function 0s",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,63 @@
|
||||
import * as csstree from "css-tree";
|
||||
import type {
|
||||
InvalidValue,
|
||||
KeywordValue,
|
||||
LayerValueItem,
|
||||
LayersValue,
|
||||
TupleValue,
|
||||
TupleValueItem,
|
||||
Unit,
|
||||
UnparsedValue,
|
||||
UnitValue,
|
||||
} from "@webstudio-is/css-engine";
|
||||
import { animatableProperties } from "../";
|
||||
import { isTimingFunction } from "./transition-property-extractor";
|
||||
import { cssTryParseValue } from "../parse-css-value";
|
||||
import { kebabCase } from "change-case";
|
||||
|
||||
type AnimatableProperty = (typeof animatableProperties)[number];
|
||||
|
||||
export const transitionLongHandProperties = [
|
||||
"transitionProperty",
|
||||
"transitionTimingFunction",
|
||||
"transitionDelay",
|
||||
"transitionDuration",
|
||||
] as const;
|
||||
|
||||
export const commonTransitionProperties = [
|
||||
"all",
|
||||
"opacity",
|
||||
"margin",
|
||||
"padding",
|
||||
"border",
|
||||
"transform",
|
||||
"filter",
|
||||
"flex",
|
||||
"background-color",
|
||||
];
|
||||
|
||||
export const isAnimatableProperty = (
|
||||
property: string
|
||||
): property is AnimatableProperty => {
|
||||
return animatableProperties.some((item) => item === property);
|
||||
if (property === "all") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return [...commonTransitionProperties, ...animatableProperties].some(
|
||||
(item) => item === property
|
||||
);
|
||||
};
|
||||
|
||||
export const isTransitionLongHandProperty = (
|
||||
property: string
|
||||
): property is (typeof transitionLongHandProperties)[number] => {
|
||||
return transitionLongHandProperties.some((item) => item === property);
|
||||
};
|
||||
|
||||
export const isValidTransitionValue = (
|
||||
value: LayerValueItem
|
||||
): value is KeywordValue | UnitValue => {
|
||||
return value.type === "keyword" || value.type === "unit";
|
||||
};
|
||||
|
||||
export const parseTransition = (
|
||||
@@ -34,7 +77,6 @@ export const parseTransition = (
|
||||
}
|
||||
|
||||
const cssAst = cssTryParseValue(tokenStream);
|
||||
|
||||
if (cssAst === undefined) {
|
||||
return {
|
||||
type: "invalid",
|
||||
@@ -119,3 +161,46 @@ export const parseTransition = (
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const parseTransitionLonghands = (
|
||||
property: (typeof transitionLongHandProperties)[number],
|
||||
input: string
|
||||
): LayersValue | InvalidValue | UnparsedValue => {
|
||||
const cssAst = cssTryParseValue(input);
|
||||
if (cssAst === undefined) {
|
||||
return {
|
||||
type: "invalid",
|
||||
value: input,
|
||||
};
|
||||
}
|
||||
|
||||
const parsed = csstree.lexer.matchProperty(kebabCase(property), cssAst);
|
||||
if (parsed.error) {
|
||||
return {
|
||||
type: "invalid",
|
||||
value: input,
|
||||
};
|
||||
}
|
||||
|
||||
const layers: LayersValue = { type: "layers", value: [] };
|
||||
csstree.walk(cssAst, (node) => {
|
||||
if (node.type === "Identifier") {
|
||||
if (
|
||||
isAnimatableProperty(node.name) === true ||
|
||||
isTimingFunction(node.name) === true
|
||||
) {
|
||||
layers.value.push({ type: "keyword", value: node.name });
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === "Dimension") {
|
||||
layers.value.push({
|
||||
type: "unit",
|
||||
value: Number(node.value),
|
||||
unit: node.unit as Unit,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return layers.value.length > 0 ? layers : { type: "invalid", value: input };
|
||||
};
|
||||
@@ -16,6 +16,7 @@ export const UnitValue = z.object({
|
||||
type: z.literal("unit"),
|
||||
unit: Unit,
|
||||
value: z.number(),
|
||||
hidden: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type UnitValue = z.infer<typeof UnitValue>;
|
||||
@@ -24,6 +25,7 @@ export const KeywordValue = z.object({
|
||||
type: z.literal("keyword"),
|
||||
// @todo use exact type
|
||||
value: z.string(),
|
||||
hidden: z.boolean().optional(),
|
||||
});
|
||||
export type KeywordValue = z.infer<typeof KeywordValue>;
|
||||
|
||||
|
||||
Reference in new issue
Block a user