import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState, type DragEvent } from "react"; import { MDXEditor, type MDXEditorMethods, headingsPlugin, imagePlugin, linkDialogPlugin, linkPlugin, listsPlugin, markdownShortcutPlugin, quotePlugin, thematicBreakPlugin, type RealmPlugin, } from "@mdxeditor/editor"; import { cn } from "../lib/utils"; interface MarkdownEditorProps { value: string; onChange: (value: string) => void; placeholder?: string; className?: string; contentClassName?: string; onBlur?: () => void; imageUploadHandler?: (file: File) => Promise; bordered?: boolean; } export interface MarkdownEditorRef { focus: () => void; } export const MarkdownEditor = forwardRef(function MarkdownEditor({ value, onChange, placeholder, className, contentClassName, onBlur, imageUploadHandler, bordered = true, }: MarkdownEditorProps, forwardedRef) { const containerRef = useRef(null); const ref = useRef(null); const latestValueRef = useRef(value); const [uploadError, setUploadError] = useState(null); const [isDragOver, setIsDragOver] = useState(false); const dragDepthRef = useRef(0); useImperativeHandle(forwardedRef, () => ({ focus: () => { ref.current?.focus(undefined, { defaultSelection: "rootEnd" }); }, }), []); const plugins = useMemo(() => { const imageHandler = imageUploadHandler ? async (file: File) => { try { const src = await imageUploadHandler(file); setUploadError(null); return src; } catch (err) { const message = err instanceof Error ? err.message : "Image upload failed"; setUploadError(message); throw err; } } : undefined; const withImage = Boolean(imageHandler); const all: RealmPlugin[] = [ headingsPlugin(), listsPlugin(), quotePlugin(), linkPlugin(), linkDialogPlugin(), thematicBreakPlugin(), markdownShortcutPlugin(), ]; if (imageHandler) { all.push(imagePlugin({ imageUploadHandler: imageHandler })); } return all; }, [imageUploadHandler]); useEffect(() => { if (value !== latestValueRef.current) { ref.current?.setMarkdown(value); latestValueRef.current = value; } }, [value]); function hasFilePayload(evt: DragEvent) { return Array.from(evt.dataTransfer?.types ?? []).includes("Files"); } const canDropImage = Boolean(imageUploadHandler); return (
{ if (!canDropImage || !hasFilePayload(evt)) return; dragDepthRef.current += 1; setIsDragOver(true); }} onDragOver={(evt) => { if (!canDropImage || !hasFilePayload(evt)) return; evt.preventDefault(); evt.dataTransfer.dropEffect = "copy"; }} onDragLeave={(evt) => { if (!canDropImage) return; dragDepthRef.current = Math.max(0, dragDepthRef.current - 1); if (dragDepthRef.current === 0) setIsDragOver(false); }} onDrop={() => { dragDepthRef.current = 0; setIsDragOver(false); }} > { latestValueRef.current = next; onChange(next); }} onBlur={() => onBlur?.()} className={cn("paperclip-mdxeditor", !bordered && "paperclip-mdxeditor--borderless")} contentEditableClassName={cn( "paperclip-mdxeditor-content focus:outline-none", contentClassName, )} overlayContainer={containerRef.current} plugins={plugins} /> {isDragOver && canDropImage && (
Drop image to upload
)} {uploadError && (

{uploadError}

)}
); });