"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;