422 lines
14 KiB
TypeScript
422 lines
14 KiB
TypeScript
'use client';
|
|
|
|
import React, { useEffect, useRef, useState } from 'react';
|
|
import 'quill/dist/quill.snow.css';
|
|
import { sanitizeHtml } from '@/lib/utils/email-utils';
|
|
import { processContentWithDirection } from '@/lib/utils/text-direction';
|
|
|
|
interface RichEmailEditorProps {
|
|
initialContent: string;
|
|
onChange: (content: string) => void;
|
|
placeholder?: string;
|
|
minHeight?: string;
|
|
maxHeight?: string;
|
|
preserveFormatting?: boolean;
|
|
}
|
|
|
|
const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
|
|
initialContent,
|
|
onChange,
|
|
placeholder = 'Write your message here...',
|
|
minHeight = '200px',
|
|
maxHeight = 'calc(100vh - 400px)',
|
|
preserveFormatting = false,
|
|
}) => {
|
|
const editorRef = useRef<HTMLDivElement>(null);
|
|
const toolbarRef = useRef<HTMLDivElement>(null);
|
|
const quillRef = useRef<any>(null);
|
|
const [isReady, setIsReady] = useState(false);
|
|
|
|
// Initialize Quill editor when component mounts
|
|
useEffect(() => {
|
|
// Import Quill dynamically (client-side only)
|
|
const initializeQuill = async () => {
|
|
if (!editorRef.current || !toolbarRef.current) return;
|
|
|
|
const Quill = (await import('quill')).default;
|
|
|
|
// Import quill-better-table
|
|
let tableModule = null;
|
|
try {
|
|
const QuillBetterTable = await import('quill-better-table');
|
|
|
|
// Register the table module if available
|
|
if (QuillBetterTable && QuillBetterTable.default) {
|
|
Quill.register({
|
|
'modules/better-table': QuillBetterTable.default
|
|
}, true);
|
|
tableModule = QuillBetterTable.default;
|
|
console.log('Better Table module registered successfully');
|
|
}
|
|
} catch (err) {
|
|
console.warn('Table module not available:', err);
|
|
}
|
|
|
|
// Define custom formats/modules with table support
|
|
const emailToolbarOptions = [
|
|
['bold', 'italic', 'underline', 'strike'],
|
|
[{ 'color': [] }, { 'background': [] }],
|
|
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
|
[{ 'indent': '-1'}, { 'indent': '+1' }],
|
|
[{ 'align': [] }],
|
|
[{ 'direction': 'rtl' }], // Add direction to toolbar
|
|
['link'],
|
|
['clean'],
|
|
];
|
|
|
|
// Create new Quill instance with the DOM element and custom toolbar
|
|
const editorElement = editorRef.current;
|
|
quillRef.current = new Quill(editorElement, {
|
|
modules: {
|
|
toolbar: {
|
|
container: toolbarRef.current,
|
|
handlers: {
|
|
// Add any custom toolbar handlers here
|
|
}
|
|
},
|
|
clipboard: {
|
|
matchVisual: false // Disable clipboard matching for better HTML handling
|
|
},
|
|
// Don't initialize better-table yet - we'll do it after content is loaded
|
|
'better-table': false,
|
|
},
|
|
placeholder: placeholder,
|
|
theme: 'snow',
|
|
});
|
|
|
|
// Process initial content to detect direction
|
|
const { direction, html: processedContent } = processContentWithDirection(initialContent);
|
|
|
|
// Set initial content properly
|
|
if (initialContent) {
|
|
try {
|
|
console.log('Setting initial content in editor', {
|
|
length: initialContent.length,
|
|
startsWithHtml: initialContent.trim().startsWith('<'),
|
|
direction
|
|
});
|
|
|
|
// Simplify complex email content to something Quill can handle better
|
|
const sanitizedContent = sanitizeHtml(processedContent || initialContent);
|
|
|
|
// Use direct innerHTML setting for the initial content
|
|
quillRef.current.root.innerHTML = sanitizedContent;
|
|
|
|
// Set the direction for the content
|
|
quillRef.current.format('direction', direction);
|
|
if (direction === 'rtl') {
|
|
quillRef.current.format('align', 'right');
|
|
}
|
|
|
|
// Set cursor at the beginning
|
|
quillRef.current.setSelection(0, 0);
|
|
|
|
// Ensure the cursor and scroll position is at the top of the editor
|
|
if (editorRef.current) {
|
|
editorRef.current.scrollTop = 0;
|
|
|
|
// Find and scroll parent containers that might have scroll
|
|
const scrollable = [
|
|
editorRef.current.closest('.ql-container'),
|
|
editorRef.current.closest('.rich-email-editor-container'),
|
|
editorRef.current.closest('.overflow-y-auto'),
|
|
document.querySelector('.overflow-y-auto')
|
|
];
|
|
|
|
scrollable.forEach(el => {
|
|
if (el instanceof HTMLElement) {
|
|
el.scrollTop = 0;
|
|
}
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error('Error setting initial content:', err);
|
|
// Fallback: just set text
|
|
quillRef.current.setText('');
|
|
|
|
// Extract text as a last resort
|
|
try {
|
|
// Create a temporary div to extract text from HTML
|
|
const tempDiv = document.createElement('div');
|
|
tempDiv.innerHTML = initialContent;
|
|
const textContent = tempDiv.textContent || tempDiv.innerText || '';
|
|
quillRef.current.setText(textContent);
|
|
} catch (e) {
|
|
console.error('Fallback failed too:', e);
|
|
quillRef.current.setText('');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add change listener
|
|
quillRef.current.on('text-change', () => {
|
|
const html = quillRef.current.root.innerHTML;
|
|
onChange(html);
|
|
});
|
|
|
|
// Improve editor layout
|
|
const editorContainer = editorElement.closest('.ql-container');
|
|
if (editorContainer) {
|
|
editorContainer.classList.add('email-editor-container');
|
|
}
|
|
|
|
setIsReady(true);
|
|
};
|
|
|
|
initializeQuill().catch(err => {
|
|
console.error('Failed to initialize Quill editor:', err);
|
|
});
|
|
|
|
// Clean up on unmount
|
|
return () => {
|
|
if (quillRef.current) {
|
|
// Clean up any event listeners or resources
|
|
quillRef.current.off('text-change');
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
// Update content from props if changed externally - using a simpler approach
|
|
useEffect(() => {
|
|
if (quillRef.current && isReady && initialContent) {
|
|
const currentContent = quillRef.current.root.innerHTML;
|
|
|
|
// Only update if content changed to avoid editor position reset
|
|
if (initialContent !== currentContent) {
|
|
try {
|
|
// Process content to ensure correct direction
|
|
const { direction, html: processedContent } = processContentWithDirection(initialContent);
|
|
|
|
// Sanitize the HTML
|
|
const sanitizedContent = sanitizeHtml(processedContent || initialContent);
|
|
|
|
// SIMPLIFIED: Set content directly to the root element rather than using clipboard
|
|
quillRef.current.root.innerHTML = sanitizedContent;
|
|
|
|
// Set the direction for the content
|
|
quillRef.current.format('direction', direction);
|
|
if (direction === 'rtl') {
|
|
quillRef.current.format('align', 'right');
|
|
}
|
|
|
|
// Force update
|
|
quillRef.current.update();
|
|
|
|
// Set selection to beginning
|
|
quillRef.current.setSelection(0, 0);
|
|
} catch (err) {
|
|
console.error('Error updating content:', err);
|
|
// Safer fallback that avoids clipboard API
|
|
try {
|
|
// Extract basic text if everything else fails
|
|
const tempDiv = document.createElement('div');
|
|
tempDiv.innerHTML = initialContent;
|
|
const textContent = tempDiv.textContent || tempDiv.innerText || '';
|
|
quillRef.current.setText(textContent);
|
|
} catch (e) {
|
|
console.error('All fallbacks failed:', e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, [initialContent, isReady]);
|
|
|
|
return (
|
|
<div className="rich-email-editor-wrapper">
|
|
{/* Custom toolbar container */}
|
|
<div ref={toolbarRef} className="ql-toolbar ql-snow">
|
|
<span className="ql-formats">
|
|
<button className="ql-bold"></button>
|
|
<button className="ql-italic"></button>
|
|
<button className="ql-underline"></button>
|
|
<button className="ql-strike"></button>
|
|
</span>
|
|
<span className="ql-formats">
|
|
<select className="ql-color"></select>
|
|
<select className="ql-background"></select>
|
|
</span>
|
|
<span className="ql-formats">
|
|
<button className="ql-list" value="ordered"></button>
|
|
<button className="ql-list" value="bullet"></button>
|
|
</span>
|
|
<span className="ql-formats">
|
|
<button className="ql-indent" value="-1"></button>
|
|
<button className="ql-indent" value="+1"></button>
|
|
</span>
|
|
<span className="ql-formats">
|
|
<select className="ql-align"></select>
|
|
</span>
|
|
<span className="ql-formats">
|
|
<button className="ql-direction" value="rtl"></button>
|
|
</span>
|
|
<span className="ql-formats">
|
|
<button className="ql-link"></button>
|
|
</span>
|
|
<span className="ql-formats">
|
|
<button className="ql-clean"></button>
|
|
</span>
|
|
</div>
|
|
|
|
{/* Editor container with improved scrolling */}
|
|
<div className="rich-email-editor-container">
|
|
<div
|
|
ref={editorRef}
|
|
className="quill-editor"
|
|
/>
|
|
|
|
{/* Loading indicator */}
|
|
{!isReady && (
|
|
<div className="flex items-center justify-center py-8">
|
|
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Custom styles for email context */}
|
|
<style jsx>{`
|
|
.rich-email-editor-wrapper {
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
border-radius: 6px;
|
|
flex: 1;
|
|
border: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.rich-email-editor-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: auto;
|
|
flex: 1;
|
|
position: relative;
|
|
}
|
|
|
|
.quill-editor {
|
|
width: 100%;
|
|
min-height: ${minHeight};
|
|
max-height: ${maxHeight};
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
/* Hide the editor until it's ready */
|
|
.quill-editor ${!isReady ? '{ display: none; }' : ''}
|
|
|
|
/* Hide duplicate toolbar */
|
|
:global(.ql-toolbar.ql-snow + .ql-toolbar.ql-snow) {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Add RTL support styles */
|
|
:global([dir="rtl"] .ql-editor) {
|
|
text-align: right;
|
|
direction: rtl;
|
|
}
|
|
|
|
:global(.ql-editor[dir="rtl"]) {
|
|
text-align: right;
|
|
direction: rtl;
|
|
}
|
|
|
|
:global(.ql-container) {
|
|
border: none !important;
|
|
height: auto !important;
|
|
min-height: ${minHeight};
|
|
max-height: none !important;
|
|
overflow: visible;
|
|
}
|
|
|
|
:global(.ql-editor) {
|
|
padding: 12px;
|
|
min-height: ${minHeight};
|
|
overflow-y: auto !important;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
color: #333 !important;
|
|
}
|
|
|
|
/* Ensure all text is visible */
|
|
:global(.ql-editor p),
|
|
:global(.ql-editor div),
|
|
:global(.ql-editor span),
|
|
:global(.ql-editor li) {
|
|
color: #333 !important;
|
|
}
|
|
|
|
/* Ensure placeholder text is visible but distinct */
|
|
:global(.ql-editor.ql-blank::before) {
|
|
color: #aaa !important;
|
|
font-style: italic !important;
|
|
}
|
|
|
|
/* Force blockquote styling */
|
|
:global(.ql-editor blockquote) {
|
|
border-left: 2px solid #ddd !important;
|
|
margin: 0 !important;
|
|
padding: 10px 0 10px 15px !important;
|
|
color: #505050 !important;
|
|
background-color: #f9f9f9 !important;
|
|
border-radius: 4px !important;
|
|
font-size: 13px !important;
|
|
}
|
|
|
|
/* RTL blockquote styling */
|
|
:global(.ql-editor[dir="rtl"] blockquote),
|
|
:global([dir="rtl"] .ql-editor blockquote) {
|
|
border-left: none !important;
|
|
border-right: 2px solid #ddd !important;
|
|
padding: 10px 15px 10px 0 !important;
|
|
}
|
|
|
|
/* Fix table rendering */
|
|
:global(.ql-editor table) {
|
|
width: 100% !important;
|
|
border-collapse: collapse !important;
|
|
table-layout: fixed !important;
|
|
margin: 10px 0 !important;
|
|
border: 1px solid #ddd !important;
|
|
}
|
|
|
|
:global(.ql-editor td),
|
|
:global(.ql-editor th) {
|
|
border: 1px solid #ddd !important;
|
|
padding: 6px 8px !important;
|
|
overflow-wrap: break-word !important;
|
|
word-break: break-word !important;
|
|
min-width: 30px !important;
|
|
font-size: 13px !important;
|
|
}
|
|
|
|
/* Email quote styling */
|
|
:global(.email-original-content) {
|
|
margin-top: 20px !important;
|
|
padding-top: 10px !important;
|
|
border-top: 1px solid #ddd !important;
|
|
color: #555 !important;
|
|
font-size: 13px !important;
|
|
}
|
|
|
|
/* Fix quoted paragraphs */
|
|
:global(.ql-editor blockquote p) {
|
|
margin-bottom: 8px !important;
|
|
margin-top: 0 !important;
|
|
}
|
|
|
|
/* Fix for reply headers */
|
|
:global(.ql-editor div[style*="font-weight: 400"]) {
|
|
margin-top: 20px !important;
|
|
margin-bottom: 8px !important;
|
|
color: #555 !important;
|
|
font-size: 13px !important;
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default RichEmailEditor;
|