Files
webstudio/apps/builder/app/shared/copy-paste/plugin-instance.ts
T
Bogdan Chadkin bac73301c8 Refactor insert and reparent operations with selectors (#1283)
- in many places root check can be replaced with selector.length === 1
- reparent and insert utilities now accept selectors
- findClosestDroppableTarget output end instead of last index and always
fallback to root
2023-03-24 14:56:30 +07:00

209 lines
5.6 KiB
TypeScript

import store from "immerhin";
import { z } from "zod";
import {
findTreeInstanceIds,
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 = findTreeInstanceIds(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);
};