courrier preview

This commit is contained in:
alma 2025-05-01 17:52:18 +02:00
parent caa6a62e67
commit fbfababb22

View File

@ -24,10 +24,16 @@ interface RichEmailEditorProps {
// Register better table module // Register better table module
function registerTableModule() { function registerTableModule() {
try { try {
Quill.register({ // Only attempt to register if the module exists
'modules/better-table': QuillBetterTable if (typeof QuillBetterTable !== 'undefined') {
}, true); Quill.register({
return true; 'modules/better-table': QuillBetterTable
}, true);
return true;
} else {
console.warn('QuillBetterTable module is not available, skipping registration');
return false;
}
} catch (error) { } catch (error) {
console.error('Error registering table module:', error); console.error('Error registering table module:', error);
return false; return false;
@ -114,17 +120,17 @@ function cleanupTableStructures(htmlContent: string, isReplyOrForward: boolean =
} }
// For regular content, just add width attributes to make quill-better-table happy // For regular content, just add width attributes to make quill-better-table happy
else { else {
// Skip simple tables that are likely to work fine with Quill // Skip simple tables that are likely to work fine with Quill
// Check more conditions to identify simple tables // Check more conditions to identify simple tables
const isSimpleTable = const isSimpleTable =
table.rows.length <= 3 && table.rows.length <= 3 &&
table.querySelectorAll('td, th').length <= 6 && table.querySelectorAll('td, th').length <= 6 &&
!table.querySelector('table') && // No nested tables !table.querySelector('table') && // No nested tables
!table.innerHTML.includes('rowspan') && // No rowspan !table.innerHTML.includes('rowspan') && // No rowspan
!table.innerHTML.includes('colspan'); // No colspan !table.innerHTML.includes('colspan'); // No colspan
if (isSimpleTable) { if (isSimpleTable) {
console.log('Preserving simple table structure'); console.log('Preserving simple table structure');
// Add width attribute // Add width attribute
table.setAttribute('width', '100%'); table.setAttribute('width', '100%');
@ -136,20 +142,20 @@ function cleanupTableStructures(htmlContent: string, isReplyOrForward: boolean =
} }
}); });
} else { } else {
// Convert complex tables to divs // Convert complex tables to divs
const replacementDiv = document.createElement('div'); const replacementDiv = document.createElement('div');
replacementDiv.className = 'converted-table'; replacementDiv.className = 'converted-table';
replacementDiv.style.border = '1px solid #ddd'; replacementDiv.style.border = '1px solid #ddd';
replacementDiv.style.margin = '10px 0'; replacementDiv.style.margin = '10px 0';
replacementDiv.style.padding = '10px'; replacementDiv.style.padding = '10px';
// Copy the table's innerHTML // Copy the table's innerHTML
replacementDiv.innerHTML = table.innerHTML; replacementDiv.innerHTML = table.innerHTML;
// Replace the table with the div // Replace the table with the div
if (table.parentNode) { if (table.parentNode) {
table.parentNode.replaceChild(replacementDiv, table); table.parentNode.replaceChild(replacementDiv, table);
convertedCount++; convertedCount++;
} }
} }
} }
@ -166,51 +172,6 @@ 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 // Define toolbar options for consistency
const emailToolbarOptions = [ const emailToolbarOptions = [
['bold', 'italic', 'underline', 'strike'], ['bold', 'italic', 'underline', 'strike'],
@ -238,9 +199,47 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
const editorRef = useRef<HTMLDivElement>(null); const editorRef = useRef<HTMLDivElement>(null);
const toolbarRef = useRef<HTMLDivElement>(null); const toolbarRef = useRef<HTMLDivElement>(null);
const quillRef = useRef<any>(null); const quillRef = useRef<any>(null);
const quillInitTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const [isReady, setIsReady] = useState(false); const [isReady, setIsReady] = useState(false);
const [isReplyOrForward, setIsReplyOrForward] = useState(false); const [isReplyOrForward, setIsReplyOrForward] = useState(false);
const quillInitTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Helper function to clean up existing editor
const cleanupEditor = () => {
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
if (editorRef.current) {
// Clear the content
editorRef.current.innerHTML = '';
}
} catch (err) {
console.warn('Error removing Quill event listeners:', err);
}
// Set to null to ensure garbage collection
quillRef.current = null;
}
return true;
} catch (error) {
console.error('Error cleaning up editor:', error);
return false;
}
};
// Initialize editor effect // Initialize editor effect
useEffect(() => { useEffect(() => {
@ -251,7 +250,7 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
const initEditor = async () => { const initEditor = async () => {
try { try {
// First cleanup any existing editor instances to prevent memory leaks // First cleanup any existing editor instances to prevent memory leaks
cleanupExistingEditor(); cleanupEditor();
if (!editorRef.current) { if (!editorRef.current) {
console.error('Editor reference is not available'); console.error('Editor reference is not available');
@ -268,29 +267,24 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
registerTableModule(); registerTableModule();
console.log('Better Table module registered successfully'); console.log('Better Table module registered successfully');
// Create a modules configuration that works
const modules = {
toolbar: emailToolbarOptions,
keyboard: {
bindings: customKeyBindings,
}
};
// Set up Quill with configurations // Set up Quill with configurations
const quill = new Quill(editorRef.current, { const quill = new Quill(editorRef.current, {
modules: { modules,
toolbar: emailToolbarOptions,
betterTable: {
operationMenu: {
items: {
unmergeCells: {
text: 'Unmerge cells',
},
},
},
},
keyboard: {
bindings: customKeyBindings,
},
},
theme: 'snow', theme: 'snow',
placeholder: placeholder || 'Write your message here...', placeholder: placeholder || 'Write your message here...',
formats: allowedFormats, formats: allowedFormats,
}); });
// Store the instance for cleanup // Store the instance for cleanup
quillRef.current = quill;
editorInstance = quill; editorInstance = quill;
// Process and set initial content if available // Process and set initial content if available
@ -304,12 +298,9 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
// Use dangerouslyPasteHTML which accepts string content directly // Use dangerouslyPasteHTML which accepts string content directly
quill.clipboard.dangerouslyPasteHTML(processedContent); quill.clipboard.dangerouslyPasteHTML(processedContent);
// Emit initial content // Emit initial content with the string HTML
if (onChange) { if (onChange) {
onChange({ onChange(quill.root.innerHTML);
html: quill.root.innerHTML,
text: quill.getText()
});
} }
initialContentSet = true; initialContentSet = true;
@ -319,9 +310,19 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
// Fallback to direct HTML setting if conversion fails // Fallback to direct HTML setting if conversion fails
try { try {
quill.root.innerHTML = handleBlockedContent(initialContent); quill.root.innerHTML = handleBlockedContent(initialContent);
// Still emit the change even with the fallback approach
if (onChange) {
onChange(quill.root.innerHTML);
}
} catch (innerErr) { } catch (innerErr) {
console.error('Fallback content setting failed:', innerErr); console.error('Fallback content setting failed:', innerErr);
quill.root.innerHTML = '<p>Error loading content. Please start typing or paste content manually.</p>'; quill.root.innerHTML = '<p>Error loading content. Please start typing or paste content manually.</p>';
// Emit the fallback content
if (onChange) {
onChange(quill.root.innerHTML);
}
} }
} }
} }
@ -341,6 +342,9 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
quill.setSelection(0, 0); quill.setSelection(0, 0);
}, 100); }, 100);
} }
// Mark editor as ready
setIsReady(true);
} catch (error) { } catch (error) {
console.error('Error initializing editor:', error); console.error('Error initializing editor:', error);
} }
@ -352,7 +356,7 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
console.warn('Editor initialization timed out after 3 seconds'); console.warn('Editor initialization timed out after 3 seconds');
// Force cleanup and try one more time with minimal settings // Force cleanup and try one more time with minimal settings
cleanupExistingEditor(); cleanupEditor();
try { try {
// Create a simple editor without complex modules // Create a simple editor without complex modules
@ -370,13 +374,18 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
onChange(html); onChange(html);
}); });
quillRef.current = fallbackQuill;
editorInstance = fallbackQuill; editorInstance = fallbackQuill;
setIsReady(true);
} catch (fallbackError) { } catch (fallbackError) {
console.error('Fallback editor initialization also failed:', fallbackError); console.error('Fallback editor initialization also failed:', fallbackError);
} }
} }
}, 3000); }, 3000);
// Store timeout reference to allow cleanup
quillInitTimeoutRef.current = initializationTimeout;
// Initialize the editor // Initialize the editor
initEditor(); initEditor();
@ -386,6 +395,11 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
clearTimeout(initializationTimeout); clearTimeout(initializationTimeout);
} }
if (quillInitTimeoutRef.current) {
clearTimeout(quillInitTimeoutRef.current);
quillInitTimeoutRef.current = null;
}
if (editorInstance) { if (editorInstance) {
try { try {
// Remove event listeners // Remove event listeners
@ -401,7 +415,7 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
} }
// Final cleanup of any Quill instances // Final cleanup of any Quill instances
cleanupExistingEditor(); cleanupEditor();
}; };
}, [initialContent, onChange, placeholder, autofocus, mode, allowedFormats, customKeyBindings]); }, [initialContent, onChange, placeholder, autofocus, mode, allowedFormats, customKeyBindings]);
@ -562,41 +576,41 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
// Pre-process to handle blocked content // Pre-process to handle blocked content
const preProcessedContent = handleBlockedContent(initialContent); const preProcessedContent = handleBlockedContent(initialContent);
// Process HTML content using centralized utility // Process HTML content using centralized utility
const processed = processHtmlContent(preProcessedContent, { const processed = processHtmlContent(preProcessedContent, {
sanitize: true, sanitize: true,
preserveReplyFormat: contentIsReplyOrForward preserveReplyFormat: contentIsReplyOrForward
}); });
const sanitizedContent = processed.sanitizedContent; const sanitizedContent = processed.sanitizedContent;
const direction = processed.direction; // Use direction from processed result const direction = processed.direction; // Use direction from processed result
// Log sanitized content details for debugging // Log sanitized content details for debugging
console.log('Sanitized content details:', { console.log('Sanitized content details:', {
length: sanitizedContent.length, length: sanitizedContent.length,
isEmpty: sanitizedContent.trim().length === 0, isEmpty: sanitizedContent.trim().length === 0,
startsWithDiv: sanitizedContent.trim().startsWith('<div'), startsWithDiv: sanitizedContent.trim().startsWith('<div'),
containsForwardedMessage: sanitizedContent.includes('---------- Forwarded message ----------'), containsForwardedMessage: sanitizedContent.includes('---------- Forwarded message ----------'),
containsQuoteHeader: sanitizedContent.includes('wrote:'), containsQuoteHeader: sanitizedContent.includes('wrote:'),
hasTable: sanitizedContent.includes('<table'), hasTable: sanitizedContent.includes('<table'),
hasBlockquote: sanitizedContent.includes('<blockquote'), hasBlockquote: sanitizedContent.includes('<blockquote'),
isReplyOrForward: contentIsReplyOrForward, isReplyOrForward: contentIsReplyOrForward,
firstNChars: sanitizedContent.substring(0, 100).replace(/\n/g, '\\n') firstNChars: sanitizedContent.substring(0, 100).replace(/\n/g, '\\n')
}); });
// Check if content is valid HTML // Check if content is valid HTML
if (sanitizedContent.trim().length === 0) { if (sanitizedContent.trim().length === 0) {
console.warn('Sanitized content is empty, using original content'); console.warn('Sanitized content is empty, using original content');
// If sanitized content is empty, try to extract text from original // If sanitized content is empty, try to extract text from original
const tempDiv = document.createElement('div'); const tempDiv = document.createElement('div');
tempDiv.innerHTML = preProcessedContent; tempDiv.innerHTML = preProcessedContent;
const textContent = tempDiv.textContent || tempDiv.innerText || ''; const textContent = tempDiv.textContent || tempDiv.innerText || '';
// Create simple HTML with text content // Create simple HTML with text content
if (quillRef.current) { if (quillRef.current) {
quillRef.current.setText(textContent || 'No content available'); quillRef.current.setText(textContent || 'No content available');
} }
} else { } else {
// Process content based on type // Process content based on type
if (contentIsReplyOrForward) { if (contentIsReplyOrForward) {
console.log('Using special handling for reply/forward content update'); console.log('Using special handling for reply/forward content update');
@ -605,8 +619,8 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
const cleanedContent = cleanupTableStructures(sanitizedContent, true); const cleanedContent = cleanupTableStructures(sanitizedContent, true);
// Set content without table handling // Set content without table handling
if (quillRef.current && quillRef.current.root) { if (quillRef.current && quillRef.current.root) {
quillRef.current.root.innerHTML = cleanedContent; quillRef.current.root.innerHTML = cleanedContent;
// Delay applying formatting to ensure Quill is fully ready // Delay applying formatting to ensure Quill is fully ready
setTimeout(() => { setTimeout(() => {
@ -648,12 +662,12 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
// Set selection to beginning // Set selection to beginning
quillRef.current.setSelection(0, 0); quillRef.current.setSelection(0, 0);
} catch (formatError) { } catch (formatError) {
console.error('Error applying formatting:', formatError); console.error('Error applying formatting:', formatError);
} }
} }
}
} }
}
} catch (err) { } catch (err) {
console.error('Error updating content:', err); console.error('Error updating content:', err);
// Safer fallback that avoids clipboard API // Safer fallback that avoids clipboard API