import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from "react"; import { cn } from "@/lib/utils"; import { EDITOR_HEIGHT } from "../constants"; import { editorCommands } from "./commands"; import SlashCommands from "./SlashCommands"; import TagSuggestions from "./TagSuggestions"; import { useListCompletion } from "./useListCompletion"; export interface EditorRefActions { getEditor: () => HTMLTextAreaElement | null; focus: FunctionType; scrollToCursor: FunctionType; insertText: (text: string, prefix?: string, suffix?: string) => void; removeText: (start: number, length: number) => void; setContent: (text: string) => void; getContent: () => string; getSelectedContent: () => string; getCursorPosition: () => number; setCursorPosition: (startPos: number, endPos?: number) => void; getCursorLineNumber: () => number; getLine: (lineNumber: number) => string; setLine: (lineNumber: number, text: string) => void; } interface Props { className: string; initialContent: string; placeholder: string; onContentChange: (content: string) => void; onPaste: (event: React.ClipboardEvent) => void; isFocusMode?: boolean; isInIME?: boolean; onCompositionStart?: () => void; onCompositionEnd?: () => void; } const Editor = forwardRef(function Editor(props: Props, ref: React.ForwardedRef) { const { className, initialContent, placeholder, onPaste, onContentChange: handleContentChangeCallback, isFocusMode, isInIME = false, onCompositionStart, onCompositionEnd, } = props; const editorRef = useRef(null); useEffect(() => { if (editorRef.current && initialContent) { editorRef.current.value = initialContent; handleContentChangeCallback(initialContent); updateEditorHeight(); } }, []); const updateEditorHeight = () => { if (editorRef.current) { editorRef.current.style.height = "auto"; editorRef.current.style.height = (editorRef.current.scrollHeight ?? 0) + "px"; } }; const updateContent = () => { if (editorRef.current) { handleContentChangeCallback(editorRef.current.value); updateEditorHeight(); } }; const editorActions: EditorRefActions = { getEditor: () => editorRef.current, focus: () => editorRef.current?.focus(), scrollToCursor: () => { editorRef.current && (editorRef.current.scrollTop = editorRef.current.scrollHeight); }, insertText: (content = "", prefix = "", suffix = "") => { const editor = editorRef.current; if (!editor) return; const cursorPos = editor.selectionStart; const endPos = editor.selectionEnd; const prev = editor.value; const actual = content || prev.slice(cursorPos, endPos); editor.value = prev.slice(0, cursorPos) + prefix + actual + suffix + prev.slice(endPos); editor.focus(); editor.setSelectionRange(cursorPos + prefix.length + actual.length, cursorPos + prefix.length + actual.length); updateContent(); }, removeText: (start: number, length: number) => { const editor = editorRef.current; if (!editor) return; editor.value = editor.value.slice(0, start) + editor.value.slice(start + length); editor.focus(); editor.selectionEnd = start; updateContent(); }, setContent: (text: string) => { const editor = editorRef.current; if (editor) { editor.value = text; updateContent(); } }, getContent: () => editorRef.current?.value ?? "", getCursorPosition: () => editorRef.current?.selectionStart ?? 0, getSelectedContent: () => { const editor = editorRef.current; if (!editor) return ""; return editor.value.slice(editor.selectionStart, editor.selectionEnd); }, setCursorPosition: (startPos: number, endPos?: number) => { const endPosition = isNaN(endPos as number) ? startPos : (endPos as number); editorRef.current?.setSelectionRange(startPos, endPosition); }, getCursorLineNumber: () => { const editor = editorRef.current; if (!editor) return 0; const lines = editor.value.slice(0, editor.selectionStart).split("\n"); return lines.length - 1; }, getLine: (lineNumber: number) => editorRef.current?.value.split("\n")[lineNumber] ?? "", setLine: (lineNumber: number, text: string) => { const editor = editorRef.current; if (!editor) return; const lines = editor.value.split("\n"); lines[lineNumber] = text; editor.value = lines.join("\n"); editor.focus(); updateContent(); }, }; useImperativeHandle(ref, () => editorActions, []); const handleEditorInput = useCallback(() => { handleContentChangeCallback(editorRef.current?.value ?? ""); updateEditorHeight(); }, []); // Auto-complete markdown lists when pressing Enter useListCompletion({ editorRef, editorActions, isInIME, }); return (
); }); export default Editor;