Bugfixes
This commit is contained in:
@@ -15,6 +15,7 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
@@ -339,8 +340,10 @@ public class AuthorService {
|
||||
existing.setAuthorRating(updates.getAuthorRating());
|
||||
}
|
||||
if (updates.getUrls() != null) {
|
||||
// Create a defensive copy to avoid issues when existing and updates are the same object
|
||||
List<String> urlsCopy = new ArrayList<>(updates.getUrls());
|
||||
existing.getUrls().clear();
|
||||
existing.getUrls().addAll(updates.getUrls());
|
||||
existing.getUrls().addAll(urlsCopy);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
const sanitizedHtml = sanitizeHtmlSync(htmlContent);
|
||||
console.log('Final clipboard data:');
|
||||
console.log('HTML content length:', htmlContent.length);
|
||||
console.log('Plain text length:', plainText.length);
|
||||
|
||||
// Simply append the sanitized HTML to current content
|
||||
// This approach maintains the HTML formatting while being simpler
|
||||
const newHtmlValue = value + sanitizedHtml;
|
||||
// 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));
|
||||
|
||||
// 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 if (plainText) {
|
||||
// For plain text, convert to paragraphs and append
|
||||
const textAsHtml = plainText
|
||||
} 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');
|
||||
|
||||
const newHtmlValue = value + textAsHtml;
|
||||
onChange(newHtmlValue);
|
||||
setHtmlValue(newHtmlValue);
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user