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";
|
||||
|
||||
function CustomEditor(props) {
|
||||
const maxHeight = props.maxHeight || 600;
|
||||
|
||||
return (
|
||||
<CKEditor
|
||||
editor={Editor}
|
||||
data={props.initialData}
|
||||
onChange={(event, editor) => {
|
||||
const data = editor.getData();
|
||||
console.log({ event, editor, data });
|
||||
props.onChange(data);
|
||||
}}
|
||||
config={{
|
||||
toolbar: [
|
||||
"heading",
|
||||
"fontsize",
|
||||
"bold",
|
||||
"italic",
|
||||
"link",
|
||||
"numberedList",
|
||||
"bulletedList",
|
||||
"undo",
|
||||
"redo",
|
||||
"alignment",
|
||||
"outdent",
|
||||
"indent",
|
||||
"blockQuote",
|
||||
"insertTable",
|
||||
"codeBlock",
|
||||
"sourceEditing",
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<div className="ckeditor-wrapper">
|
||||
<CKEditor
|
||||
editor={Editor}
|
||||
data={props.initialData}
|
||||
onChange={(event, editor) => {
|
||||
const data = editor.getData();
|
||||
console.log({ event, editor, data });
|
||||
props.onChange(data);
|
||||
}}
|
||||
config={{
|
||||
toolbar: [
|
||||
"heading",
|
||||
"fontsize",
|
||||
"bold",
|
||||
"italic",
|
||||
"link",
|
||||
"numberedList",
|
||||
"bulletedList",
|
||||
"undo",
|
||||
"redo",
|
||||
"alignment",
|
||||
"outdent",
|
||||
"indent",
|
||||
"blockQuote",
|
||||
"insertTable",
|
||||
"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";
|
||||
|
||||
function ViewEditor(props) {
|
||||
const maxHeight = props.maxHeight || 600; // Default max height 600px
|
||||
|
||||
return (
|
||||
<CKEditor
|
||||
editor={Editor}
|
||||
data={props.initialData}
|
||||
disabled={true}
|
||||
config={{
|
||||
// toolbar: [],
|
||||
isReadOnly: true,
|
||||
}}
|
||||
/>
|
||||
<div className="ckeditor-view-wrapper">
|
||||
<CKEditor
|
||||
editor={Editor}
|
||||
data={props.initialData}
|
||||
disabled={true}
|
||||
config={{
|
||||
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;
|
||||
|
||||
// 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>
|
||||
<Controller
|
||||
{/* <Controller
|
||||
control={control}
|
||||
name="description"
|
||||
render={({ field: { onChange, value } }) =>
|
||||
|
|
@ -572,8 +572,17 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
|||
<p className="text-red-400 text-sm mb-3">
|
||||
{errors.description?.message}
|
||||
</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>
|
||||
{!isDetail && (
|
||||
<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">
|
||||
<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>
|
||||
<h3 className="text-sm font-semibold text-white leading-snug mb-1">
|
||||
{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