Files
webstudio/apps/builder/app/shared/copy-paste/plugin-instance.ts
T
Oleg Isonen 3836e2d2cb Turn instance copy paste into a plugin for copy/paste (#1076)
## Description

This effectively is the beginning of copy/paste plugin system. Now
instance copy paste is a plugin that is fully encapsulated and we can
add more of them

## Steps for reproduction

1. copy/paste instances on the same page
2. copy/paste instances across pages
3. copy paste across projects

## Code Review

- [ ] hi @TrySound , I need you to do
  - conceptual review (architecture, feature-correctness)
  - detailed review (read every line)
  - test it on preview

## Before requesting a review

- [ ] made a self-review
- [ ] added inline comments where things may be not obvious (the "why",
not "what")

## Before merging

- [ ] tested locally and on preview environment (preview dev login:
5de6)
- [ ] updated [test
cases](https://github.com/webstudio-is/webstudio-builder/blob/main/apps/builder/docs/test-cases.md)
document
- [ ] added tests
- [ ] if any new env variables are added, added them to `.env.example`
and the `builder/env-check.js` if mandatory
2023-02-18 15:21:29 +01:00

185 lines
4.6 KiB
TypeScript

import store from "immerhin";
import { z } from "zod";
import {
getStyleDeclKey,
Instance,
Prop,
StyleDecl,
StyleSource,
StyleSourceSelection,
} from "@webstudio-is/project-build";
import {
rootInstanceContainer,
propsStore,
stylesStore,
selectedInstanceIdStore,
styleSourceSelectionsStore,
styleSourcesStore,
instancesIndexStore,
} from "../nano-states";
import {
cloneInstance,
cloneProps,
cloneStyles,
cloneStyleSources,
cloneStyleSourceSelections,
createInstancesIndex,
findClosestDroppableTarget,
findSubtree,
findSubtreeLocalStyleSources,
insertInstanceMutable,
} from "../tree-utils";
import { deleteInstance } from "../instance-utils";
import { initCopyPaste } from "./init-copy-paste";
const version = "@webstudio/instance/v0.1";
const InstanceData = z.object({
instance: Instance,
props: z.array(Prop),
styleSourceSelections: z.array(StyleSourceSelection),
styleSources: z.array(StyleSource),
styles: z.array(StyleDecl),
});
type InstanceData = z.infer<typeof InstanceData>;
const copyInstanceData = (targetInstanceId: string) => {
const rootInstance = rootInstanceContainer.get();
if (rootInstance === undefined) {
return;
}
// @todo tell user they can't copy or cut root
if (targetInstanceId === rootInstance.id) {
return;
}
const { targetInstance, subtreeIds } = findSubtree(
rootInstance,
targetInstanceId
);
const styleSources = styleSourcesStore.get();
const styleSourceSelections = styleSourceSelectionsStore.get();
const subtreeLocalStyleSourceIds = findSubtreeLocalStyleSources(
subtreeIds,
styleSources,
styleSourceSelections
);
if (targetInstance === undefined) {
return;
}
// clone all instance related data and link it with new ids
const { clonedInstance, clonedInstanceIds } = cloneInstance(targetInstance);
const clonedProps = cloneProps(propsStore.get(), clonedInstanceIds);
const { clonedStyleSources, clonedStyleSourceIds } = cloneStyleSources(
styleSources,
subtreeLocalStyleSourceIds
);
const clonedStyleSourceSelections = cloneStyleSourceSelections(
styleSourceSelections,
clonedInstanceIds,
clonedStyleSourceIds
);
const clonedStyles = cloneStyles(stylesStore.get(), clonedStyleSourceIds);
return {
instance: clonedInstance,
props: clonedProps,
styleSourceSelections: clonedStyleSourceSelections,
styleSources: clonedStyleSources,
styles: clonedStyles,
};
};
const stringify = (data: InstanceData) => {
return JSON.stringify({ [version]: data });
};
const ClipboardData = z.object({ [version]: InstanceData });
const parse = (text: string): InstanceData | undefined => {
try {
const data = ClipboardData.parse(JSON.parse(text));
return data[version];
} catch {
return;
}
};
const onPaste = (clipboardData: string) => {
const data = parse(clipboardData);
if (data === undefined) {
return;
}
const dropTarget = findClosestDroppableTarget(
instancesIndexStore.get(),
selectedInstanceIdStore.get()
);
store.createTransaction(
[
rootInstanceContainer,
propsStore,
styleSourceSelectionsStore,
styleSourcesStore,
stylesStore,
],
(rootInstance, props, styleSourceSelections, styleSources, styles) => {
const instancesIndex = createInstancesIndex(rootInstance);
insertInstanceMutable(instancesIndex, data.instance, dropTarget);
for (const prop of data.props) {
props.set(prop.id, prop);
}
for (const styleSourceSelection of data.styleSourceSelections) {
styleSourceSelections.set(
styleSourceSelection.instanceId,
styleSourceSelection
);
}
for (const styleSource of data.styleSources) {
styleSources.set(styleSource.id, styleSource);
}
for (const styleDecl of data.styles) {
styles.set(getStyleDeclKey(styleDecl), styleDecl);
}
}
);
selectedInstanceIdStore.set(data.instance.id);
};
const onCopy = () => {
const selectedInstanceId = selectedInstanceIdStore.get();
if (selectedInstanceId === undefined) {
return;
}
const data = copyInstanceData(selectedInstanceId);
if (data === undefined) {
return;
}
return stringify(data);
};
const onCut = () => {
const selectedInstanceId = selectedInstanceIdStore.get();
if (selectedInstanceId === undefined) {
return;
}
const data = copyInstanceData(selectedInstanceId);
if (data === undefined) {
return;
}
deleteInstance(selectedInstanceId);
if (data === undefined) {
return;
}
return stringify(data);
};
export const init = () => {
return initCopyPaste({
onCopy,
onCut,
onPaste,
});
};