import React, { useEffect, useState, useCallback, useRef } from 'react'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { $getSelection, $isRangeSelection, $isTextNode, TextFormatType, LexicalEditor, COMMAND_PRIORITY_NORMAL, COMMAND_PRIORITY_LOW, } from 'lexical'; import { $patchStyleText, } from '@lexical/selection'; // $patchStyleText is more efficient for merging styles. import { CopiedFormat, ACTIVATE_FORMAT_PAINTER_COMMAND, DEACTIVATE_FORMAT_PAINTER_COMMAND, FORMAT_PAINTER_STATE_UPDATE_COMMAND, ActivateFormatPainterPayload, FormatPainterState, } from './FormatPainterCommands'; // parse style string to object for $patchStyleText function parseStyleText(style: string): Record { const styleObj: Record = {}; style.split(';').forEach((rule) => { const [key, value] = rule.split(':'); if (key && value && key.trim() && value.trim()) { styleObj[key.trim()] = value.trim(); } }); return styleObj; } // map format flags to TextFormatType const textFormatTypeMap: { flag: number; type: TextFormatType }[] = [ { flag: 1, type: 'bold' }, { flag: 2, type: 'italic' }, { flag: 4, type: 'strikethrough' }, { flag: 8, type: 'underline' }, { flag: 16, type: 'code' }, { flag: 32, type: 'subscript' }, { flag: 64, type: 'superscript' }, ]; export function FormatPainterPlugin(): null { const [editor] = useLexicalComposerContext(); const [copiedFormat, setCopiedFormat] = useState(null); const [isActive, setIsActive] = useState(false); const [isSticky, setIsSticky] = useState(false); // 避免多次复制 const isPickingUpRef = useRef(false); const broadcastState = useCallback(() => { editor.dispatchCommand(FORMAT_PAINTER_STATE_UPDATE_COMMAND, { isActive, isSticky, }); }, [editor, isActive, isSticky]); // Update broadcast whenever state changes useEffect(() => { broadcastState(); }, [isActive, isSticky, broadcastState]); // Activate Format Painter (Copy Format) useEffect(() => { return editor.registerCommand( ACTIVATE_FORMAT_PAINTER_COMMAND, (payload) => { isPickingUpRef.current = true; editor.update(() => { const selection = $getSelection(); if ($isRangeSelection(selection) && !selection.isCollapsed()) { const anchorNode = selection.anchor.getNode(); let formatToCopy: CopiedFormat | null = null; if ($isTextNode(anchorNode)) { formatToCopy = { textFormatFlags: anchorNode.getFormat(), style: anchorNode.getStyle(), }; } else { // ? todo: 从第一个字符获取格式 const nodes = selection.getNodes(); for (const node of nodes) { if ($isTextNode(node)) { formatToCopy = { textFormatFlags: node.getFormat(), style: node.getStyle(), }; break; } } } if (formatToCopy) { setCopiedFormat(formatToCopy); setIsActive(true); setIsSticky(payload.sticky); // console.log('Format Painter Activated. Sticky:', payload.sticky, 'Format:', formatToCopy); } } }); // 鼠标抬起 setTimeout(() => { isPickingUpRef.current = false; }, 50); return true; }, COMMAND_PRIORITY_NORMAL, ); }, [editor]); // Deactivate Format Painter useEffect(() => { return editor.registerCommand( DEACTIVATE_FORMAT_PAINTER_COMMAND, () => { if (!isActive) return false; setIsActive(false); setIsSticky(false); // 不保留 setCopiedFormat(null); // console.log('Format Painter Deactivated.'); return true; }, COMMAND_PRIORITY_NORMAL, ); }, [editor, isActive]); // 应用复制的格式 const applyFormat = useCallback(() => { if (!isActive || !copiedFormat || isPickingUpRef.current) { return false; } editor.update(() => { const selection = $getSelection(); if ($isRangeSelection(selection) && copiedFormat) { // console.log('copiedFormat:', copiedFormat, '\ntextFormatTypeMap:', textFormatTypeMap); // TextNode (bold, italic, ...) textFormatTypeMap.forEach(fmt => { if (copiedFormat.textFormatFlags & fmt.flag) { selection.formatText(fmt.type); } else { const currentSelection = $getSelection(); if ($isRangeSelection(currentSelection)) { textFormatTypeMap.forEach(fmt => { const shouldHaveFormat = (copiedFormat.textFormatFlags & fmt.flag) > 0; if (currentSelection.hasFormat(fmt.type) !== shouldHaveFormat) { currentSelection.formatText(fmt.type); } }); } } }); // ensure applied let newSelection = $getSelection(); if ($isRangeSelection(newSelection)) { textFormatTypeMap.forEach(fmt => { if (copiedFormat.textFormatFlags & fmt.flag) { if (!newSelection!.hasFormat(fmt.type)) newSelection!.formatText(fmt.type); } else { if (newSelection!.hasFormat(fmt.type)) newSelection!.formatText(fmt.type); } }); } // inline styles (font-family, color, font-size, ...) const stylesToApply = parseStyleText(copiedFormat.style); // console.log('inline style', stylesToApply); if (Object.keys(stylesToApply).length > 0) { newSelection = $getSelection(); if ($isRangeSelection(newSelection)) { $patchStyleText(newSelection as any, stylesToApply); } } else { // 清除格式 const selectedNodes = newSelection.getNodes(); selectedNodes.forEach(node => { if ($isTextNode(node)) { if (node.getStyle() !== "") { node.setStyle(""); // 清除 } } // todo:

node }); } // console.log('Format Applied. Sticky:', isSticky); if (!isSticky) { setIsActive(false); } } }); return true; }, [editor, isActive, isSticky, copiedFormat]); // 鼠标抬起 useEffect(() => { if (!isActive || !copiedFormat) return; const editorElement = editor.getRootElement(); if (!editorElement) return; const handleMouseUp = (event: MouseEvent) => { if (isPickingUpRef.current) return; if (editorElement.contains(event.target as Node)) { // todo: 改为在下一帧更新 setTimeout(() => { const selection = editor.getEditorState().read($getSelection); if ($isRangeSelection(selection) && !selection.isCollapsed()) { applyFormat(); } else if ($isRangeSelection(selection) && selection.isCollapsed()) { // 折叠的选区, 也应用 // applyFormat(); } }, 0); } }; document.addEventListener('mouseup', handleMouseUp); return () => { document.removeEventListener('mouseup', handleMouseUp); }; }, [editor, isActive, copiedFormat, applyFormat]); // 按 esc 键取消格式 useEffect(() => { if (!isActive) return; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { editor.dispatchCommand(DEACTIVATE_FORMAT_PAINTER_COMMAND, undefined); } }; document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('keydown', handleKeyDown); }; }, [editor, isActive]); // 鼠标样式 useEffect(() => { const editorElement = editor.getRootElement(); if (editorElement) { editorElement.style.cursor = isActive ? 'copy' : 'auto'; } return () => { if (editorElement) { editorElement.style.cursor = 'auto'; } }; }, [editor, isActive]); return null; } export default FormatPainterPlugin;