diff --git a/apps/web/src/components/PlainTextEditor.tsx b/apps/web/src/components/PlainTextEditor.tsx new file mode 100644 index 00000000..8525ad1c --- /dev/null +++ b/apps/web/src/components/PlainTextEditor.tsx @@ -0,0 +1,130 @@ +import Placeholder from "@tiptap/extension-placeholder"; +import { EditorContent, useEditor } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; +import { useEffect, useRef } from "react"; +import { twMerge } from "tailwind-merge"; + +interface PlainTextEditorProps { + content: string; + onChange?: (value: string) => void; + onBlur?: (value: string) => void; + onEnter?: (value: string) => void; + onEscape?: () => void; + readOnly?: boolean; + placeholder?: string; + className?: string; +} + +export default function PlainTextEditor({ + content, + onChange, + onBlur, + onEnter, + onEscape, + readOnly = false, + placeholder, + className, +}: PlainTextEditorProps) { + const onEnterRef = useRef(onEnter); + const onEscapeRef = useRef(onEscape); + const onBlurRef = useRef(onBlur); + const onChangeRef = useRef(onChange); + const contentRef = useRef(content); + + useEffect(() => { + onEnterRef.current = onEnter; + }, [onEnter]); + useEffect(() => { + onEscapeRef.current = onEscape; + }, [onEscape]); + useEffect(() => { + onBlurRef.current = onBlur; + }, [onBlur]); + useEffect(() => { + onChangeRef.current = onChange; + }, [onChange]); + useEffect(() => { + contentRef.current = content; + }, [content]); + + const editor = useEditor( + { + extensions: [ + StarterKit.configure({ + bold: false, + italic: false, + strike: false, + code: false, + codeBlock: false, + blockquote: false, + heading: false, + bulletList: false, + orderedList: false, + listItem: false, + horizontalRule: false, + hardBreak: false, + }), + Placeholder.configure({ placeholder }), + ], + content, + editable: !readOnly, + onUpdate: ({ editor }) => onChangeRef.current?.(editor.getText()), + onBlur: ({ editor }) => onBlurRef.current?.(editor.getText()), + editorProps: { + handleKeyDown: (view, event) => { + if (event.key === "Enter") { + event.preventDefault(); + onEnterRef.current?.(view.state.doc.textContent.trim()); + return true; + } + if (event.key === "Escape") { + event.preventDefault(); + // Reset to original content before calling the callback + editor?.commands.setContent(contentRef.current, false); + editor?.commands.blur(); + onEscapeRef.current?.(); + return true; + } + return false; + }, + attributes: { + class: "outline-none focus:outline-none focus-visible:ring-0", + }, + }, + }, + [], + ); + + useEffect(() => { + if (!editor) return; + if (content !== editor.getText()) { + editor.commands.setContent(content, false); + } + }, [content, editor]); + + useEffect(() => { + if (!editor) return; + editor.setEditable(!readOnly); + }, [readOnly, editor]); + + return ( + <> + + + + ); +}