mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 22:14:29 +09:00
Ref https://github.com/webstudio-is/webstudio-builder/issues/1116 Slot is copy pastable part of the tree which is always in sync. Kinda like primitive components. The idea is the following. Slot is copy pasted as any other instance though its children are preserved as is without cloning with new ids. This lead to the problem with multiple slot children. While copy paste they are preserved as is though when add new child to slot, the other instance of it will not get new child. To workaround this we add another Fragment component which groups all children into single one and new children are always reflected in all slot copies.
212 lines
5.6 KiB
TypeScript
212 lines
5.6 KiB
TypeScript
import store from "immerhin";
|
|
import { z } from "zod";
|
|
import {
|
|
findTreeInstanceIdsExcludingSlotDescendants,
|
|
InstancesItem,
|
|
Prop,
|
|
StyleDecl,
|
|
StyleSource,
|
|
StyleSourceSelection,
|
|
} from "@webstudio-is/project-build";
|
|
import {
|
|
propsStore,
|
|
stylesStore,
|
|
selectedInstanceSelectorStore,
|
|
styleSourceSelectionsStore,
|
|
styleSourcesStore,
|
|
instancesStore,
|
|
selectedPageStore,
|
|
} from "../nano-states";
|
|
import {
|
|
type InstanceSelector,
|
|
findClosestDroppableTarget,
|
|
findSubtreeLocalStyleSources,
|
|
insertInstancesCopyMutable,
|
|
insertPropsCopyMutable,
|
|
insertStylesCopyMutable,
|
|
insertStyleSourcesCopyMutable,
|
|
insertStyleSourceSelectionsCopyMutable,
|
|
} from "../tree-utils";
|
|
import { deleteInstance } from "../instance-utils";
|
|
|
|
const version = "@webstudio/instance/v0.1";
|
|
|
|
const InstanceData = z.object({
|
|
instances: z.array(InstancesItem),
|
|
props: z.array(Prop),
|
|
styleSourceSelections: z.array(StyleSourceSelection),
|
|
styleSources: z.array(StyleSource),
|
|
styles: z.array(StyleDecl),
|
|
});
|
|
|
|
type InstanceData = z.infer<typeof InstanceData>;
|
|
|
|
const getTreeData = (targetInstanceSelector: InstanceSelector) => {
|
|
// @todo tell user they can't copy or cut root
|
|
if (targetInstanceSelector.length === 1) {
|
|
return;
|
|
}
|
|
|
|
const [targetInstanceId] = targetInstanceSelector;
|
|
const instances = instancesStore.get();
|
|
const treeInstanceIds = findTreeInstanceIdsExcludingSlotDescendants(
|
|
instances,
|
|
targetInstanceId
|
|
);
|
|
const styleSources = styleSourcesStore.get();
|
|
const styleSourceSelections = styleSourceSelectionsStore.get();
|
|
const subtreeLocalStyleSourceIds = findSubtreeLocalStyleSources(
|
|
treeInstanceIds,
|
|
styleSources,
|
|
styleSourceSelections
|
|
);
|
|
|
|
// first item is guaranteed root of copied tree
|
|
const treeInstances: InstancesItem[] = [];
|
|
for (const instanceId of treeInstanceIds) {
|
|
const instance = instances.get(instanceId);
|
|
if (instance) {
|
|
treeInstances.push(instance);
|
|
}
|
|
}
|
|
|
|
const treeStyleSources: StyleSource[] = [];
|
|
for (const styleSourceId of subtreeLocalStyleSourceIds) {
|
|
const styleSource = styleSources.get(styleSourceId);
|
|
if (styleSource) {
|
|
treeStyleSources.push(styleSource);
|
|
}
|
|
}
|
|
|
|
const props = propsStore.get();
|
|
const treeProps: Prop[] = [];
|
|
for (const prop of props.values()) {
|
|
if (treeInstanceIds.has(prop.instanceId)) {
|
|
treeProps.push(prop);
|
|
}
|
|
}
|
|
|
|
const treeStyleSourceSelections: StyleSourceSelection[] = [];
|
|
for (const styleSourceSelection of styleSourceSelections.values()) {
|
|
if (treeInstanceIds.has(styleSourceSelection.instanceId)) {
|
|
treeStyleSourceSelections.push(styleSourceSelection);
|
|
}
|
|
}
|
|
|
|
const styles = stylesStore.get();
|
|
const treeStyles: StyleDecl[] = [];
|
|
for (const styleDecl of styles.values()) {
|
|
if (subtreeLocalStyleSourceIds.has(styleDecl.styleSourceId)) {
|
|
treeStyles.push(styleDecl);
|
|
}
|
|
}
|
|
|
|
return {
|
|
instances: treeInstances,
|
|
styleSources: treeStyleSources,
|
|
props: treeProps,
|
|
styleSourceSelections: treeStyleSourceSelections,
|
|
styles: treeStyles,
|
|
};
|
|
};
|
|
|
|
const stringify = (data: InstanceData) => {
|
|
return JSON.stringify({ [version]: data });
|
|
};
|
|
|
|
const ClipboardData = z.object({ [version]: InstanceData });
|
|
|
|
const parse = (clipboardData: string): InstanceData | undefined => {
|
|
try {
|
|
const data = ClipboardData.parse(JSON.parse(clipboardData));
|
|
return data[version];
|
|
} catch {
|
|
return;
|
|
}
|
|
};
|
|
|
|
export const mimeType = "application/json";
|
|
|
|
export const onPaste = (clipboardData: string) => {
|
|
const data = parse(clipboardData);
|
|
const selectedPage = selectedPageStore.get();
|
|
if (data === undefined || selectedPage === undefined) {
|
|
return;
|
|
}
|
|
// paste to the root if nothing is selected
|
|
const instanceSelector = selectedInstanceSelectorStore.get() ?? [
|
|
selectedPage.rootInstanceId,
|
|
];
|
|
const dropTarget = findClosestDroppableTarget(
|
|
instancesStore.get(),
|
|
instanceSelector
|
|
);
|
|
store.createTransaction(
|
|
[
|
|
instancesStore,
|
|
styleSourcesStore,
|
|
propsStore,
|
|
styleSourceSelectionsStore,
|
|
stylesStore,
|
|
],
|
|
(instances, styleSources, props, styleSourceSelections, styles) => {
|
|
const copiedInstanceIds = insertInstancesCopyMutable(
|
|
instances,
|
|
data.instances,
|
|
dropTarget
|
|
);
|
|
const copiedStyleSourceIds = insertStyleSourcesCopyMutable(
|
|
styleSources,
|
|
data.styleSources
|
|
);
|
|
insertPropsCopyMutable(props, data.props, copiedInstanceIds);
|
|
insertStyleSourceSelectionsCopyMutable(
|
|
styleSourceSelections,
|
|
data.styleSourceSelections,
|
|
copiedInstanceIds,
|
|
copiedStyleSourceIds
|
|
);
|
|
insertStylesCopyMutable(styles, data.styles, copiedStyleSourceIds);
|
|
|
|
// first item is guaranteed root of copied tree
|
|
const copiedRootInstanceId = Array.from(copiedInstanceIds.values())[0];
|
|
selectedInstanceSelectorStore.set([
|
|
copiedRootInstanceId,
|
|
...instanceSelector,
|
|
]);
|
|
}
|
|
);
|
|
};
|
|
|
|
export const onCopy = () => {
|
|
const selectedInstanceSelector = selectedInstanceSelectorStore.get();
|
|
if (selectedInstanceSelector === undefined) {
|
|
return;
|
|
}
|
|
const data = getTreeData(selectedInstanceSelector);
|
|
if (data === undefined) {
|
|
return;
|
|
}
|
|
return stringify(data);
|
|
};
|
|
|
|
export const onCut = () => {
|
|
const selectedInstanceSelector = selectedInstanceSelectorStore.get();
|
|
if (selectedInstanceSelector === undefined) {
|
|
return;
|
|
}
|
|
// @todo tell user they can't delete root
|
|
if (selectedInstanceSelector.length === 1) {
|
|
return;
|
|
}
|
|
const data = getTreeData(selectedInstanceSelector);
|
|
if (data === undefined) {
|
|
return;
|
|
}
|
|
deleteInstance(selectedInstanceSelector);
|
|
if (data === undefined) {
|
|
return;
|
|
}
|
|
return stringify(data);
|
|
};
|