Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions apps/builder/app/builder/features/components/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
import {
getComponentTemplateData,
getInstanceLabel,
insertWebstudioElementAt,
insertWebstudioFragmentAt,
} from "~/shared/instance-utils";
import type { Publish } from "~/shared/pubsub";
Expand Down Expand Up @@ -215,9 +216,13 @@ export const ComponentsPanel = ({
const [selectedComponent, setSelectedComponent] = useState<string>();

const handleInsert = (component: string) => {
const fragment = getComponentTemplateData(component);
if (fragment) {
insertWebstudioFragmentAt(fragment);
if (component === elementComponent) {
insertWebstudioElementAt();
} else {
const fragment = getComponentTemplateData(component);
if (fragment) {
insertWebstudioFragmentAt(fragment);
}
}
onClose();
};
Expand Down
51 changes: 28 additions & 23 deletions apps/builder/app/canvas/shared/use-drag-drop.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useLayoutEffect, useRef } from "react";
import type { Instance } from "@webstudio-is/sdk";
import { elementComponent, type Instance } from "@webstudio-is/sdk";
import {
type Point,
useAutoScroll,
Expand All @@ -17,6 +17,7 @@ import {
import { publish, useSubscribe } from "~/shared/pubsub";
import {
getComponentTemplateData,
insertWebstudioElementAt,
insertWebstudioFragmentAt,
reparentInstance,
} from "~/shared/instance-utils";
Expand Down Expand Up @@ -79,15 +80,20 @@ const findClosestDroppableInstanceSelector = (
});
let droppableIndex = -1;
if (dragPayload?.type === "insert") {
const fragment = getComponentTemplateData(dragPayload.dragComponent);
if (fragment) {
droppableIndex = findClosestInstanceMatchingFragment({
instances,
props,
metas,
instanceSelector,
fragment,
});
// allow dropping element into any container
if (dragPayload.dragComponent === elementComponent) {
droppableIndex = 0;
} else {
const fragment = getComponentTemplateData(dragPayload.dragComponent);
if (fragment) {
droppableIndex = findClosestInstanceMatchingFragment({
instances,
props,
metas,
instanceSelector,
fragment,
});
}
}
}
if (dragPayload?.type === "reparent") {
Expand Down Expand Up @@ -316,23 +322,22 @@ export const useDragAndDrop = () => {
const { dropTarget, dragPayload } = state.current;

if (dropTarget && dragPayload && isCanceled === false) {
const insertable = {
parentSelector: dropTarget.itemSelector,
position: dropTarget.indexWithinChildren,
};
if (dragPayload.type === "insert") {
const templateData = getComponentTemplateData(
dragPayload.dragComponent
);
if (templateData === undefined) {
return;
if (dragPayload.dragComponent === elementComponent) {
insertWebstudioElementAt(insertable);
} else {
const fragment = getComponentTemplateData(dragPayload.dragComponent);
if (fragment) {
insertWebstudioFragmentAt(fragment, insertable);
}
}
insertWebstudioFragmentAt(templateData, {
parentSelector: dropTarget.itemSelector,
position: dropTarget.indexWithinChildren,
});
}
if (dragPayload.type === "reparent") {
reparentInstance(dragPayload.dragInstanceSelector, {
parentSelector: dropTarget.itemSelector,
position: dropTarget.indexWithinChildren,
});
reparentInstance(dragPayload.dragInstanceSelector, insertable);
}
}

Expand Down
145 changes: 144 additions & 1 deletion apps/builder/app/shared/instance-utils.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
insertInstanceChildrenMutable,
findClosestInsertable,
insertWebstudioFragmentAt,
insertWebstudioElementAt,
} from "./instance-utils";
import {
$assets,
Expand All @@ -51,7 +52,12 @@ import {
$resources,
} from "./nano-states";
import { registerContainers } from "./sync";
import { $awareness, getInstancePath, selectInstance } from "./awareness";
import {
$awareness,
getInstancePath,
selectInstance,
selectPage,
} from "./awareness";

enableMapSet();
registerContainers();
Expand Down Expand Up @@ -339,6 +345,143 @@ describe("insert instance children", () => {
});
});

describe("insert webstudio element at", () => {
beforeEach(() => {
$styleSourceSelections.set(new Map());
$styleSources.set(new Map());
$breakpoints.set(new Map());
$styles.set(new Map());
$dataSources.set(new Map());
$resources.set(new Map());
$props.set(new Map());
$assets.set(new Map());
});

test("insert element with div tag into body", () => {
$instances.set(renderData(<$.Body ws:id="bodyId"></$.Body>).instances);
insertWebstudioElementAt({
parentSelector: ["bodyId"],
position: "end",
});
const [_bodyId, newInstanceId] = $instances.get().keys();
expect($instances.get()).toEqual(
renderData(
<$.Body ws:id="bodyId">
<ws.element ws:id={newInstanceId} ws:tag="div" />
</$.Body>
).instances
);
});

test("insert element with li tag into ul", () => {
$instances.set(
renderData(
<$.Body ws:id="bodyId">
<ws.element ws:id="listId" ws:tag="ul"></ws.element>
</$.Body>
).instances
);
insertWebstudioElementAt({
parentSelector: ["listId", "bodyId"],
position: "end",
});
const [_bodyId, _listId, newInstanceId] = $instances.get().keys();
expect($instances.get()).toEqual(
renderData(
<$.Body ws:id="bodyId">
<ws.element ws:id="listId" ws:tag="ul">
<ws.element ws:id={newInstanceId} ws:tag="li" />
</ws.element>
</$.Body>
).instances
);
});

test("insert element into selected instance", () => {
$pages.set(
createDefaultPages({ homePageId: "homePageId", rootInstanceId: "bodyId" })
);
$instances.set(
renderData(
<$.Body ws:id="bodyId">
<ws.element ws:id="divId" ws:tag="div"></ws.element>
</$.Body>
).instances
);
selectPage("homePageId");
selectInstance(["divId", "bodyId"]);
insertWebstudioElementAt();
const [_bodyId, _divId, newInstanceId] = $instances.get().keys();
expect($instances.get()).toEqual(
renderData(
<$.Body ws:id="bodyId">
<ws.element ws:id="divId" ws:tag="div">
<ws.element ws:id={newInstanceId} ws:tag="div" />
</ws.element>
</$.Body>
).instances
);
});

test("insert element into closest non-textual container", () => {
$pages.set(
createDefaultPages({ homePageId: "homePageId", rootInstanceId: "bodyId" })
);
$instances.set(
renderData(
<$.Body ws:id="bodyId">
<ws.element ws:id="divId" ws:tag="div">
text
</ws.element>
<ws.element ws:id="spanId" ws:tag="span"></ws.element>
</$.Body>
).instances
);
selectPage("homePageId");
selectInstance(["divId", "bodyId"]);
insertWebstudioElementAt();
const [_bodyId, _divId, _spanId, newInstanceId] = $instances.get().keys();
expect($instances.get()).toEqual(
renderData(
<$.Body ws:id="bodyId">
<ws.element ws:id="divId" ws:tag="div">
text
</ws.element>
<ws.element ws:id={newInstanceId} ws:tag="div" />
<ws.element ws:id="spanId" ws:tag="span"></ws.element>
</$.Body>
).instances
);
});

test("insert element into closest non-empty container", () => {
$pages.set(
createDefaultPages({ homePageId: "homePageId", rootInstanceId: "bodyId" })
);
$instances.set(
renderData(
<$.Body ws:id="bodyId">
<ws.element ws:id="imgId" ws:tag="img"></ws.element>
<ws.element ws:id="spanId" ws:tag="span"></ws.element>
</$.Body>
).instances
);
selectPage("homePageId");
selectInstance(["imgId", "bodyId"]);
insertWebstudioElementAt();
const [_bodyId, _imgId, _spanId, newInstanceId] = $instances.get().keys();
expect($instances.get()).toEqual(
renderData(
<$.Body ws:id="bodyId">
<ws.element ws:id="imgId" ws:tag="img"></ws.element>
<ws.element ws:id={newInstanceId} ws:tag="div" />
<ws.element ws:id="spanId" ws:tag="span"></ws.element>
</$.Body>
).instances
);
});
});

describe("insert webstudio fragment at", () => {
beforeEach(() => {
$styleSourceSelections.set(new Map());
Expand Down
77 changes: 77 additions & 0 deletions apps/builder/app/shared/instance-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
parseComponentName,
Props,
elementComponent,
tags,
} from "@webstudio-is/sdk";
import {
$props,
Expand Down Expand Up @@ -62,6 +63,7 @@ import { setDifference, setUnion } from "./shim";
import { breakCyclesMutable, findCycles } from "@webstudio-is/project-build";
import {
$awareness,
$selectedInstancePath,
$selectedPage,
getInstancePath,
selectInstance,
Expand All @@ -76,6 +78,7 @@ import {
import {
findClosestNonTextualContainer,
isRichTextTree,
isTreeSatisfyingContentModel,
} from "./content-model";
import type { Project } from "@webstudio-is/project";

Expand Down Expand Up @@ -258,6 +261,80 @@ export const insertInstanceChildrenMutable = (
}
};

export const insertWebstudioElementAt = (insertable?: Insertable) => {
const instances = $instances.get();
const props = $props.get();
const metas = $registeredComponentMetas.get();
// find closest container and try to match new element with it
if (insertable === undefined) {
const instancePath = $selectedInstancePath.get();
if (instancePath === undefined) {
return false;
}
const [{ instanceSelector }] = instancePath;
const containerSelector = findClosestNonTextualContainer({
instances,
props,
metas,
instanceSelector,
});
const insertableIndex = instanceSelector.length - containerSelector.length;
if (insertableIndex === 0) {
insertable = {
parentSelector: containerSelector,
position: "end",
};
} else {
const containerInstance = instances.get(containerSelector[0]);
if (containerInstance === undefined) {
return false;
}
const lastChildInstanceId = instanceSelector[insertableIndex - 1];
const lastChildPosition = containerInstance.children.findIndex(
(child) => child.type === "id" && child.value === lastChildInstanceId
);
insertable = {
parentSelector: containerSelector,
position: lastChildPosition + 1,
};
}
}
// create element and find matching tag
const element: Instance = {
type: "instance",
id: nanoid(),
component: elementComponent,
children: [],
};
const newInstances = new Map(instances);
newInstances.set(element.id, element);
let matchingTag: undefined | string;
for (const tag of tags) {
element.tag = tag;
const isSatisfying = isTreeSatisfyingContentModel({
instances: newInstances,
props,
metas,
instanceSelector: [element.id, ...insertable.parentSelector],
});
if (isSatisfying) {
matchingTag = tag;
break;
}
}
if (matchingTag === undefined) {
return false;
}
// insert element
updateWebstudioData((data) => {
data.instances.set(element.id, element);
const children: Instance["children"] = [{ type: "id", value: element.id }];
insertInstanceChildrenMutable(data, children, insertable);
});
selectInstance([element.id, ...insertable.parentSelector]);
return true;
};

export const insertWebstudioFragmentAt = (
fragment: WebstudioFragment,
insertable?: Insertable
Expand Down
11 changes: 11 additions & 0 deletions packages/html-data/bin/elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ for (const [tag, element] of Object.entries(elementsByTag)) {
}
tags.push(tag);
}
const getTagScore = (tag: string) => {
if (tag === "div") {
return 20;
}
if (tag === "span") {
return 10;
}
return 0;
};
// put div and span first
tags.sort((left, right) => getTagScore(right) - getTagScore(left));
const tagsContent = `export const tags: string[] = ${JSON.stringify(tags, null, 2)};
`;
const tagsFile = "../sdk/src/__generated__/tags.ts";
Expand Down
Loading