Improve RichTextEditor to preserve formatting on paste
**Enhanced Visual Mode:** - Add paste event handler that preserves HTML formatting when pasting - Integrate with shared sanitization configuration for consistent filtering - Preload sanitization config for optimal performance - Support for bold, italic, and other basic formatting in visual mode **Updated Sanitization Config:** - Add more useful HTML tags: kbd, samp, var, details, summary, colgroup, col - Add attributes for better table support: start, type for ol - Add style attributes for more elements: table, ul, ol, li, blockquote, pre, code - Maintain security while allowing richer content formatting **User Experience:** - Users can now paste formatted content (bold, italic, lists, etc.) in visual mode - Content is automatically sanitized using backend configuration - Updated help text to reflect new capabilities - Maintains backward compatibility with plain text input **Technical Improvements:** - Async clipboard API support with fallbacks - Error handling for paste operations - Consistent sanitization between manual input and paste operations Resolves issue where pasted formatted content was stripped to plain text in visual mode. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef } from 'react';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Textarea } from '../ui/Input';
|
||||
import Button from '../ui/Button';
|
||||
import { sanitizeHtmlSync, preloadSanitizationConfig } from '../../lib/sanitization';
|
||||
|
||||
interface RichTextEditorProps {
|
||||
value: string;
|
||||
@@ -20,6 +21,12 @@ export default function RichTextEditor({
|
||||
const [viewMode, setViewMode] = useState<'visual' | 'html'>('visual');
|
||||
const [htmlValue, setHtmlValue] = useState(value);
|
||||
const previewRef = useRef<HTMLDivElement>(null);
|
||||
const visualTextareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
// Preload sanitization config
|
||||
useEffect(() => {
|
||||
preloadSanitizationConfig().catch(console.error);
|
||||
}, []);
|
||||
|
||||
const handleVisualChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const plainText = e.target.value;
|
||||
@@ -34,6 +41,61 @@ export default function RichTextEditor({
|
||||
setHtmlValue(htmlContent);
|
||||
};
|
||||
|
||||
const handlePaste = async (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
||||
if (viewMode !== 'visual') return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
// Try to get HTML content from clipboard
|
||||
const items = e.clipboardData?.items;
|
||||
let htmlContent = '';
|
||||
let plainText = '';
|
||||
|
||||
if (items) {
|
||||
for (const item of Array.from(items)) {
|
||||
if (item.type === 'text/html') {
|
||||
htmlContent = await new Promise<string>((resolve) => {
|
||||
item.getAsString(resolve);
|
||||
});
|
||||
} else if (item.type === 'text/plain') {
|
||||
plainText = await new Promise<string>((resolve) => {
|
||||
item.getAsString(resolve);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we have HTML content, sanitize it and merge with current content
|
||||
if (htmlContent) {
|
||||
const sanitizedHtml = sanitizeHtmlSync(htmlContent);
|
||||
|
||||
// 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);
|
||||
}
|
||||
} 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>);
|
||||
}
|
||||
};
|
||||
|
||||
const handleHtmlChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const html = e.target.value;
|
||||
setHtmlValue(html);
|
||||
@@ -137,8 +199,10 @@ export default function RichTextEditor({
|
||||
<div className="border theme-border rounded-b-lg overflow-hidden">
|
||||
{viewMode === 'visual' ? (
|
||||
<Textarea
|
||||
ref={visualTextareaRef}
|
||||
value={getPlainText(value)}
|
||||
onChange={handleVisualChange}
|
||||
onPaste={handlePaste}
|
||||
placeholder={placeholder}
|
||||
rows={12}
|
||||
className="border-0 rounded-none focus:ring-0"
|
||||
@@ -172,11 +236,12 @@ export default function RichTextEditor({
|
||||
|
||||
<div className="text-xs theme-text">
|
||||
<p>
|
||||
<strong>Visual mode:</strong> Write in plain text, paragraphs will be automatically formatted.
|
||||
<strong>Visual mode:</strong> Write in plain text or paste formatted content.
|
||||
Bold, italic, and other basic formatting will be preserved when pasting.
|
||||
</p>
|
||||
<p>
|
||||
<strong>HTML mode:</strong> Write HTML directly for advanced formatting.
|
||||
Allowed tags: p, br, strong, em, ul, ol, li, h1-h6, blockquote.
|
||||
Allowed tags: p, br, div, span, strong, em, b, i, u, s, h1-h6, ul, ol, li, blockquote, and more.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user