translate comment of frontend/components/Editor

This commit is contained in:
david_bai
2025-06-22 08:09:13 +08:00
parent c47895b938
commit 9eec4f14c2
7 changed files with 63 additions and 63 deletions
+13 -13
View File
@@ -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'
);
+9 -9
View File
@@ -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) {