diff --git a/packages/editor/src/extensions/react/react-node-view.tsx b/packages/editor/src/extensions/react/react-node-view.tsx index 4273ca3da..f5e026021 100644 --- a/packages/editor/src/extensions/react/react-node-view.tsx +++ b/packages/editor/src/extensions/react/react-node-view.tsx @@ -46,12 +46,32 @@ export class ReactNodeView
implements NodeView {
private domRef!: HTMLElement;
private contentDOMWrapper?: Node;
- contentDOM: HTMLElement | undefined;
+ contentDOM?: HTMLElement;
node: PMNode;
isDragging = false;
selected = false;
pos = -1;
posEnd: number | undefined;
+
+ // in order to cleanly unmount a React Portal, we have to preserve the
+ // dom structure for the full node. However, Prosemirror/browser can,
+ // sometimes, detach the node before React is able to call "unmount".
+ // Unmounting a React child whose DOM counterpart is already removed
+ // results in a DOMException.
+ // To fix that, we observe and store all the detached subnodes and later add
+ // them back when we are ready to destroy our node view once & for all.
+ // This wouldn't be necessary if React allowed for a way to "sync" the DOM
+ // changes to its Virtual DOM.
+ detached: Set implements NodeView {
init() {
this.domRef = this.createDomRef();
this.domRef.ondragstart = (ev) => this.onDragStart(ev);
+ this.detachObserver.observe(this.domRef, {
+ childList: true,
+ subtree: true
+ });
// this.setDomAttrs(this.node, this.domRef);
const { dom: contentDOMWrapper, contentDOM } = this.getContentDOM() ?? {};
@@ -471,8 +495,15 @@ export class ReactNodeView implements NodeView {
}
destroy() {
- if (!this.portalProviderAPI) return;
+ this.detachObserver.disconnect();
+ // add back the detached nodes because React expects an untouched
+ // DOM representation (and there's no way to reconcile the DOM later).
+ for (const node of this.detached) {
+ this.domRef.appendChild(node);
+ }
this.portalProviderAPI.remove(this.domRef);
+ this.detached.clear();
+ this.domRef.remove();
}
}