Thought I will share some details of how I made it eventually. The general concept is that on copy we update attribute with external data and remove it on paste when it’s not needed. This approach may be helpful to support some similar workflows.
First, I added new attrs to my custom Node called copyPasteData
which expects a stringified object with external data structure.
...
toDOM: (node: ProsemirrorNode): DOMOutputSpecArray => {
return [
"span",
{
...,
"data-copy-paste-data": JSON.stringify(node.attrs.copyPasteData),
},
];
},
parseDOM: [
{
tag: "span[data-custom-field]",
getAttrs: (dom: string | Node) => {
const { ..., copyPasteData } = (dom as HTMLElement).dataset;
return {
...,
copyPasteData: copyPasteData && JSON.parse(copyPasteData),
};
},
},
Then, I created reusable mapSlice
and mapFragement
functions that will traverse the Slice and with callback function update Nodes we need (remember to not mutate the Fragment directly)
import { Slice, Fragment, Node } from "prosemirror-model";
export const mapFragment = (
fragment: Fragment,
callback: (node: Node) => Node | Node[] | Fragment | null
): Fragment =>
Fragment.fromArray(
(fragment as any).content.map((node: Node) => {
if (node.content.childCount > 0) {
return node.type.create(
node.attrs,
mapFragment(node.content, callback)
);
}
return callback(node);
})
);
export const mapSlice = (
slice: Slice,
callback: (node: Node) => Node | Node[] | Fragment | null
): Slice => {
const fragment = mapFragment(slice.content, callback);
return new Slice(fragment, slice.openStart, slice.openEnd);
};
Then I have a single plugin with transformCopied
and transformPasted
that accepts external context
export const copyPastePlugin = ({ editorContext }: Args) =>
new Plugin({
key: new PluginKey("copyPastePlugin"),
props: {
transformCopied: (slice: Slice) => {
/* Add temporary dataForCopyPaste to live in the clipboard. */
const addCopyPasteDataToDataFields = (node: Node) => {
if (node.type === schema.nodes.custom_field) {
// do something with external context
return node.type.create(
{
...node.attrs,
...(someData && {
copyPasteData: { ...someData },
}),
},
node.content
);
}
return node.copy(node.content);
};
return mapSlice(slice, addCopyPasteDataToDataFields);
},
transformPasted: (slice: Slice) => {
// get dataForCopyPaste and do whatever with data + clean it up
const fetchAndClearCopyPasteData = (node: Node) => {
if (
node.type === schema.nodes.custom_field &&
node.attrs.copyPasteData
) {
// do something with copyPasteData and clean it up
return node.type.create(
{ ...node.attrs, copyPasteData: undefined },
node.content
);
}
return node.copy(node.content);
};
const newSlice = mapSlice(slice, fetchAndClearCopyPasteData);
return newSlice;
},
},
});
Also, a side note, if your external context can change during Editor updates you’ll need to call state.reconfigure()
on each change to initialize new set of plugins with fresh context (or at least that’s the only working solution I found).
In my case, I crated a helper function which updates plugins whenever I need to:
const newState = editorView.state.reconfigure({
plugins: [...otherPlugins, copyPastePlugin(editorContext)],
});
editorView.updateState(newState);