BlocksUI.#prepareBlocksHolder() (packages/ui/src/Blocks/Blocks.ts) registers three DOM listeners directly on blocksHolder:
beforeinput (line 89)
keydown (line 118)
copy (line 140)
BlocksUI.destroy() only cleans up rendered block elements:
public destroy(): void {
this.#blocks.forEach(block => block.remove());
this.#blocks = [];
}
None of the three listeners above are ever removed, and #blocksHolder itself is never detached. The holder is handed off via BlocksHolderRenderedUIEvent to EditorUI, which appends it into #editorWrapper (packages/ui/src/index.ts) — but EditorUI.destroy() is currently a no-op (// Cleanup if needed), so nothing ever detaches it either.
Impact
If a blocksHolder element is reused, or a new BlocksUI/Core instance is created against a holder from a previous instance, the old listeners stay attached and continue firing alongside the new instance's listeners. Concretely this means duplicate BeforeInputUIEvent/CopyUIEvent dispatches per user action, and stale keydown (undo/redo) handling from a destroyed instance.
Where this came from
Flagged by review on #129, specifically for the new copy listener: #129 (comment). Investigation showed the same gap already existed for beforeinput/keydown before this PR — this issue is to track fixing all three (and the missing holder detachment) rather than just the newest one.
Suggested fix
- Store references to the three listener functions (or use an
AbortController/AbortSignal passed to addEventListener for the whole group) so destroy() can call removeEventListener for each, matching the pattern already used in ClipboardPlugin/ShortcutsPlugin elsewhere in this codebase.
- Consider detaching
#blocksHolder from the DOM in destroy(), and implementing EditorUI.destroy() (currently a no-op) to tear down #editorWrapper.
- Add test coverage — there's currently no
Blocks.spec.ts at all, so destroy() behavior is completely unverified.
BlocksUI.#prepareBlocksHolder()(packages/ui/src/Blocks/Blocks.ts) registers three DOM listeners directly onblocksHolder:beforeinput(line 89)keydown(line 118)copy(line 140)BlocksUI.destroy()only cleans up rendered block elements:None of the three listeners above are ever removed, and
#blocksHolderitself is never detached. The holder is handed off viaBlocksHolderRenderedUIEventtoEditorUI, which appends it into#editorWrapper(packages/ui/src/index.ts) — butEditorUI.destroy()is currently a no-op (// Cleanup if needed), so nothing ever detaches it either.Impact
If a
blocksHolderelement is reused, or a newBlocksUI/Coreinstance is created against a holder from a previous instance, the old listeners stay attached and continue firing alongside the new instance's listeners. Concretely this means duplicateBeforeInputUIEvent/CopyUIEventdispatches per user action, and stalekeydown(undo/redo) handling from a destroyed instance.Where this came from
Flagged by review on #129, specifically for the new
copylistener: #129 (comment). Investigation showed the same gap already existed forbeforeinput/keydownbefore this PR — this issue is to track fixing all three (and the missing holder detachment) rather than just the newest one.Suggested fix
AbortController/AbortSignalpassed toaddEventListenerfor the whole group) sodestroy()can callremoveEventListenerfor each, matching the pattern already used inClipboardPlugin/ShortcutsPluginelsewhere in this codebase.#blocksHolderfrom the DOM indestroy(), and implementingEditorUI.destroy()(currently a no-op) to tear down#editorWrapper.Blocks.spec.tsat all, sodestroy()behavior is completely unverified.