courrier preview
This commit is contained in:
parent
caa6a62e67
commit
fbfababb22
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user