879 lines
32 KiB
TypeScript
879 lines
32 KiB
TypeScript
'use client';
|
|
|
|
import React, { useEffect, useRef, useState } from 'react';
|
|
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;
|
|
onChange: (content: string) => void;
|
|
placeholder?: string;
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean up problematic table structures that cause issues with quill-better-table
|
|
*/
|
|
function cleanupTableStructures(htmlContent: string, isReplyOrForward: boolean = false): string {
|
|
if (!htmlContent) return htmlContent;
|
|
|
|
try {
|
|
const tempDiv = document.createElement('div');
|
|
tempDiv.innerHTML = htmlContent;
|
|
|
|
// Find tables with problematic nested structures
|
|
const tables = tempDiv.querySelectorAll('table');
|
|
|
|
// Check if content looks like a forwarded email or reply content
|
|
const isForwardedEmail =
|
|
htmlContent.includes('---------- Forwarded message ----------') ||
|
|
htmlContent.includes('Forwarded message') ||
|
|
htmlContent.includes('forwarded message') ||
|
|
(htmlContent.includes('From:') && htmlContent.includes('Date:') && htmlContent.includes('Subject:'));
|
|
|
|
const isReplyEmail =
|
|
htmlContent.includes('wrote:') ||
|
|
htmlContent.includes('<blockquote') ||
|
|
htmlContent.includes('gmail_quote');
|
|
|
|
// For reply/forward content, force convert ALL tables to divs to avoid Quill errors
|
|
const shouldConvertAllTables = isReplyOrForward || isForwardedEmail || isReplyEmail;
|
|
|
|
if (tables.length > 0) {
|
|
console.log(`Found ${tables.length} tables in ${shouldConvertAllTables ? 'reply/forward' : 'regular'} content`);
|
|
|
|
let convertedCount = 0;
|
|
tables.forEach(table => {
|
|
// In reply/forward mode, convert ALL tables to divs to avoid quill-better-table issues
|
|
if (shouldConvertAllTables) {
|
|
const replacementDiv = document.createElement('div');
|
|
replacementDiv.className = 'converted-table';
|
|
replacementDiv.style.border = '1px solid #ddd';
|
|
replacementDiv.style.margin = '10px 0';
|
|
replacementDiv.style.padding = '10px';
|
|
|
|
// Preserve the original table structure visually
|
|
// Create a simplified HTML representation of the table
|
|
let tableHtml = '';
|
|
|
|
// 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');
|
|
}
|
|
});
|
|
} else {
|
|
// 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} tables to divs to prevent Quill errors`);
|
|
return tempDiv.innerHTML;
|
|
}
|
|
|
|
return htmlContent;
|
|
} catch (error) {
|
|
console.error('Error cleaning up table structures:', error);
|
|
return htmlContent;
|
|
}
|
|
}
|
|
|
|
// 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> = ({
|
|
initialContent,
|
|
onChange,
|
|
placeholder = 'Write your message here...',
|
|
minHeight = '200px',
|
|
maxHeight = 'calc(100vh - 400px)',
|
|
preserveFormatting = false,
|
|
autofocus = false,
|
|
mode = 'compose',
|
|
allowedFormats,
|
|
customKeyBindings,
|
|
}) => {
|
|
const editorRef = useRef<HTMLDivElement>(null);
|
|
const toolbarRef = useRef<HTMLDivElement>(null);
|
|
const quillRef = useRef<any>(null);
|
|
const [isReady, setIsReady] = useState(false);
|
|
const [isReplyOrForward, setIsReplyOrForward] = useState(false);
|
|
const quillInitTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
// Initialize editor effect
|
|
useEffect(() => {
|
|
let editorInstance: any = null;
|
|
let initialContentSet = false;
|
|
let initializationTimeout: NodeJS.Timeout | null = null;
|
|
|
|
const initEditor = async () => {
|
|
try {
|
|
// 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: emailToolbarOptions,
|
|
betterTable: {
|
|
operationMenu: {
|
|
items: {
|
|
unmergeCells: {
|
|
text: 'Unmerge cells',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
keyboard: {
|
|
bindings: customKeyBindings,
|
|
},
|
|
},
|
|
theme: 'snow',
|
|
placeholder: placeholder || 'Write your message here...',
|
|
formats: allowedFormats,
|
|
});
|
|
|
|
// Store the instance for cleanup
|
|
editorInstance = quill;
|
|
|
|
// Process and set initial content if available
|
|
if (initialContent) {
|
|
try {
|
|
console.log('Setting initial editor content');
|
|
|
|
// Process the content to handle blocked elements like CID images
|
|
const processedContent = handleBlockedContent(initialContent);
|
|
|
|
// Use dangerouslyPasteHTML which accepts string content directly
|
|
quill.clipboard.dangerouslyPasteHTML(processedContent);
|
|
|
|
// Emit initial content
|
|
if (onChange) {
|
|
onChange({
|
|
html: quill.root.innerHTML,
|
|
text: quill.getText()
|
|
});
|
|
}
|
|
|
|
initialContentSet = true;
|
|
} catch (err) {
|
|
console.error('Error setting initial content:', err);
|
|
|
|
// Fallback to direct HTML setting if conversion fails
|
|
try {
|
|
quill.root.innerHTML = handleBlockedContent(initialContent);
|
|
} catch (innerErr) {
|
|
console.error('Fallback content setting failed:', innerErr);
|
|
quill.root.innerHTML = '<p>Error loading content. Please start typing or paste content manually.</p>';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle content change events
|
|
quill.on('text-change', (delta: any, oldDelta: any, source: string) => {
|
|
if (source === 'user') {
|
|
const html = quill.root.innerHTML;
|
|
onChange(html);
|
|
}
|
|
});
|
|
|
|
// Set focus if autofocus is true
|
|
if (autofocus) {
|
|
setTimeout(() => {
|
|
quill.focus();
|
|
quill.setSelection(0, 0);
|
|
}, 100);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error initializing editor:', error);
|
|
}
|
|
};
|
|
|
|
// 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 || '<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 () => {
|
|
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]);
|
|
|
|
// Handle blocked content like CID images that cause loading issues
|
|
function handleBlockedContent(htmlContent: string): string {
|
|
if (!htmlContent) return '';
|
|
|
|
try {
|
|
// Create a DOM parser to work with the HTML
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(htmlContent, 'text/html');
|
|
|
|
// 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');
|
|
imageCount++;
|
|
|
|
// 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 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');
|
|
}
|
|
});
|
|
|
|
// 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 processing blocked content:', error);
|
|
// Return original content if processing fails
|
|
return htmlContent;
|
|
}
|
|
}
|
|
|
|
// Update content from props if changed externally - using a simpler approach
|
|
useEffect(() => {
|
|
if (quillRef.current && isReady && initialContent) {
|
|
const currentContent = quillRef.current.root.innerHTML;
|
|
|
|
// Only update if content changed to avoid editor position reset
|
|
if (initialContent !== currentContent) {
|
|
try {
|
|
console.log('Updating content in editor:', {
|
|
contentLength: initialContent.length,
|
|
startsWithHtml: initialContent.trim().startsWith('<'),
|
|
containsForwardedMessage: initialContent.includes('---------- Forwarded message ----------'),
|
|
containsQuoteHeader: initialContent.includes('wrote:'),
|
|
hasBlockquote: initialContent.includes('<blockquote'),
|
|
hasTable: initialContent.includes('<table'),
|
|
firstNChars: initialContent.substring(0, 100).replace(/\n/g, '\\n')
|
|
});
|
|
|
|
// Check if content is reply or forward to use special handling
|
|
const contentIsReplyOrForward =
|
|
initialContent.includes('wrote:') ||
|
|
initialContent.includes('<blockquote') ||
|
|
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');
|
|
|
|
// Clear content to prevent flashing
|
|
try {
|
|
quillRef.current.root.innerHTML = '<div>Loading...</div>';
|
|
} catch (e) {
|
|
console.warn('Error clearing editor content:', e);
|
|
}
|
|
|
|
quillRef.current = null;
|
|
}
|
|
setIsReady(false);
|
|
|
|
// Force a small delay before reinitializing to ensure cleanup completes
|
|
setTimeout(() => {
|
|
// Explicitly empty out the editor DOM node to ensure clean start
|
|
if (editorRef.current) {
|
|
while (editorRef.current.firstChild) {
|
|
editorRef.current.removeChild(editorRef.current.firstChild);
|
|
}
|
|
}
|
|
}, 50);
|
|
|
|
return;
|
|
}
|
|
|
|
// Pre-process to handle blocked content
|
|
const preProcessedContent = handleBlockedContent(initialContent);
|
|
|
|
// Process HTML content using centralized utility
|
|
const processed = processHtmlContent(preProcessedContent, {
|
|
sanitize: true,
|
|
preserveReplyFormat: contentIsReplyOrForward
|
|
});
|
|
const sanitizedContent = processed.sanitizedContent;
|
|
const direction = processed.direction; // Use direction from processed result
|
|
|
|
// Log sanitized content details for debugging
|
|
console.log('Sanitized content details:', {
|
|
length: sanitizedContent.length,
|
|
isEmpty: sanitizedContent.trim().length === 0,
|
|
startsWithDiv: sanitizedContent.trim().startsWith('<div'),
|
|
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 content is valid HTML
|
|
if (sanitizedContent.trim().length === 0) {
|
|
console.warn('Sanitized content is empty, using original content');
|
|
// If sanitized content is empty, try to extract text from original
|
|
const tempDiv = document.createElement('div');
|
|
tempDiv.innerHTML = preProcessedContent;
|
|
const textContent = tempDiv.textContent || tempDiv.innerText || '';
|
|
|
|
// Create simple HTML with text content
|
|
if (quillRef.current) {
|
|
quillRef.current.setText(textContent || 'No content available');
|
|
}
|
|
} else {
|
|
// Process content based on type
|
|
if (contentIsReplyOrForward) {
|
|
console.log('Using special handling for reply/forward content update');
|
|
|
|
// 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) {
|
|
quillRef.current.root.innerHTML = cleanedContent;
|
|
|
|
// Delay applying formatting to ensure Quill is fully ready
|
|
setTimeout(() => {
|
|
if (quillRef.current) {
|
|
try {
|
|
// Set the direction for the content
|
|
quillRef.current.format('direction', direction);
|
|
if (direction === 'rtl') {
|
|
quillRef.current.format('align', 'right');
|
|
}
|
|
|
|
// Force update
|
|
quillRef.current.update();
|
|
|
|
// Set selection to beginning
|
|
quillRef.current.setSelection(0, 0);
|
|
} catch (innerError) {
|
|
console.error('Error applying delayed formatting:', innerError);
|
|
}
|
|
}
|
|
}, 100);
|
|
}
|
|
} else {
|
|
// For regular content, use normal processing
|
|
const cleanedContent = cleanupTableStructures(sanitizedContent, false);
|
|
|
|
if (quillRef.current && quillRef.current.root) {
|
|
quillRef.current.root.innerHTML = cleanedContent;
|
|
|
|
// Safely apply formatting
|
|
try {
|
|
quillRef.current.format('direction', direction);
|
|
if (direction === 'rtl') {
|
|
quillRef.current.format('align', 'right');
|
|
}
|
|
|
|
// Force update
|
|
quillRef.current.update();
|
|
|
|
// Set selection to beginning
|
|
quillRef.current.setSelection(0, 0);
|
|
} catch (formatError) {
|
|
console.error('Error applying formatting:', formatError);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Error updating content:', err);
|
|
// Safer fallback that avoids clipboard API
|
|
try {
|
|
// Extract basic text if everything else fails
|
|
const tempDiv = document.createElement('div');
|
|
tempDiv.innerHTML = initialContent;
|
|
const textContent = tempDiv.textContent || tempDiv.innerText || '';
|
|
|
|
if (quillRef.current) {
|
|
quillRef.current.setText(textContent || 'Error loading content');
|
|
}
|
|
} catch (e) {
|
|
console.error('All fallbacks failed:', e);
|
|
// Last resort
|
|
if (quillRef.current) {
|
|
quillRef.current.setText('Error loading content');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, [initialContent, isReady, isReplyOrForward]);
|
|
|
|
return (
|
|
<div className="rich-email-editor-wrapper">
|
|
{/* Custom toolbar container */}
|
|
<div ref={toolbarRef} className="ql-toolbar ql-snow">
|
|
<span className="ql-formats">
|
|
<button className="ql-bold"></button>
|
|
<button className="ql-italic"></button>
|
|
<button className="ql-underline"></button>
|
|
<button className="ql-strike"></button>
|
|
</span>
|
|
<span className="ql-formats">
|
|
<select className="ql-color"></select>
|
|
<select className="ql-background"></select>
|
|
</span>
|
|
<span className="ql-formats">
|
|
<button className="ql-list" value="ordered"></button>
|
|
<button className="ql-list" value="bullet"></button>
|
|
</span>
|
|
<span className="ql-formats">
|
|
<button className="ql-indent" value="-1"></button>
|
|
<button className="ql-indent" value="+1"></button>
|
|
</span>
|
|
<span className="ql-formats">
|
|
<select className="ql-align"></select>
|
|
</span>
|
|
<span className="ql-formats">
|
|
<button className="ql-direction" value="rtl"></button>
|
|
</span>
|
|
<span className="ql-formats">
|
|
<button className="ql-link"></button>
|
|
</span>
|
|
<span className="ql-formats">
|
|
<button className="ql-clean"></button>
|
|
</span>
|
|
</div>
|
|
|
|
{/* Editor container with improved scrolling */}
|
|
<div className="rich-email-editor-container">
|
|
<div
|
|
ref={editorRef}
|
|
className="quill-editor"
|
|
/>
|
|
|
|
{/* Loading indicator */}
|
|
{!isReady && (
|
|
<div className="flex items-center justify-center py-8">
|
|
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Custom styles for email context */}
|
|
<style jsx>{`
|
|
.rich-email-editor-wrapper {
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
border-radius: 6px;
|
|
flex: 1;
|
|
border: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.rich-email-editor-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: auto;
|
|
flex: 1;
|
|
position: relative;
|
|
}
|
|
|
|
.quill-editor {
|
|
width: 100%;
|
|
min-height: ${minHeight};
|
|
max-height: ${maxHeight};
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
/* Hide the editor until it's ready */
|
|
.quill-editor ${!isReady ? '{ display: none; }' : ''}
|
|
|
|
/* Hide duplicate toolbar */
|
|
:global(.ql-toolbar.ql-snow + .ql-toolbar.ql-snow) {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Add RTL support styles */
|
|
:global([dir="rtl"] .ql-editor) {
|
|
text-align: right;
|
|
direction: rtl;
|
|
}
|
|
|
|
:global(.ql-editor[dir="rtl"]) {
|
|
text-align: right;
|
|
direction: rtl;
|
|
}
|
|
|
|
:global(.ql-container) {
|
|
border: none !important;
|
|
height: auto !important;
|
|
min-height: ${minHeight};
|
|
max-height: none !important;
|
|
overflow: visible;
|
|
}
|
|
|
|
:global(.ql-editor) {
|
|
padding: 12px;
|
|
min-height: ${minHeight};
|
|
overflow-y: auto !important;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
color: #333 !important;
|
|
}
|
|
|
|
/* Ensure all text is visible */
|
|
:global(.ql-editor p),
|
|
:global(.ql-editor div),
|
|
:global(.ql-editor span),
|
|
:global(.ql-editor li) {
|
|
color: #333 !important;
|
|
}
|
|
|
|
/* Ensure placeholder text is visible but distinct */
|
|
:global(.ql-editor.ql-blank::before) {
|
|
color: #aaa !important;
|
|
font-style: italic !important;
|
|
}
|
|
|
|
/* Force blockquote styling */
|
|
:global(.ql-editor blockquote) {
|
|
border-left: 2px solid #ddd !important;
|
|
margin: 0 !important;
|
|
padding: 10px 0 10px 15px !important;
|
|
color: #505050 !important;
|
|
background-color: #f9f9f9 !important;
|
|
border-radius: 4px !important;
|
|
font-size: 13px !important;
|
|
}
|
|
|
|
/* RTL blockquote styling */
|
|
:global(.ql-editor[dir="rtl"] blockquote),
|
|
:global([dir="rtl"] .ql-editor blockquote) {
|
|
border-left: none !important;
|
|
border-right: 2px solid #ddd !important;
|
|
padding: 10px 15px 10px 0 !important;
|
|
}
|
|
|
|
/* Fix table rendering */
|
|
:global(.ql-editor table) {
|
|
width: 100% !important;
|
|
border-collapse: collapse !important;
|
|
table-layout: fixed !important;
|
|
margin: 10px 0 !important;
|
|
border: 1px solid #ddd !important;
|
|
}
|
|
|
|
:global(.ql-editor td),
|
|
:global(.ql-editor th) {
|
|
border: 1px solid #ddd !important;
|
|
padding: 6px 8px !important;
|
|
overflow-wrap: break-word !important;
|
|
word-break: break-word !important;
|
|
min-width: 30px !important;
|
|
font-size: 13px !important;
|
|
}
|
|
|
|
/* Email quote styling */
|
|
:global(.email-original-content) {
|
|
margin-top: 20px !important;
|
|
padding-top: 10px !important;
|
|
border-top: 1px solid #ddd !important;
|
|
color: #555 !important;
|
|
font-size: 13px !important;
|
|
}
|
|
|
|
/* Fix quoted paragraphs */
|
|
:global(.ql-editor blockquote p) {
|
|
margin-bottom: 8px !important;
|
|
margin-top: 0 !important;
|
|
}
|
|
|
|
/* Fix for reply headers */
|
|
:global(.ql-editor div[style*="font-weight: 400"]) {
|
|
margin-top: 20px !important;
|
|
margin-bottom: 8px !important;
|
|
color: #555 !important;
|
|
font-size: 13px !important;
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default RichEmailEditor;
|