courrier preview
This commit is contained in:
parent
e8989f2016
commit
caa6a62e67
@ -143,10 +143,54 @@ export default function ComposeEmail(props: ComposeEmailProps) {
|
|||||||
console.warn('Email content initialization timed out after 5 seconds, using fallback template');
|
console.warn('Email content initialization timed out after 5 seconds, using fallback template');
|
||||||
// Create a basic fallback template
|
// Create a basic fallback template
|
||||||
const { fromStr, dateStr } = getFormattedInfoForEmail(initialEmail);
|
const { fromStr, dateStr } = getFormattedInfoForEmail(initialEmail);
|
||||||
const fallbackContent = type === 'forward'
|
|
||||||
? `<div style="margin: 20px 0 10px 0; color: #666;">---------- Forwarded message ----------<br>Unable to load original message content</div>`
|
// Use a different template based on email type
|
||||||
: `<div style="margin: 20px 0 10px 0; color: #666;">On ${dateStr}, ${fromStr} wrote:<br>Unable to load original message content</div>`;
|
let fallbackContent = '';
|
||||||
|
if (type === 'forward') {
|
||||||
|
fallbackContent = `
|
||||||
|
<div style="margin: 20px 0 10px 0; color: #666; font-family: Arial, sans-serif;">
|
||||||
|
<div style="border-bottom: 1px solid #ccc; margin-bottom: 10px; padding-bottom: 5px;">
|
||||||
|
<div>---------------------------- Forwarded Message ----------------------------</div>
|
||||||
|
</div>
|
||||||
|
<table style="margin-bottom: 10px; font-size: 14px;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">From:</td>
|
||||||
|
<td style="padding: 3px 0;">${fromStr}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Date:</td>
|
||||||
|
<td style="padding: 3px 0;">${dateStr}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Subject:</td>
|
||||||
|
<td style="padding: 3px 0;">${initialEmail.subject || ''}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div style="border-bottom: 1px solid #ccc; margin-top: 5px; margin-bottom: 15px; padding-bottom: 5px;">
|
||||||
|
<div>----------------------------------------------------------------------</div>
|
||||||
|
</div>
|
||||||
|
<div>[Could not load original content. Please use plain text or start a new message.]</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
// For replies
|
||||||
|
fallbackContent = `
|
||||||
|
<div style="margin: 20px 0 10px 0; color: #666; border-bottom: 1px solid #ddd; padding-bottom: 5px;">
|
||||||
|
On ${dateStr}, ${fromStr} wrote:
|
||||||
|
</div>
|
||||||
|
<blockquote style="margin: 0; padding-left: 10px; border-left: 3px solid #ddd; color: #505050; background-color: #f9f9f9; padding: 10px;">
|
||||||
|
[Could not load original content. Please use plain text or start a new message.]
|
||||||
|
</blockquote>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
setEmailContent(fallbackContent);
|
setEmailContent(fallbackContent);
|
||||||
|
|
||||||
|
// Also update the Quill editor directly if possible
|
||||||
|
const editorElement = document.querySelector('.ql-editor');
|
||||||
|
if (editorElement instanceof HTMLElement) {
|
||||||
|
editorElement.innerHTML = fallbackContent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import 'quill/dist/quill.snow.css';
|
|||||||
import { sanitizeHtml } from '@/lib/utils/dom-purify-config';
|
import { sanitizeHtml } from '@/lib/utils/dom-purify-config';
|
||||||
import { detectTextDirection } from '@/lib/utils/text-direction';
|
import { detectTextDirection } from '@/lib/utils/text-direction';
|
||||||
import { processHtmlContent } from '@/lib/utils/email-content';
|
import { processHtmlContent } from '@/lib/utils/email-content';
|
||||||
|
import Quill from 'quill';
|
||||||
|
import QuillBetterTable from 'quill-better-table';
|
||||||
|
|
||||||
interface RichEmailEditorProps {
|
interface RichEmailEditorProps {
|
||||||
initialContent: string;
|
initialContent: string;
|
||||||
@ -13,6 +15,23 @@ interface RichEmailEditorProps {
|
|||||||
minHeight?: string;
|
minHeight?: string;
|
||||||
maxHeight?: string;
|
maxHeight?: string;
|
||||||
preserveFormatting?: boolean;
|
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<RichEmailEditorProps> = ({
|
const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
|
||||||
initialContent,
|
initialContent,
|
||||||
onChange,
|
onChange,
|
||||||
@ -154,301 +230,273 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
|
|||||||
minHeight = '200px',
|
minHeight = '200px',
|
||||||
maxHeight = 'calc(100vh - 400px)',
|
maxHeight = 'calc(100vh - 400px)',
|
||||||
preserveFormatting = false,
|
preserveFormatting = false,
|
||||||
|
autofocus = false,
|
||||||
|
mode = 'compose',
|
||||||
|
allowedFormats,
|
||||||
|
customKeyBindings,
|
||||||
}) => {
|
}) => {
|
||||||
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 [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);
|
||||||
|
|
||||||
// Initialize Quill editor when component mounts
|
// Initialize editor effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Import Quill dynamically (client-side only)
|
let editorInstance: any = null;
|
||||||
const initializeQuill = async () => {
|
let initialContentSet = false;
|
||||||
if (!editorRef.current || !toolbarRef.current) return;
|
let initializationTimeout: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
// First, detect if content is reply/forward to determine editor mode
|
const initEditor = async () => {
|
||||||
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');
|
|
||||||
|
|
||||||
// Clean up any existing Quill instance
|
|
||||||
if (quillRef.current) {
|
|
||||||
console.log('Cleaning up existing Quill instance before reinitializing');
|
|
||||||
quillRef.current.off('text-change');
|
|
||||||
try {
|
|
||||||
// Safely remove the Quill instance to prevent memory leaks
|
|
||||||
const editorContainer = editorRef.current.querySelector('.ql-editor');
|
|
||||||
if (editorContainer) {
|
|
||||||
editorContainer.innerHTML = '';
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Error cleaning up editor:', e);
|
|
||||||
}
|
|
||||||
quillRef.current = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Quill = (await import('quill')).default;
|
|
||||||
|
|
||||||
// Import quill-better-table conditionally based on content type
|
|
||||||
let tableModule = null;
|
|
||||||
if (!contentIsReplyOrForward) {
|
|
||||||
// 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 and not in reply/forward mode
|
|
||||||
if (QuillBetterTable && QuillBetterTable.default) {
|
|
||||||
Quill.register({
|
|
||||||
'modules/better-table': QuillBetterTable.default
|
|
||||||
}, true);
|
|
||||||
tableModule = QuillBetterTable.default;
|
|
||||||
console.log('Better Table module registered successfully');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('Table module not available:', err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('Skipping better-table module for reply/forward content');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define custom formats/modules with table support
|
|
||||||
const emailToolbarOptions = [
|
|
||||||
['bold', 'italic', 'underline', 'strike'],
|
|
||||||
[{ 'color': [] }, { 'background': [] }],
|
|
||||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
|
||||||
[{ 'indent': '-1'}, { 'indent': '+1' }],
|
|
||||||
[{ 'align': [] }],
|
|
||||||
[{ 'direction': 'rtl' }], // Add direction to toolbar
|
|
||||||
['link'],
|
|
||||||
['clean'],
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create new Quill instance with the DOM element and custom toolbar
|
// First cleanup any existing editor instances to prevent memory leaks
|
||||||
const editorElement = editorRef.current;
|
cleanupExistingEditor();
|
||||||
quillRef.current = new Quill(editorElement, {
|
|
||||||
|
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: {
|
modules: {
|
||||||
toolbar: {
|
toolbar: emailToolbarOptions,
|
||||||
container: toolbarRef.current,
|
betterTable: {
|
||||||
handlers: {
|
operationMenu: {
|
||||||
// Add any custom toolbar handlers here
|
items: {
|
||||||
}
|
unmergeCells: {
|
||||||
|
text: 'Unmerge cells',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
clipboard: {
|
keyboard: {
|
||||||
matchVisual: false // Disable clipboard matching for better HTML handling
|
bindings: customKeyBindings,
|
||||||
},
|
},
|
||||||
// Only enable better-table for regular content, not for replies/forwards
|
|
||||||
'better-table': tableModule && !contentIsReplyOrForward ? true : false,
|
|
||||||
},
|
},
|
||||||
placeholder: placeholder,
|
|
||||||
theme: 'snow',
|
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) {
|
if (initialContent) {
|
||||||
try {
|
try {
|
||||||
console.log('Setting initial content in editor', {
|
console.log('Setting initial editor content');
|
||||||
length: initialContent.length,
|
|
||||||
startsWithHtml: initialContent.trim().startsWith('<'),
|
|
||||||
containsForwardedMessage: initialContent.includes('---------- Forwarded message ----------'),
|
|
||||||
containsReplyIndicator: initialContent.includes('wrote:'),
|
|
||||||
hasBlockquote: initialContent.includes('<blockquote')
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pre-process to handle blocked content like external images
|
// Process the content to handle blocked elements like CID images
|
||||||
const preProcessedContent = handleBlockedContent(initialContent);
|
const processedContent = handleBlockedContent(initialContent);
|
||||||
|
|
||||||
// Process HTML content using centralized utility with special settings for replies/forwards
|
// Use dangerouslyPasteHTML which accepts string content directly
|
||||||
const processed = processHtmlContent(preProcessedContent, {
|
quill.clipboard.dangerouslyPasteHTML(processedContent);
|
||||||
sanitize: true,
|
|
||||||
preserveReplyFormat: contentIsReplyOrForward
|
|
||||||
});
|
|
||||||
const sanitizedContent = processed.sanitizedContent;
|
|
||||||
const direction = processed.direction; // Use direction from processed result
|
|
||||||
|
|
||||||
// Log sanitized content details for debugging
|
// Emit initial content
|
||||||
console.log('Sanitized content details:', {
|
if (onChange) {
|
||||||
length: sanitizedContent.length,
|
onChange({
|
||||||
isEmpty: sanitizedContent.trim().length === 0,
|
html: quill.root.innerHTML,
|
||||||
startsWithDiv: sanitizedContent.trim().startsWith('<div'),
|
text: quill.getText()
|
||||||
containsForwardedMessage: sanitizedContent.includes('---------- Forwarded message ----------'),
|
|
||||||
containsQuoteHeader: sanitizedContent.includes('wrote:'),
|
|
||||||
hasTable: sanitizedContent.includes('<table'),
|
|
||||||
hasBlockquote: sanitizedContent.includes('<blockquote'),
|
|
||||||
isReplyOrForward: contentIsReplyOrForward,
|
|
||||||
firstNChars: sanitizedContent.substring(0, 100).replace(/\n/g, '\\n')
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if sanitized content is valid
|
|
||||||
if (sanitizedContent.trim().length === 0) {
|
|
||||||
console.warn('Sanitized content is empty after processing, using fallback approach');
|
|
||||||
// Try to extract text content if HTML processing failed
|
|
||||||
try {
|
|
||||||
const tempDiv = document.createElement('div');
|
|
||||||
tempDiv.innerHTML = preProcessedContent;
|
|
||||||
const textContent = tempDiv.textContent || tempDiv.innerText || 'Empty content';
|
|
||||||
|
|
||||||
// Set text directly to ensure something displays
|
|
||||||
quillRef.current.setText(textContent);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Text extraction fallback failed:', e);
|
|
||||||
quillRef.current.setText('Error loading content');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Special handling for reply or forwarded content
|
|
||||||
if (contentIsReplyOrForward) {
|
|
||||||
console.log('Using special handling for reply/forward content');
|
|
||||||
|
|
||||||
// For reply/forward content, convert ALL tables to divs
|
|
||||||
const cleanedContent = cleanupTableStructures(sanitizedContent, true);
|
|
||||||
|
|
||||||
// Use direct innerHTML setting with minimal processing for reply/forward content
|
|
||||||
quillRef.current.root.innerHTML = cleanedContent;
|
|
||||||
} else {
|
|
||||||
// For regular content, use normal processing
|
|
||||||
const cleanedContent = cleanupTableStructures(sanitizedContent, false);
|
|
||||||
|
|
||||||
// Use direct innerHTML setting for regular content
|
|
||||||
quillRef.current.root.innerHTML = cleanedContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the direction for the content
|
|
||||||
if (quillRef.current && quillRef.current.format) {
|
|
||||||
quillRef.current.format('direction', direction);
|
|
||||||
if (direction === 'rtl') {
|
|
||||||
quillRef.current.format('align', 'right');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('Cannot format content: editor not fully initialized');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set cursor at the beginning
|
|
||||||
quillRef.current.setSelection(0, 0);
|
|
||||||
|
|
||||||
// Ensure the cursor and scroll position is at the top of the editor
|
|
||||||
if (editorRef.current) {
|
|
||||||
editorRef.current.scrollTop = 0;
|
|
||||||
|
|
||||||
// Find and scroll parent containers that might have scroll
|
|
||||||
const scrollable = [
|
|
||||||
editorRef.current.closest('.ql-container'),
|
|
||||||
editorRef.current.closest('.rich-email-editor-container'),
|
|
||||||
editorRef.current.closest('.overflow-y-auto'),
|
|
||||||
document.querySelector('.overflow-y-auto')
|
|
||||||
];
|
|
||||||
|
|
||||||
scrollable.forEach(el => {
|
|
||||||
if (el instanceof HTMLElement) {
|
|
||||||
el.scrollTop = 0;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initialContentSet = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error setting initial content:', err);
|
console.error('Error setting initial content:', err);
|
||||||
|
|
||||||
// Enhanced fallback mechanism for complex content
|
// Fallback to direct HTML setting if conversion fails
|
||||||
try {
|
try {
|
||||||
// First try to extract text from HTML
|
quill.root.innerHTML = handleBlockedContent(initialContent);
|
||||||
const tempDiv = document.createElement('div');
|
} catch (innerErr) {
|
||||||
tempDiv.innerHTML = initialContent;
|
console.error('Fallback content setting failed:', innerErr);
|
||||||
const textContent = tempDiv.textContent || tempDiv.innerText || '';
|
quill.root.innerHTML = '<p>Error loading content. Please start typing or paste content manually.</p>';
|
||||||
|
|
||||||
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
|
// Handle content change events
|
||||||
quillRef.current.on('text-change', () => {
|
quill.on('text-change', (delta: any, oldDelta: any, source: string) => {
|
||||||
const html = quillRef.current.root.innerHTML;
|
if (source === 'user') {
|
||||||
onChange(html);
|
const html = quill.root.innerHTML;
|
||||||
|
onChange(html);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Improve editor layout
|
// Set focus if autofocus is true
|
||||||
const editorContainer = editorElement.closest('.ql-container');
|
if (autofocus) {
|
||||||
if (editorContainer) {
|
setTimeout(() => {
|
||||||
editorContainer.classList.add('email-editor-container');
|
quill.focus();
|
||||||
}
|
quill.setSelection(0, 0);
|
||||||
|
}, 100);
|
||||||
setIsReady(true);
|
|
||||||
} catch (initError) {
|
|
||||||
console.error('Critical error initializing editor:', initError);
|
|
||||||
// Provide fallback in UI
|
|
||||||
if (editorRef.current) {
|
|
||||||
editorRef.current.innerHTML = '<div style="padding: 15px; color: #555;">Error loading editor. Please try again or use plain text mode.</div>';
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error initializing editor:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
initializeQuill().catch(err => {
|
// Set a timeout to prevent endless waiting for editor initialization
|
||||||
console.error('Failed to initialize Quill editor:', err);
|
initializationTimeout = setTimeout(() => {
|
||||||
});
|
if (!initialContentSet && editorRef.current) {
|
||||||
|
console.warn('Editor initialization timed out after 3 seconds');
|
||||||
// Clean up on unmount
|
|
||||||
|
// 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 || '<p>Editor ready. Start typing...</p>';
|
||||||
|
|
||||||
|
// 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 () => {
|
return () => {
|
||||||
if (quillRef.current) {
|
if (initializationTimeout) {
|
||||||
// Clean up any event listeners or resources
|
clearTimeout(initializationTimeout);
|
||||||
quillRef.current.off('text-change');
|
|
||||||
quillRef.current = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Handle blocked content like CID images that cause loading issues
|
||||||
/**
|
|
||||||
* Pre-process content to handle blocked images and other resources
|
|
||||||
*/
|
|
||||||
function handleBlockedContent(htmlContent: string): string {
|
function handleBlockedContent(htmlContent: string): string {
|
||||||
if (!htmlContent) return htmlContent;
|
if (!htmlContent) return '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tempDiv = document.createElement('div');
|
// Create a DOM parser to work with the HTML
|
||||||
tempDiv.innerHTML = htmlContent;
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(htmlContent, 'text/html');
|
||||||
|
|
||||||
// Replace CID and other problematic image sources
|
// Count images processed for logging
|
||||||
const images = tempDiv.querySelectorAll('img');
|
let imageCount = 0;
|
||||||
|
let cidCount = 0;
|
||||||
|
|
||||||
|
// Process all images to prevent loading issues
|
||||||
|
const images = doc.querySelectorAll('img');
|
||||||
images.forEach(img => {
|
images.forEach(img => {
|
||||||
const src = img.getAttribute('src') || '';
|
const src = img.getAttribute('src');
|
||||||
|
imageCount++;
|
||||||
|
|
||||||
// Handle CID attachments that would be blocked
|
// Handle CID images which cause loading issues
|
||||||
if (src.startsWith('cid:')) {
|
if (src && src.startsWith('cid:')) {
|
||||||
console.log('Replacing CID image source:', src);
|
cidCount++;
|
||||||
img.setAttribute('src', 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="80" viewBox="0 0 100 80"><rect width="100" height="80" fill="%23eee"/><text x="50%" y="50%" font-family="sans-serif" font-size="12" text-anchor="middle" dominant-baseline="middle" fill="%23999">[Image: Attachment]</text></svg>');
|
// Replace with placeholder or remove src
|
||||||
img.setAttribute('data-original-src', 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.style.maxWidth = '300px';
|
img.setAttribute('data-original-cid', src);
|
||||||
img.style.border = '1px dashed #ddd';
|
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
|
// Handle possible blocked remote images
|
||||||
if (src.includes('open?') || src.includes('tracking') || src.includes('pixel')) {
|
if (src && (src.startsWith('http://') || src.startsWith('https://'))) {
|
||||||
console.log('Removing tracking pixel:', src);
|
// Set alt text and a class for better UX
|
||||||
img.remove();
|
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) {
|
} catch (error) {
|
||||||
console.error('Error handling blocked content:', error);
|
console.error('Error processing blocked content:', error);
|
||||||
|
// Return original content if processing fails
|
||||||
return htmlContent;
|
return htmlContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user