2026-07-14 07:36:00 -07:00
|
|
|
import type { Node, NodeId, NodeTree } from '@craftjs/core';
|
|
|
|
|
|
2026-07-12 15:01:30 -07:00
|
|
|
/**
|
|
|
|
|
* 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).
|
2026-07-14 07:36:00 -07:00
|
|
|
*
|
|
|
|
|
* 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.
|
2026-07-12 15:01:30 -07:00
|
|
|
*/
|
2026-07-14 07:36:00 -07:00
|
|
|
let clipboardTree: NodeTree | null = null;
|
2026-07-12 15:01:30 -07:00
|
|
|
|
2026-07-14 07:36:00 -07:00
|
|
|
/**
|
|
|
|
|
* 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<NodeId, Node> = {};
|
|
|
|
|
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 };
|
2026-07-12 15:01:30 -07:00
|
|
|
}
|
|
|
|
|
|
2026-07-14 07:36:00 -07:00
|
|
|
/**
|
|
|
|
|
* 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;
|
2026-07-12 15:01:30 -07:00
|
|
|
}
|