Skip to content

Commit a44530b

Browse files
authored
experimental: adapt tag control to html content model (#5208)
Ref #3632 Here improved tag control to allow updating with tag satisfying content model. For example when ul has li it can only be changed to ol and menu. li inside ul cannot be changed with anything. Also switch to combobox when there is more than 10 satisfying tags. https://github.com/user-attachments/assets/ad42bba7-9be1-4f50-9b1a-6f6cda52f3e5
1 parent 28e61f4 commit a44530b

File tree

1 file changed

+86
-29
lines changed

1 file changed

+86
-29
lines changed
Lines changed: 86 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,70 @@
1+
import { useState } from "react";
2+
import { computed } from "nanostores";
13
import { useStore } from "@nanostores/react";
2-
import { Box, Select, theme } from "@webstudio-is/design-system";
4+
import { Box, Combobox, Select, theme } from "@webstudio-is/design-system";
35
import { elementsByTag } from "@webstudio-is/html-data";
4-
import { $selectedInstance } from "~/shared/awareness";
6+
import { tags } from "@webstudio-is/sdk";
7+
import { $selectedInstance, $selectedInstancePath } from "~/shared/awareness";
58
import { updateWebstudioData } from "~/shared/instance-utils";
9+
import { isTreeSatisfyingContentModel } from "~/shared/content-model";
10+
import {
11+
$instances,
12+
$props,
13+
$registeredComponentMetas,
14+
} from "~/shared/nano-states";
615
import { type ControlProps, VerticalLayout } from "../shared";
716
import { FieldLabel } from "../property-label";
817

18+
const $satisfyingTags = computed(
19+
[$selectedInstancePath, $instances, $props, $registeredComponentMetas],
20+
(instancePath, instances, props, metas) => {
21+
const satisfyingTags: string[] = [];
22+
if (instancePath === undefined) {
23+
return satisfyingTags;
24+
}
25+
const [{ instance, instanceSelector }] = instancePath;
26+
const newInstances = new Map(instances);
27+
for (const tag of tags) {
28+
newInstances.set(instance.id, { ...instance, tag });
29+
const isSatisfying = isTreeSatisfyingContentModel({
30+
instances: newInstances,
31+
props,
32+
metas,
33+
instanceSelector,
34+
});
35+
if (isSatisfying) {
36+
satisfyingTags.push(tag);
37+
}
38+
}
39+
return satisfyingTags;
40+
}
41+
);
42+
943
export const TagControl = ({ meta, prop }: ControlProps<"tag">) => {
1044
const instance = useStore($selectedInstance);
1145
const propTag = prop?.type === "string" ? prop.value : undefined;
1246
const instanceTag = instance?.tag;
1347
const defaultTag = meta.options[0];
14-
const value = propTag ?? instanceTag ?? defaultTag;
48+
const computedTag = instanceTag ?? propTag ?? defaultTag;
49+
const satisfyingTags = useStore($satisfyingTags);
50+
const options = meta.options.filter((tag) => satisfyingTags.includes(tag));
51+
const [value, setValue] = useState<undefined | string>();
52+
const updateTag = (value: string) => {
53+
if (instance === undefined) {
54+
return;
55+
}
56+
const instanceId = instance.id;
57+
updateWebstudioData((data) => {
58+
// clean legacy <Box tag> and <Text tag>
59+
if (prop) {
60+
data.props.delete(prop.id);
61+
}
62+
const instance = data.instances.get(instanceId);
63+
if (instance) {
64+
instance.tag = value;
65+
}
66+
});
67+
};
1568
return (
1669
<VerticalLayout
1770
label={
@@ -20,32 +73,36 @@ export const TagControl = ({ meta, prop }: ControlProps<"tag">) => {
2073
</FieldLabel>
2174
}
2275
>
23-
<Select
24-
fullWidth
25-
value={value}
26-
options={meta.options}
27-
onChange={(value) => {
28-
if (instance === undefined) {
29-
return;
30-
}
31-
const instanceId = instance.id;
32-
updateWebstudioData((data) => {
33-
// clean legacy <Box tag> and <Text tag>
34-
if (prop) {
35-
data.props.delete(prop.id);
36-
}
37-
const instance = data.instances.get(instanceId);
38-
if (instance) {
39-
instance.tag = value;
40-
}
41-
});
42-
}}
43-
getDescription={(item) => (
44-
<Box css={{ width: theme.spacing[28] }}>
45-
{elementsByTag[item]?.description}
46-
</Box>
47-
)}
48-
/>
76+
{options.length > 10 ? (
77+
<Combobox<string>
78+
defaultHighlightedIndex={0}
79+
getItems={() => options}
80+
onItemSelect={(item) => {
81+
updateTag(item);
82+
setValue(undefined);
83+
}}
84+
itemToString={(item) => item ?? options[0]}
85+
value={value ?? computedTag}
86+
onChange={(value) => setValue(value ?? undefined)}
87+
getDescription={(item) => (
88+
<Box css={{ width: theme.spacing[28] }}>
89+
{elementsByTag[item ?? ""]?.description}
90+
</Box>
91+
)}
92+
/>
93+
) : (
94+
<Select
95+
fullWidth
96+
value={computedTag}
97+
options={options}
98+
onChange={updateTag}
99+
getDescription={(item) => (
100+
<Box css={{ width: theme.spacing[28] }}>
101+
{elementsByTag[item]?.description}
102+
</Box>
103+
)}
104+
/>
105+
)}
49106
</VerticalLayout>
50107
);
51108
};

0 commit comments

Comments
 (0)