Compare commits
2 Commits
030aac7846
...
4bbc14d165
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bbc14d165 | ||
|
|
d489078721 |
@@ -2,10 +2,10 @@
|
|||||||
"allowedTags": [
|
"allowedTags": [
|
||||||
"p", "br", "div", "span", "h1", "h2", "h3", "h4", "h5", "h6",
|
"p", "br", "div", "span", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||||
"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",
|
"sup", "sub", "small", "big", "mark", "pre", "code", "kbd", "samp", "var",
|
||||||
"ul", "ol", "li", "dl", "dt", "dd",
|
"ul", "ol", "li", "dl", "dt", "dd",
|
||||||
"a", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "caption",
|
"a", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "caption", "colgroup", "col",
|
||||||
"blockquote", "cite", "q", "hr"
|
"blockquote", "cite", "q", "hr", "details", "summary"
|
||||||
],
|
],
|
||||||
"allowedAttributes": {
|
"allowedAttributes": {
|
||||||
"p": ["class", "style"],
|
"p": ["class", "style"],
|
||||||
@@ -18,9 +18,21 @@
|
|||||||
"h5": ["class", "style"],
|
"h5": ["class", "style"],
|
||||||
"h6": ["class", "style"],
|
"h6": ["class", "style"],
|
||||||
"a": ["class"],
|
"a": ["class"],
|
||||||
"table": ["class"],
|
"table": ["class", "style"],
|
||||||
"td": ["class", "colspan", "rowspan"],
|
"th": ["class", "style", "colspan", "rowspan"],
|
||||||
"th": ["class", "colspan", "rowspan"]
|
"td": ["class", "style", "colspan", "rowspan"],
|
||||||
|
"tr": ["class", "style"],
|
||||||
|
"thead": ["class", "style"],
|
||||||
|
"tbody": ["class", "style"],
|
||||||
|
"tfoot": ["class", "style"],
|
||||||
|
"ul": ["class", "style"],
|
||||||
|
"ol": ["class", "style", "start", "type"],
|
||||||
|
"li": ["class", "style"],
|
||||||
|
"blockquote": ["class", "style"],
|
||||||
|
"pre": ["class", "style"],
|
||||||
|
"code": ["class", "style"],
|
||||||
|
"details": ["class", "style"],
|
||||||
|
"summary": ["class", "style"]
|
||||||
},
|
},
|
||||||
"allowedCssProperties": [
|
"allowedCssProperties": [
|
||||||
"color", "background-color", "font-size", "font-weight",
|
"color", "background-color", "font-size", "font-weight",
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useRef } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { Textarea } from '../ui/Input';
|
import { Textarea } from '../ui/Input';
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
|
import { sanitizeHtmlSync, preloadSanitizationConfig } from '../../lib/sanitization';
|
||||||
|
|
||||||
interface RichTextEditorProps {
|
interface RichTextEditorProps {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -20,6 +21,12 @@ export default function RichTextEditor({
|
|||||||
const [viewMode, setViewMode] = useState<'visual' | 'html'>('visual');
|
const [viewMode, setViewMode] = useState<'visual' | 'html'>('visual');
|
||||||
const [htmlValue, setHtmlValue] = useState(value);
|
const [htmlValue, setHtmlValue] = useState(value);
|
||||||
const previewRef = useRef<HTMLDivElement>(null);
|
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 handleVisualChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
const plainText = e.target.value;
|
const plainText = e.target.value;
|
||||||
@@ -34,6 +41,61 @@ export default function RichTextEditor({
|
|||||||
setHtmlValue(htmlContent);
|
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 handleHtmlChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
const html = e.target.value;
|
const html = e.target.value;
|
||||||
setHtmlValue(html);
|
setHtmlValue(html);
|
||||||
@@ -137,8 +199,10 @@ export default function RichTextEditor({
|
|||||||
<div className="border theme-border rounded-b-lg overflow-hidden">
|
<div className="border theme-border rounded-b-lg overflow-hidden">
|
||||||
{viewMode === 'visual' ? (
|
{viewMode === 'visual' ? (
|
||||||
<Textarea
|
<Textarea
|
||||||
|
ref={visualTextareaRef}
|
||||||
value={getPlainText(value)}
|
value={getPlainText(value)}
|
||||||
onChange={handleVisualChange}
|
onChange={handleVisualChange}
|
||||||
|
onPaste={handlePaste}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
rows={12}
|
rows={12}
|
||||||
className="border-0 rounded-none focus:ring-0"
|
className="border-0 rounded-none focus:ring-0"
|
||||||
@@ -172,11 +236,12 @@ export default function RichTextEditor({
|
|||||||
|
|
||||||
<div className="text-xs theme-text">
|
<div className="text-xs theme-text">
|
||||||
<p>
|
<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>
|
||||||
<p>
|
<p>
|
||||||
<strong>HTML mode:</strong> Write HTML directly for advanced formatting.
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user