diff --git a/components/email/ComposeEmail.tsx b/components/email/ComposeEmail.tsx index 28004908..109de02b 100644 --- a/components/email/ComposeEmail.tsx +++ b/components/email/ComposeEmail.tsx @@ -143,10 +143,54 @@ export default function ComposeEmail(props: ComposeEmailProps) { console.warn('Email content initialization timed out after 5 seconds, using fallback template'); // Create a basic fallback template const { fromStr, dateStr } = getFormattedInfoForEmail(initialEmail); - const fallbackContent = type === 'forward' - ? `
| From: | +${fromStr} | +
| Date: | +${dateStr} | +
| Subject: | +${initialEmail.subject || ''} | +
+ [Could not load original content. Please use plain text or start a new message.] ++ `; + } + setEmailContent(fallbackContent); + + // Also update the Quill editor directly if possible + const editorElement = document.querySelector('.ql-editor'); + if (editorElement instanceof HTMLElement) { + editorElement.innerHTML = fallbackContent; + } } }, 5000); diff --git a/components/email/RichEmailEditor.tsx b/components/email/RichEmailEditor.tsx index c08e7703..bc9f0a22 100644 --- a/components/email/RichEmailEditor.tsx +++ b/components/email/RichEmailEditor.tsx @@ -5,6 +5,8 @@ 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'; +import Quill from 'quill'; +import QuillBetterTable from 'quill-better-table'; interface RichEmailEditorProps { initialContent: string; @@ -13,6 +15,23 @@ interface RichEmailEditorProps { minHeight?: string; maxHeight?: string; preserveFormatting?: boolean; + autofocus?: boolean; + mode?: string; + allowedFormats?: any; + customKeyBindings?: any; +} + +// Register better table module +function registerTableModule() { + try { + Quill.register({ + 'modules/better-table': QuillBetterTable + }, true); + return true; + } catch (error) { + console.error('Error registering table module:', error); + return false; + } } /** @@ -147,6 +166,63 @@ function cleanupTableStructures(htmlContent: string, isReplyOrForward: boolean = } } +// Clean up existing editor before creating a new one +const cleanupExistingEditor = () => { + try { + // Clear any existing timeouts + if (quillInitTimeoutRef.current) { + clearTimeout(quillInitTimeoutRef.current); + quillInitTimeoutRef.current = null; + } + + // Remove existing Quill instance if it exists + if (quillRef.current) { + console.log('Cleaning up existing Quill editor'); + + // Remove event listeners + try { + quillRef.current.off('text-change'); + quillRef.current.off('selection-change'); + + // Get the container of the editor + const editorContainer = document.querySelector('#quill-editor'); + if (editorContainer instanceof HTMLElement) { + // Clear the content + editorContainer.innerHTML = ''; + } + } catch (err) { + console.warn('Error removing Quill event listeners:', err); + } + + // Set to null to ensure garbage collection + quillRef.current = null; + } + + // Also ensure the editor element is empty + const editorElement = document.querySelector('#quill-editor'); + if (editorElement) { + editorElement.innerHTML = ''; + } + + return true; + } catch (error) { + console.error('Error cleaning up editor:', error); + return false; + } +}; + +// Define toolbar options for consistency +const emailToolbarOptions = [ + ['bold', 'italic', 'underline', 'strike'], + [{ 'color': [] }, { 'background': [] }], + [{ 'list': 'ordered'}, { 'list': 'bullet' }], + [{ 'indent': '-1'}, { 'indent': '+1' }], + [{ 'align': [] }], + [{ 'direction': 'rtl' }], + ['link'], + ['clean'], +]; + const RichEmailEditor: React.FC
{ try { - // Create new Quill instance with the DOM element and custom toolbar - const editorElement = editorRef.current; - quillRef.current = new Quill(editorElement, { + // First cleanup any existing editor instances to prevent memory leaks + cleanupExistingEditor(); + + if (!editorRef.current) { + console.error('Editor reference is not available'); + return; + } + + // Log the initialization + console.log('Initializing editor in', mode, 'mode'); + + // Clear any existing content + editorRef.current.innerHTML = ''; + + // Register better table module + registerTableModule(); + console.log('Better Table module registered successfully'); + + // Set up Quill with configurations + const quill = new Quill(editorRef.current, { modules: { - toolbar: { - container: toolbarRef.current, - handlers: { - // Add any custom toolbar handlers here - } + toolbar: emailToolbarOptions, + betterTable: { + operationMenu: { + items: { + unmergeCells: { + text: 'Unmerge cells', + }, + }, + }, }, - clipboard: { - matchVisual: false // Disable clipboard matching for better HTML handling + keyboard: { + bindings: customKeyBindings, }, - // Only enable better-table for regular content, not for replies/forwards - 'better-table': tableModule && !contentIsReplyOrForward ? true : false, }, - placeholder: placeholder, theme: 'snow', + placeholder: placeholder || 'Write your message here...', + formats: allowedFormats, }); - - // Set initial content properly + + // Store the instance for cleanup + editorInstance = quill; + + // Process and set initial content if available if (initialContent) { try { - console.log('Setting initial content in editor', { - length: initialContent.length, - startsWithHtml: initialContent.trim().startsWith('<'), - containsForwardedMessage: initialContent.includes('---------- Forwarded message ----------'), - containsReplyIndicator: initialContent.includes('wrote:'), - hasBlockquote: initialContent.includes('{ - if (el instanceof HTMLElement) { - el.scrollTop = 0; - } + // Emit initial content + if (onChange) { + onChange({ + html: quill.root.innerHTML, + text: quill.getText() }); } + + initialContentSet = true; } catch (err) { console.error('Error setting initial content:', err); - // Enhanced fallback mechanism for complex content + // Fallback to direct HTML setting if conversion fails 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'); + quill.root.innerHTML = handleBlockedContent(initialContent); + } catch (innerErr) { + console.error('Fallback content setting failed:', innerErr); + quill.root.innerHTML = 'Error loading content. Please start typing or paste content manually.
'; } } } - - // Add change listener - quillRef.current.on('text-change', () => { - const html = quillRef.current.root.innerHTML; - onChange(html); + + // Handle content change events + quill.on('text-change', (delta: any, oldDelta: any, source: string) => { + if (source === 'user') { + const html = quill.root.innerHTML; + onChange(html); + } }); - - // Improve editor layout - const editorContainer = editorElement.closest('.ql-container'); - if (editorContainer) { - editorContainer.classList.add('email-editor-container'); - } - - setIsReady(true); - } catch (initError) { - console.error('Critical error initializing editor:', initError); - // Provide fallback in UI - if (editorRef.current) { - editorRef.current.innerHTML = 'Error loading editor. Please try again or use plain text mode.'; + + // Set focus if autofocus is true + if (autofocus) { + setTimeout(() => { + quill.focus(); + quill.setSelection(0, 0); + }, 100); } + } catch (error) { + console.error('Error initializing editor:', error); } }; - - initializeQuill().catch(err => { - console.error('Failed to initialize Quill editor:', err); - }); - - // Clean up on unmount + + // Set a timeout to prevent endless waiting for editor initialization + initializationTimeout = setTimeout(() => { + if (!initialContentSet && editorRef.current) { + console.warn('Editor initialization timed out after 3 seconds'); + + // Force cleanup and try one more time with minimal settings + cleanupExistingEditor(); + + try { + // Create a simple editor without complex modules + const fallbackQuill = new Quill(editorRef.current, { + theme: 'snow', + placeholder: placeholder || 'Write your message here...', + }); + + // Add a simple placeholder message + fallbackQuill.root.innerHTML = initialContent || 'Editor ready. Start typing...
'; + + // Handle content change events + fallbackQuill.on('text-change', () => { + const html = fallbackQuill.root.innerHTML; + onChange(html); + }); + + editorInstance = fallbackQuill; + } catch (fallbackError) { + console.error('Fallback editor initialization also failed:', fallbackError); + } + } + }, 3000); + + // Initialize the editor + initEditor(); + + // Cleanup function return () => { - if (quillRef.current) { - // Clean up any event listeners or resources - quillRef.current.off('text-change'); - quillRef.current = null; + if (initializationTimeout) { + clearTimeout(initializationTimeout); } + + if (editorInstance) { + try { + // Remove event listeners + editorInstance.off('text-change'); + + // Properly destroy Quill instance if possible + if (editorInstance.emitter) { + editorInstance.emitter.removeAllListeners(); + } + } catch (error) { + console.error('Error during editor cleanup:', error); + } + } + + // Final cleanup of any Quill instances + cleanupExistingEditor(); }; - }, []); + }, [initialContent, onChange, placeholder, autofocus, mode, allowedFormats, customKeyBindings]); - // Add utility function to handle blocked content - /** - * Pre-process content to handle blocked images and other resources - */ + // Handle blocked content like CID images that cause loading issues function handleBlockedContent(htmlContent: string): string { - if (!htmlContent) return htmlContent; + if (!htmlContent) return ''; try { - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = htmlContent; + // Create a DOM parser to work with the HTML + const parser = new DOMParser(); + const doc = parser.parseFromString(htmlContent, 'text/html'); - // Replace CID and other problematic image sources - const images = tempDiv.querySelectorAll('img'); + // Count images processed for logging + let imageCount = 0; + let cidCount = 0; + + // Process all images to prevent loading issues + const images = doc.querySelectorAll('img'); images.forEach(img => { - const src = img.getAttribute('src') || ''; + const src = img.getAttribute('src'); + imageCount++; - // Handle CID attachments that would be blocked - if (src.startsWith('cid:')) { - console.log('Replacing CID image source:', src); - img.setAttribute('src', 'data:image/svg+xml;utf8,'); - img.setAttribute('data-original-src', src); - img.style.maxWidth = '300px'; - img.style.border = '1px dashed #ddd'; + // Handle CID images which cause loading issues + if (src && src.startsWith('cid:')) { + cidCount++; + // Replace with placeholder or remove src + img.setAttribute('src', 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"%3E%3Cpath fill="%23ccc" d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/%3E%3C/svg%3E'); + img.setAttribute('data-original-cid', src); + img.setAttribute('title', 'Image reference not available'); + img.setAttribute('alt', 'Image placeholder'); + img.style.border = '1px dashed #ccc'; + img.style.padding = '4px'; + img.style.maxWidth = '100%'; } - // Handle tracking pixels and potentially blocked remote content - if (src.includes('open?') || src.includes('tracking') || src.includes('pixel')) { - console.log('Removing tracking pixel:', src); - img.remove(); + // Handle possible blocked remote images + if (src && (src.startsWith('http://') || src.startsWith('https://'))) { + // Set alt text and a class for better UX + if (!img.getAttribute('alt')) { + img.setAttribute('alt', 'Remote image'); + } + img.classList.add('remote-image'); } }); - return tempDiv.innerHTML; + // Process table elements to ensure consistent styling + const tables = doc.querySelectorAll('table'); + tables.forEach(table => { + // Ensure tables have consistent styling to prevent layout issues + const htmlTable = table as HTMLTableElement; + htmlTable.style.borderCollapse = 'collapse'; + htmlTable.style.maxWidth = '100%'; + + // Handle cell padding and borders for better readability + const cells = htmlTable.querySelectorAll('td, th'); + cells.forEach(cell => { + const htmlCell = cell as HTMLTableCellElement; + if (!htmlCell.style.padding) { + htmlCell.style.padding = '4px 8px'; + } + // Only add border if not already styled + if (!htmlCell.style.border) { + htmlCell.style.border = '1px solid #e0e0e0'; + } + }); + }); + + // Ensure all blockquotes have consistent styling + const blockquotes = doc.querySelectorAll('blockquote'); + blockquotes.forEach(blockquote => { + const htmlBlockquote = blockquote as HTMLElement; + if (!htmlBlockquote.style.borderLeft) { + htmlBlockquote.style.borderLeft = '3px solid #ddd'; + } + if (!htmlBlockquote.style.paddingLeft) { + htmlBlockquote.style.paddingLeft = '10px'; + } + if (!htmlBlockquote.style.margin) { + htmlBlockquote.style.margin = '10px 0'; + } + if (!htmlBlockquote.style.color) { + htmlBlockquote.style.color = '#505050'; + } + }); + + // Log the number of images processed for debugging + if (imageCount > 0) { + console.log(`Processed ${imageCount} images in content (${cidCount} CID images replaced)`); + } + + // Return the cleaned HTML + return doc.body.innerHTML; } catch (error) { - console.error('Error handling blocked content:', error); + console.error('Error processing blocked content:', error); + // Return original content if processing fails return htmlContent; } }