'use client'; import React, { useEffect, useRef, useState } from 'react'; import 'quill/dist/quill.snow.css'; import { sanitizeHtml } from '@/lib/utils/dom-purify-config'; import { detectTextDirection } from '@/lib/utils/text-direction'; import { processHtmlContent } from '@/lib/utils/email-content'; interface RichEmailEditorProps { initialContent: string; onChange: (content: string) => void; placeholder?: string; minHeight?: string; maxHeight?: string; preserveFormatting?: boolean; } /** * Clean up problematic table structures that cause issues with quill-better-table */ function cleanupTableStructures(htmlContent: string, isReplyOrForward: boolean = false): string { if (!htmlContent) return htmlContent; try { const tempDiv = document.createElement('div'); tempDiv.innerHTML = htmlContent; // Find tables with problematic nested structures const tables = tempDiv.querySelectorAll('table'); // Check if content looks like a forwarded email or reply content const isForwardedEmail = htmlContent.includes('---------- Forwarded message ----------') || htmlContent.includes('Forwarded message') || htmlContent.includes('forwarded message') || (htmlContent.includes('From:') && htmlContent.includes('Date:') && htmlContent.includes('Subject:')); const isReplyEmail = htmlContent.includes('wrote:') || htmlContent.includes('
0) { console.log(`Found ${tables.length} tables in ${shouldConvertAllTables ? 'reply/forward' : 'regular'} content`); let convertedCount = 0; tables.forEach(table => { // In reply/forward mode, convert ALL tables to divs to avoid quill-better-table issues if (shouldConvertAllTables) { const replacementDiv = document.createElement('div'); replacementDiv.className = 'converted-table'; replacementDiv.style.border = '1px solid #ddd'; replacementDiv.style.margin = '10px 0'; replacementDiv.style.padding = '10px'; // Preserve the original table structure visually // Create a simplified HTML representation of the table let tableHtml = ''; // Process each row const rows = table.querySelectorAll('tr'); rows.forEach(row => { const rowDiv = document.createElement('div'); rowDiv.style.display = 'flex'; rowDiv.style.flexWrap = 'wrap'; rowDiv.style.marginBottom = '5px'; // Process each cell in the row const cells = row.querySelectorAll('td, th'); cells.forEach(cell => { const cellDiv = document.createElement('div'); cellDiv.style.flex = '1'; cellDiv.style.padding = '5px'; cellDiv.style.borderBottom = '1px solid #eee'; cellDiv.innerHTML = cell.innerHTML; rowDiv.appendChild(cellDiv); }); replacementDiv.appendChild(rowDiv); }); // If no rows were processed, just use the table's inner HTML if (rows.length === 0) { replacementDiv.innerHTML = table.innerHTML; } // Replace the table with the div if (table.parentNode) { table.parentNode.replaceChild(replacementDiv, table); convertedCount++; } } // For regular content, just add width attributes to make quill-better-table happy else { // Skip simple tables that are likely to work fine with Quill // Check more conditions to identify simple tables const isSimpleTable = table.rows.length <= 3 && table.querySelectorAll('td, th').length <= 6 && !table.querySelector('table') && // No nested tables !table.innerHTML.includes('rowspan') && // No rowspan !table.innerHTML.includes('colspan'); // No colspan if (isSimpleTable) { console.log('Preserving simple table structure'); // Add width attribute table.setAttribute('width', '100%'); // Add width to cells const cells = table.querySelectorAll('td, th'); cells.forEach(cell => { if (cell instanceof HTMLTableCellElement && !cell.hasAttribute('width')) { cell.setAttribute('width', '100'); } }); } else { // Convert complex tables to divs const replacementDiv = document.createElement('div'); replacementDiv.className = 'converted-table'; replacementDiv.style.border = '1px solid #ddd'; replacementDiv.style.margin = '10px 0'; replacementDiv.style.padding = '10px'; // Copy the table's innerHTML replacementDiv.innerHTML = table.innerHTML; // Replace the table with the div if (table.parentNode) { table.parentNode.replaceChild(replacementDiv, table); convertedCount++; } } } }); console.log(`Converted ${convertedCount} tables to divs to prevent Quill errors`); return tempDiv.innerHTML; } return htmlContent; } catch (error) { console.error('Error cleaning up table structures:', error); return htmlContent; } } 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); const [isReplyOrForward, setIsReplyOrForward] = useState(false); // Initialize Quill editor when component mounts useEffect(() => { // Import Quill dynamically (client-side only) const initializeQuill = async () => { if (!editorRef.current || !toolbarRef.current) return; // First, detect if content is reply/forward to determine editor mode const contentIsReplyOrForward = initialContent ? ( initialContent.includes('wrote:') || initialContent.includes(' { if (el instanceof HTMLElement) { el.scrollTop = 0; } }); } } catch (err) { console.error('Error setting initial content:', err); // Enhanced fallback mechanism for complex content try { // First try to extract text from HTML const tempDiv = document.createElement('div'); tempDiv.innerHTML = initialContent; const textContent = tempDiv.textContent || tempDiv.innerText || ''; if (textContent.trim()) { console.log('Using extracted text fallback, length:', textContent.length); quillRef.current.setText(textContent); } else { // If text extraction fails or returns empty, provide a message console.log('Using empty content fallback'); quillRef.current.setText('Unable to load original content'); } } catch (e) { console.error('All fallbacks failed:', e); quillRef.current.setText('Error loading content'); } } } // 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 { console.log('Updating content in editor:', { contentLength: initialContent.length, startsWithHtml: initialContent.trim().startsWith('<'), containsForwardedMessage: initialContent.includes('---------- Forwarded message ----------'), containsQuoteHeader: initialContent.includes('wrote:'), hasBlockquote: initialContent.includes('{ if (quillRef.current) { try { // 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 (innerError) { console.error('Error applying delayed formatting:', innerError); } } }, 100); } } else { // For regular content, use normal processing const cleanedContent = cleanupTableStructures(sanitizedContent, false); if (quillRef.current && quillRef.current.root) { quillRef.current.root.innerHTML = cleanedContent; // Safely apply formatting try { 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 (formatError) { console.error('Error applying formatting:', formatError); } } } } } 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 || ''; if (quillRef.current) { quillRef.current.setText(textContent || 'Error loading content'); } } catch (e) { console.error('All fallbacks failed:', e); // Last resort if (quillRef.current) { quillRef.current.setText('Error loading content'); } } } } } }, [initialContent, isReady, isReplyOrForward]); return ({/* Custom toolbar container */}); }; export default RichEmailEditor;{/* Editor container with improved scrolling */}{/* Loading indicator */} {!isReady && ({/* Custom styles for email context */})}