web-mikul-news/components/editor/tinymce-editor.tsx

264 lines
7.6 KiB
TypeScript

"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<string, string>;
className?: string;
autoSave?: boolean;
autoSaveInterval?: number;
}
const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
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<any>(null);
const [isEditorLoaded, setIsEditorLoaded] = useState(false);
const [lastSaved, setLastSaved] = useState<Date | null>(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 (
<div className={`tinymce-editor-container ${className}`}>
<Editor
onInit={handleEditorInit}
initialValue={initialData}
onEditorChange={handleEditorChange}
disabled={disabled}
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
init={editorConfig}
/>
{/* Status bar */}
{isEditorLoaded && (
<div className="text-xs text-gray-500 mt-2 flex items-center justify-between">
<div className="flex items-center space-x-4">
<span>
{autoSave && !readOnly ? 'Auto-save enabled' : 'Read-only mode'}
</span>
{lastSaved && autoSave && !readOnly && (
<span> Last saved: {lastSaved.toLocaleTimeString()}</span>
)}
<span> {wordCount} characters</span>
</div>
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
{features} mode
</span>
</div>
)}
{/* Performance indicator */}
<div className="text-xs text-gray-400 mt-1">
Bundle size: {features === 'basic' ? '~150KB' : features === 'standard' ? '~200KB' : '~300KB'}
</div>
</div>
);
};
export default TinyMCEEditor;