courrier preview

This commit is contained in:
alma 2025-05-01 17:22:15 +02:00
parent 149a49d6f3
commit e9f104f40b

View File

@ -18,7 +18,7 @@ interface RichEmailEditorProps {
/** /**
* Clean up problematic table structures that cause issues with quill-better-table * Clean up problematic table structures that cause issues with quill-better-table
*/ */
function cleanupTableStructures(htmlContent: string): string { function cleanupTableStructures(htmlContent: string, isReplyOrForward: boolean = false): string {
if (!htmlContent) return htmlContent; if (!htmlContent) return htmlContent;
try { try {
@ -40,101 +40,103 @@ function cleanupTableStructures(htmlContent: string): string {
htmlContent.includes('<blockquote') || htmlContent.includes('<blockquote') ||
htmlContent.includes('gmail_quote'); htmlContent.includes('gmail_quote');
// Check if content has complex tables that might cause issues // For reply/forward content, force convert ALL tables to divs to avoid Quill errors
const hasComplexTables = tables.length > 0 && const shouldConvertAllTables = isReplyOrForward || isForwardedEmail || isReplyEmail;
(isForwardedEmail || isReplyEmail);
if (hasComplexTables) { if (tables.length > 0) {
console.log(`Found ${tables.length} tables in complex email content`); console.log(`Found ${tables.length} tables in ${shouldConvertAllTables ? 'reply/forward' : 'regular'} content`);
let convertedCount = 0; let convertedCount = 0;
tables.forEach(table => { tables.forEach(table => {
// Special handling for tables inside blockquotes (quoted content) // In reply/forward mode, convert ALL tables to divs to avoid quill-better-table issues
if (table.closest('blockquote') || if (shouldConvertAllTables) {
(isReplyEmail && table.innerHTML.includes('wrote:'))) { const replacementDiv = document.createElement('div');
console.log('Preserving table inside quoted content'); replacementDiv.className = 'converted-table';
replacementDiv.style.border = '1px solid #ddd';
replacementDiv.style.margin = '10px 0';
replacementDiv.style.padding = '10px';
// Add width attribute to all cells to prevent width calculation issues // Preserve the original table structure visually
const cells = table.querySelectorAll('td, th'); // Create a simplified HTML representation of the table
cells.forEach(cell => { let tableHtml = '';
if (cell instanceof HTMLTableCellElement) {
if (!cell.hasAttribute('width')) { // 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'); cell.setAttribute('width', '100');
} }
cell.style.padding = '4px'; });
cell.style.textAlign = 'left'; } else {
cell.style.verticalAlign = 'top'; // 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++;
} }
}); }
// Apply minimal styling to ensure it renders correctly
table.setAttribute('style', 'border-collapse: collapse; width: 100%; max-width: 100%; margin: 8px 0;');
// Add explicit width attribute to the table
table.setAttribute('width', '100%');
return;
}
// Preserve the main forwarded email header table
if (isForwardedEmail &&
table.innerHTML.includes('From:') &&
table.innerHTML.includes('Date:') &&
table.innerHTML.includes('Subject:')) {
console.log('Preserving forwarded email header table');
// Ensure the table has proper styling
table.setAttribute('style', 'margin: 10px 0; border-collapse: collapse; font-size: 13px; color: #333;');
// Add explicit 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')) {
// First column (labels) narrower
if (cell.textContent?.includes(':')) {
cell.setAttribute('width', '80');
} else {
cell.setAttribute('width', '400');
}
}
});
return;
}
// 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('style=') && // No complex styling
!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%');
return;
}
// 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} complex tables to divs to prevent Quill errors`); console.log(`Converted ${convertedCount} tables to divs to prevent Quill errors`);
return tempDiv.innerHTML; return tempDiv.innerHTML;
} }
@ -157,6 +159,7 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
const toolbarRef = useRef<HTMLDivElement>(null); const toolbarRef = useRef<HTMLDivElement>(null);
const quillRef = useRef<any>(null); const quillRef = useRef<any>(null);
const [isReady, setIsReady] = useState(false); const [isReady, setIsReady] = useState(false);
const [isReplyOrForward, setIsReplyOrForward] = useState(false);
// Initialize Quill editor when component mounts // Initialize Quill editor when component mounts
useEffect(() => { useEffect(() => {
@ -164,23 +167,41 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
const initializeQuill = async () => { const initializeQuill = async () => {
if (!editorRef.current || !toolbarRef.current) return; 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('<blockquote') ||
initialContent.includes('Forwarded message') ||
initialContent.includes('---------- Forwarded message ----------')
) : false;
// Store this information for future reference
setIsReplyOrForward(contentIsReplyOrForward);
console.log('Initializing editor in', contentIsReplyOrForward ? 'reply/forward' : 'compose', 'mode');
const Quill = (await import('quill')).default; const Quill = (await import('quill')).default;
// Import quill-better-table // Import quill-better-table conditionally based on content type
let tableModule = null; let tableModule = null;
try { if (!contentIsReplyOrForward) {
const QuillBetterTable = await import('quill-better-table'); // Only try to load table module for regular content, not for replies/forwards
try {
const QuillBetterTable = await import('quill-better-table');
// Register the table module if available // Register the table module if available and not in reply/forward mode
if (QuillBetterTable && QuillBetterTable.default) { if (QuillBetterTable && QuillBetterTable.default) {
Quill.register({ Quill.register({
'modules/better-table': QuillBetterTable.default 'modules/better-table': QuillBetterTable.default
}, true); }, true);
tableModule = QuillBetterTable.default; tableModule = QuillBetterTable.default;
console.log('Better Table module registered successfully'); console.log('Better Table module registered successfully');
}
} catch (err) {
console.warn('Table module not available:', err);
} }
} catch (err) { } else {
console.warn('Table module not available:', err); console.log('Skipping better-table module for reply/forward content');
} }
// Define custom formats/modules with table support // Define custom formats/modules with table support
@ -208,8 +229,8 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
clipboard: { clipboard: {
matchVisual: false // Disable clipboard matching for better HTML handling matchVisual: false // Disable clipboard matching for better HTML handling
}, },
// Don't initialize better-table yet - we'll do it after content is loaded // Only enable better-table for regular content, not for replies/forwards
'better-table': false, 'better-table': tableModule && !contentIsReplyOrForward ? true : false,
}, },
placeholder: placeholder, placeholder: placeholder,
theme: 'snow', theme: 'snow',
@ -226,17 +247,10 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
hasBlockquote: initialContent.includes('<blockquote') hasBlockquote: initialContent.includes('<blockquote')
}); });
// Check if content is reply or forward to use special handling
const isReplyOrForward =
initialContent.includes('wrote:') ||
initialContent.includes('<blockquote') ||
initialContent.includes('Forwarded message') ||
initialContent.includes('---------- Forwarded message ----------');
// Process HTML content using centralized utility with special settings for replies/forwards // Process HTML content using centralized utility with special settings for replies/forwards
const processed = processHtmlContent(initialContent, { const processed = processHtmlContent(initialContent, {
sanitize: true, sanitize: true,
preserveReplyFormat: isReplyOrForward 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
@ -250,7 +264,7 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
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: isReplyOrForward, isReplyOrForward: contentIsReplyOrForward,
firstNChars: sanitizedContent.substring(0, 100).replace(/\n/g, '\\n') firstNChars: sanitizedContent.substring(0, 100).replace(/\n/g, '\\n')
}); });
@ -271,17 +285,20 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
} }
} else { } else {
// Special handling for reply or forwarded content // Special handling for reply or forwarded content
if (isReplyOrForward) { if (contentIsReplyOrForward) {
console.log('Using special handling for reply/forward content'); console.log('Using special handling for reply/forward content');
// Clean up any problematic table structures, with special care for quoted content // For reply/forward content, convert ALL tables to divs
const cleanedContent = cleanupTableStructures(sanitizedContent); const cleanedContent = cleanupTableStructures(sanitizedContent, true);
// Use direct innerHTML setting with minimal processing for reply/forward content // Use direct innerHTML setting with minimal processing for reply/forward content
quillRef.current.root.innerHTML = cleanedContent; quillRef.current.root.innerHTML = cleanedContent;
} else { } else {
// For regular content, use normal processing
const cleanedContent = cleanupTableStructures(sanitizedContent, false);
// Use direct innerHTML setting for regular content // Use direct innerHTML setting for regular content
quillRef.current.root.innerHTML = sanitizedContent; quillRef.current.root.innerHTML = cleanedContent;
} }
// Set the direction for the content // Set the direction for the content
@ -353,43 +370,6 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
editorContainer.classList.add('email-editor-container'); editorContainer.classList.add('email-editor-container');
} }
// Safe initialization of better-table module if available
if (tableModule) {
try {
// Wait a small delay to ensure content is properly set before initializing table module
setTimeout(() => {
if (quillRef.current) {
// First check if content has tables and whether it's a reply/forward
const hasReplyForwardContent =
quillRef.current.root.innerHTML.includes('wrote:') ||
quillRef.current.root.innerHTML.includes('blockquote') ||
quillRef.current.root.innerHTML.includes('Forwarded message');
const hasTables = quillRef.current.root.innerHTML.includes('<table');
console.log('Checking if better-table should be enabled:', {
hasReplyForwardContent,
hasTables
});
// Only initialize table module for regular content, not for replies/forwards
if (!hasReplyForwardContent && hasTables) {
console.log('Initializing better-table module for regular content with tables');
quillRef.current.getModule('better-table').setTableSelection(true);
} else if (hasReplyForwardContent && hasTables) {
// For reply/forward content with tables, we'll use a more limited approach
console.log('Tables in reply/forward content detected - using limited table handling');
// Don't initialize selection tools to avoid errors
} else {
console.log('No tables detected or simple content - proceeding without table module');
}
}
}, 300);
} catch (err) {
console.warn('Error initializing better-table module:', err);
}
}
setIsReady(true); setIsReady(true);
}; };
@ -425,16 +405,31 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
}); });
// Check if content is reply or forward to use special handling // Check if content is reply or forward to use special handling
const isReplyOrForward = const contentIsReplyOrForward =
initialContent.includes('wrote:') || initialContent.includes('wrote:') ||
initialContent.includes('<blockquote') || initialContent.includes('<blockquote') ||
initialContent.includes('Forwarded message') || initialContent.includes('Forwarded message') ||
initialContent.includes('---------- Forwarded message ----------'); initialContent.includes('---------- Forwarded message ----------');
// If content type changed (from reply to regular or vice versa), we need to reload
if (contentIsReplyOrForward !== isReplyOrForward) {
console.log('Content type changed from', isReplyOrForward ? 'reply/forward' : 'regular',
'to', contentIsReplyOrForward ? 'reply/forward' : 'regular',
'- reloading editor');
setIsReplyOrForward(contentIsReplyOrForward);
// Force a complete re-initialization of the editor by unmounting
if (quillRef.current) {
quillRef.current.off('text-change');
quillRef.current = null;
}
setIsReady(false);
return;
}
// Process HTML content using centralized utility // Process HTML content using centralized utility
const processed = processHtmlContent(initialContent, { const processed = processHtmlContent(initialContent, {
sanitize: true, sanitize: true,
preserveReplyFormat: isReplyOrForward 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
@ -448,7 +443,7 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
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: isReplyOrForward, isReplyOrForward: contentIsReplyOrForward,
firstNChars: sanitizedContent.substring(0, 100).replace(/\n/g, '\\n') firstNChars: sanitizedContent.substring(0, 100).replace(/\n/g, '\\n')
}); });
@ -465,23 +460,16 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
quillRef.current.setText(textContent || 'No content available'); quillRef.current.setText(textContent || 'No content available');
} }
} else { } else {
// Special handling for reply or forward content // Process content based on type
let contentToSet = sanitizedContent; if (contentIsReplyOrForward) {
if (isReplyOrForward) {
console.log('Using special handling for reply/forward content update'); console.log('Using special handling for reply/forward content update');
// Clean up tables with special care for quoted content
contentToSet = cleanupTableStructures(sanitizedContent);
// Set content directly to the root element // For reply/forward content, convert ALL tables to divs
const cleanedContent = cleanupTableStructures(sanitizedContent, true);
// Set content without table handling
if (quillRef.current && quillRef.current.root) { if (quillRef.current && quillRef.current.root) {
// Temporarily disable the better-table module if it's initialized quillRef.current.root.innerHTML = cleanedContent;
if (quillRef.current.getModule('better-table')) {
quillRef.current.getModule('better-table').hideTableTools();
}
// Set content without table handling by the module
quillRef.current.root.innerHTML = contentToSet;
// Delay applying formatting to ensure Quill is fully ready // Delay applying formatting to ensure Quill is fully ready
setTimeout(() => { setTimeout(() => {
@ -505,9 +493,11 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
}, 100); }, 100);
} }
} else { } else {
// For regular content, use Quill's normal process // For regular content, use normal processing
const cleanedContent = cleanupTableStructures(sanitizedContent, false);
if (quillRef.current && quillRef.current.root) { if (quillRef.current && quillRef.current.root) {
quillRef.current.root.innerHTML = contentToSet; quillRef.current.root.innerHTML = cleanedContent;
// Safely apply formatting // Safely apply formatting
try { try {
@ -549,7 +539,7 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
} }
} }
} }
}, [initialContent, isReady]); }, [initialContent, isReady, isReplyOrForward]);
return ( return (
<div className="rich-email-editor-wrapper"> <div className="rich-email-editor-wrapper">