diff --git a/src/components/editor/MentionEditor.tsx b/src/components/editor/MentionEditor.tsx index 976f5f7..8795a24 100644 --- a/src/components/editor/MentionEditor.tsx +++ b/src/components/editor/MentionEditor.tsx @@ -5,6 +5,7 @@ import { useMemo, useCallback, useRef, + useState, } from "react"; import { useEditor, EditorContent, ReactRenderer } from "@tiptap/react"; import { Extension, Node, mergeAttributes } from "@tiptap/core"; @@ -384,6 +385,9 @@ export const MentionEditor = forwardRef< }, ref, ) => { + // Track when editor is fully ready (view mounted) + const [isEditorReady, setIsEditorReady] = useState(false); + // Ref to access handleSubmit from suggestion plugins (defined early so useMemo can access it) const handleSubmitRef = useRef<(editor: any) => void>(() => {}); @@ -958,39 +962,72 @@ export const MentionEditor = forwardRef< }, }, autofocus: autoFocus, + onCreate: () => { + // Editor view is now mounted and ready + setIsEditorReady(true); + }, + onDestroy: () => { + // Editor is being destroyed + setIsEditorReady(false); + }, }); + // Helper to check if editor view is ready (prevents "view not available" errors) + const checkEditorReady = useCallback(() => { + return ( + isEditorReady && + editor && + !editor.isDestroyed && + editor.view && + editor.view.dom + ); + }, [editor, isEditorReady]); + // Expose editor methods useImperativeHandle( ref, () => ({ - focus: () => editor?.commands.focus(), - clear: () => editor?.commands.clearContent(), - getContent: () => editor?.getText({ blockSeparator: "\n" }) || "", + focus: () => { + if (checkEditorReady()) { + editor!.commands.focus(); + } + }, + clear: () => { + if (checkEditorReady()) { + editor!.commands.clearContent(); + } + }, + getContent: () => { + if (!checkEditorReady()) return ""; + return editor!.getText({ blockSeparator: "\n" }) || ""; + }, getSerializedContent: () => { - if (!editor) + if (!checkEditorReady()) return { text: "", emojiTags: [], blobAttachments: [], addressRefs: [], }; - return serializeContent(editor); + return serializeContent(editor!); + }, + isEmpty: () => { + if (!checkEditorReady()) return true; + return editor!.isEmpty; }, - isEmpty: () => editor?.isEmpty ?? true, submit: () => { - if (editor) { - handleSubmit(editor); + if (checkEditorReady()) { + handleSubmit(editor!); } }, insertText: (text: string) => { - if (editor) { - editor.chain().focus().insertContent(text).run(); + if (checkEditorReady()) { + editor!.chain().focus().insertContent(text).run(); } }, insertBlob: (blob: BlobAttachment) => { - if (editor) { - editor + if (checkEditorReady()) { + editor! .chain() .focus() .insertContent([ @@ -1010,15 +1047,10 @@ export const MentionEditor = forwardRef< } }, }), - [editor, serializeContent, handleSubmit], + [editor, serializeContent, handleSubmit, checkEditorReady], ); - // Cleanup on unmount - useEffect(() => { - return () => { - editor?.destroy(); - }; - }, [editor]); + // Note: useEditor handles cleanup automatically, no need for manual destroy if (!editor) { return null; diff --git a/src/components/editor/RichEditor.tsx b/src/components/editor/RichEditor.tsx index ff808bc..0c1387b 100644 --- a/src/components/editor/RichEditor.tsx +++ b/src/components/editor/RichEditor.tsx @@ -5,6 +5,7 @@ import { useMemo, useCallback, useRef, + useState, } from "react"; import { useEditor, EditorContent, ReactRenderer } from "@tiptap/react"; import { Extension } from "@tiptap/core"; @@ -234,6 +235,9 @@ export const RichEditor = forwardRef( }, ref, ) => { + // Track when editor is fully ready (view mounted) + const [isEditorReady, setIsEditorReady] = useState(false); + // Ref to access handleSubmit from keyboard shortcuts const handleSubmitRef = useRef<(editor: any) => void>(() => {}); @@ -523,84 +527,108 @@ export const RichEditor = forwardRef( }, }, autofocus: autoFocus, + onCreate: () => { + // Editor view is now mounted and ready + setIsEditorReady(true); + }, + onDestroy: () => { + // Editor is being destroyed + setIsEditorReady(false); + }, onUpdate: () => { onChange?.(); }, }); // Helper to check if editor view is ready (prevents "view not available" errors) - const isEditorReady = useCallback(() => { - return editor && editor.view && editor.view.dom; - }, [editor]); + // This checks both the state flag AND the actual editor/view existence + const checkEditorReady = useCallback(() => { + return ( + isEditorReady && + editor && + !editor.isDestroyed && + editor.view && + editor.view.dom + ); + }, [editor, isEditorReady]); // Expose editor methods useImperativeHandle( ref, () => ({ focus: () => { - if (isEditorReady()) { - editor?.commands.focus(); + if (checkEditorReady()) { + editor!.commands.focus(); } }, clear: () => { - if (isEditorReady()) { - editor?.commands.clearContent(); + if (checkEditorReady()) { + editor!.commands.clearContent(); } }, getContent: () => { - if (!isEditorReady()) return ""; - return editor?.getText({ blockSeparator: "\n" }) || ""; + if (!checkEditorReady()) return ""; + return editor!.getText({ blockSeparator: "\n" }) || ""; }, getSerializedContent: () => { - if (!isEditorReady() || !editor) + if (!checkEditorReady()) return { text: "", emojiTags: [], blobAttachments: [], addressRefs: [], }; - return serializeContent(editor); + return serializeContent(editor!); }, isEmpty: () => { - if (!isEditorReady()) return true; - return editor?.isEmpty ?? true; + if (!checkEditorReady()) return true; + return editor!.isEmpty; }, submit: () => { - if (isEditorReady() && editor) { - handleSubmit(editor); + if (checkEditorReady()) { + handleSubmit(editor!); } }, insertText: (text: string) => { - if (isEditorReady()) { - editor?.commands.insertContent(text); + if (checkEditorReady()) { + editor!.commands.insertContent(text); } }, insertBlob: (blob: BlobAttachment) => { - if (isEditorReady()) { - editor?.commands.insertContent({ + if (checkEditorReady()) { + editor!.commands.insertContent({ type: "blobAttachment", attrs: blob, }); } }, getJSON: () => { - if (!isEditorReady()) return null; - return editor?.getJSON() || null; + if (!checkEditorReady()) return null; + return editor!.getJSON(); }, setContent: (json: any) => { // Check editor and view are ready before setting content - if (isEditorReady() && json) { - editor?.commands.setContent(json); + if (checkEditorReady() && json) { + editor!.commands.setContent(json); } }, }), - [editor, handleSubmit, isEditorReady], + [editor, handleSubmit, checkEditorReady], ); // Handle submit on Ctrl/Cmd+Enter useEffect(() => { - // Check both editor and editor.view exist (view may not be ready immediately) - if (!editor?.view?.dom) return; + // Wait until editor is fully ready + if ( + !isEditorReady || + !editor || + editor.isDestroyed || + !editor.view?.dom + ) { + return; + } + + const dom = editor.view.dom; const handleKeyDown = (event: KeyboardEvent) => { if ((event.ctrlKey || event.metaKey) && event.key === "Enter") { @@ -609,12 +637,12 @@ export const RichEditor = forwardRef( } }; - editor.view.dom.addEventListener("keydown", handleKeyDown); + dom.addEventListener("keydown", handleKeyDown); return () => { - // Also check view.dom exists in cleanup (editor might be destroyed) - editor.view?.dom?.removeEventListener("keydown", handleKeyDown); + // Check dom still exists in cleanup (editor might be destroyed) + dom.removeEventListener("keydown", handleKeyDown); }; - }, [editor, handleSubmit]); + }, [editor, handleSubmit, isEditorReady]); if (!editor) { return null;