Bugfixes
This commit is contained in:
@@ -15,6 +15,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -339,8 +340,10 @@ public class AuthorService {
|
|||||||
existing.setAuthorRating(updates.getAuthorRating());
|
existing.setAuthorRating(updates.getAuthorRating());
|
||||||
}
|
}
|
||||||
if (updates.getUrls() != null) {
|
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().clear();
|
||||||
existing.getUrls().addAll(updates.getUrls());
|
existing.getUrls().addAll(urlsCopy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,18 +47,35 @@ export default function RichTextEditor({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try to get HTML content from clipboard
|
// Try multiple approaches to get clipboard data
|
||||||
const items = e.clipboardData?.items;
|
const clipboardData = e.clipboardData;
|
||||||
let htmlContent = '';
|
let htmlContent = '';
|
||||||
let plainText = '';
|
let plainText = '';
|
||||||
|
|
||||||
if (items) {
|
// Method 1: Try direct getData calls first (more reliable)
|
||||||
for (const item of Array.from(items)) {
|
try {
|
||||||
if (item.type === 'text/html') {
|
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) => {
|
htmlContent = await new Promise<string>((resolve) => {
|
||||||
item.getAsString(resolve);
|
item.getAsString(resolve);
|
||||||
});
|
});
|
||||||
} else if (item.type === 'text/plain') {
|
} else if (item.type === 'text/plain' && !plainText) {
|
||||||
plainText = await new Promise<string>((resolve) => {
|
plainText = await new Promise<string>((resolve) => {
|
||||||
item.getAsString(resolve);
|
item.getAsString(resolve);
|
||||||
});
|
});
|
||||||
@@ -66,33 +83,98 @@ export default function RichTextEditor({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have HTML content, sanitize it and merge with current content
|
console.log('Final clipboard data:');
|
||||||
if (htmlContent) {
|
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);
|
const sanitizedHtml = sanitizeHtmlSync(htmlContent);
|
||||||
|
console.log('Sanitized HTML:', sanitizedHtml.substring(0, 500));
|
||||||
|
|
||||||
// Simply append the sanitized HTML to current content
|
// Insert at cursor position instead of just appending
|
||||||
// This approach maintains the HTML formatting while being simpler
|
const textarea = visualTextareaRef.current;
|
||||||
const newHtmlValue = value + sanitizedHtml;
|
if (textarea) {
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const end = textarea.selectionEnd;
|
||||||
|
const currentPlainText = getPlainText(value);
|
||||||
|
|
||||||
onChange(newHtmlValue);
|
// Split current content at cursor position
|
||||||
setHtmlValue(newHtmlValue);
|
const beforeCursor = currentPlainText.substring(0, start);
|
||||||
} else if (plainText) {
|
const afterCursor = currentPlainText.substring(end);
|
||||||
// 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;
|
// Convert plain text parts back to HTML
|
||||||
onChange(newHtmlValue);
|
const beforeHtml = beforeCursor ? beforeCursor.split('\n\n').filter(p => p.trim()).map(p => `<p>${p.replace(/\n/g, '<br>')}</p>`).join('\n') : '';
|
||||||
setHtmlValue(newHtmlValue);
|
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) {
|
} catch (error) {
|
||||||
console.error('Error handling paste:', error);
|
console.error('Error handling paste:', error);
|
||||||
// Fallback to default paste behavior
|
// Fallback to default paste behavior
|
||||||
const plainText = e.clipboardData.getData('text/plain');
|
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');
|
console.log('Using fallback sanitization configuration with formatting support');
|
||||||
const fallbackConfig: DOMPurify.Config = {
|
const fallbackConfig: DOMPurify.Config = {
|
||||||
ALLOWED_TAGS: [
|
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',
|
'b', 'strong', 'i', 'em', 'u', 's', 'strike', 'del', 'ins',
|
||||||
'sup', 'sub', 'small', 'big', 'mark', 'pre', 'code', 'kbd', 'samp', 'var',
|
'sup', 'sub', 'small', 'big', 'mark', 'abbr', 'dfn',
|
||||||
'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'a',
|
// 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',
|
'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: [
|
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,
|
ALLOW_UNKNOWN_PROTOCOLS: false,
|
||||||
SANITIZE_DOM: true,
|
SANITIZE_DOM: true,
|
||||||
KEEP_CONTENT: true,
|
KEEP_CONTENT: true,
|
||||||
ALLOW_DATA_ATTR: false,
|
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();
|
return DOMPurify.sanitize(html, fallbackConfig as any).toString();
|
||||||
|
|||||||
Reference in New Issue
Block a user