Skip to content

Commit cfeb75c

Browse files
authored
refactor: replace detachable: false with constraints (#4548)
Here got rid of detachable flag in favor of more universal constraints queries. Despite of detachable constraints allow to drag such nodes but limits drop target more precisely. Try any radix component.
1 parent b867e67 commit cfeb75c

File tree

17 files changed

+321
-118
lines changed

17 files changed

+321
-118
lines changed

apps/builder/app/builder/features/navigator/navigator-tree.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,13 +440,16 @@ const canDrag = (instance: Instance, instanceSelector: InstanceSelector) => {
440440
return false;
441441
}
442442
}
443+
// prevent moving block template out of first position
444+
if (instance.component === blockTemplateComponent) {
445+
return false;
446+
}
443447

444448
const meta = $registeredComponentMetas.get().get(instance.component);
445449
if (meta === undefined) {
446450
return true;
447451
}
448-
const detachable =
449-
meta.type !== "rich-text-child" && (meta.detachable ?? true);
452+
const detachable = meta.type !== "rich-text-child";
450453
if (detachable === false) {
451454
toast.error(
452455
"This instance can not be moved outside of its parent component."

apps/builder/app/canvas/shared/use-drag-drop.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
} from "~/shared/nano-states";
1616
import { publish, useSubscribe } from "~/shared/pubsub";
1717
import {
18-
findClosestDetachableInstanceSelector,
1918
getComponentTemplateData,
2019
insertTemplateData,
2120
reparentInstance,
@@ -28,6 +27,7 @@ import {
2827
import {
2928
type InstanceSelector,
3029
areInstanceSelectorsEqual,
30+
getAncestorInstanceSelector,
3131
} from "~/shared/tree-utils";
3232
import {
3333
findClosestInstanceMatchingFragment,
@@ -114,6 +114,26 @@ const sharedDropOptions = {
114114
},
115115
};
116116

117+
const findClosestDraggable = (instanceSelector: InstanceSelector) => {
118+
// cannot drag root
119+
if (instanceSelector.length === 1) {
120+
return;
121+
}
122+
const instances = $instances.get();
123+
const metas = $registeredComponentMetas.get();
124+
for (const instanceId of instanceSelector) {
125+
const instance = instances.get(instanceId);
126+
if (instance === undefined) {
127+
return;
128+
}
129+
const meta = metas.get(instance.component);
130+
if (meta?.type === "rich-text-child") {
131+
continue;
132+
}
133+
return getAncestorInstanceSelector(instanceSelector, instanceId);
134+
}
135+
};
136+
117137
export const useDragAndDrop = () => {
118138
const state = useRef({ ...initialState });
119139

@@ -201,22 +221,12 @@ export const useDragAndDrop = () => {
201221
if (instanceSelector === undefined) {
202222
return false;
203223
}
204-
// cannot drag root
205-
if (instanceSelector.length === 1) {
206-
return false;
207-
}
208224
// cannot drag while editing text
209225
if (element.closest("[contenteditable=true]")) {
210226
return false;
211227
}
212228
// When trying to drag an instance inside editor, drag the editor instead
213-
return (
214-
findClosestDetachableInstanceSelector(
215-
instanceSelector,
216-
$instances.get(),
217-
$registeredComponentMetas.get()
218-
) ?? false
219-
);
229+
return findClosestDraggable(instanceSelector) ?? false;
220230
},
221231

222232
onStart({ data: dragInstanceSelector }) {

apps/builder/app/shared/instance-utils.ts

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -255,28 +255,6 @@ export const findClosestEditableInstanceSelector = (
255255
}
256256
};
257257

258-
export const findClosestDetachableInstanceSelector = (
259-
instanceSelector: InstanceSelector,
260-
instances: Instances,
261-
metas: Map<string, WsComponentMeta>
262-
) => {
263-
for (const instanceId of instanceSelector) {
264-
const instance = instances.get(instanceId);
265-
if (instance === undefined) {
266-
return;
267-
}
268-
const meta = metas.get(instance.component);
269-
if (meta === undefined) {
270-
return;
271-
}
272-
const detachable = meta.detachable ?? true;
273-
if (meta.type === "rich-text-child" || detachable === false) {
274-
continue;
275-
}
276-
return getAncestorInstanceSelector(instanceSelector, instanceId);
277-
}
278-
};
279-
280258
export const isInstanceDetachable = (
281259
instances: Instances,
282260
instanceSelector: InstanceSelector
@@ -296,17 +274,11 @@ export const isInstanceDetachable = (
296274
newInstances.set(parentInstance.id, parentInstance);
297275
}
298276
// check parent can follow constraints without selected instance
299-
const matches = isTreeMatching({
277+
return isTreeMatching({
300278
instances: newInstances,
301279
metas,
302280
instanceSelector: instanceSelector.slice(1),
303281
});
304-
const instance = instances.get(instanceId);
305-
if (instance === undefined) {
306-
return false;
307-
}
308-
const meta = metas.get(instance.component);
309-
return (meta?.detachable ?? true) && matches;
310282
};
311283

312284
export const insertInstanceChildrenMutable = (

apps/builder/app/shared/matcher.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import type {
2-
Instance,
3-
Instances,
4-
Matcher,
5-
MatcherOperation,
6-
MatcherRelation,
7-
WebstudioFragment,
1+
import {
2+
parseComponentName,
3+
type Instance,
4+
type Instances,
5+
type Matcher,
6+
type MatcherOperation,
7+
type MatcherRelation,
8+
type WebstudioFragment,
89
} from "@webstudio-is/sdk";
910
import type { InstanceSelector } from "./tree-utils";
1011
import type { WsComponentMeta } from "@webstudio-is/react-sdk";
@@ -65,7 +66,12 @@ const formatList = (operation: MatcherOperation) => {
6566
if (operation.$nin) {
6667
list.push(...operation.$nin);
6768
}
68-
return listFormat.format(list);
69+
return listFormat.format(
70+
list.map((component) => {
71+
const [_namespace, name] = parseComponentName(component);
72+
return name;
73+
})
74+
);
6975
};
7076

7177
/**

packages/react-sdk/src/components/component-meta.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,6 @@ export const WsComponentMeta = z.object({
6767
// naming every item manually
6868
indexWithinAncestor: z.optional(z.string()),
6969
stylable: z.optional(z.boolean()),
70-
// specifies whether the instance can be deleted,
71-
// copied or dragged out of its parent instance
72-
// true by default
73-
detachable: z.optional(z.boolean()),
7470
label: z.optional(z.string()),
7571
description: z.string().optional(),
7672
icon: z.string(),

packages/react-sdk/src/core-components.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ const descendantMeta: WsComponentMeta = {
9494
type: "control",
9595
label: "Descendant",
9696
icon: PaintBrushIcon,
97-
detachable: false,
97+
constraints: {
98+
relation: "parent",
99+
component: { $eq: "HtmlEmbed" },
100+
},
98101
};
99102

100103
const descendantPropsMeta: WsComponentPropsMeta = {
@@ -126,33 +129,42 @@ const descendantPropsMeta: WsComponentPropsMeta = {
126129
initialProps: ["selector"],
127130
};
128131

132+
export const blockComponent = "ws:block";
133+
129134
export const blockTemplateComponent = "ws:block-template";
130135

131136
export const blockTemplateMeta: WsComponentMeta = {
132137
category: "hidden",
133-
detachable: false,
134138
type: "container",
135139
icon: AddTemplateInstanceIcon,
136140
stylable: false,
141+
constraints: {
142+
relation: "parent",
143+
component: { $eq: blockComponent },
144+
},
137145
};
138146

139147
const blockTemplatePropsMeta: WsComponentPropsMeta = {
140148
props: {},
141149
initialProps: [],
142150
};
143151

144-
export const blockComponent = "ws:block";
145-
146152
const blockMeta: WsComponentMeta = {
147153
category: "data",
148154
order: 2,
149155
type: "container",
150156
label: "Content Block",
151157
icon: EditIcon,
152-
constraints: {
153-
relation: "ancestor",
154-
component: { $nin: [collectionComponent, blockComponent] },
155-
},
158+
constraints: [
159+
{
160+
relation: "ancestor",
161+
component: { $nin: [collectionComponent, blockComponent] },
162+
},
163+
{
164+
relation: "child",
165+
component: { $eq: blockTemplateComponent },
166+
},
167+
],
156168
stylable: false,
157169
template: [
158170
{

packages/sdk-components-react-radix/src/accordion.ws.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,12 @@ export const metaAccordion: WsComponentMeta = {
127127
presetStyle,
128128
description:
129129
"A vertically stacked set of interactive headings that each reveal an associated section of content. Clicking on the heading will open the item and close other items.",
130+
constraints: [
131+
{
132+
relation: "descendant",
133+
component: { $eq: "AccordionItem" },
134+
},
135+
],
130136
template: [
131137
{
132138
type: "instance",
@@ -222,10 +228,20 @@ export const metaAccordionItem: WsComponentMeta = {
222228
type: "container",
223229
label: "Item",
224230
icon: ItemIcon,
225-
constraints: {
226-
relation: "ancestor",
227-
component: { $eq: "Accordion" },
228-
},
231+
constraints: [
232+
{
233+
relation: "ancestor",
234+
component: { $eq: "Accordion" },
235+
},
236+
{
237+
relation: "descendant",
238+
component: { $eq: "AccordionHeader" },
239+
},
240+
{
241+
relation: "descendant",
242+
component: { $eq: "AccordionContent" },
243+
},
244+
],
229245
indexWithinAncestor: "Accordion",
230246
presetStyle,
231247
};
@@ -235,11 +251,16 @@ export const metaAccordionHeader: WsComponentMeta = {
235251
type: "container",
236252
label: "Item Header",
237253
icon: HeaderIcon,
238-
constraints: {
239-
relation: "ancestor",
240-
component: { $eq: "AccordionItem" },
241-
},
242-
detachable: false,
254+
constraints: [
255+
{
256+
relation: "ancestor",
257+
component: { $eq: "AccordionItem" },
258+
},
259+
{
260+
relation: "descendant",
261+
component: { $eq: "AccordionTrigger" },
262+
},
263+
],
243264
presetStyle: {
244265
h3: [h3, tc.my(0)].flat(),
245266
},
@@ -254,7 +275,6 @@ export const metaAccordionTrigger: WsComponentMeta = {
254275
relation: "ancestor",
255276
component: { $eq: "AccordionHeader" },
256277
},
257-
detachable: false,
258278
states: [
259279
...defaultStates,
260280
{
@@ -277,7 +297,6 @@ export const metaAccordionContent: WsComponentMeta = {
277297
relation: "ancestor",
278298
component: { $eq: "AccordionItem" },
279299
},
280-
detachable: false,
281300
presetStyle,
282301
};
283302

packages/sdk-components-react-radix/src/checkbox.ws.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export const metaCheckbox: WsComponentMeta = {
2020
category: "radix",
2121
order: 101,
2222
type: "container",
23+
constraints: {
24+
relation: "descendant",
25+
component: { $eq: "CheckboxIndicator" },
26+
},
2327
icon: CheckboxCheckedIcon,
2428
description:
2529
"Use within a form to allow your users to toggle between checked and not checked. Group checkboxes by matching their “Name” properties. Unlike radios, any number of checkboxes in a group can be checked.",
@@ -136,7 +140,10 @@ export const metaCheckbox: WsComponentMeta = {
136140
export const metaCheckboxIndicator: WsComponentMeta = {
137141
category: "hidden",
138142
type: "container",
139-
detachable: false,
143+
constraints: {
144+
relation: "ancestor",
145+
component: { $eq: "Checkbox" },
146+
},
140147
icon: TriggerIcon,
141148
states: defaultStates,
142149
presetStyle: {

packages/sdk-components-react-radix/src/collapsible.ws.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ export const metaCollapsible: WsComponentMeta = {
2424
category: "radix",
2525
order: 5,
2626
type: "container",
27+
constraints: [
28+
{
29+
relation: "descendant",
30+
component: { $eq: "CollapsibleTrigger" },
31+
},
32+
{
33+
relation: "descendant",
34+
component: { $eq: "CollapsibleContent" },
35+
},
36+
],
2737
presetStyle,
2838
icon: CollapsibleIcon,
2939
description:
@@ -79,15 +89,21 @@ export const metaCollapsibleTrigger: WsComponentMeta = {
7989
type: "container",
8090
icon: TriggerIcon,
8191
stylable: false,
82-
detachable: false,
92+
constraints: {
93+
relation: "ancestor",
94+
component: { $eq: "Collapsible" },
95+
},
8396
};
8497

8598
export const metaCollapsibleContent: WsComponentMeta = {
8699
category: "hidden",
87100
type: "container",
88101
presetStyle,
89102
icon: ContentIcon,
90-
detachable: false,
103+
constraints: {
104+
relation: "ancestor",
105+
component: { $eq: "Collapsible" },
106+
},
91107
};
92108

93109
export const propsMetaCollapsible: WsComponentPropsMeta = {

0 commit comments

Comments
 (0)