translate comment of frontend/components/Editor
This commit is contained in:
@@ -19,10 +19,10 @@ const RichTextEditor: React.FC<EditorProps> = ({ onChange, value = '' }) => {
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
//在挂载后更新编辑区内容,监听外部 value 变化
|
||||
// Update editor content after mounting, listen for external value changes
|
||||
useEffect(() => {
|
||||
if (isMounted && editorRef.current && !isInternalChange.current) {
|
||||
// 只有当内容真正不同时才更新
|
||||
// Only update when the content is truly different
|
||||
if (editorRef.current.innerHTML !== value) {
|
||||
editorRef.current.innerHTML = value;
|
||||
setHtml(value);
|
||||
@@ -31,11 +31,11 @@ const RichTextEditor: React.FC<EditorProps> = ({ onChange, value = '' }) => {
|
||||
isInternalChange.current = false;
|
||||
}, [value, isMounted]);
|
||||
|
||||
// 处理内容变化
|
||||
// Handle content change
|
||||
const handleChange = useCallback(() => {
|
||||
if (editorRef.current) {
|
||||
const content = (editorRef.current as HTMLDivElement).innerHTML;
|
||||
if (content !== html) {// 如果内容没有变化,不触发更新
|
||||
if (content !== html) {// If the content has not changed, do not trigger an update
|
||||
isInternalChange.current = true;
|
||||
setHtml(content);
|
||||
onChange(content);
|
||||
@@ -54,7 +54,7 @@ const RichTextEditor: React.FC<EditorProps> = ({ onChange, value = '' }) => {
|
||||
|
||||
const getSelection = useSelection();
|
||||
const { findStyleParent } = useStyleManagement(editorRef);
|
||||
// 检查当前选中文本的样式
|
||||
// Check the style of the currently selected text
|
||||
const isStyleActive = useCallback((style: string): boolean => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
const selectionInfo = getSelection();
|
||||
@@ -68,7 +68,7 @@ const RichTextEditor: React.FC<EditorProps> = ({ onChange, value = '' }) => {
|
||||
}, [findStyleParent, getSelection]);
|
||||
|
||||
const handlePaste = useCallback((e: CustomClipboardEvent) => {
|
||||
// 处理图片粘贴
|
||||
// Handle image pasting
|
||||
if (Array.from(e.clipboardData.items).some(item => item.type.indexOf('image') !== -1)) {
|
||||
const items = Array.from(e.clipboardData.items);
|
||||
const imageItem = items.find(item => item.type.indexOf('image') !== -1);
|
||||
@@ -100,7 +100,7 @@ const RichTextEditor: React.FC<EditorProps> = ({ onChange, value = '' }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理普通文本
|
||||
// Handle plain text
|
||||
e.preventDefault();
|
||||
const text = e.clipboardData.getData('text/plain');
|
||||
if (typeof document !== 'undefined') {
|
||||
@@ -115,16 +115,16 @@ const RichTextEditor: React.FC<EditorProps> = ({ onChange, value = '' }) => {
|
||||
return (
|
||||
<div className="w-full space-x-2 mb-4">
|
||||
<div className="border rounded-lg shadow-sm overflow-hidden">
|
||||
{/* 工具栏 - 添加浅灰色背景和底部边框 */}
|
||||
{/* Toolbar - Add light gray background and bottom border */}
|
||||
<div className="flex flex-wrap gap-1 p-2 bg-gray-50 border-b">
|
||||
{/* 基础格式工具组 */}
|
||||
{/* Basic format tool group */}
|
||||
<BasicFormatTools
|
||||
isStyleActive={isStyleActive}
|
||||
formatText={formatText}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 字体相关选择器组 */}
|
||||
{/* Font-related selector group */}
|
||||
<FontTools
|
||||
fontFamilies={fontFamilies}
|
||||
fontSizes={fontSizes}
|
||||
@@ -133,11 +133,11 @@ const RichTextEditor: React.FC<EditorProps> = ({ onChange, value = '' }) => {
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 对齐工具组 */}
|
||||
{/* Alignment tool group */}
|
||||
<AlignmentTools alignText={alignText} />
|
||||
<Divider />
|
||||
|
||||
{/* 插入工具组 */}
|
||||
{/* Insert tool group */}
|
||||
<InsertTools
|
||||
insertLink={insertLink}
|
||||
insertImage={insertImage}
|
||||
@@ -145,7 +145,7 @@ const RichTextEditor: React.FC<EditorProps> = ({ onChange, value = '' }) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 编辑区域 - 添加纯白背景和内部阴影效果 */}
|
||||
{/* Editor area - Add pure white background and inner shadow effect */}
|
||||
<div
|
||||
ref={editorRef}
|
||||
className="p-4 min-h-[200px] md:min-h-[400px] focus:outline-none bg-white shadow-inner"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { SelectMenuProps } from '../types';
|
||||
// 下拉选择组件
|
||||
// Dropdown selection component
|
||||
export const SelectMenu: React.FC<SelectMenuProps> = ({
|
||||
options,
|
||||
onChange,
|
||||
|
||||
@@ -12,7 +12,7 @@ export const useEditorCommands = (
|
||||
const getSelection = useSelection();
|
||||
const { findStyleParent, cleanupSpan } = useStyleManagement(editorRef);
|
||||
|
||||
// Format text (bold, italic, underline)--格式化文本
|
||||
// Format text (bold, italic, underline)
|
||||
const formatText = useCallback((format: FormatType) => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
@@ -24,10 +24,10 @@ export const useEditorCommands = (
|
||||
const styleParent = findStyleParent(selection.anchorNode as DOMNodeWithStyle, styleMap[format]);
|
||||
|
||||
if (styleParent) {
|
||||
// 移除样式
|
||||
// Remove style
|
||||
removeStyle(styleParent, format);
|
||||
} else {
|
||||
// 添加样式
|
||||
// Add style
|
||||
const span = document.createElement('span');
|
||||
|
||||
switch (format) {
|
||||
@@ -42,7 +42,7 @@ export const useEditorCommands = (
|
||||
break;
|
||||
}
|
||||
|
||||
// 如果选中的内容在一个span内,且该span没有目标样式,直接添加样式
|
||||
// If the selected content is within a span and that span does not have the target style, add the style directly
|
||||
const parentElement = selection.anchorNode?.parentElement;
|
||||
if (parentElement &&
|
||||
parentElement.tagName === 'SPAN' &&
|
||||
@@ -52,36 +52,36 @@ export const useEditorCommands = (
|
||||
(parentElement as StyledElement).style[styleMap[format]] = span.style[styleMap[format]];
|
||||
|
||||
} else {
|
||||
// 否则创建新的span
|
||||
// Otherwise, create a new span
|
||||
span.appendChild(range.extractContents());
|
||||
range.insertNode(span);
|
||||
}
|
||||
}
|
||||
// 保持选区
|
||||
// Maintain selection
|
||||
const newRange = document.createRange();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(newRange);
|
||||
|
||||
// 更新 HTML
|
||||
// Update HTML
|
||||
handleChange();
|
||||
}, [findStyleParent, getSelection, removeStyle]);
|
||||
|
||||
// Align text--对齐文本
|
||||
// Align text
|
||||
const alignText = useCallback((alignment: AlignmentType) => {
|
||||
if (!editorRef.current || typeof window === 'undefined') return;
|
||||
|
||||
const selectionInfo = getSelection();
|
||||
if (!selectionInfo) return;
|
||||
|
||||
// 找到当前文本节点或其容器
|
||||
// Find the current text node or its container
|
||||
let textNode = selectionInfo.selection.anchorNode as DOMNodeWithStyle;
|
||||
|
||||
// 如果是文本节点,获取其父元素
|
||||
// If it is a text node, get its parent element
|
||||
if (textNode.nodeType === 3) {
|
||||
textNode = textNode.parentElement as DOMNodeWithStyle;
|
||||
}
|
||||
|
||||
// 向外查找最外层的样式容器(例如带有颜色或大小的span)
|
||||
// Search outwards for the outermost style container (e.g., a span with color or size)
|
||||
let outerContainer = textNode;
|
||||
while (
|
||||
outerContainer.parentElement &&
|
||||
@@ -91,25 +91,25 @@ export const useEditorCommands = (
|
||||
outerContainer = outerContainer.parentElement as DOMNodeWithStyle;
|
||||
}
|
||||
|
||||
// 创建或找到div容器来处理对齐
|
||||
// Create or find a div container to handle alignment
|
||||
let alignmentContainer: HTMLElement;
|
||||
if (
|
||||
outerContainer.parentElement === editorRef.current ||
|
||||
(outerContainer.parentElement as HTMLElement).tagName !== 'DIV'
|
||||
) {
|
||||
// 需要创建新的对齐容器
|
||||
// A new alignment container needs to be created
|
||||
alignmentContainer = document.createElement('div');
|
||||
alignmentContainer.style.textAlign = alignment;
|
||||
// 包装现有内容
|
||||
// Wrap existing content
|
||||
outerContainer.parentElement?.insertBefore(alignmentContainer, outerContainer);
|
||||
alignmentContainer.appendChild(outerContainer);
|
||||
} else {
|
||||
// 使用已存在的对齐容器
|
||||
// Use the existing alignment container
|
||||
alignmentContainer = outerContainer.parentElement as HTMLElement;
|
||||
alignmentContainer.style.textAlign = alignment;
|
||||
}
|
||||
|
||||
// 更新 HTML
|
||||
// Update HTML
|
||||
handleChange();
|
||||
}, [getSelection]);
|
||||
|
||||
@@ -119,22 +119,22 @@ export const useEditorCommands = (
|
||||
const selectionInfo = getSelection();
|
||||
if (!selectionInfo || !selectionInfo.selection.toString()) return;
|
||||
const { selection, range } = selectionInfo;
|
||||
// 映射样式类型到实际的 CSS 属性名
|
||||
// Map style type to actual CSS property name
|
||||
const stylePropertyMap = {
|
||||
'family': 'fontFamily',
|
||||
'size': 'fontSize',
|
||||
'color': 'color'
|
||||
};
|
||||
const styleProperty = stylePropertyMap[type];
|
||||
// 获取选中内容的范围
|
||||
// Get the range of the selected content
|
||||
const rangeContent = range.cloneContents();
|
||||
// 检查选中内容是否包含块级<p> / <div>元素
|
||||
// Check if the selected content contains block-level <p> / <div> elements
|
||||
const containsBlock = Array.from(rangeContent.childNodes).some(
|
||||
node => node.nodeType === 1 && ['P', 'DIV'].includes((node as HTMLElement).tagName)
|
||||
);
|
||||
|
||||
if (containsBlock) {
|
||||
// 如果选中内容包含块级元素,遍历处理每个块级元素内的文本
|
||||
// If the selected content includes block-level elements, iterate through and process the text within each block-level element
|
||||
const blocks = Array.from(rangeContent.childNodes).filter(
|
||||
node => node.nodeType === 1 && ['P', 'DIV'].includes((node as HTMLElement).tagName)
|
||||
);
|
||||
@@ -151,7 +151,7 @@ export const useEditorCommands = (
|
||||
}
|
||||
|
||||
textNodes.forEach(textNode => {
|
||||
// 检查父元素是否已经是span
|
||||
// Check if the parent element is already a span
|
||||
const parent = textNode.parentNode as HTMLElement;
|
||||
if (parent.tagName === 'SPAN') {
|
||||
(parent as StyledElement).style[styleProperty] = value;
|
||||
@@ -163,11 +163,11 @@ export const useEditorCommands = (
|
||||
}
|
||||
});
|
||||
});
|
||||
// 清除原有内容并插入新内容
|
||||
// Clear the original content and insert new content
|
||||
range.deleteContents();
|
||||
range.insertNode(rangeContent);
|
||||
} else {
|
||||
// 如果是普通文本,使用原来的逻辑
|
||||
// If it's plain text, use the original logic
|
||||
let styleParent = findStyleParent(selection.anchorNode as DOMNodeWithStyle, styleProperty);
|
||||
if (styleParent && !['P', 'DIV'].includes(styleParent.tagName)) {
|
||||
if (value === 'inherit') {
|
||||
@@ -177,7 +177,7 @@ export const useEditorCommands = (
|
||||
styleParent.style[styleProperty] = value;
|
||||
}
|
||||
} else {
|
||||
// 否则创建新的 span
|
||||
// Otherwise, create a new span
|
||||
const span = document.createElement('span') as StyledElement;
|
||||
span.style[styleProperty] = value;
|
||||
span.appendChild(range.extractContents());
|
||||
@@ -185,7 +185,7 @@ export const useEditorCommands = (
|
||||
}
|
||||
}
|
||||
|
||||
// 保持选区
|
||||
// Maintain selection
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
|
||||
@@ -197,17 +197,17 @@ export const useEditorCommands = (
|
||||
const selection = window.getSelection();
|
||||
let text = "test";
|
||||
if (selection && !selection.isCollapsed) {
|
||||
// 如果有选中文本,则使用选中的文本作为链接文字
|
||||
// If there is selected text, use the selected text as the link text
|
||||
text = selection.toString();
|
||||
}
|
||||
|
||||
// 使用一个prompt,用空格分隔链接和文字
|
||||
// Use a prompt to separate the link and text with a space
|
||||
const input = prompt('Please enter the link address and text (separated by space):', `https:// ${text}`);
|
||||
|
||||
if (input) {
|
||||
// 分割输入得到url和text
|
||||
// Split the input to get the url and text
|
||||
const [url, ...textParts] = input.split(' ');
|
||||
const text = textParts.join(' '); // 处理文字中可能包含空格的情况
|
||||
const text = textParts.join(' '); // Handle cases where the text may contain spaces
|
||||
|
||||
if (url && text) {
|
||||
const selectionInfo = getSelection();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback } from 'react';
|
||||
import { SelectionInfo } from '../types';
|
||||
|
||||
export const useSelection = () => {
|
||||
// 获取选区
|
||||
// Get selection
|
||||
return useCallback((): SelectionInfo | null => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
const selection = window.getSelection();
|
||||
|
||||
@@ -2,11 +2,11 @@ import { useCallback } from 'react';
|
||||
import { DOMNodeWithStyle, StyledElement } from '../types';
|
||||
|
||||
export const useStyleManagement = (editorRef: React.RefObject<HTMLDivElement>) => {
|
||||
// 查找拥有指定样式的最近父元素
|
||||
// Find the nearest parent element with the specified style
|
||||
const findStyleParent = useCallback((node: DOMNodeWithStyle, styleType: string): StyledElement | null => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
let current = node;
|
||||
// 如果当前节点是文本节点,从其父节点开始查找
|
||||
// If the current node is a text node, start searching from its parent node
|
||||
if (current.nodeType === 3) {
|
||||
current = current.parentElement as DOMNodeWithStyle;
|
||||
}
|
||||
@@ -23,15 +23,15 @@ export const useStyleManagement = (editorRef: React.RefObject<HTMLDivElement>) =
|
||||
}
|
||||
return null;
|
||||
}, [editorRef]);
|
||||
// 清理空的或只有继承值的 span 标签
|
||||
// Clean up empty span tags or those with only inherited values
|
||||
const cleanupSpan = useCallback((span: StyledElement | null) => {
|
||||
// 首先检查 span 是否存在
|
||||
// First, check if the span exists
|
||||
if (!span) return;
|
||||
|
||||
// 然后检查 editorRef.current 是否存在,并进行比较
|
||||
// 修改比较逻辑,使用 HTMLElement 作为共同基类进行比较
|
||||
// Then check if editorRef.current exists and compare
|
||||
// Modify the comparison logic, using HTMLElement as a common base class for comparison
|
||||
if (editorRef.current && span.contains(editorRef.current)) return;
|
||||
// 检查是否只有 inherit 值或没有样式
|
||||
// Check if there are only inherit values or no styles
|
||||
const hasOnlyInherit = Array.from(span.style).every(
|
||||
style => !span.style[style] || span.style[style] === 'inherit'
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// 选项类型定义
|
||||
// Option type definition
|
||||
export interface StyleOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
// 选择菜单组件的 props 类型
|
||||
// Props type for the SelectMenu component
|
||||
export interface SelectMenuProps {
|
||||
options: StyleOption[];
|
||||
onChange: (value: string) => void;
|
||||
@@ -13,27 +13,27 @@ export interface SelectMenuProps {
|
||||
className: string;
|
||||
}
|
||||
|
||||
// 编辑器内部使用的类型
|
||||
// Types used internally by the editor
|
||||
export interface SelectionInfo {
|
||||
selection: Selection;
|
||||
range: Range;
|
||||
}
|
||||
|
||||
// 样式格式类型
|
||||
// Style format type
|
||||
export type FormatType = 'bold' | 'italic' | 'underline';
|
||||
|
||||
// 对齐方式类型
|
||||
// Alignment type
|
||||
export type AlignmentType = 'left' | 'center' | 'right';
|
||||
|
||||
// 字体样式类型
|
||||
// Font style type
|
||||
export type FontStyleType = 'family' | 'size' | 'color';
|
||||
|
||||
// 粘贴事件处理函数类型
|
||||
// Paste event handler function type
|
||||
export interface CustomClipboardEvent extends React.ClipboardEvent<HTMLDivElement> {
|
||||
clipboardData: DataTransfer;
|
||||
}
|
||||
|
||||
// 扩展 HTMLElement 以支持我们需要的样式属性
|
||||
// Extend HTMLElement to support the style properties we need
|
||||
export interface StyledElement extends HTMLElement {
|
||||
style: CSSStyleDeclaration & {
|
||||
[key: string]: string;
|
||||
@@ -44,7 +44,7 @@ export interface StyledElement extends HTMLElement {
|
||||
firstChild: ChildNode | null;
|
||||
}
|
||||
|
||||
// 修改DOM节点类型定义
|
||||
// Modify DOM node type definition
|
||||
export interface DOMNodeWithStyle extends Node {
|
||||
nodeType: number;
|
||||
parentElement: HTMLElement & {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { FormatType, StyledElement } from '../types';
|
||||
import { styleMap } from '../constants';
|
||||
// 移除样式
|
||||
// Remove style
|
||||
export const removeStyle = (element: StyledElement, style: FormatType) => {
|
||||
element.style[styleMap[style]] = '';// 移除指定样式
|
||||
// 如果span没有其他样式,则移除span标签
|
||||
element.style[styleMap[style]] = '';// Remove the specified style
|
||||
// If the span has no other styles, remove the span tag
|
||||
if (element.tagName === 'SPAN' && !element.getAttribute('style')) {
|
||||
const parent = element.parentNode;
|
||||
while (element.firstChild) {
|
||||
|
||||
Reference in New Issue
Block a user