"use client" import { useEffect, useRef } from "react" import { useEditor, EditorContent, type Editor } from "@tiptap/react" import { buildRegionEditorExtensions, RICHTEXT_REGION_EDITOR_CLASS, } from "@/lib/drive/richtext-extensions" import { cn } from "@/lib/utils" function regionEditorProseStyle(maxHeightPx?: number, minHeightPx?: number): string | undefined { const parts: string[] = [] if (minHeightPx != null) parts.push(`min-height:${minHeightPx}px`) if (maxHeightPx != null) { parts.push(`max-height:${maxHeightPx}px`, "overflow-y:auto") } return parts.length > 0 ? parts.join(";") : undefined } export function DocsRegionEditor({ content, editable, placeholder, className, maxHeightPx, minHeightPx, onUpdate, onBlur, onEditorReady, onContentHeightChange, autoFocus, }: { content: Record | undefined editable: boolean placeholder?: string className?: string /** When set, content scrolls inside. Omit to grow with content. */ maxHeightPx?: number /** Minimum editable band height (header/footer margin zone). */ minHeightPx?: number onUpdate?: (json: Record) => void onBlur?: () => void onEditorReady?: (editor: Editor | null) => void onContentHeightChange?: (height: number) => void autoFocus?: boolean }) { const syncingRef = useRef(false) const rootRef = useRef(null) const proseStyle = regionEditorProseStyle(maxHeightPx, minHeightPx) const editor = useEditor({ immediatelyRender: false, editable, extensions: buildRegionEditorExtensions(placeholder), content: content ?? { type: "doc", content: [{ type: "paragraph" }] }, editorProps: { attributes: { class: RICHTEXT_REGION_EDITOR_CLASS, ...(proseStyle ? { style: proseStyle } : {}), }, }, onUpdate: ({ editor: ed }) => { if (syncingRef.current) return onUpdate?.(ed.getJSON() as Record) }, onBlur: () => onBlur?.(), }) useEffect(() => { onEditorReady?.(editor) return () => onEditorReady?.(null) }, [editor, onEditorReady]) useEffect(() => { if (!editor || !content) return syncingRef.current = true editor.commands.setContent(content, { emitUpdate: false }) syncingRef.current = false }, [content, editor]) useEffect(() => { editor?.setEditable(editable) if (editable && autoFocus) { requestAnimationFrame(() => editor?.commands.focus("end")) } }, [autoFocus, editable, editor]) useEffect(() => { if (!editor) return const style = regionEditorProseStyle(maxHeightPx, minHeightPx) editor.setOptions({ editorProps: { attributes: { class: RICHTEXT_REGION_EDITOR_CLASS, ...(style ? { style } : {}), }, }, }) }, [editor, maxHeightPx, minHeightPx]) useEffect(() => { const root = rootRef.current if (!root || !onContentHeightChange) return const prose = root.querySelector(".ProseMirror") as HTMLElement | null if (!prose) return const report = () => { requestAnimationFrame(() => onContentHeightChange(prose.offsetHeight)) } report() const ro = new ResizeObserver(report) ro.observe(prose) return () => ro.disconnect() }, [editor, onContentHeightChange]) if (!editor) return null return (
) }