From 361afc242614a16bedab05b29614884d379f2077 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Mon, 27 Apr 2026 04:53:36 +0100 Subject: [PATCH] feat(editor-ext): block typing/paste from replacing a selected base embed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a ProseMirror plugin with handleTextInput and handlePaste that return true (handled, no-op) when the current selection is a NodeSelection of the base embed. Pairs with the existing Backspace/ Delete keyboard guard — between them the two accidental-deletion paths (focus + delete key, focus + type a character) are blocked. Other deletion routes still work: range selections covering the node, programmatic deletes, and so on. Pressing an arrow key deselects the node so the user can type elsewhere. --- .../src/lib/base-embed/base-embed.ts | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/packages/editor-ext/src/lib/base-embed/base-embed.ts b/packages/editor-ext/src/lib/base-embed/base-embed.ts index 90cd7584c..414663418 100644 --- a/packages/editor-ext/src/lib/base-embed/base-embed.ts +++ b/packages/editor-ext/src/lib/base-embed/base-embed.ts @@ -1,5 +1,5 @@ import { Node, mergeAttributes } from '@tiptap/core'; -import { NodeSelection } from '@tiptap/pm/state'; +import { EditorState, NodeSelection, Plugin } from '@tiptap/pm/state'; export interface BaseEmbedOptions { HTMLAttributes: Record; @@ -62,11 +62,11 @@ export const BaseEmbed = Node.create({ addKeyboardShortcuts() { // Block Backspace / Delete when the base embed itself is the - // current selection — that's the "click on the embed and hit - // delete" accidental-delete path. Returning true tells TipTap - // we've handled the key, preventing the default removal. - // Other deletion paths (range selections covering the node, - // programmatic transactions) still go through. + // current selection — the "click on the embed and hit delete" + // accidental-delete path. Returning true tells TipTap we've + // handled the key, preventing the default removal. Range + // selections covering the node and programmatic deletes still + // work normally. const isThisNodeSelected = (): boolean => { const { selection } = this.editor.state; return ( @@ -79,4 +79,28 @@ export const BaseEmbed = Node.create({ Delete: () => isThisNodeSelected(), }; }, + + addProseMirrorPlugins() { + // Same idea as the Backspace/Delete shortcuts above, but for the + // other accidental-delete path: when the embed is the selection, + // a typed character or paste would replace the whole node. These + // hooks return true (handled, no-op) so the node stays put. The + // user can still press an arrow key to deselect and then type. + const nodeName = this.name; + const isThisNodeSelected = (state: EditorState): boolean => { + const { selection } = state; + return ( + selection instanceof NodeSelection && + selection.node.type.name === nodeName + ); + }; + return [ + new Plugin({ + props: { + handleTextInput: (view) => isThisNodeSelected(view.state), + handlePaste: (view) => isThisNodeSelected(view.state), + }, + }), + ]; + }, });