This commit is contained in:
Anang Yusman 2025-09-19 16:07:58 +08:00
parent dbaf98700c
commit 3a906a755b
4 changed files with 167 additions and 87 deletions

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from "react";
import { Editor } from '@tinymce/tinymce-react'; import { Editor } from "@tinymce/tinymce-react";
interface OptimizedEditorProps { interface OptimizedEditorProps {
initialData?: string; initialData?: string;
@ -13,10 +13,10 @@ interface OptimizedEditorProps {
} }
const OptimizedEditor: React.FC<OptimizedEditorProps> = ({ const OptimizedEditor: React.FC<OptimizedEditorProps> = ({
initialData = '', initialData = "",
onChange, onChange,
height = 400, height = 400,
placeholder = 'Start typing...', placeholder = "Start typing...",
disabled = false, disabled = false,
readOnly = false, readOnly = false,
}) => { }) => {
@ -42,14 +42,30 @@ const OptimizedEditor: React.FC<OptimizedEditorProps> = ({
height, height,
menubar: false, menubar: false,
plugins: [ plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview', "advlist",
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen', "autolink",
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount' "lists",
"link",
"image",
"charmap",
"preview",
"anchor",
"searchreplace",
"visualblocks",
"code",
"fullscreen",
"insertdatetime",
"media",
"table",
"code",
"help",
"wordcount",
], ],
toolbar: 'undo redo | blocks | ' + toolbar:
'bold italic forecolor | alignleft aligncenter ' + "undo redo | blocks | " +
'alignright alignjustify | bullist numlist outdent indent | ' + "bold italic forecolor | alignleft aligncenter " +
'removeformat | table | code | help', "alignright alignjustify | bullist numlist outdent indent | " +
"removeformat | table | code | help",
content_style: ` content_style: `
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
@ -63,27 +79,27 @@ const OptimizedEditor: React.FC<OptimizedEditorProps> = ({
} }
`, `,
placeholder, placeholder,
readonly: readOnly, // readonly: readOnly,
branding: false, branding: false,
elementpath: false, elementpath: false,
resize: false, resize: false,
statusbar: false, statusbar: false,
// Performance optimizations // Performance optimizations
cache_suffix: '?v=1.0', cache_suffix: "?v=1.0",
browser_spellcheck: false, browser_spellcheck: false,
gecko_spellcheck: false, gecko_spellcheck: false,
// Auto-save feature // Auto-save feature
auto_save: true, auto_save: true,
auto_save_interval: '30s', auto_save_interval: "30s",
// Better mobile support // Better mobile support
mobile: { mobile: {
theme: 'silver', theme: "silver",
plugins: ['lists', 'autolink', 'link', 'image', 'table'], plugins: ["lists", "autolink", "link", "image", "table"],
toolbar: 'bold italic | bullist numlist | link image' toolbar: "bold italic | bullist numlist | link image",
} },
}} }}
/> />
); );
}; };
export default OptimizedEditor; export default OptimizedEditor;

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import React, { useRef, useState, useEffect } from 'react'; import React, { useRef, useState, useEffect } from "react";
import { Editor } from '@tinymce/tinymce-react'; import { Editor } from "@tinymce/tinymce-react";
interface TinyMCEEditorProps { interface TinyMCEEditorProps {
initialData?: string; initialData?: string;
@ -11,7 +11,7 @@ interface TinyMCEEditorProps {
placeholder?: string; placeholder?: string;
disabled?: boolean; disabled?: boolean;
readOnly?: boolean; readOnly?: boolean;
features?: 'basic' | 'standard' | 'full'; features?: "basic" | "standard" | "full";
toolbar?: string; toolbar?: string;
language?: string; language?: string;
uploadUrl?: string; uploadUrl?: string;
@ -22,21 +22,21 @@ interface TinyMCEEditorProps {
} }
const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
initialData = '', initialData = "",
onChange, onChange,
onReady, onReady,
height = 400, height = 400,
placeholder = 'Start typing...', placeholder = "Start typing...",
disabled = false, disabled = false,
readOnly = false, readOnly = false,
features = 'standard', features = "standard",
toolbar, toolbar,
language = 'en', language = "en",
uploadUrl, uploadUrl,
uploadHeaders, uploadHeaders,
className = '', className = "",
autoSave = true, autoSave = true,
autoSaveInterval = 30000 autoSaveInterval = 30000,
}) => { }) => {
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const [isEditorLoaded, setIsEditorLoaded] = useState(false); const [isEditorLoaded, setIsEditorLoaded] = useState(false);
@ -47,35 +47,74 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
const getFeatureConfig = (featureLevel: string) => { const getFeatureConfig = (featureLevel: string) => {
const configs = { const configs = {
basic: { basic: {
plugins: ['lists', 'link', 'autolink', 'wordcount'], plugins: ["lists", "link", "autolink", "wordcount"],
toolbar: 'bold italic | bullist numlist | link', toolbar: "bold italic | bullist numlist | link",
menubar: false menubar: false,
}, },
standard: { standard: {
plugins: [ plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview', "advlist",
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen', "autolink",
'insertdatetime', 'media', 'table', 'help', 'wordcount' "lists",
"link",
"image",
"charmap",
"preview",
"anchor",
"searchreplace",
"visualblocks",
"code",
"fullscreen",
"insertdatetime",
"media",
"table",
"help",
"wordcount",
], ],
toolbar: 'undo redo | blocks | ' + toolbar:
'bold italic forecolor | alignleft aligncenter ' + "undo redo | blocks | " +
'alignright alignjustify | bullist numlist outdent indent | ' + "bold italic forecolor | alignleft aligncenter " +
'removeformat | table | code | help', "alignright alignjustify | bullist numlist outdent indent | " +
menubar: false "removeformat | table | code | help",
menubar: false,
}, },
full: { full: {
plugins: [ plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview', "advlist",
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen', "autolink",
'insertdatetime', 'media', 'table', 'help', 'wordcount', 'emoticons', "lists",
'paste', 'textcolor', 'colorpicker', 'hr', 'pagebreak', 'nonbreaking', "link",
'toc', 'imagetools', 'textpattern', 'codesample' "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 | ' + toolbar:
'alignleft aligncenter alignright alignjustify | ' + "undo redo | formatselect | bold italic backcolor | " +
'bullist numlist outdent indent | removeformat | help', "alignleft aligncenter alignright alignjustify | " +
menubar: 'file edit view insert format tools table help' "bullist numlist outdent indent | removeformat | help",
} menubar: "file edit view insert format tools table help",
},
}; };
return configs[featureLevel as keyof typeof configs] || configs.standard; return configs[featureLevel as keyof typeof configs] || configs.standard;
}; };
@ -89,13 +128,13 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
const handleEditorInit = (evt: any, editor: any) => { const handleEditorInit = (evt: any, editor: any) => {
editorRef.current = editor; editorRef.current = editor;
setIsEditorLoaded(true); setIsEditorLoaded(true);
if (onReady) { if (onReady) {
onReady(editor); onReady(editor);
} }
// Set up word count tracking // Set up word count tracking
editor.on('keyup', () => { editor.on("keyup", () => {
const count = editor.plugins.wordcount.body.getCharacterCount(); const count = editor.plugins.wordcount.body.getCharacterCount();
setWordCount(count); setWordCount(count);
}); });
@ -104,24 +143,24 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
if (autoSave && !readOnly) { if (autoSave && !readOnly) {
setInterval(() => { setInterval(() => {
const content = editor.getContent(); const content = editor.getContent();
localStorage.setItem('tinymce-autosave', content); localStorage.setItem("tinymce-autosave", content);
setLastSaved(new Date()); setLastSaved(new Date());
}, autoSaveInterval); }, autoSaveInterval);
} }
// Fix cursor jumping issues // Fix cursor jumping issues
editor.on('keyup', (e: any) => { editor.on("keyup", (e: any) => {
// Prevent cursor jumping on content changes // Prevent cursor jumping on content changes
e.stopPropagation(); e.stopPropagation();
}); });
editor.on('input', (e: any) => { editor.on("input", (e: any) => {
// Prevent unnecessary re-renders // Prevent unnecessary re-renders
e.stopPropagation(); e.stopPropagation();
}); });
// Handle paste events properly // Handle paste events properly
editor.on('paste', (e: any) => { editor.on("paste", (e: any) => {
// Allow default paste behavior // Allow default paste behavior
return true; return true;
}); });
@ -130,23 +169,23 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
const handleImageUpload = (blobInfo: any, progress: any) => { const handleImageUpload = (blobInfo: any, progress: any) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!uploadUrl) { if (!uploadUrl) {
reject('No upload URL configured'); reject("No upload URL configured");
return; return;
} }
const formData = new FormData(); const formData = new FormData();
formData.append('file', blobInfo.blob(), blobInfo.filename()); formData.append("file", blobInfo.blob(), blobInfo.filename());
fetch(uploadUrl, { fetch(uploadUrl, {
method: 'POST', method: "POST",
headers: uploadHeaders || {}, headers: uploadHeaders || {},
body: formData body: formData,
}) })
.then(response => response.json()) .then((response) => response.json())
.then(result => { .then((result) => {
resolve(result.url); resolve(result.url);
}) })
.catch(error => { .catch((error) => {
reject(error); reject(error);
}); });
}); });
@ -165,7 +204,7 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
resize: false, resize: false,
statusbar: !readOnly, statusbar: !readOnly,
// Performance optimizations // Performance optimizations
cache_suffix: '?v=1.0', cache_suffix: "?v=1.0",
browser_spellcheck: false, browser_spellcheck: false,
gecko_spellcheck: false, gecko_spellcheck: false,
// Content styling // Content styling
@ -188,40 +227,41 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
// Image upload configuration // Image upload configuration
images_upload_handler: uploadUrl ? handleImageUpload : undefined, images_upload_handler: uploadUrl ? handleImageUpload : undefined,
automatic_uploads: !!uploadUrl, automatic_uploads: !!uploadUrl,
file_picker_types: 'image', file_picker_types: "image",
// Better mobile support // Better mobile support
mobile: { mobile: {
theme: 'silver', theme: "silver",
plugins: ['lists', 'autolink', 'link', 'image', 'table'], plugins: ["lists", "autolink", "link", "image", "table"],
toolbar: 'bold italic | bullist numlist | link image' toolbar: "bold italic | bullist numlist | link image",
}, },
// Paste configuration // Paste configuration
paste_as_text: false, paste_as_text: false,
paste_enable_default_filters: true, paste_enable_default_filters: true,
paste_word_valid_elements: 'b,strong,i,em,h1,h2,h3,h4,h5,h6', 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', paste_retain_style_properties:
"color background-color font-size font-weight",
// Table configuration // Table configuration
table_default_styles: { table_default_styles: {
width: '100%' width: "100%",
}, },
table_default_attributes: { table_default_attributes: {
border: '1' border: "1",
}, },
// Code configuration // Code configuration
codesample_languages: [ codesample_languages: [
{ text: 'HTML/XML', value: 'markup' }, { text: "HTML/XML", value: "markup" },
{ text: 'JavaScript', value: 'javascript' }, { text: "JavaScript", value: "javascript" },
{ text: 'CSS', value: 'css' }, { text: "CSS", value: "css" },
{ text: 'PHP', value: 'php' }, { text: "PHP", value: "php" },
{ text: 'Python', value: 'python' }, { text: "Python", value: "python" },
{ text: 'Java', value: 'java' }, { text: "Java", value: "java" },
{ text: 'C', value: 'c' }, { text: "C", value: "c" },
{ text: 'C++', value: 'cpp' } { text: "C++", value: "cpp" },
], ],
// ...feature config // ...feature config
...featureConfig, ...featureConfig,
// Custom toolbar if provided // Custom toolbar if provided
...(toolbar && { toolbar }) ...(toolbar && { toolbar }),
}; };
return ( return (
@ -232,15 +272,15 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
onEditorChange={handleEditorChange} onEditorChange={handleEditorChange}
disabled={disabled} disabled={disabled}
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY} apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
init={editorConfig} // init={editorConfig}
/> />
{/* Status bar */} {/* Status bar */}
{isEditorLoaded && ( {isEditorLoaded && (
<div className="text-xs text-gray-500 mt-2 flex items-center justify-between"> <div className="text-xs text-gray-500 mt-2 flex items-center justify-between">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<span> <span>
{autoSave && !readOnly ? 'Auto-save enabled' : 'Read-only mode'} {autoSave && !readOnly ? "Auto-save enabled" : "Read-only mode"}
</span> </span>
{lastSaved && autoSave && !readOnly && ( {lastSaved && autoSave && !readOnly && (
<span> Last saved: {lastSaved.toLocaleTimeString()}</span> <span> Last saved: {lastSaved.toLocaleTimeString()}</span>
@ -255,10 +295,15 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
{/* Performance indicator */} {/* Performance indicator */}
<div className="text-xs text-gray-400 mt-1"> <div className="text-xs text-gray-400 mt-1">
Bundle size: {features === 'basic' ? '~150KB' : features === 'standard' ? '~200KB' : '~300KB'} Bundle size:{" "}
{features === "basic"
? "~150KB"
: features === "standard"
? "~200KB"
: "~300KB"}
</div> </div>
</div> </div>
); );
}; };
export default TinyMCEEditor; export default TinyMCEEditor;

22
package-lock.json generated
View File

@ -26,6 +26,7 @@
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tooltip": "^1.2.7", "@radix-ui/react-tooltip": "^1.2.7",
"@tinymce/tinymce-react": "^6.3.0",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"apexcharts": "^4.7.0", "apexcharts": "^4.7.0",
"axios": "^1.10.0", "axios": "^1.10.0",
@ -2739,6 +2740,24 @@
"tailwindcss": "4.1.11" "tailwindcss": "4.1.11"
} }
}, },
"node_modules/@tinymce/tinymce-react": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/@tinymce/tinymce-react/-/tinymce-react-6.3.0.tgz",
"integrity": "sha512-E++xnn0XzDzpKr40jno2Kj7umfAE6XfINZULEBBeNjTMvbACWzA6CjiR6V8eTDc9yVmdVhIPqVzV4PqD5TZ/4g==",
"dependencies": {
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": "^19.0.0 || ^18.0.0 || ^17.0.1 || ^16.7.0",
"react-dom": "^19.0.0 || ^18.0.0 || ^17.0.1 || ^16.7.0",
"tinymce": "^8.0.0 || ^7.0.0 || ^6.0.0 || ^5.5.1"
},
"peerDependenciesMeta": {
"tinymce": {
"optional": true
}
}
},
"node_modules/@types/color-convert": { "node_modules/@types/color-convert": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.4.tgz",
@ -2798,7 +2817,6 @@
"version": "19.1.8", "version": "19.1.8",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
"dev": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
@ -2807,7 +2825,7 @@
"version": "19.1.6", "version": "19.1.6",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz",
"integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==",
"dev": true, "devOptional": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.0.0" "@types/react": "^19.0.0"
} }

View File

@ -27,6 +27,7 @@
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tooltip": "^1.2.7", "@radix-ui/react-tooltip": "^1.2.7",
"@tinymce/tinymce-react": "^6.3.0",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"apexcharts": "^4.7.0", "apexcharts": "^4.7.0",
"axios": "^1.10.0", "axios": "^1.10.0",