(
+ 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;
diff --git a/src/components/LexicalEditor/plugins/ToolbarPlugin.jsx b/src/components/LexicalEditor/plugins/ToolbarPlugin.jsx
index a258eff..ae436b0 100644
--- a/src/components/LexicalEditor/plugins/ToolbarPlugin.jsx
+++ b/src/components/LexicalEditor/plugins/ToolbarPlugin.jsx
@@ -39,6 +39,7 @@ import {
// InsertImagePayload,
} from './ImagesPlugin';
import {InsertInlineImageDialog} from './InlineImagePlugin';
+import FormatPainterToolbarButton from './FormatPaint/FormatPainterToolbarButton';
import useModal from './../hooks/useModal';
@@ -793,6 +794,7 @@ export default function ToolbarPlugin() {
aria-label='Redo'>
+
{supportedBlockTypes.has(blockType) && (
<>
diff --git a/src/components/LexicalEditor/styles.css b/src/components/LexicalEditor/styles.css
index 173bd1a..f065d4d 100644
--- a/src/components/LexicalEditor/styles.css
+++ b/src/components/LexicalEditor/styles.css
@@ -430,7 +430,8 @@ pre::-webkit-scrollbar-thumb {
}
.toolbar button.toolbar-item.active {
- background-color: rgba(223, 232, 250, 0.3);
+ /* background-color: rgba(223, 232, 250, 0.8); */
+ background-color: #eef2ff;
}
.toolbar button.toolbar-item.active i {
@@ -604,7 +605,8 @@ i.chevron-down {
background-size: contain;
}
button.item.dropdown-item-active {
- background-color: #dfe8fa4d;
+ /* background-color: #dfe8fa4d; */
+ background-color: #eef2ff;
}
.dropdown .item:first-child {
@@ -836,6 +838,10 @@ i.outdent {
background-image: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='16'%20height='16'%20fill='currentColor'%20class='bi%20bi-text-indent-right'%3e%3cpath%20d='M2%203.5a.5.5%200%200%201%20.5-.5h11a.5.5%200%200%201%200%201h-11a.5.5%200%200%201-.5-.5zm10.646%202.146a.5.5%200%200%201%20.708.708L11.707%208l1.647%201.646a.5.5%200%200%201-.708.708l-2-2a.5.5%200%200%201%200-.708l2-2zM2%206.5a.5.5%200%200%201%20.5-.5h6a.5.5%200%200%201%200%201h-6a.5.5%200%200%201-.5-.5zm0%203a.5.5%200%200%201%20.5-.5h6a.5.5%200%200%201%200%201h-6a.5.5%200%200%201-.5-.5zm0%203a.5.5%200%200%201%20.5-.5h11a.5.5%200%200%201%200%201h-11a.5.5%200%200%201-.5-.5z'/%3e%3c/svg%3e")
}
+i.painter {
+ background-image: url(/images/icons/paint-brush-line.svg);
+}
+
i.bold {
background-image: url(/images/icons/type-bold.svg);
}