'use client'; import React, { useEffect, useRef, useState } from 'react'; import 'quill/dist/quill.snow.css'; import { sanitizeHtml } from '@/lib/utils/dom-sanitizer'; interface RichEmailEditorProps { initialContent: string; onChange: (content: string) => void; placeholder?: string; minHeight?: string; maxHeight?: string; preserveFormatting?: boolean; } const RichEmailEditor: React.FC = ({ initialContent, onChange, placeholder = 'Write your message here...', minHeight = '200px', maxHeight = 'calc(100vh - 400px)', preserveFormatting = false, }) => { const editorRef = useRef(null); const toolbarRef = useRef(null); const quillRef = useRef(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': [] }], ['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 } }, // Don't initialize better-table yet - we'll do it after content is loaded 'better-table': false, }, placeholder: placeholder, theme: 'snow', }); // Set initial content properly if (initialContent) { try { console.log('Setting initial content in editor', { length: initialContent.length, startsWithHtml: initialContent.trim().startsWith('<') }); // Make sure content is properly sanitized before injecting it const cleanContent = initialContent .replace(/)<[^<]*)*<\/script>/gi, '') // Remove scripts .replace(/on\w+="[^"]*"/g, '') // Remove event handlers .replace(/(javascript|jscript|vbscript|mocha):/gi, 'removed:'); // Remove protocol handlers // First, directly set the content if (editorRef.current) { editorRef.current.innerHTML = cleanContent; } // Then let Quill parse and format it correctly setTimeout(() => { // Only proceed if editor ref is still available if (!editorRef.current) return; // Get the content from the editor element const content = editorRef.current.innerHTML; // Clear the editor quillRef.current.setText(''); // Insert clean content quillRef.current.clipboard.dangerouslyPasteHTML(0, content); // Set cursor at the beginning (before the quoted content) 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; } }); } }, 100); } catch (err) { console.error('Error setting initial content:', err); // Fallback: just set text quillRef.current.setText(''); // Try simplest approach try { quillRef.current.clipboard.dangerouslyPasteHTML(initialContent); } catch (e) { console.error('Fallback failed too:', e); // Last resort: strip all HTML quillRef.current.setText(initialContent.replace(/<[^>]*>/g, '')); } } } // 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 useEffect(() => { if (quillRef.current && isReady) { const currentContent = quillRef.current.root.innerHTML; // Only update if content changed to avoid editor position reset if (initialContent !== currentContent) { try { // Preserve cursor position if possible const selection = quillRef.current.getSelection(); // First clear the content quillRef.current.root.innerHTML = ''; // Then insert the new content at position 0 quillRef.current.clipboard.dangerouslyPasteHTML(0, sanitizeHtml(initialContent)); // Force update quillRef.current.update(); // Restore selection if possible if (selection) { setTimeout(() => quillRef.current.setSelection(selection), 10); } } catch (err) { console.error('Error updating content:', err); // Fallback update method quillRef.current.clipboard.dangerouslyPasteHTML(sanitizeHtml(initialContent)); } } } }, [initialContent, isReady]); return (
{/* Custom toolbar container */}
{/* Editor container with improved scrolling */}
{/* Loading indicator */} {!isReady && (
)}
{/* Custom styles for email context */}
); }; export default RichEmailEditor;