"use client" import { useEditor, EditorContent } from "@tiptap/react" import { BubbleMenu } from "@tiptap/react/menus" import type { Editor } from "@tiptap/core" import { Markdown } from "@tiptap/markdown" import { useRef, useEffect, useCallback } from "react" import { defaultExtensions } from "./extensions" import { slashCommand } from "./suggestions" import { Bold, Italic, Code } from "lucide-react" import { useDebouncedCallback } from "use-debounce" import { cn } from "@lib/utils" const extensions = [...defaultExtensions, slashCommand, Markdown] export function TextEditor({ content: initialContent, onContentChange, onSubmit, }: { content: string | undefined onContentChange: (content: string) => void onSubmit: () => void }) { const containerRef = useRef(null) const editorRef = useRef(null) const onSubmitRef = useRef(onSubmit) const hasUserEditedRef = useRef(false) useEffect(() => { onSubmitRef.current = onSubmit }, [onSubmit]) const debouncedUpdates = useDebouncedCallback((editor: Editor) => { if (!hasUserEditedRef.current) return const json = editor.getJSON() const markdown = editor.storage.markdown?.manager?.serialize(json) ?? "" onContentChange?.(markdown) }, 500) const editor = useEditor({ extensions, content: initialContent, contentType: "markdown", immediatelyRender: true, onCreate: ({ editor }) => { editorRef.current = editor }, onUpdate: ({ editor }) => { editorRef.current = editor debouncedUpdates(editor) }, editorProps: { handleKeyDown: (_view, event) => { if ((event.metaKey || event.ctrlKey) && event.key === "Enter") { event.preventDefault() debouncedUpdates.flush() onSubmitRef.current?.() return true } hasUserEditedRef.current = true return false }, handleTextInput: () => { hasUserEditedRef.current = true return false }, handlePaste: () => { hasUserEditedRef.current = true return false }, handleDrop: () => { hasUserEditedRef.current = true return false }, }, }) useEffect(() => { if (editor && initialContent) { hasUserEditedRef.current = false editor.commands.setContent(initialContent, { contentType: "markdown" }) } }, [editor, initialContent]) const handleClick = useCallback((e: React.MouseEvent) => { const target = e.target as HTMLElement if (target.closest(".ProseMirror")) { return } if (target.closest("button, a")) { return } const proseMirror = containerRef.current?.querySelector( ".ProseMirror", ) as HTMLElement if (proseMirror && editorRef.current) { setTimeout(() => { proseMirror.focus() editorRef.current?.commands.focus("end") }, 0) } }, []) useEffect(() => { return () => { // Flush any pending debounced updates before destroying editor debouncedUpdates.flush() editor?.destroy() } }, [editor, debouncedUpdates]) return ( <> {/* biome-ignore lint/a11y/useSemanticElements: div is needed as container for editor, cannot use button */} {/* biome-ignore lint/a11y/useKeyWithClickEvents: we need to use a div to get the focus on the editor */}
{editor && (
)} ) }