import type { Node, NodeId, NodeTree } from '@craftjs/core'; /** * Tiny shared clipboard for canvas node copy/paste. * * Both the context menu (right-click Copy/Paste) and the keyboard shortcuts * hook (Ctrl/Cmd+C / Ctrl/Cmd+V) read and write this single module-level * store, so copying a node via one entry point and pasting via the other * behaves consistently instead of each maintaining its own clipboard. * * Deliberately not React state -- nothing in the UI needs to re-render * reactively when the clipboard changes; consumers just read the current * value at the moment they need it (on paste, or when a menu opens). * * Historical bug (cross-page copy/paste): this used to store only the copied * node's bare id (`clipboardNodeId`) and re-resolve it via `query.node(id)` * at paste time. That works fine same-page, but the moment the user switches * pages the canvas is re-deserialized to the target page's Craft.js state -- * the copied id no longer exists in `query` at all -- so a cross-page paste * silently no-op'd (or threw, caught, and swallowed). Storing a detached * TREE SNAPSHOT at copy time instead means paste never needs to look the * source id up again: it just hands the snapshot to `regenerateTreeIds` + * `actions.addNodeTree`, which works identically regardless of which page's * state is currently loaded on the canvas. */ let clipboardTree: NodeTree | null = null; /** * Deep, detached clone of a live Craft.js `NodeTree` (as returned by * `query.node(id).toNodeTree()`). * * Not a plain `structuredClone(tree)`: for a REAL (live) Craft.js node, * `data.type` is the actual component function/class reference (not a * serializable `{resolvedName}` wrapper) -- `structuredClone` cannot clone a * function and throws `DataCloneError` (see the identical note on * `regenerateTreeIds` in `utils/craft-tree.ts`, which hit this exact bug * historically). `type` is a stable reference shared by every node of that * component across the whole app (it doesn't change per page), so it's safe * to keep by reference -- only the mutable per-node data (`props`, `custom`, * `nodes`, `linkedNodes`) needs an actual deep copy so a later mutation (a * subsequent paste's `setProp`, or a fresh copy of the same live node) * can never reach back into this stored snapshot. */ function cloneNodeTree(tree: NodeTree): NodeTree { const nodes: Record = {}; for (const [id, node] of Object.entries(tree.nodes)) { nodes[id] = { ...node, data: { ...node.data, props: structuredClone(node.data.props), custom: structuredClone(node.data.custom), nodes: [...(node.data.nodes || [])], linkedNodes: { ...(node.data.linkedNodes || {}) }, }, }; } return { rootNodeId: tree.rootNodeId, nodes }; } /** * Returns the tree snapshot currently on the clipboard, or null if empty. * The returned tree is safe to hand straight to `regenerateTreeIds` -- * `regenerateTreeIds` never mutates its input, so repeated pastes of the * same clipboard contents (including across a page switch) all work off the * same untouched snapshot. */ export function getClipboardTree(): NodeTree | null { return clipboardTree; } /** * Sets (or clears, with `null`) the tree snapshot on the clipboard. The tree * is deep-cloned before being stored (see `cloneNodeTree`) so it is fully * detached from the live Craft.js node it was captured from -- it survives * that node being deleted, mutated, or (the whole point) the canvas being * re-deserialized to a different page entirely. */ export function setClipboardTree(tree: NodeTree | null): void { clipboardTree = tree ? cloneNodeTree(tree) : null; }