statebuddy/src/App/VisualEditor/hooks/useCopyPaste.ts

137 lines
No EOL
5.4 KiB
TypeScript

import { Arrow, Diamond, Rountangle, Text, History } from "@/statecharts/concrete_syntax";
import { ClipboardEvent, Dispatch, SetStateAction, useCallback, useEffect } from "react";
import { Selection, VisualEditorState } from "../VisualEditor";
import { addV2D } from "@/util/geometry";
// const offset = {x: 40, y: 40};
const offset = {x: 0, y: 0};
export function useCopyPaste(makeCheckPoint: () => void, state: VisualEditorState, setState: Dispatch<(v:VisualEditorState) => VisualEditorState>, selection: Selection) {
const onPaste = useCallback((e: ClipboardEvent) => {
const data = e.clipboardData?.getData("text/plain");
if (data) {
try {
const parsed = JSON.parse(data);
setState(state => {
try {
let nextID = state.nextID;
const copiedRountangles: Rountangle[] = parsed.rountangles.map((r: Rountangle) => ({
...r,
uid: (nextID++).toString(),
topLeft: addV2D(r.topLeft, offset),
} as Rountangle));
const copiedDiamonds: Diamond[] = parsed.diamonds.map((r: Diamond) => ({
...r,
uid: (nextID++).toString(),
topLeft: addV2D(r.topLeft, offset),
} as Diamond));
const copiedArrows: Arrow[] = parsed.arrows.map((a: Arrow) => ({
...a,
uid: (nextID++).toString(),
start: addV2D(a.start, offset),
end: addV2D(a.end, offset),
} as Arrow));
const copiedTexts: Text[] = parsed.texts.map((t: Text) => ({
...t,
uid: (nextID++).toString(),
topLeft: addV2D(t.topLeft, offset),
} as Text));
const copiedHistories: History[] = parsed.history.map((h: History) => ({
...h,
uid: (nextID++).toString(),
topLeft: addV2D(h.topLeft, offset),
}))
// @ts-ignore
const newSelection: Selection = [
...copiedRountangles.map(r => ({uid: r.uid, parts: ["left", "top", "right", "bottom"]})),
...copiedDiamonds.map(d => ({uid: d.uid, parts: ["left", "top", "right", "bottom"]})),
...copiedArrows.map(a => ({uid: a.uid, parts: ["start", "end"]})),
...copiedTexts.map(t => ({uid: t.uid, parts: ["text"]})),
...copiedHistories.map(h => ({uid: h.uid, parts: ["history"]})),
];
makeCheckPoint();
return {
...state,
rountangles: [...state.rountangles, ...copiedRountangles],
diamonds: [...state.diamonds, ...copiedDiamonds],
arrows: [...state.arrows, ...copiedArrows],
texts: [...state.texts, ...copiedTexts],
history: [...state.history, ...copiedHistories],
nextID: nextID,
selection: newSelection,
};
}
catch (e) {
console.warn("error pasting data. most likely you're tying to paste nonsense. ", e);
return state;
}
});
}
catch (e) {
console.warn("error pasting data. most likely you're tying to paste nonsense. ", e);
}
e.preventDefault();
}
}, [setState]);
const copyInternal = useCallback((state: VisualEditorState, selection: Selection, e: ClipboardEvent) => {
const uidsToCopy = new Set(selection.map(shape => shape.uid));
const rountanglesToCopy = state.rountangles.filter(r => uidsToCopy.has(r.uid));
const diamondsToCopy = state.diamonds.filter(d => uidsToCopy.has(d.uid));
const historiesToCopy = state.history.filter(h => uidsToCopy.has(h.uid));
const arrowsToCopy = state.arrows.filter(a => uidsToCopy.has(a.uid));
const textsToCopy = state.texts.filter(t => uidsToCopy.has(t.uid));
e.clipboardData?.setData("text/plain", JSON.stringify({
rountangles: rountanglesToCopy,
diamonds: diamondsToCopy,
history: historiesToCopy,
arrows: arrowsToCopy,
texts: textsToCopy,
}));
}, []);
const onCopy = useCallback((e: ClipboardEvent) => {
if (selection.length > 0) {
e.preventDefault();
copyInternal(state, selection, e);
}
}, [state, selection]);
const onCut = useCallback((e: ClipboardEvent) => {
if (selection.length > 0) {
copyInternal(state, selection, e);
deleteSelection();
e.preventDefault();
}
}, [state, selection]);
const deleteSelection = useCallback(() => {
setState(state => ({
...state,
rountangles: state.rountangles.filter(r => !state.selection.some(rs => rs.uid === r.uid)),
diamonds: state.diamonds.filter(d => !state.selection.some(ds => ds.uid === d.uid)),
history: state.history.filter(h => !state.selection.some(hs => hs.uid === h.uid)),
arrows: state.arrows.filter(a => !state.selection.some(as => as.uid === a.uid)),
texts: state.texts.filter(t => !state.selection.some(ts => ts.uid === t.uid)),
selection: [],
}));
}, [setState]);
const onKeyDown = (e: KeyboardEvent) => {
// @ts-ignore
if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return;
if (e.key === "Delete") {
// delete selection
makeCheckPoint();
deleteSelection();
e.preventDefault();
}
}
useEffect(() => {
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
})
return {onCopy, onPaste, onCut, deleteSelection};
}