背景
在网上找了一些wangEditor编辑器实现格式刷的代码,但是都是基于v4版本的,完全不适用于v5版本,所以只能根据文档和源码,自己实现了一个简易版本的
实现
第一步
注册新菜单(https://www.wangeditor.com/v5...)
格式刷内部主要实现,点击格式刷时保存选中文字的样式
// FormatterBtn.tsx
import type { IButtonMenu, IDomEditor } from '@wangeditor/editor';
import { SlateEditor } from '@wangeditor/editor';
class FormatterBtn implements IButtonMenu {
title = '格式刷';
tag = 'button'; // JS 语法
iconSvg = '<svg t="1665739261235" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6325" width="200" height="200"><path d="M1024 1012.48a2119.68 2119.68 0 0 1-45.312-409.6v-228.864a145.92 145.92 0 0 0-146.944-146.176h-128v-128a100.096 100.096 0 0 0-99.84-99.84h-182.016a100.608 100.608 0 0 0-100.352 99.84v128h-128a145.92 145.92 0 0 0-145.664 146.176v228.608a2139.904 2139.904 0 0 1-45.312 409.6L0 1024h1024zM421.888 99.84h182.016v128h-182.272v-128z m274.432 742.4l3.328-146.944a51.2 51.2 0 0 0-25.6-43.52 48.896 48.896 0 0 0-51.2 0 51.2 51.2 0 0 0-25.6 43.52l-2.304 121.344-4.352 69.12L588.8 921.6h-185.856a532.992 532.992 0 0 0 23.296-160.256 51.2 51.2 0 1 0-100.096 0A420.352 420.352 0 0 1 296.448 921.6H120.832l2.304-13.824a2033.92 2033.92 0 0 0 25.6-307.2v-39.168h728.576v67.072a2063.104 2063.104 0 0 0 25.6 281.6l2.304 13.824h-216.064zM877.312 460.8H148.48v-91.392a45.568 45.568 0 0 1 45.312-40.96h642.56a45.312 45.312 0 0 1 40.96 45.312z" p-id="6326"></path></svg>';
// 获取菜单执行时的 value ,用不到则返回空 字符串或 false
getValue(): string | boolean {
return '';
}
// 菜单是否需要激活(如选中加粗文本,“加粗”菜单会激活),用不到则返回 false
isActive(editor: IDomEditor & { copyStyleObject: any }): boolean {
return !!editor?.copyStyleObject;
}
// 菜单是否需要禁用(如选中 H1 ,“引用”菜单被禁用),用不到则返回 false
isDisabled(editor: IDomEditor): boolean {
return !editor.getSelectionText();
}
// 点击菜单时触发的函数
exec(editor: IDomEditor & { copyStyleObject: any }) {
if (this.isDisabled(editor)) {
return;
}
editor.copyStyleObject = SlateEditor.marks(editor);
}
}
export default FormatterBtn;
第二步
格式刷插入到菜单中,文档中使用insertKeys可以插入对应位置,我在代码中尝试没生效,只能采用笨办法,重写编辑器的菜单,你们可以再尝试下
第三步
监听编辑器内部鼠标按下和抬起来给选中内容赋值
import React, { useCallback, useEffect, useState, useMemo } from 'react';
import type { IButtonMenu, IDomEditor } from '@wangeditor/editor';
import { SlateEditor, Boot, SlateText } from '@wangeditor/editor';
import { Editor, Toolbar } from '@wangeditor/editor-for-react';
import FormatterBtn from './FormatterBtn';
import '@wangeditor/editor/dist/css/style.css'; // 引入 css
const defaultToolbarConfig = {
// 和编辑器默认的配置一样,只多加了格式刷
toolbarKeys: [
'headerSelect',
'blockquote',
'|',
'bold',
'underline',
'italic',
{
key: 'group-more-style', // 以 group 开头
title: '更多',
iconSvg:
'<svg viewBox="0 0 1024 1024"><path d="M204.8 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z"></path><path d="M505.6 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z"></path><path d="M806.4 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z"></path></svg>',
menuKeys: ['through', 'code', 'sup', 'sub', 'clearStyle'],
},
'color',
'bgColor',
'|',
'fontSize',
'fontFamily',
'lineHeight',
'|',
'bulletedList',
'numberedList',
'todo',
{
key: 'group-justify', // 以 group 开头
title: '两端对齐',
iconSvg:
'<svg viewBox="0 0 1024 1024"><path d="M768 793.6v102.4H51.2v-102.4h716.8z m204.8-230.4v102.4H51.2v-102.4h921.6z m-204.8-230.4v102.4H51.2v-102.4h716.8zM972.8 102.4v102.4H51.2V102.4h921.6z"></path></svg>',
menuKeys: ['justifyLeft', 'justifyRight', 'justifyCenter', 'justifyJustify'],
},
{
key: 'group-indent', // 以 group 开头
title: '缩进',
iconSvg:
'<svg viewBox="0 0 1024 1024"><path d="M0 64h1024v128H0z m384 192h640v128H384z m0 192h640v128H384z m0 192h640v128H384zM0 832h1024v128H0z m0-128V320l256 192z"></path></svg>',
menuKeys: ['indent', 'delIndent'],
},
'|',
'emotion',
'insertLink',
{
key: 'group-image', // 以 group 开头
title: '图片',
iconSvg:
'<svg viewBox="0 0 1024 1024"><path d="M959.877 128l0.123 0.123v767.775l-0.123 0.122H64.102l-0.122-0.122V128.123l0.122-0.123h895.775zM960 64H64C28.795 64 0 92.795 0 128v768c0 35.205 28.795 64 64 64h896c35.205 0 64-28.795 64-64V128c0-35.205-28.795-64-64-64zM832 288.01c0 53.023-42.988 96.01-96.01 96.01s-96.01-42.987-96.01-96.01S682.967 192 735.99 192 832 234.988 832 288.01zM896 832H128V704l224.01-384 256 320h64l224.01-192z"></path></svg>',
menuKeys: ['insertImage', 'uploadImage'],
},
{
key: 'group-video', // 以 group 开头
title: '视频',
iconSvg:
'<svg viewBox="0 0 1024 1024"><path d="M981.184 160.096C837.568 139.456 678.848 128 512 128S186.432 139.456 42.816 160.096C15.296 267.808 0 386.848 0 512s15.264 244.16 42.816 351.904C186.464 884.544 345.152 896 512 896s325.568-11.456 469.184-32.096C1008.704 756.192 1024 637.152 1024 512s-15.264-244.16-42.816-351.904zM384 704V320l320 192-320 192z"></path></svg>',
menuKeys: ['insertVideo', 'uploadVideo'],
},
'insertTable',
'codeBlock',
'divider',
'|',
'undo',
'redo',
'formatBrush',
'|',
'fullScreen',
],
};
const menu1Conf = {
key: 'formatBrush', // 定义 menu key :要保证唯一、不重复(重要)
factory() {
return new FormatterBtn() as IButtonMenu;
},
};
// 注册格式刷菜单
Boot.registerMenu(menu1Conf);
let isMouseDown = false;
const MyEditor = () => {
// editor 实例
const [editor, setEditor] = useState<(IDomEditor & { copyStyleObject?: any }) | null>(null); // TS 语法
const changeMouseDown = () => {
isMouseDown = true;
};
const changeMouseup = useCallback(() => {
const selectText = editor.getSelectionText();
if (isMouseDown && editor?.copyStyleObject && selectText) {
// 先清除选中节点的样式
// 获取所有 text node
const nodeEntries = SlateEditor.nodes(editor, {
match: (n) => SlateText.isText(n),
universal: true,
});
for (const nodeEntry of nodeEntries) {
// 单个 text node
const n = nodeEntry[0];
const keys = Object.keys(n as object);
keys.forEach((key) => {
if (key === 'text') {
// 保留 text 属性,text node 必须的
return;
}
// 其他属性,全部清除
SlateEditor.removeMark(editor, key);
});
}
// 再赋值新样式
Object.entries(editor.copyStyleObject).forEach(([key, value]) => {
if (key === 'text') {
// 保留 text 属性,text node 必须的
return;
}
editor.addMark(key, value);
});
editor.copyStyleObject = undefined;
isMouseDown = false;
}
}, [editor]);
// 因为本次需求编辑器在抽屉里 开不同的抽屉会注册多个编辑器,导致编辑器的id不是一个 监听时要监听对应的编辑器,如果只有一个编辑器,这块可以删除,监听直接用w-e-textarea-1
const domId = useMemo(() => {
return editor?.id?.split('-')?.[1] ? `w-e-textarea-${editor?.id?.split('-')?.[1]}` : undefined;
}, [editor]);
useEffect(() => {
if (domId) {
document.getElementById(domId)?.addEventListener('mousedown', changeMouseDown);
document.getElementById(domId)?.addEventListener('mouseup', changeMouseup);
}
}, [domId, changeMouseup]);
useEffect(() => {
return () => {
document.getElementById(domId)?.removeEventListener('mousedown', changeMouseDown);
document.getElementById(domId)?.removeEventListener('mouseup', changeMouseup);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div style={{ border: '1px solid #ccc', zIndex: 100 }}>
<Toolbar
editor={editor}
defaultConfig={defaultToolbarConfig}
style={{ borderBottom: '1px solid #ccc' }}
/>
<Editor onCreated={setEditor} />
</div>
);
};
export default MyEditor;
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。