From 21eb2e519709261b6725c603c36e517be81f92cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Wed, 18 Mar 2026 16:16:10 +0400 Subject: [PATCH] Add document node styling + extensions to handle clicking links with ctrl or cmd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Émile Ré --- packages/ui/src/RichEditor/RichEditor.tsx | 68 +++++++++++++++++++++-- packages/ui/src/theme.css | 64 +++++++++++++++++++++ 2 files changed, 127 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/RichEditor/RichEditor.tsx b/packages/ui/src/RichEditor/RichEditor.tsx index d4ba31d61..1de2569c0 100644 --- a/packages/ui/src/RichEditor/RichEditor.tsx +++ b/packages/ui/src/RichEditor/RichEditor.tsx @@ -14,23 +14,76 @@ import { Strike } from "@tiptap/extension-strike"; import { Text } from "@tiptap/extension-text"; import { Underline } from "@tiptap/extension-underline"; import { Dropcursor, Gapcursor, UndoRedo } from "@tiptap/extensions"; -import { type Content, EditorContent, useEditor, useEditorState } from "@tiptap/react"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; +import { type Content, EditorContent, getAttributes, useEditor, useEditorState } from "@tiptap/react"; import { BubbleMenu, FloatingMenu } from "@tiptap/react/menus"; import { type ComponentProps, useEffect } from "react"; import { tv } from "tailwind-variants"; +export const ControlClickLink = Link.extend({ + addProseMirrorPlugins: () => { + return [ + new Plugin({ + key: new PluginKey("handleControlClick"), + props: { + handleKeyDown: (view, event) => { + if (event.key === "Control" || event.key === "Meta") { + view.dom.classList.add("pointer-on-hovered-link"); + } + }, + handleDOMEvents: { + keyup: (view, event) => { + if (event.key === "Control" || event.key === "Meta") { + view.dom.classList.remove("pointer-on-hovered-link"); + } + }, + }, + handleClick: (view, _, event) => { + const { ctrlKey, metaKey } = event; // Check for Ctrl (Windows) or Cmd (Mac) + const keyPressed = ctrlKey || metaKey; + + if (keyPressed) { + // Get attributes of the mark at the clicked position + const attrs = getAttributes(view.state, "link"); + const link = (event.target as Element | null)?.closest("a"); + + if (link && attrs.href) { + window.open(attrs.href as string, "_blank", "noopener,noreferrer"); // Open link in a new tab + return true; // Handle the event + } + } + return false; // Let other handlers run + }, + }, + }), + ]; + }, +}).configure({ + openOnClick: false, +}); + const extensions = [ Document, - Paragraph, - Text, - Heading.configure({ levels: [1, 2, 3] }), + Paragraph.configure({ + HTMLAttributes: { + class: "text-base py-2", + }, + }), + Text.configure({ + HTMLAttributes: { + class: "text-base", + }, + }), + Heading.configure({ + levels: [1, 2, 3], + }), Bold, Italic, Strike, Underline, Code, CodeBlock, - Link.configure({ openOnClick: false }), + ControlClickLink, Blockquote, BulletList, OrderedList, @@ -190,6 +243,11 @@ export function RichEditor(props: RichEditorProps) { active={editor.isActive("orderedList")} onClick={() => editor.chain().focus().toggleOrderedList().run()} /> + editor.chain().focus().toggleCode().run()} + />