This commit is contained in:
Stefan Hardegger
2025-07-24 13:07:36 +02:00
parent 90428894b4
commit 131e2e8c25
3 changed files with 134 additions and 32 deletions

View File

@@ -47,18 +47,35 @@ export default function RichTextEditor({
e.preventDefault();
try {
// Try to get HTML content from clipboard
const items = e.clipboardData?.items;
// Try multiple approaches to get clipboard data
const clipboardData = e.clipboardData;
let htmlContent = '';
let plainText = '';
if (items) {
for (const item of Array.from(items)) {
if (item.type === 'text/html') {
// Method 1: Try direct getData calls first (more reliable)
try {
htmlContent = clipboardData.getData('text/html');
plainText = clipboardData.getData('text/plain');
console.log('Paste debug - Direct method:');
console.log('HTML length:', htmlContent.length);
console.log('HTML preview:', htmlContent.substring(0, 200));
console.log('Plain text length:', plainText.length);
} catch (e) {
console.log('Direct getData failed:', e);
}
// Method 2: If direct method didn't work, try items approach
if (!htmlContent && clipboardData?.items) {
console.log('Trying items approach...');
const items = Array.from(clipboardData.items);
console.log('Available clipboard types:', items.map(item => item.type));
for (const item of items) {
if (item.type === 'text/html' && !htmlContent) {
htmlContent = await new Promise<string>((resolve) => {
item.getAsString(resolve);
});
} else if (item.type === 'text/plain') {
} else if (item.type === 'text/plain' && !plainText) {
plainText = await new Promise<string>((resolve) => {
item.getAsString(resolve);
});
@@ -66,33 +83,98 @@ export default function RichTextEditor({
}
}
// If we have HTML content, sanitize it and merge with current content
if (htmlContent) {
console.log('Final clipboard data:');
console.log('HTML content length:', htmlContent.length);
console.log('Plain text length:', plainText.length);
// Additional debugging for clipboard types and content
if (clipboardData?.types) {
console.log('Clipboard types available:', clipboardData.types);
for (const type of clipboardData.types) {
try {
const data = clipboardData.getData(type);
console.log(`Type "${type}" content length:`, data.length);
if (data.length > 0 && data.length < 1000) {
console.log(`Type "${type}" content:`, data);
}
} catch (e) {
console.log(`Failed to get data for type "${type}":`, e);
}
}
}
// Process HTML content if available
if (htmlContent && htmlContent.trim().length > 0) {
console.log('Processing HTML content...');
console.log('Raw HTML:', htmlContent.substring(0, 500));
const sanitizedHtml = sanitizeHtmlSync(htmlContent);
console.log('Sanitized HTML:', sanitizedHtml.substring(0, 500));
// Simply append the sanitized HTML to current content
// This approach maintains the HTML formatting while being simpler
const newHtmlValue = value + sanitizedHtml;
onChange(newHtmlValue);
setHtmlValue(newHtmlValue);
} else if (plainText) {
// For plain text, convert to paragraphs and append
const textAsHtml = plainText
.split('\n\n')
.filter(paragraph => paragraph.trim())
.map(paragraph => `<p>${paragraph.replace(/\n/g, '<br>')}</p>`)
.join('\n');
const newHtmlValue = value + textAsHtml;
onChange(newHtmlValue);
setHtmlValue(newHtmlValue);
// Insert at cursor position instead of just appending
const textarea = visualTextareaRef.current;
if (textarea) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const currentPlainText = getPlainText(value);
// Split current content at cursor position
const beforeCursor = currentPlainText.substring(0, start);
const afterCursor = currentPlainText.substring(end);
// Convert plain text parts back to HTML
const beforeHtml = beforeCursor ? beforeCursor.split('\n\n').filter(p => p.trim()).map(p => `<p>${p.replace(/\n/g, '<br>')}</p>`).join('\n') : '';
const afterHtml = afterCursor ? afterCursor.split('\n\n').filter(p => p.trim()).map(p => `<p>${p.replace(/\n/g, '<br>')}</p>`).join('\n') : '';
const newHtmlValue = beforeHtml + (beforeHtml ? '\n' : '') + sanitizedHtml + (afterHtml ? '\n' : '') + afterHtml;
onChange(newHtmlValue);
setHtmlValue(newHtmlValue);
} else {
// Fallback: just append
const newHtmlValue = value + sanitizedHtml;
onChange(newHtmlValue);
setHtmlValue(newHtmlValue);
}
} else if (plainText && plainText.trim().length > 0) {
console.log('Processing plain text content...');
// For plain text, convert to paragraphs and insert at cursor
const textarea = visualTextareaRef.current;
if (textarea) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const currentPlainText = getPlainText(value);
const beforeCursor = currentPlainText.substring(0, start);
const afterCursor = currentPlainText.substring(end);
const newPlainText = beforeCursor + plainText + afterCursor;
// Convert to HTML
const textAsHtml = newPlainText
.split('\n\n')
.filter(paragraph => paragraph.trim())
.map(paragraph => `<p>${paragraph.replace(/\n/g, '<br>')}</p>`)
.join('\n');
onChange(textAsHtml);
setHtmlValue(textAsHtml);
}
} else {
console.log('No usable clipboard content found');
}
} catch (error) {
console.error('Error handling paste:', error);
// Fallback to default paste behavior
const plainText = e.clipboardData.getData('text/plain');
handleVisualChange({ target: { value: plainText } } as React.ChangeEvent<HTMLTextAreaElement>);
if (plainText) {
const textAsHtml = plainText
.split('\n\n')
.filter(paragraph => paragraph.trim())
.map(paragraph => `<p>${paragraph.replace(/\n/g, '<br>')}</p>`)
.join('\n');
onChange(value + textAsHtml);
setHtmlValue(value + textAsHtml);
}
}
};

View File

@@ -165,20 +165,37 @@ export function sanitizeHtmlSync(html: string): string {
console.log('Using fallback sanitization configuration with formatting support');
const fallbackConfig: DOMPurify.Config = {
ALLOWED_TAGS: [
'p', 'br', 'div', 'span', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
// Basic block elements
'p', 'br', 'div', 'span', 'section', 'article',
// Headers
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
// Text formatting
'b', 'strong', 'i', 'em', 'u', 's', 'strike', 'del', 'ins',
'sup', 'sub', 'small', 'big', 'mark', 'pre', 'code', 'kbd', 'samp', 'var',
'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'a',
'sup', 'sub', 'small', 'big', 'mark', 'abbr', 'dfn',
// Code and preformatted
'pre', 'code', 'kbd', 'samp', 'var', 'tt',
// Lists
'ul', 'ol', 'li', 'dl', 'dt', 'dd',
// Links (but href will be removed by backend config)
'a',
// Tables
'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'caption', 'colgroup', 'col',
'blockquote', 'cite', 'q', 'hr', 'details', 'summary'
// Quotes and misc
'blockquote', 'cite', 'q', 'hr', 'details', 'summary',
// Common website elements that might have formatting
'font', 'center'
],
ALLOWED_ATTR: [
'class', 'style', 'colspan', 'rowspan'
'class', 'style', 'colspan', 'rowspan', 'align', 'valign',
// Font attributes (though deprecated, websites still use them)
'color', 'size', 'face'
],
ALLOW_UNKNOWN_PROTOCOLS: false,
SANITIZE_DOM: true,
KEEP_CONTENT: true,
ALLOW_DATA_ATTR: false,
// Don't strip style attributes completely - let them through for basic formatting
FORBID_ATTR: [],
};
return DOMPurify.sanitize(html, fallbackConfig as any).toString();