fix:ckeditor
This commit is contained in:
parent
92b7d4de19
commit
dc95533c92
Binary file not shown.
|
|
@ -1,164 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
// Import the optimized editor (choose one based on your migration)
|
|
||||||
// import OptimizedEditor from './optimized-editor'; // TinyMCE
|
|
||||||
// import OptimizedCKEditor from './optimized-ckeditor'; // CKEditor5 Classic
|
|
||||||
// import MinimalEditor from './minimal-editor'; // React Quill
|
|
||||||
|
|
||||||
interface EditorExampleProps {
|
|
||||||
editorType?: 'tinymce' | 'ckeditor' | 'quill';
|
|
||||||
}
|
|
||||||
|
|
||||||
const EditorExample: React.FC<EditorExampleProps> = ({
|
|
||||||
editorType = 'tinymce'
|
|
||||||
}) => {
|
|
||||||
const [content, setContent] = useState('<p>Hello, this is the editor content!</p>');
|
|
||||||
const [savedContent, setSavedContent] = useState('');
|
|
||||||
|
|
||||||
const handleContentChange = (newContent: string) => {
|
|
||||||
setContent(newContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
setSavedContent(content);
|
|
||||||
// console.log('Content saved:', content);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
setContent('<p>Content has been reset!</p>');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-6 max-w-4xl mx-auto">
|
|
||||||
<div className="mb-6">
|
|
||||||
<h2 className="text-2xl font-bold mb-4">Rich Text Editor Example</h2>
|
|
||||||
<p className="text-gray-600 mb-4">
|
|
||||||
This is an optimized editor with {editorType} - much smaller bundle size and better performance!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
{/* Editor Panel */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h3 className="text-lg font-semibold">Editor</h3>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
onClick={handleSave}
|
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleReset}
|
|
||||||
className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border border-gray-200 rounded-lg">
|
|
||||||
{/* Choose your editor based on migration */}
|
|
||||||
{editorType === 'tinymce' && (
|
|
||||||
<div className="p-4">
|
|
||||||
<p className="text-gray-500 text-sm mb-2">
|
|
||||||
TinyMCE Editor (200KB bundle)
|
|
||||||
</p>
|
|
||||||
{/* <OptimizedEditor
|
|
||||||
initialData={content}
|
|
||||||
onChange={handleContentChange}
|
|
||||||
height={400}
|
|
||||||
placeholder="Start typing your content..."
|
|
||||||
/> */}
|
|
||||||
<div className="h-96 bg-gray-50 border border-gray-200 rounded flex items-center justify-center">
|
|
||||||
<p className="text-gray-500">TinyMCE Editor Component</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{editorType === 'ckeditor' && (
|
|
||||||
<div className="p-4">
|
|
||||||
<p className="text-gray-500 text-sm mb-2">
|
|
||||||
CKEditor5 Classic (800KB bundle)
|
|
||||||
</p>
|
|
||||||
{/* <OptimizedCKEditor
|
|
||||||
initialData={content}
|
|
||||||
onChange={handleContentChange}
|
|
||||||
height={400}
|
|
||||||
placeholder="Start typing your content..."
|
|
||||||
/> */}
|
|
||||||
<div className="h-96 bg-gray-50 border border-gray-200 rounded flex items-center justify-center">
|
|
||||||
<p className="text-gray-500">CKEditor5 Classic Component</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{editorType === 'quill' && (
|
|
||||||
<div className="p-4">
|
|
||||||
<p className="text-gray-500 text-sm mb-2">
|
|
||||||
React Quill (100KB bundle)
|
|
||||||
</p>
|
|
||||||
{/* <MinimalEditor
|
|
||||||
initialData={content}
|
|
||||||
onChange={handleContentChange}
|
|
||||||
height={400}
|
|
||||||
placeholder="Start typing your content..."
|
|
||||||
/> */}
|
|
||||||
<div className="h-96 bg-gray-50 border border-gray-200 rounded flex items-center justify-center">
|
|
||||||
<p className="text-gray-500">React Quill Component</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Preview Panel */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-lg font-semibold">Preview</h3>
|
|
||||||
|
|
||||||
<div className="border border-gray-200 rounded-lg p-4">
|
|
||||||
<h4 className="font-medium mb-2">Current Content:</h4>
|
|
||||||
<div
|
|
||||||
className="prose max-w-none"
|
|
||||||
dangerouslySetInnerHTML={{ __html: content }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{savedContent && (
|
|
||||||
<div className="border border-gray-200 rounded-lg p-4">
|
|
||||||
<h4 className="font-medium mb-2">Saved Content:</h4>
|
|
||||||
<div
|
|
||||||
className="prose max-w-none"
|
|
||||||
dangerouslySetInnerHTML={{ __html: savedContent }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="border border-gray-200 rounded-lg p-4">
|
|
||||||
<h4 className="font-medium mb-2">Raw HTML:</h4>
|
|
||||||
<pre className="text-xs bg-gray-100 p-2 rounded overflow-auto max-h-32">
|
|
||||||
{content}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Performance Info */}
|
|
||||||
<div className="mt-8 p-4 bg-blue-50 rounded-lg">
|
|
||||||
<h4 className="font-medium text-blue-900 mb-2">Performance Benefits:</h4>
|
|
||||||
<ul className="text-sm text-blue-800 space-y-1">
|
|
||||||
<li>• 90% smaller bundle size compared to custom CKEditor5</li>
|
|
||||||
<li>• Faster initial load time</li>
|
|
||||||
<li>• Better mobile performance</li>
|
|
||||||
<li>• Reduced memory usage</li>
|
|
||||||
<li>• Improved Lighthouse score</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditorExample;
|
|
||||||
|
|
@ -1,176 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
import { Card } from '@/components/ui/card';
|
|
||||||
import CustomEditor from './custom-editor';
|
|
||||||
import FormEditor from './form-editor';
|
|
||||||
|
|
||||||
export default function EditorTest() {
|
|
||||||
const [testData, setTestData] = useState('Initial test content');
|
|
||||||
const [editorType, setEditorType] = useState('custom');
|
|
||||||
|
|
||||||
const { control, setValue, watch, handleSubmit } = useForm({
|
|
||||||
defaultValues: {
|
|
||||||
title: 'Test Title',
|
|
||||||
description: testData,
|
|
||||||
creatorName: 'Test Creator'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const watchedValues = watch();
|
|
||||||
|
|
||||||
const handleSetValue = () => {
|
|
||||||
const newContent = `<p>Updated content at ${new Date().toLocaleTimeString()}</p><p>This content was set via setValue</p>`;
|
|
||||||
setValue('description', newContent);
|
|
||||||
setTestData(newContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSetEmpty = () => {
|
|
||||||
setValue('description', '');
|
|
||||||
setTestData('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSetHTML = () => {
|
|
||||||
const htmlContent = `
|
|
||||||
<h2>HTML Content Test</h2>
|
|
||||||
<p>This is a <strong>bold</strong> paragraph with <em>italic</em> text.</p>
|
|
||||||
<ul>
|
|
||||||
<li>List item 1</li>
|
|
||||||
<li>List item 2</li>
|
|
||||||
<li>List item 3</li>
|
|
||||||
</ul>
|
|
||||||
<p>Updated at: ${new Date().toLocaleTimeString()}</p>
|
|
||||||
`;
|
|
||||||
setValue('description', htmlContent);
|
|
||||||
setTestData(htmlContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = (data: any) => {
|
|
||||||
// console.log('Form submitted:', data);
|
|
||||||
alert('Form submitted! Check console for data.');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-6 max-w-4xl mx-auto space-y-6">
|
|
||||||
<h1 className="text-2xl font-bold">Editor Test Component</h1>
|
|
||||||
|
|
||||||
<Card className="p-4">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<Label>Editor Type:</Label>
|
|
||||||
<div className="flex gap-2 mt-2">
|
|
||||||
<Button
|
|
||||||
variant={editorType === 'custom' ? 'default' : 'outline'}
|
|
||||||
onClick={() => setEditorType('custom')}
|
|
||||||
>
|
|
||||||
CustomEditor
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={editorType === 'form' ? 'default' : 'outline'}
|
|
||||||
onClick={() => setEditorType('form')}
|
|
||||||
>
|
|
||||||
FormEditor
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
|
||||||
<Button onClick={handleSetValue} variant="outline">
|
|
||||||
Set Value (Current Time)
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSetEmpty} variant="outline">
|
|
||||||
Set Empty
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSetHTML} variant="outline">
|
|
||||||
Set HTML Content
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label>Current Test Data:</Label>
|
|
||||||
<div className="mt-2 p-2 bg-gray-100 rounded text-sm">
|
|
||||||
{testData || '(empty)'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label>Watched Form Values:</Label>
|
|
||||||
<div className="mt-2 p-2 bg-gray-100 rounded text-sm">
|
|
||||||
<pre>{JSON.stringify(watchedValues, null, 2)}</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
|
||||||
<Card className="p-4">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<Label>Title:</Label>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="title"
|
|
||||||
render={({ field }) => (
|
|
||||||
<Input {...field} className="mt-1" />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label>Description (Editor):</Label>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="description"
|
|
||||||
render={({ field }) => (
|
|
||||||
editorType === 'custom' ? (
|
|
||||||
<CustomEditor
|
|
||||||
onChange={field.onChange}
|
|
||||||
initialData={field.value}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<FormEditor
|
|
||||||
onChange={field.onChange}
|
|
||||||
initialData={field.value}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label>Creator Name:</Label>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="creatorName"
|
|
||||||
render={({ field }) => (
|
|
||||||
<Input {...field} className="mt-1" />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button type="submit" className="w-full">
|
|
||||||
Submit Form
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<Card className="p-4">
|
|
||||||
<h3 className="font-semibold mb-2">Instructions:</h3>
|
|
||||||
<ul className="list-disc list-inside space-y-1 text-sm">
|
|
||||||
<li>Switch between CustomEditor and FormEditor to test both</li>
|
|
||||||
<li>Click "Set Value" to test setValue functionality</li>
|
|
||||||
<li>Click "Set Empty" to test empty content handling</li>
|
|
||||||
<li>Click "Set HTML Content" to test rich HTML content</li>
|
|
||||||
<li>Type in the editor to test onChange functionality</li>
|
|
||||||
<li>Submit the form to see all data</li>
|
|
||||||
</ul>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
|
@ -1,102 +0,0 @@
|
||||||
import React, { useRef, useEffect, useState, useCallback } from "react";
|
|
||||||
import { Editor } from "@tinymce/tinymce-react";
|
|
||||||
|
|
||||||
function FormEditor({ onChange, initialData }) {
|
|
||||||
const editorRef = useRef(null);
|
|
||||||
const [isEditorReady, setIsEditorReady] = useState(false);
|
|
||||||
const [editorContent, setEditorContent] = useState(initialData || "");
|
|
||||||
|
|
||||||
// Handle editor initialization
|
|
||||||
const handleInit = useCallback((evt, editor) => {
|
|
||||||
editorRef.current = editor;
|
|
||||||
setIsEditorReady(true);
|
|
||||||
|
|
||||||
// Set initial content when editor is ready
|
|
||||||
if (editorContent) {
|
|
||||||
editor.setContent(editorContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle content changes
|
|
||||||
editor.on('change', () => {
|
|
||||||
const content = editor.getContent();
|
|
||||||
setEditorContent(content);
|
|
||||||
if (onChange) {
|
|
||||||
onChange(content);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [editorContent, onChange]);
|
|
||||||
|
|
||||||
// Watch for initialData changes (from setValue)
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialData !== editorContent) {
|
|
||||||
setEditorContent(initialData || "");
|
|
||||||
|
|
||||||
// Update editor content if ready
|
|
||||||
if (editorRef.current && isEditorReady) {
|
|
||||||
editorRef.current.setContent(initialData || "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [initialData, editorContent, isEditorReady]);
|
|
||||||
|
|
||||||
// Handle initial data when editor becomes ready
|
|
||||||
useEffect(() => {
|
|
||||||
if (isEditorReady && editorContent && editorRef.current) {
|
|
||||||
editorRef.current.setContent(editorContent);
|
|
||||||
}
|
|
||||||
}, [isEditorReady, editorContent]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Editor
|
|
||||||
onInit={handleInit}
|
|
||||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
|
||||||
init={{
|
|
||||||
height: 400,
|
|
||||||
menubar: false,
|
|
||||||
plugins: [
|
|
||||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
|
||||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
|
||||||
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
|
||||||
],
|
|
||||||
toolbar: 'undo redo | blocks | ' +
|
|
||||||
'bold italic forecolor | alignleft aligncenter ' +
|
|
||||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
|
||||||
'removeformat | table | code | help',
|
|
||||||
content_style: `
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.mce-content-body {
|
|
||||||
padding: 16px;
|
|
||||||
min-height: 368px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
placeholder: 'Start typing...',
|
|
||||||
branding: false,
|
|
||||||
elementpath: false,
|
|
||||||
resize: false,
|
|
||||||
statusbar: false,
|
|
||||||
auto_focus: false,
|
|
||||||
forced_root_block: 'p',
|
|
||||||
entity_encoding: 'raw',
|
|
||||||
verify_html: false,
|
|
||||||
cleanup: false,
|
|
||||||
cleanup_on_startup: false,
|
|
||||||
auto_resize: false,
|
|
||||||
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',
|
|
||||||
mobile: {
|
|
||||||
theme: 'silver',
|
|
||||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
|
||||||
toolbar: 'bold italic | bullist numlist | link image'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FormEditor;
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
// components/minimal-editor.js
|
|
||||||
|
|
||||||
import React, { useRef } from "react";
|
|
||||||
import { Editor } from "@tinymce/tinymce-react";
|
|
||||||
|
|
||||||
function MinimalEditor(props) {
|
|
||||||
const editorRef = useRef(null);
|
|
||||||
|
|
||||||
const handleInit = (evt, editor) => {
|
|
||||||
editorRef.current = editor;
|
|
||||||
|
|
||||||
// Set initial content if provided
|
|
||||||
if (props.initialData) {
|
|
||||||
editor.setContent(props.initialData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple onChange handler - no debouncing, no complex logic
|
|
||||||
editor.on('change', () => {
|
|
||||||
if (props.onChange) {
|
|
||||||
props.onChange(editor.getContent());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Editor
|
|
||||||
onInit={handleInit}
|
|
||||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
|
||||||
init={{
|
|
||||||
height: 400,
|
|
||||||
menubar: false,
|
|
||||||
plugins: [
|
|
||||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
|
||||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
|
||||||
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
|
||||||
],
|
|
||||||
toolbar: 'undo redo | blocks | ' +
|
|
||||||
'bold italic forecolor | alignleft aligncenter ' +
|
|
||||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
|
||||||
'removeformat | table | code | help',
|
|
||||||
content_style: `
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.mce-content-body {
|
|
||||||
padding: 16px;
|
|
||||||
min-height: 368px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
placeholder: 'Start typing...',
|
|
||||||
branding: false,
|
|
||||||
elementpath: false,
|
|
||||||
resize: false,
|
|
||||||
statusbar: false,
|
|
||||||
// Minimal settings to prevent cursor jumping
|
|
||||||
auto_focus: false,
|
|
||||||
forced_root_block: 'p',
|
|
||||||
entity_encoding: 'raw',
|
|
||||||
// Disable problematic features
|
|
||||||
verify_html: false,
|
|
||||||
cleanup: false,
|
|
||||||
cleanup_on_startup: false,
|
|
||||||
auto_resize: false,
|
|
||||||
// Basic content handling
|
|
||||||
paste_as_text: false,
|
|
||||||
paste_enable_default_filters: true,
|
|
||||||
// Mobile support
|
|
||||||
mobile: {
|
|
||||||
theme: 'silver',
|
|
||||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
|
||||||
toolbar: 'bold italic | bullist numlist | link image'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MinimalEditor;
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useEffect, useRef } from 'react';
|
|
||||||
import { Editor } from '@tinymce/tinymce-react';
|
|
||||||
|
|
||||||
interface OptimizedEditorProps {
|
|
||||||
initialData?: string;
|
|
||||||
onChange?: (data: string) => void;
|
|
||||||
height?: number;
|
|
||||||
placeholder?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
readOnly?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const OptimizedEditor: React.FC<OptimizedEditorProps> = ({
|
|
||||||
initialData = '',
|
|
||||||
onChange,
|
|
||||||
height = 400,
|
|
||||||
placeholder = 'Start typing...',
|
|
||||||
disabled = false,
|
|
||||||
readOnly = false,
|
|
||||||
}) => {
|
|
||||||
const editorRef = useRef<any>(null);
|
|
||||||
|
|
||||||
const handleEditorChange = (content: string) => {
|
|
||||||
if (onChange) {
|
|
||||||
onChange(content);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInit = (evt: any, editor: any) => {
|
|
||||||
editorRef.current = editor;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Editor
|
|
||||||
onInit={handleInit}
|
|
||||||
initialValue={initialData}
|
|
||||||
onEditorChange={handleEditorChange}
|
|
||||||
disabled={disabled}
|
|
||||||
init={{
|
|
||||||
height,
|
|
||||||
menubar: false,
|
|
||||||
plugins: [
|
|
||||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
|
||||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
|
||||||
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
|
||||||
],
|
|
||||||
toolbar: 'undo redo | blocks | ' +
|
|
||||||
'bold italic forecolor | alignleft aligncenter ' +
|
|
||||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
|
||||||
'removeformat | table | code | help',
|
|
||||||
content_style: `
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.mce-content-body {
|
|
||||||
padding: 16px;
|
|
||||||
min-height: ${height - 32}px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
placeholder,
|
|
||||||
readonly: readOnly,
|
|
||||||
branding: false,
|
|
||||||
elementpath: false,
|
|
||||||
resize: false,
|
|
||||||
statusbar: false,
|
|
||||||
// Performance optimizations
|
|
||||||
cache_suffix: '?v=1.0',
|
|
||||||
browser_spellcheck: false,
|
|
||||||
gecko_spellcheck: false,
|
|
||||||
// Auto-save feature
|
|
||||||
auto_save: true,
|
|
||||||
auto_save_interval: '30s',
|
|
||||||
// Better mobile support
|
|
||||||
mobile: {
|
|
||||||
theme: 'silver',
|
|
||||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
|
||||||
toolbar: 'bold italic | bullist numlist | link image'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OptimizedEditor;
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
// components/readonly-editor.js
|
|
||||||
|
|
||||||
import React, { useRef, useEffect } from "react";
|
|
||||||
import { Editor } from "@tinymce/tinymce-react";
|
|
||||||
|
|
||||||
function ReadOnlyEditor(props) {
|
|
||||||
const editorRef = useRef(null);
|
|
||||||
|
|
||||||
const handleInit = (evt, editor) => {
|
|
||||||
editorRef.current = editor;
|
|
||||||
|
|
||||||
// Set initial content if provided
|
|
||||||
if (props.initialData) {
|
|
||||||
editor.setContent(props.initialData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable all editing capabilities
|
|
||||||
editor.on('keydown keyup keypress input', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.on('paste', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.on('drop', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Disable mouse events that might allow editing
|
|
||||||
editor.on('mousedown mousemove mouseup click dblclick', (e) => {
|
|
||||||
if (e.target.closest('.mce-content-body')) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update content when props change
|
|
||||||
useEffect(() => {
|
|
||||||
if (editorRef.current && props.initialData) {
|
|
||||||
editorRef.current.setContent(props.initialData);
|
|
||||||
}
|
|
||||||
}, [props.initialData]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Editor
|
|
||||||
onInit={handleInit}
|
|
||||||
initialValue={props.initialData || ''}
|
|
||||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
|
||||||
init={{
|
|
||||||
height: props.height || 400,
|
|
||||||
menubar: false,
|
|
||||||
toolbar: false, // No toolbar for read-only mode
|
|
||||||
plugins: [
|
|
||||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap',
|
|
||||||
'anchor', 'searchreplace', 'visualblocks', 'code',
|
|
||||||
'insertdatetime', 'media', 'table'
|
|
||||||
],
|
|
||||||
content_style: `
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
|
||||||
.mce-content-body {
|
|
||||||
padding: 16px;
|
|
||||||
min-height: ${(props.height || 400) - 32}px;
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
|
||||||
.mce-content-body * {
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
readonly: true,
|
|
||||||
branding: false,
|
|
||||||
elementpath: false,
|
|
||||||
resize: false,
|
|
||||||
statusbar: false,
|
|
||||||
// Minimal settings to prevent cursor jumping
|
|
||||||
auto_focus: false,
|
|
||||||
forced_root_block: 'p',
|
|
||||||
entity_encoding: 'raw',
|
|
||||||
// Disable problematic features
|
|
||||||
verify_html: false,
|
|
||||||
cleanup: false,
|
|
||||||
cleanup_on_startup: false,
|
|
||||||
auto_resize: false,
|
|
||||||
// Performance optimizations for read-only
|
|
||||||
cache_suffix: '?v=1.0',
|
|
||||||
browser_spellcheck: false,
|
|
||||||
gecko_spellcheck: false,
|
|
||||||
// Disable editing features
|
|
||||||
paste_as_text: true,
|
|
||||||
paste_enable_default_filters: false,
|
|
||||||
paste_word_valid_elements: false,
|
|
||||||
paste_retain_style_properties: false,
|
|
||||||
// Additional read-only settings
|
|
||||||
contextmenu: false,
|
|
||||||
selection: false,
|
|
||||||
// Disable all editing
|
|
||||||
object_resizing: false,
|
|
||||||
element_format: 'html',
|
|
||||||
// Mobile support
|
|
||||||
mobile: {
|
|
||||||
theme: 'silver',
|
|
||||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
|
||||||
toolbar: false
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ReadOnlyEditor;
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
// components/simple-editor.js
|
|
||||||
|
|
||||||
import React, { useRef, useState, useCallback } from "react";
|
|
||||||
import { Editor } from "@tinymce/tinymce-react";
|
|
||||||
|
|
||||||
function SimpleEditor(props) {
|
|
||||||
const editorRef = useRef(null);
|
|
||||||
const [editorInstance, setEditorInstance] = useState(null);
|
|
||||||
|
|
||||||
const handleInit = useCallback((evt, editor) => {
|
|
||||||
editorRef.current = editor;
|
|
||||||
setEditorInstance(editor);
|
|
||||||
|
|
||||||
// Set initial content
|
|
||||||
if (props.initialData) {
|
|
||||||
editor.setContent(props.initialData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable automatic content updates
|
|
||||||
editor.settings.auto_focus = false;
|
|
||||||
editor.settings.forced_root_block = 'p';
|
|
||||||
|
|
||||||
// Store the onChange callback
|
|
||||||
editor.onChangeCallback = props.onChange;
|
|
||||||
|
|
||||||
// Handle content changes without triggering re-renders
|
|
||||||
editor.on('change keyup input', (e) => {
|
|
||||||
if (editor.onChangeCallback) {
|
|
||||||
const content = editor.getContent();
|
|
||||||
editor.onChangeCallback(content);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}, [props.initialData, props.onChange]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Editor
|
|
||||||
onInit={handleInit}
|
|
||||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
|
||||||
init={{
|
|
||||||
height: 400,
|
|
||||||
menubar: false,
|
|
||||||
plugins: [
|
|
||||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
|
||||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
|
||||||
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
|
||||||
],
|
|
||||||
toolbar: 'undo redo | blocks | ' +
|
|
||||||
'bold italic forecolor | alignleft aligncenter ' +
|
|
||||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
|
||||||
'removeformat | table | code | help',
|
|
||||||
content_style: `
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.mce-content-body {
|
|
||||||
padding: 16px;
|
|
||||||
min-height: 368px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
placeholder: 'Start typing...',
|
|
||||||
branding: false,
|
|
||||||
elementpath: false,
|
|
||||||
resize: false,
|
|
||||||
statusbar: false,
|
|
||||||
// Critical settings to prevent cursor jumping
|
|
||||||
auto_focus: false,
|
|
||||||
forced_root_block: 'p',
|
|
||||||
entity_encoding: 'raw',
|
|
||||||
keep_styles: true,
|
|
||||||
// Disable problematic features
|
|
||||||
verify_html: false,
|
|
||||||
cleanup: false,
|
|
||||||
cleanup_on_startup: false,
|
|
||||||
auto_resize: false,
|
|
||||||
// Better content handling
|
|
||||||
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',
|
|
||||||
// Mobile support
|
|
||||||
mobile: {
|
|
||||||
theme: 'silver',
|
|
||||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
|
||||||
toolbar: 'bold italic | bullist numlist | link image'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SimpleEditor;
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
// components/simple-readonly-editor.js
|
|
||||||
|
|
||||||
import React, { useRef } from "react";
|
|
||||||
import { Editor } from "@tinymce/tinymce-react";
|
|
||||||
|
|
||||||
function SimpleReadOnlyEditor(props) {
|
|
||||||
const editorRef = useRef(null);
|
|
||||||
|
|
||||||
const handleInit = (evt, editor) => {
|
|
||||||
editorRef.current = editor;
|
|
||||||
|
|
||||||
// Disable all editing capabilities
|
|
||||||
editor.on('keydown keyup keypress input', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.on('paste', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.on('drop', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Disable mouse events that might allow editing
|
|
||||||
editor.on('mousedown mousemove mouseup click dblclick', (e) => {
|
|
||||||
if (e.target.closest('.mce-content-body')) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Editor
|
|
||||||
onInit={handleInit}
|
|
||||||
initialValue={props.initialData || ''}
|
|
||||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
|
||||||
init={{
|
|
||||||
height: props.height || 400,
|
|
||||||
menubar: false,
|
|
||||||
toolbar: false,
|
|
||||||
plugins: [
|
|
||||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap',
|
|
||||||
'anchor', 'searchreplace', 'visualblocks', 'code',
|
|
||||||
'insertdatetime', 'media', 'table'
|
|
||||||
],
|
|
||||||
content_style: `
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
|
||||||
.mce-content-body {
|
|
||||||
padding: 16px;
|
|
||||||
min-height: ${(props.height || 400) - 32}px;
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
|
||||||
.mce-content-body * {
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
readonly: true,
|
|
||||||
branding: false,
|
|
||||||
elementpath: false,
|
|
||||||
resize: false,
|
|
||||||
statusbar: false,
|
|
||||||
auto_focus: false,
|
|
||||||
forced_root_block: 'p',
|
|
||||||
entity_encoding: 'raw',
|
|
||||||
verify_html: false,
|
|
||||||
cleanup: false,
|
|
||||||
cleanup_on_startup: false,
|
|
||||||
auto_resize: false,
|
|
||||||
browser_spellcheck: false,
|
|
||||||
gecko_spellcheck: false,
|
|
||||||
paste_as_text: true,
|
|
||||||
paste_enable_default_filters: false,
|
|
||||||
contextmenu: false,
|
|
||||||
selection: false,
|
|
||||||
object_resizing: false,
|
|
||||||
element_format: 'html'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SimpleReadOnlyEditor;
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
import React, { useRef, useEffect } from "react";
|
|
||||||
import { Editor } from "@tinymce/tinymce-react";
|
|
||||||
|
|
||||||
function StableEditor(props) {
|
|
||||||
const editorRef = useRef(null);
|
|
||||||
const onChangeRef = useRef(props.onChange);
|
|
||||||
|
|
||||||
// Update onChange ref when props change
|
|
||||||
useEffect(() => {
|
|
||||||
onChangeRef.current = props.onChange;
|
|
||||||
}, [props.onChange]);
|
|
||||||
|
|
||||||
const handleInit = (evt, editor) => {
|
|
||||||
editorRef.current = editor;
|
|
||||||
|
|
||||||
// Set initial content if provided
|
|
||||||
if (props.initialData) {
|
|
||||||
editor.setContent(props.initialData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use a simple change handler that doesn't trigger re-renders
|
|
||||||
editor.on('change', () => {
|
|
||||||
if (onChangeRef.current) {
|
|
||||||
onChangeRef.current(editor.getContent());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Editor
|
|
||||||
onInit={handleInit}
|
|
||||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
|
||||||
init={{
|
|
||||||
height: 400,
|
|
||||||
menubar: false,
|
|
||||||
plugins: [
|
|
||||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
|
||||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
|
||||||
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
|
||||||
],
|
|
||||||
toolbar: 'undo redo | blocks | ' +
|
|
||||||
'bold italic forecolor | alignleft aligncenter ' +
|
|
||||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
|
||||||
'removeformat | table | code | help',
|
|
||||||
content_style: `
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.mce-content-body {
|
|
||||||
padding: 16px;
|
|
||||||
min-height: 368px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
placeholder: 'Start typing...',
|
|
||||||
branding: false,
|
|
||||||
elementpath: false,
|
|
||||||
resize: false,
|
|
||||||
statusbar: false,
|
|
||||||
// Critical settings for stability
|
|
||||||
auto_focus: false,
|
|
||||||
forced_root_block: 'p',
|
|
||||||
entity_encoding: 'raw',
|
|
||||||
keep_styles: true,
|
|
||||||
// Disable all problematic features
|
|
||||||
verify_html: false,
|
|
||||||
cleanup: false,
|
|
||||||
cleanup_on_startup: false,
|
|
||||||
auto_resize: false,
|
|
||||||
// Content handling
|
|
||||||
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',
|
|
||||||
// Prevent automatic updates
|
|
||||||
element_format: 'html',
|
|
||||||
valid_children: '+body[style]',
|
|
||||||
extended_valid_elements: 'span[*]',
|
|
||||||
custom_elements: '~span',
|
|
||||||
// Mobile support
|
|
||||||
mobile: {
|
|
||||||
theme: 'silver',
|
|
||||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
|
||||||
toolbar: 'bold italic | bullist numlist | link image'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StableEditor;
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
import React, { useRef, useEffect } from "react";
|
|
||||||
import { Editor } from "@tinymce/tinymce-react";
|
|
||||||
|
|
||||||
function StaticEditor(props) {
|
|
||||||
const editorRef = useRef(null);
|
|
||||||
const onChangeRef = useRef(props.onChange);
|
|
||||||
|
|
||||||
// Update onChange ref when props change
|
|
||||||
useEffect(() => {
|
|
||||||
onChangeRef.current = props.onChange;
|
|
||||||
}, [props.onChange]);
|
|
||||||
|
|
||||||
const handleInit = (evt, editor) => {
|
|
||||||
editorRef.current = editor;
|
|
||||||
|
|
||||||
// Set initial content if provided
|
|
||||||
if (props.initialData) {
|
|
||||||
editor.setContent(props.initialData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use a simple change handler that doesn't trigger re-renders
|
|
||||||
editor.on('change', () => {
|
|
||||||
if (onChangeRef.current) {
|
|
||||||
onChangeRef.current(editor.getContent());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Editor
|
|
||||||
onInit={handleInit}
|
|
||||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
|
||||||
init={{
|
|
||||||
height: 400,
|
|
||||||
menubar: false,
|
|
||||||
plugins: [
|
|
||||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
|
||||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
|
||||||
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
|
||||||
],
|
|
||||||
toolbar: 'undo redo | blocks | ' +
|
|
||||||
'bold italic forecolor | alignleft aligncenter ' +
|
|
||||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
|
||||||
'removeformat | table | code | help',
|
|
||||||
content_style: `
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.mce-content-body {
|
|
||||||
padding: 16px;
|
|
||||||
min-height: 368px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
placeholder: 'Start typing...',
|
|
||||||
branding: false,
|
|
||||||
elementpath: false,
|
|
||||||
resize: false,
|
|
||||||
statusbar: false,
|
|
||||||
// Critical settings to prevent cursor jumping
|
|
||||||
auto_focus: false,
|
|
||||||
forced_root_block: 'p',
|
|
||||||
entity_encoding: 'raw',
|
|
||||||
keep_styles: true,
|
|
||||||
// Disable all problematic features
|
|
||||||
verify_html: false,
|
|
||||||
cleanup: false,
|
|
||||||
cleanup_on_startup: false,
|
|
||||||
auto_resize: false,
|
|
||||||
// Content handling
|
|
||||||
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',
|
|
||||||
// Prevent automatic updates
|
|
||||||
element_format: 'html',
|
|
||||||
valid_children: '+body[style]',
|
|
||||||
extended_valid_elements: 'span[*]',
|
|
||||||
custom_elements: '~span',
|
|
||||||
// Mobile support
|
|
||||||
mobile: {
|
|
||||||
theme: 'silver',
|
|
||||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
|
||||||
toolbar: 'bold italic | bullist numlist | link image'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StaticEditor;
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
// components/strict-readonly-editor.js
|
|
||||||
|
|
||||||
import React, { useRef } from "react";
|
|
||||||
import { Editor } from "@tinymce/tinymce-react";
|
|
||||||
|
|
||||||
function StrictReadOnlyEditor(props) {
|
|
||||||
const editorRef = useRef(null);
|
|
||||||
|
|
||||||
const handleInit = (evt, editor) => {
|
|
||||||
editorRef.current = editor;
|
|
||||||
|
|
||||||
// Disable all possible editing events
|
|
||||||
const disableEvents = ['keydown', 'keyup', 'keypress', 'input', 'paste', 'drop', 'cut', 'copy'];
|
|
||||||
|
|
||||||
disableEvents.forEach(eventType => {
|
|
||||||
editor.on(eventType, (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
e.stopImmediatePropagation();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Disable mouse events that might allow editing
|
|
||||||
editor.on('mousedown mousemove mouseup click dblclick', (e) => {
|
|
||||||
if (e.target.closest('.mce-content-body')) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Disable focus events
|
|
||||||
editor.on('focus blur', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Editor
|
|
||||||
onInit={handleInit}
|
|
||||||
initialValue={props.initialData || ''}
|
|
||||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
|
||||||
init={{
|
|
||||||
height: props.height || 400,
|
|
||||||
menubar: false,
|
|
||||||
toolbar: false,
|
|
||||||
plugins: [
|
|
||||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap',
|
|
||||||
'anchor', 'searchreplace', 'visualblocks', 'code',
|
|
||||||
'insertdatetime', 'media', 'table'
|
|
||||||
],
|
|
||||||
content_style: `
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
pointer-events: none !important;
|
|
||||||
user-select: none !important;
|
|
||||||
-webkit-user-select: none !important;
|
|
||||||
-moz-user-select: none !important;
|
|
||||||
-ms-user-select: none !important;
|
|
||||||
}
|
|
||||||
.mce-content-body {
|
|
||||||
padding: 16px;
|
|
||||||
min-height: ${(props.height || 400) - 32}px;
|
|
||||||
pointer-events: none !important;
|
|
||||||
user-select: none !important;
|
|
||||||
-webkit-user-select: none !important;
|
|
||||||
-moz-user-select: none !important;
|
|
||||||
-ms-user-select: none !important;
|
|
||||||
}
|
|
||||||
.mce-content-body * {
|
|
||||||
pointer-events: none !important;
|
|
||||||
user-select: none !important;
|
|
||||||
-webkit-user-select: none !important;
|
|
||||||
-moz-user-select: none !important;
|
|
||||||
-ms-user-select: none !important;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
readonly: true,
|
|
||||||
branding: false,
|
|
||||||
elementpath: false,
|
|
||||||
resize: false,
|
|
||||||
statusbar: false,
|
|
||||||
auto_focus: false,
|
|
||||||
forced_root_block: 'p',
|
|
||||||
entity_encoding: 'raw',
|
|
||||||
verify_html: false,
|
|
||||||
cleanup: false,
|
|
||||||
cleanup_on_startup: false,
|
|
||||||
auto_resize: false,
|
|
||||||
browser_spellcheck: false,
|
|
||||||
gecko_spellcheck: false,
|
|
||||||
paste_as_text: true,
|
|
||||||
paste_enable_default_filters: false,
|
|
||||||
contextmenu: false,
|
|
||||||
selection: false,
|
|
||||||
object_resizing: false,
|
|
||||||
element_format: 'html',
|
|
||||||
// Additional strict settings
|
|
||||||
valid_children: false,
|
|
||||||
extended_valid_elements: false,
|
|
||||||
custom_elements: false
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StrictReadOnlyEditor;
|
|
||||||
|
|
@ -1,264 +0,0 @@
|
||||||
"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;
|
|
||||||
Loading…
Reference in New Issue