From 81b518d0d1ff141d140b71ca00547e17d39b087f Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:45:20 +0800 Subject: [PATCH] fix(editor): enable link navigation and fix copy artifacts - Set openOnClick: true so clicking a link opens it in a new tab - Add Cmd+Click / Ctrl+Click handler as fallback (skips mention:// links) - Override prosemirror-markdown link serializer to always use [text](url) format instead of autolink syntax, fixing angle brackets appearing when copying links from the editor Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/common/rich-text-editor.tsx | 57 ++++++++++++++++--- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/apps/web/components/common/rich-text-editor.tsx b/apps/web/components/common/rich-text-editor.tsx index 6ab46196..188030de 100644 --- a/apps/web/components/common/rich-text-editor.tsx +++ b/apps/web/components/common/rich-text-editor.tsx @@ -47,6 +47,41 @@ interface RichTextEditorRef { // Stores as: [@Label](mention://type/id) // --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// Link extension — always serialize as [text](url), never autolinks; +// support Cmd+Click / Ctrl+Click to open in new tab. +// --------------------------------------------------------------------------- + +const LinkExtension = Link.configure({ + openOnClick: true, + autolink: true, + HTMLAttributes: { + class: "text-primary hover:underline cursor-pointer", + }, +}).extend({ + addStorage() { + return { + markdown: { + serialize: { + open() { + return "["; + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + close(_state: any, mark: any) { + const href = (mark.attrs.href as string).replace(/[\(\)"]/g, "\\$&"); + const title = mark.attrs.title + ? ` "${(mark.attrs.title as string).replace(/"/g, '\\"')}"` + : ""; + return `](${href}${title})`; + }, + mixable: true, + }, + parse: {}, + }, + }; + }, +}); + const MentionExtension = Mention.configure({ HTMLAttributes: { class: "mention" }, suggestion: createMentionSuggestion(), @@ -146,13 +181,7 @@ const RichTextEditor = forwardRef( Placeholder.configure({ placeholder: placeholderText, }), - Link.configure({ - openOnClick: false, - autolink: true, - HTMLAttributes: { - class: "text-primary hover:underline cursor-pointer", - }, - }), + LinkExtension, Typography, MentionExtension, Markdown.configure({ @@ -170,6 +199,20 @@ const RichTextEditor = forwardRef( }, debounceMs); }, editorProps: { + handleDOMEvents: { + click(_view, event) { + if (event.metaKey || event.ctrlKey) { + const link = (event.target as HTMLElement).closest("a"); + const href = link?.getAttribute("href"); + if (href && !href.startsWith("mention://")) { + window.open(href, "_blank", "noopener,noreferrer"); + event.preventDefault(); + return true; + } + } + return false; + }, + }, attributes: { class: cn("rich-text-editor text-sm outline-none", className), },