From 2c13132d496e56778065ff78e48cae21b046d3dc Mon Sep 17 00:00:00 2001 From: ComputelessComputer Date: Mon, 23 Feb 2026 01:27:08 +0900 Subject: [PATCH 1/2] feat: improve Link extension behavior and click handling Make Link extension non-inclusive to prevent text typed after links from being linkified automatically. Refactor click handler logic to use early returns for better readability and ensure proper event handling for both modifier and non-modifier clicks on link elements. --- .../tiptap/src/shared/extensions/index.ts | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/tiptap/src/shared/extensions/index.ts b/packages/tiptap/src/shared/extensions/index.ts index bade2ef95f..e14d3c19f5 100644 --- a/packages/tiptap/src/shared/extensions/index.ts +++ b/packages/tiptap/src/shared/extensions/index.ts @@ -111,36 +111,36 @@ export const getExtensions = ( }), Hashtag, Link.extend({ + inclusive() { + return false; + }, addProseMirrorPlugins() { return [ new Plugin({ key: new PluginKey("linkCmdClick"), props: { handleClick(view, pos, event) { - if (!(event.metaKey || event.ctrlKey)) { - return false; - } - const { state } = view; const $pos = state.doc.resolve(pos); const marks = $pos.marks(); const linkMark = marks.find((mark) => mark.type.name === "link"); - - if (linkMark && linkMark.attrs.href) { - event.preventDefault(); - if (options?.onLinkOpen) { - options.onLinkOpen(linkMark.attrs.href); - } else { - window.open( - linkMark.attrs.href, - "_blank", - "noopener,noreferrer", - ); - } + if (!linkMark || !linkMark.attrs.href) { + return false; + } + if (!(event.metaKey || event.ctrlKey)) { return true; } - - return false; + event.preventDefault(); + if (options?.onLinkOpen) { + options.onLinkOpen(linkMark.attrs.href); + } else { + window.open( + linkMark.attrs.href, + "_blank", + "noopener,noreferrer", + ); + } + return true; }, }, }), From 55df48dd0d7befc2409ef46c462c01db02595566 Mon Sep 17 00:00:00 2001 From: ComputelessComputer Date: Mon, 23 Feb 2026 01:35:16 +0900 Subject: [PATCH 2/2] feat: enhance link extension with boundary guard Remove inclusive override and add link boundary guard plugin to prevent link marks from extending beyond actual URL text. Include parent plugins to maintain extension functionality. Guard ensures link marks only apply to URL portion when text contains additional content. --- .../tiptap/src/shared/extensions/index.ts | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/tiptap/src/shared/extensions/index.ts b/packages/tiptap/src/shared/extensions/index.ts index e14d3c19f5..28e41acd3e 100644 --- a/packages/tiptap/src/shared/extensions/index.ts +++ b/packages/tiptap/src/shared/extensions/index.ts @@ -111,11 +111,10 @@ export const getExtensions = ( }), Hashtag, Link.extend({ - inclusive() { - return false; - }, addProseMirrorPlugins() { + const parentPlugins = this.parent?.() || []; return [ + ...parentPlugins, new Plugin({ key: new PluginKey("linkCmdClick"), props: { @@ -144,6 +143,30 @@ export const getExtensions = ( }, }, }), + new Plugin({ + key: new PluginKey("linkBoundaryGuard"), + appendTransaction(transactions, _oldState, newState) { + if (!transactions.some((tr) => tr.docChanged)) return null; + const linkType = newState.schema.marks.link; + if (!linkType) return null; + let tr: ReturnType | null = null; + newState.doc.descendants((node, pos) => { + if (!node.isText || !node.text) return; + const linkMark = node.marks.find((m) => m.type === linkType); + if (!linkMark?.attrs.href) return; + const href: string = linkMark.attrs.href; + const text = node.text; + if (text === href) return; + const hrefIndex = text.indexOf(href); + if (hrefIndex < 0) return; + if (hrefIndex > 0) { + if (!tr) tr = newState.tr; + tr.removeMark(pos, pos + hrefIndex, linkType); + } + }); + return tr; + }, + }), ]; }, }).configure({