import { EditorContent, useEditor } from "@tiptap/react" import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react" import { editorExtensions, editorProps } from "./config" import { Toolbar } from "./Toolbar" import "./editor.css" import "./components/image-upload/image-upload-node.css" export interface TextEditorProps { value?: string onChange?: (value: string) => void placeholder?: string className?: string disabled?: boolean error?: boolean maxHeight?: number | string } export interface TextEditorRef { focus: () => void getHTML: () => string getText: () => string setContent: (content: string) => void clear: () => void } export const TextEditor = forwardRef( ( { value = "", onChange, placeholder = "", className, disabled, error, maxHeight = 400 }, ref, ) => { // Resize functionality const [currentHeight, setCurrentHeight] = useState( typeof maxHeight === "number" ? maxHeight : 400, ) const containerRef = useRef(null) const isResizingRef = useRef(false) const startHeightRef = useRef(0) const startYRef = useRef(0) // Update height when maxHeight prop changes useEffect(() => { const newHeight = typeof maxHeight === "number" ? maxHeight : 400 setCurrentHeight(newHeight) }, [maxHeight]) const editor = useEditor({ extensions: editorExtensions, content: value, shouldRerenderOnTransaction: true, editorProps: { ...editorProps, attributes: { ...editorProps.attributes, class: "tiptap", "data-placeholder": placeholder, }, }, onUpdate: ({ editor }) => { const html = editor.getHTML() onChange?.(html) }, editable: !disabled, }) useImperativeHandle(ref, () => ({ focus: () => editor?.commands.focus(), getHTML: () => editor?.getHTML() || "", getText: () => editor?.getText() || "", setContent: (content: string) => editor?.commands.setContent(content), clear: () => editor?.commands.clearContent(), })) const text = editor?.getText() || "" const manualWordCount = text.trim() === "" ? 0 : text .trim() .split(/\s+/) .filter(word => word.length > 0).length const finalWordCount = text.trim() ? manualWordCount : 0 // Resize handlers const handleResizeStart = (e: React.MouseEvent) => { e.preventDefault() isResizingRef.current = true startYRef.current = e.clientY startHeightRef.current = currentHeight // Add global mouse listeners document.addEventListener("mousemove", handleResizeMove) document.addEventListener("mouseup", handleResizeEnd) document.body.style.cursor = "ns-resize" document.body.style.userSelect = "none" } const handleResizeMove = (e: MouseEvent) => { if (!isResizingRef.current) return const deltaY = e.clientY - startYRef.current const newHeight = Math.max(200, Math.min(800, startHeightRef.current + deltaY)) setCurrentHeight(newHeight) } const handleResizeEnd = () => { isResizingRef.current = false document.removeEventListener("mousemove", handleResizeMove) document.removeEventListener("mouseup", handleResizeEnd) document.body.style.cursor = "" document.body.style.userSelect = "" } // Convert height to CSS value const heightValue = `${currentHeight}px` return (
{/* Toolbar */}
{/* Editor Content */}
{/* Footer */}
{finalWordCount} words {/* Resize handle */}
) }, ) TextEditor.displayName = "TextEditor"