update
This commit is contained in:
parent
3e97801f75
commit
04ee52fb72
Binary file not shown.
|
|
@ -5,36 +5,166 @@ import { CKEditor } from "@ckeditor/ckeditor5-react";
|
||||||
import Editor from "@/vendor/ckeditor5/build/ckeditor";
|
import Editor from "@/vendor/ckeditor5/build/ckeditor";
|
||||||
|
|
||||||
function CustomEditor(props) {
|
function CustomEditor(props) {
|
||||||
|
const maxHeight = props.maxHeight || 600;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CKEditor
|
<div className="ckeditor-wrapper">
|
||||||
editor={Editor}
|
<CKEditor
|
||||||
data={props.initialData}
|
editor={Editor}
|
||||||
onChange={(event, editor) => {
|
data={props.initialData}
|
||||||
const data = editor.getData();
|
onChange={(event, editor) => {
|
||||||
console.log({ event, editor, data });
|
const data = editor.getData();
|
||||||
props.onChange(data);
|
console.log({ event, editor, data });
|
||||||
}}
|
props.onChange(data);
|
||||||
config={{
|
}}
|
||||||
toolbar: [
|
config={{
|
||||||
"heading",
|
toolbar: [
|
||||||
"fontsize",
|
"heading",
|
||||||
"bold",
|
"fontsize",
|
||||||
"italic",
|
"bold",
|
||||||
"link",
|
"italic",
|
||||||
"numberedList",
|
"link",
|
||||||
"bulletedList",
|
"numberedList",
|
||||||
"undo",
|
"bulletedList",
|
||||||
"redo",
|
"undo",
|
||||||
"alignment",
|
"redo",
|
||||||
"outdent",
|
"alignment",
|
||||||
"indent",
|
"outdent",
|
||||||
"blockQuote",
|
"indent",
|
||||||
"insertTable",
|
"blockQuote",
|
||||||
"codeBlock",
|
"insertTable",
|
||||||
"sourceEditing",
|
"codeBlock",
|
||||||
],
|
"sourceEditing",
|
||||||
}}
|
],
|
||||||
/>
|
content_style: `
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #111 !important;
|
||||||
|
background: #fff !important;
|
||||||
|
margin: 0;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin: 1em 0 0.5em 0;
|
||||||
|
color: inherit !important;
|
||||||
|
}
|
||||||
|
ul, ol {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
blockquote {
|
||||||
|
margin: 1em 0;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
border-left: 4px solid #d1d5db;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
color: inherit !important;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
height: props.height || 400,
|
||||||
|
removePlugins: ["Title"],
|
||||||
|
mobile: {
|
||||||
|
theme: "silver",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<style jsx>{`
|
||||||
|
.ckeditor-wrapper {
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1),
|
||||||
|
0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ckeditor-wrapper :global(.ck.ck-editor__main) {
|
||||||
|
min-height: ${props.height || 400}px;
|
||||||
|
max-height: ${maxHeight}px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ckeditor-wrapper :global(.ck.ck-editor__editable) {
|
||||||
|
min-height: ${(props.height || 400) - 50}px;
|
||||||
|
max-height: ${maxHeight - 50}px;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #cbd5e1 #f1f5f9;
|
||||||
|
background: #fff !important;
|
||||||
|
color: #111 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode support */
|
||||||
|
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable) {
|
||||||
|
background: #111 !important;
|
||||||
|
color: #f9fafb !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h1),
|
||||||
|
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h2),
|
||||||
|
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h3),
|
||||||
|
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h4),
|
||||||
|
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h5),
|
||||||
|
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h6) {
|
||||||
|
color: #f9fafb !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark)
|
||||||
|
.ckeditor-wrapper
|
||||||
|
:global(.ck.ck-editor__editable blockquote) {
|
||||||
|
background-color: #1f2937 !important;
|
||||||
|
border-left-color: #374151 !important;
|
||||||
|
color: #f3f4f6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar styling for webkit browsers */
|
||||||
|
.ckeditor-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar) {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ckeditor-wrapper
|
||||||
|
:global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ckeditor-wrapper
|
||||||
|
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ckeditor-wrapper
|
||||||
|
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
|
||||||
|
background: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode scrollbar */
|
||||||
|
:global(.dark)
|
||||||
|
.ckeditor-wrapper
|
||||||
|
:global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
|
||||||
|
background: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark)
|
||||||
|
.ckeditor-wrapper
|
||||||
|
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
|
||||||
|
background: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark)
|
||||||
|
.ckeditor-wrapper
|
||||||
|
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
|
||||||
|
background: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure content doesn't overflow */
|
||||||
|
.ckeditor-wrapper :global(.ck.ck-editor__editable .ck-content) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
"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;
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
"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.
|
|
@ -0,0 +1,102 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
// 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;
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
"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;
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
// 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;
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
// 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;
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
// 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;
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
// 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;
|
||||||
|
|
@ -0,0 +1,264 @@
|
||||||
|
"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;
|
||||||
|
|
@ -3,17 +3,261 @@ import { CKEditor } from "@ckeditor/ckeditor5-react";
|
||||||
import Editor from "@/vendor/ckeditor5/build/ckeditor";
|
import Editor from "@/vendor/ckeditor5/build/ckeditor";
|
||||||
|
|
||||||
function ViewEditor(props) {
|
function ViewEditor(props) {
|
||||||
|
const maxHeight = props.maxHeight || 600; // Default max height 600px
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CKEditor
|
<div className="ckeditor-view-wrapper">
|
||||||
editor={Editor}
|
<CKEditor
|
||||||
data={props.initialData}
|
editor={Editor}
|
||||||
disabled={true}
|
data={props.initialData}
|
||||||
config={{
|
disabled={true}
|
||||||
// toolbar: [],
|
config={{
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
}}
|
content_style: `
|
||||||
/>
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #111;
|
||||||
|
background: #fff;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin: 1em 0 0.5em 0;
|
||||||
|
}
|
||||||
|
ul, ol {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
blockquote {
|
||||||
|
margin: 1em 0;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
border-left: 4px solid #d1d5db;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
height: props.height || 400,
|
||||||
|
removePlugins: ["Title"],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<style jsx>{`
|
||||||
|
.ckeditor-view-wrapper {
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1),
|
||||||
|
0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ckeditor-view-wrapper :global(.ck.ck-editor__main) {
|
||||||
|
min-height: ${props.height || 400}px;
|
||||||
|
max-height: ${maxHeight}px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ckeditor-view-wrapper :global(.ck.ck-editor__editable) {
|
||||||
|
min-height: ${(props.height || 400) - 50}px;
|
||||||
|
max-height: ${maxHeight - 50}px;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #cbd5e1 #f1f5f9;
|
||||||
|
background-color: #fdfdfd;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 🌙 Dark mode support */
|
||||||
|
:global(.dark) .ckeditor-view-wrapper :global(.ck.ck-editor__editable) {
|
||||||
|
background-color: #111 !important;
|
||||||
|
color: #f9fafb !important;
|
||||||
|
border-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .ckeditor-view-wrapper h1,
|
||||||
|
:global(.dark) .ckeditor-view-wrapper h2,
|
||||||
|
:global(.dark) .ckeditor-view-wrapper h3,
|
||||||
|
:global(.dark) .ckeditor-view-wrapper h4,
|
||||||
|
:global(.dark) .ckeditor-view-wrapper h5,
|
||||||
|
:global(.dark) .ckeditor-view-wrapper h6 {
|
||||||
|
color: #f9fafb !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .ckeditor-view-wrapper blockquote {
|
||||||
|
background-color: #1f2937 !important;
|
||||||
|
border-left: 4px solid #374151 !important;
|
||||||
|
color: #f3f4f6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar styling */
|
||||||
|
.ckeditor-view-wrapper
|
||||||
|
:global(.ck.ck-editor__editable::-webkit-scrollbar) {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ckeditor-view-wrapper
|
||||||
|
:global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ckeditor-view-wrapper
|
||||||
|
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ckeditor-view-wrapper
|
||||||
|
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
|
||||||
|
background: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 🌙 Dark mode scrollbar */
|
||||||
|
:global(.dark)
|
||||||
|
.ckeditor-view-wrapper
|
||||||
|
:global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
|
||||||
|
background: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark)
|
||||||
|
.ckeditor-view-wrapper
|
||||||
|
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
|
||||||
|
background: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark)
|
||||||
|
.ckeditor-view-wrapper
|
||||||
|
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
|
||||||
|
background: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read-only specific styling */
|
||||||
|
.ckeditor-view-wrapper :global(.ck.ck-editor__editable.ck-read-only) {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide toolbar */
|
||||||
|
.ckeditor-view-wrapper :global(.ck.ck-toolbar) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ViewEditor;
|
export default ViewEditor;
|
||||||
|
|
||||||
|
// import React from "react";
|
||||||
|
// import { CKEditor } from "@ckeditor/ckeditor5-react";
|
||||||
|
// import Editor from "ckeditor5-custom-build";
|
||||||
|
|
||||||
|
// function ViewEditor(props) {
|
||||||
|
// const maxHeight = props.maxHeight || 600;
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="ckeditor-view-wrapper">
|
||||||
|
// <CKEditor
|
||||||
|
// editor={Editor}
|
||||||
|
// data={props.initialData}
|
||||||
|
// disabled={true}
|
||||||
|
// config={{
|
||||||
|
// // toolbar: [],
|
||||||
|
// isReadOnly: true,
|
||||||
|
// // Add content styling configuration for read-only mode
|
||||||
|
// 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: 0;
|
||||||
|
// }
|
||||||
|
// p {
|
||||||
|
// margin: 0.5em 0;
|
||||||
|
// }
|
||||||
|
// h1, h2, h3, h4, h5, h6 {
|
||||||
|
// margin: 1em 0 0.5em 0;
|
||||||
|
// }
|
||||||
|
// ul, ol {
|
||||||
|
// margin: 0.5em 0;
|
||||||
|
// padding-left: 2em;
|
||||||
|
// }
|
||||||
|
// blockquote {
|
||||||
|
// margin: 1em 0;
|
||||||
|
// padding: 0.5em 1em;
|
||||||
|
// border-left: 4px solid #d1d5db;
|
||||||
|
// background-color: #f9fafb;
|
||||||
|
// }
|
||||||
|
// `,
|
||||||
|
// // Editor appearance settings
|
||||||
|
// height: props.height || 400,
|
||||||
|
// removePlugins: ['Title'],
|
||||||
|
// }}
|
||||||
|
// />
|
||||||
|
// <style jsx>{`
|
||||||
|
// .ckeditor-view-wrapper {
|
||||||
|
// border-radius: 6px;
|
||||||
|
// overflow: hidden;
|
||||||
|
// box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .ckeditor-view-wrapper :global(.ck.ck-editor__main) {
|
||||||
|
// min-height: ${props.height || 400}px;
|
||||||
|
// max-height: ${maxHeight}px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable) {
|
||||||
|
// min-height: ${(props.height || 400) - 50}px;
|
||||||
|
// max-height: ${maxHeight - 50}px;
|
||||||
|
// overflow-y: auto !important;
|
||||||
|
// scrollbar-width: thin;
|
||||||
|
// scrollbar-color: #cbd5e1 #f1f5f9;
|
||||||
|
// background-color:rgb(253, 253, 253);
|
||||||
|
// border: 1px solid #d1d5db;
|
||||||
|
// border-radius: 6px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /* Custom scrollbar styling for webkit browsers */
|
||||||
|
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar) {
|
||||||
|
// width: 8px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
|
||||||
|
// background: #f1f5f9;
|
||||||
|
// border-radius: 4px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
|
||||||
|
// background: #cbd5e1;
|
||||||
|
// border-radius: 4px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
|
||||||
|
// background: #94a3b8;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /* Ensure content doesn't overflow */
|
||||||
|
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable .ck-content) {
|
||||||
|
// overflow: hidden;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /* Read-only specific styling */
|
||||||
|
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable.ck-read-only) {
|
||||||
|
// background-color: #f8fafc;
|
||||||
|
// color: #4b5563;
|
||||||
|
// cursor: default;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /* Hide toolbar for view-only mode */
|
||||||
|
// .ckeditor-view-wrapper :global(.ck.ck-toolbar) {
|
||||||
|
// display: none !important;
|
||||||
|
// }
|
||||||
|
// `}</style>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export default ViewEditor;
|
||||||
|
|
|
||||||
|
|
@ -549,7 +549,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-sm mt-3">Deskripsi</p>
|
<p className="text-sm mt-3">Deskripsi</p>
|
||||||
<Controller
|
{/* <Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="description"
|
name="description"
|
||||||
render={({ field: { onChange, value } }) =>
|
render={({ field: { onChange, value } }) =>
|
||||||
|
|
@ -572,8 +572,17 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
<p className="text-red-400 text-sm mb-3">
|
<p className="text-red-400 text-sm mb-3">
|
||||||
{errors.description?.message}
|
{errors.description?.message}
|
||||||
</p>
|
</p>
|
||||||
|
)} */}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => (
|
||||||
|
<CustomEditor onChange={field.onChange} initialData={field.value} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.description?.message && (
|
||||||
|
<p className="text-red-400 text-sm">{errors.description.message}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-sm mt-3">File Media</p>
|
<p className="text-sm mt-3">File Media</p>
|
||||||
{!isDetail && (
|
{!isDetail && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ export default function HeaderDevelopment() {
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/40 to-transparent p-4 flex flex-col justify-end">
|
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/40 to-transparent p-4 flex flex-col justify-end">
|
||||||
<span className="text-xs bg-yellow-400 text-black px-2 py-0.5 inline-block uppercase w-[130px]">
|
<span className="text-xs bg-yellow-400 text-black px-2 py-0.5 inline-block uppercase w-[130px]">
|
||||||
{article.categoryName || "TANPA KATEGORI"}
|
{article?.categories?.[0]?.title || "TANPA KATEGORI"}
|
||||||
</span>
|
</span>
|
||||||
<h3 className="text-sm font-semibold text-white leading-snug mb-1">
|
<h3 className="text-sm font-semibold text-white leading-snug mb-1">
|
||||||
{article.title}
|
{article.title}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
/* CKEditor Custom Styling */
|
||||||
|
|
||||||
|
/* Main editor container */
|
||||||
|
.ck.ck-editor {
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toolbar styling */
|
||||||
|
.ck.ck-toolbar {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-toolbar .ck-toolbar__items {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main editable area */
|
||||||
|
.ck.ck-editor__editable {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 6px 6px;
|
||||||
|
padding: 1.5em 2em !important;
|
||||||
|
min-height: 400px;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus state */
|
||||||
|
.ck.ck-editor__editable.ck-focused {
|
||||||
|
border-color: #1a9aef;
|
||||||
|
box-shadow: 0 0 0 2px rgba(26, 154, 239, 0.2);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content styling */
|
||||||
|
.ck.ck-editor__editable .ck-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typography improvements */
|
||||||
|
.ck.ck-editor__editable p {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-editor__editable h1,
|
||||||
|
.ck.ck-editor__editable h2,
|
||||||
|
.ck.ck-editor__editable h3,
|
||||||
|
.ck.ck-editor__editable h4,
|
||||||
|
.ck.ck-editor__editable h5,
|
||||||
|
.ck.ck-editor__editable h6 {
|
||||||
|
margin: 1em 0 0.5em 0;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-editor__editable h1 { font-size: 1.75em; }
|
||||||
|
.ck.ck-editor__editable h2 { font-size: 1.5em; }
|
||||||
|
.ck.ck-editor__editable h3 { font-size: 1.25em; }
|
||||||
|
.ck.ck-editor__editable h4 { font-size: 1.1em; }
|
||||||
|
.ck.ck-editor__editable h5 { font-size: 1em; }
|
||||||
|
.ck.ck-editor__editable h6 { font-size: 0.9em; }
|
||||||
|
|
||||||
|
/* Lists */
|
||||||
|
.ck.ck-editor__editable ul,
|
||||||
|
.ck.ck-editor__editable ol {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-editor__editable li {
|
||||||
|
margin: 0.25em 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blockquotes */
|
||||||
|
.ck.ck-editor__editable blockquote {
|
||||||
|
margin: 1em 0;
|
||||||
|
padding: 0.75em 1em;
|
||||||
|
border-left: 4px solid #1a9aef;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
font-style: italic;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables */
|
||||||
|
.ck.ck-editor__editable table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-editor__editable table td,
|
||||||
|
.ck.ck-editor__editable table th {
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
padding: 0.5em 0.75em;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-editor__editable table th {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links */
|
||||||
|
.ck.ck-editor__editable a {
|
||||||
|
color: #1a9aef;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-editor__editable a:hover {
|
||||||
|
color: #0d7cd6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code blocks */
|
||||||
|
.ck.ck-editor__editable pre {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1em;
|
||||||
|
margin: 1em 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-editor__editable code {
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Images */
|
||||||
|
.ck.ck-editor__editable img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal rule */
|
||||||
|
.ck.ck-editor__editable hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #d1d5db;
|
||||||
|
margin: 2em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Placeholder text */
|
||||||
|
.ck.ck-editor__editable.ck-blurred:empty::before {
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
color: #9ca3af;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsiveness */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.ck.ck-editor__editable {
|
||||||
|
padding: 1em 1.5em !important;
|
||||||
|
font-size: 16px; /* Better for mobile */
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-toolbar {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-toolbar .ck-toolbar__items {
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode support */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.ck.ck-editor__editable {
|
||||||
|
background: #1f2937;
|
||||||
|
color: #f9fafb;
|
||||||
|
border-color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-editor__editable h1,
|
||||||
|
.ck.ck-editor__editable h2,
|
||||||
|
.ck.ck-editor__editable h3,
|
||||||
|
.ck.ck-editor__editable h4,
|
||||||
|
.ck.ck-editor__editable h5,
|
||||||
|
.ck.ck-editor__editable h6 {
|
||||||
|
color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-editor__editable blockquote {
|
||||||
|
background-color: #374151;
|
||||||
|
border-left-color: #1a9aef;
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-editor__editable pre {
|
||||||
|
background-color: #374151;
|
||||||
|
border-color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-editor__editable code {
|
||||||
|
background-color: #4b5563;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
/* @import url("https://fonts.googleapis.com/css2?family=Bytesized&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"); */
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap");
|
||||||
Loading…
Reference in New Issue