"use client"; import React, { useRef, useState, useEffect } from "react"; import { Editor } from "@tinymce/tinymce-react"; interface TinyMCEEditorProps { initialData?: string; onChange?: (data: string) => void; onReady?: (editor: any) => void; height?: number; placeholder?: string; disabled?: boolean; readOnly?: boolean; features?: "basic" | "standard" | "full"; toolbar?: string; language?: string; uploadUrl?: string; uploadHeaders?: Record; className?: string; autoSave?: boolean; autoSaveInterval?: number; } const TinyMCEEditor: React.FC = ({ initialData = "", onChange, onReady, height = 400, placeholder = "Start typing...", disabled = false, readOnly = false, features = "standard", toolbar, language = "en", uploadUrl, uploadHeaders, className = "", autoSave = true, autoSaveInterval = 30000, }) => { const editorRef = useRef(null); const [isEditorLoaded, setIsEditorLoaded] = useState(false); const [lastSaved, setLastSaved] = useState(null); const [wordCount, setWordCount] = useState(0); // Feature-based configurations const getFeatureConfig = (featureLevel: string) => { const configs = { basic: { plugins: ["lists", "link", "autolink", "wordcount"], toolbar: "bold italic | bullist numlist | link", menubar: false, }, standard: { plugins: [ "advlist", "autolink", "lists", "link", "image", "charmap", "preview", "anchor", "searchreplace", "visualblocks", "code", "fullscreen", "insertdatetime", "media", "table", "help", "wordcount", ], toolbar: "undo redo | blocks | " + "bold italic forecolor | alignleft aligncenter " + "alignright alignjustify | bullist numlist outdent indent | " + "removeformat | table | code | help", menubar: false, }, full: { plugins: [ "advlist", "autolink", "lists", "link", "image", "charmap", "preview", "anchor", "searchreplace", "visualblocks", "code", "fullscreen", "insertdatetime", "media", "table", "help", "wordcount", "emoticons", "paste", "textcolor", "colorpicker", "hr", "pagebreak", "nonbreaking", "toc", "imagetools", "textpattern", "codesample", ], toolbar: "undo redo | formatselect | bold italic backcolor | " + "alignleft aligncenter alignright alignjustify | " + "bullist numlist outdent indent | removeformat | help", menubar: "file edit view insert format tools table help", }, }; return configs[featureLevel as keyof typeof configs] || configs.standard; }; const handleEditorChange = (content: string) => { if (onChange) { onChange(content); } }; const handleEditorInit = (evt: any, editor: any) => { editorRef.current = editor; setIsEditorLoaded(true); if (onReady) { onReady(editor); } // Set up word count tracking editor.on("keyup", () => { const count = editor.plugins.wordcount.body.getCharacterCount(); setWordCount(count); }); // Set up auto-save if (autoSave && !readOnly) { setInterval(() => { const content = editor.getContent(); localStorage.setItem("tinymce-autosave", content); setLastSaved(new Date()); }, autoSaveInterval); } // Fix cursor jumping issues editor.on("keyup", (e: any) => { // Prevent cursor jumping on content changes e.stopPropagation(); }); editor.on("input", (e: any) => { // Prevent unnecessary re-renders e.stopPropagation(); }); // Handle paste events properly editor.on("paste", (e: any) => { // Allow default paste behavior return true; }); }; const handleImageUpload = (blobInfo: any, progress: any) => { return new Promise((resolve, reject) => { if (!uploadUrl) { reject("No upload URL configured"); return; } const formData = new FormData(); formData.append("file", blobInfo.blob(), blobInfo.filename()); fetch(uploadUrl, { method: "POST", headers: uploadHeaders || {}, body: formData, }) .then((response) => response.json()) .then((result) => { resolve(result.url); }) .catch((error) => { reject(error); }); }); }; const featureConfig = getFeatureConfig(features); const editorConfig = { height, language, placeholder, // readonly: readOnly, // disabled, branding: false, elementpath: false, resize: false, statusbar: !readOnly, // Performance optimizations cache_suffix: "?v=1.0", browser_spellcheck: false, gecko_spellcheck: false, // Content styling content_style: ` body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; line-height: 1.6; color: #333; margin: 0; padding: 16px; } .mce-content-body { min-height: ${height - 32}px; } .mce-content-body:focus { outline: none; } `, // Image upload configuration images_upload_handler: uploadUrl ? handleImageUpload : undefined, automatic_uploads: !!uploadUrl, file_picker_types: "image", // Better mobile support mobile: { theme: "silver", plugins: ["lists", "autolink", "link", "image", "table"], toolbar: "bold italic | bullist numlist | link image", }, // Paste configuration paste_as_text: false, paste_enable_default_filters: true, paste_word_valid_elements: "b,strong,i,em,h1,h2,h3,h4,h5,h6", paste_retain_style_properties: "color background-color font-size font-weight", // Table configuration table_default_styles: { width: "100%", }, table_default_attributes: { border: "1", }, // Code configuration codesample_languages: [ { text: "HTML/XML", value: "markup" }, { text: "JavaScript", value: "javascript" }, { text: "CSS", value: "css" }, { text: "PHP", value: "php" }, { text: "Python", value: "python" }, { text: "Java", value: "java" }, { text: "C", value: "c" }, { text: "C++", value: "cpp" }, ], // ...feature config ...featureConfig, // Custom toolbar if provided ...(toolbar && { toolbar }), }; return (
{/* Status bar */} {isEditorLoaded && (
{autoSave && !readOnly ? "Auto-save enabled" : "Read-only mode"} {lastSaved && autoSave && !readOnly && ( • Last saved: {lastSaved.toLocaleTimeString()} )} • {wordCount} characters
{features} mode
)} {/* Performance indicator */}
Bundle size:{" "} {features === "basic" ? "~150KB" : features === "standard" ? "~200KB" : "~300KB"}
); }; export default TinyMCEEditor;