310 lines
8.0 KiB
TypeScript
310 lines
8.0 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;
|