前言
最近呢,公司需要对正在使用的wangeditor编辑器增加一个格式刷功能,个人觉得wangEditor是一款体验不错的富文本编辑器,还有详细的中文文档,使用起来也十分的便捷。
实现效果:
实现过程
1. 首先通过官方文档写一个类
https://www.wangeditor.com/v5/development.html#droppanelmenu 这个我就不细说比较简单,根据文档直接复制粘贴就行,区分下ts和js写法就行。
2. 注册菜单
import { Boot } from '@wangeditor/editor'
const menu1Conf = {
key: 'menu1', // 定义 menu key :要保证唯一、不重复(重要)
factory() {
return new YourMenuClass() // 把 `YourMenuClass` 替换为你菜单的 class
},
}
Boot.registerMenu(menu1Conf);
3. 注入到工具栏
toolbarConfig.insertKeys = {
index: 5, // 插入的位置,基于当前的 toolbarKeys
keys: ['menu1']//这里注册名跟你注册时定义的key要一致
}
4. 具体实现
我们运用 SlateEditor.nodes
定位所选内容,并使用 SlateEditor.marks
获取文本样式。通过 SlateEditor.removeMark
,我们取消了文本的样式标记,然后用 SlateEditor.addMark
为文本添加了新样式标记。这确保了我们在修改文本样式的同时,保持了整体一致性。
在这个过程中,我们使用了 getSelectionParentEle
函数,它使用 SlateEditor.nodes
API 来查找所选内容的父级节点,并通过 this.editor.toDOMNode
将其转换为 DOM 元素之后拿到这个DOM的style对象,保存起来,最后把它加到需要格式刷的节点的父级节点上。
接下来我们看一下具体细节:
//这里是js的实现方式
import { SlateEditor, SlateText,SlateElement,SlateTransforms,Boot } from '@wangeditor/editor'
export default class MyPainter {
constructor() {
this.title = '格式刷' // 自定义菜单标题
// 这里是设置格式刷的样式图片跟svg都可以,但是注意要图片大小要小一点,因为要应用到鼠标手势上
this.iconSvg = ``
this.tag = 'button';//注入的菜单类型
this.savedMarks = null;//保存的样式
this.domId = null;//这个可要可不要
this.editor = null;//编辑器示例
this.parentStyle = null;//储存父节点样式
}
//添加或者移除鼠标事件
addorRemove = (type) => {
const dom = document.body;
if (type === 'add') {
dom.addEventListener('mousedown', this.changeMouseDown);
dom.addEventListener('mouseup', this.changeMouseup);
} else {
//赋值完需要做的清理工作
this.savedMarks = undefined;
dom.removeEventListener('mousedown', this.changeMouseDown);
dom.removeEventListener('mouseup', this.changeMouseup);
document.querySelector('#w-e-textarea-1').style.cursor = "auto"
}
}
//处理重复键名值不同的情况
handlerRepeatandNotStyle=(styles) => {
const addStyles=styles[0];
const notVal=[];
for (const style of styles) {
for (const key in style) {
const value=style[key];
if(!addStyles.hasOwnProperty(key)){
addStyles[key]=value;
}else{
if(addStyles[key]!==value){
notVal.push(key);
}
}
}
}
for (const key of notVal) {
delete addStyles[key];
}
return addStyles;
}
// 获取当前选中范围的父级节点
getSelectionParentEle = (type,func) => {
if(this.editor){
const parentEntry = SlateEditor.nodes(this.editor, {
match: node => SlateElement.isElement(node),
});
let styles=[];
for (const [node] of parentEntry) {
styles.push(this.editor.toDOMNode(node).style);//将node对应的DOM对应的style对象加入到数组
}
styles = styles.map((item) => {//处理不为空的style
const newItem = {};
for (const key in item) {
const val = item[key];
if(val!==''){
newItem[key] = val;
}
}
return newItem;
});
type==='get'?func(type,this.handlerRepeatandNotStyle(styles)):func(type);
}
}
//获取或者设置父级样式
getorSetparentStyle = (type,style) => {
if(type === 'get'){
this.parentStyle=style;//这里是个样式对象 例如{textAlign:'center'}
}else{
SlateTransforms.setNodes(this.editor, {...this.parentStyle}, {
mode: 'highest' // 针对最高层级的节点
})
}
}
//这里是将svg转换为Base64格式
addmouseStyle = () => {
const icon = ``// 这里是给鼠标手势添加图标
// 将字符串编码为Base64格式
const base64String = btoa(icon);
// 生成数据URI
const dataUri = `data:image/svg+xml;base64,${base64String}`;
// 将数据URI应用于鼠标图标
document.querySelector('#w-e-textarea-1').style.cursor = `url('${dataUri}'), auto`;
}
changeMouseDown = () => { }//鼠标落下
changeMouseup = () => {//鼠标抬起
if (this.editor) {
const editor = this.editor;
const selectTxt = editor.getSelectionText();//获取文本是否为null
if (this.savedMarks && selectTxt) {
//先改变父节点样式
this.getSelectionParentEle('set', this.getorSetparentStyle);
// 获取所有 text node
const nodeEntries = SlateEditor.nodes(editor, {//nodeEntries返回的是一个迭代器对象
match: (n) => SlateText.isText(n),//这里是筛选一个节点是否是 text
universal: true,//当universal为 true 时,Editor.nodes会遍历整个文档,包括根节点和所有子节点,以匹配满足条件的节点。当universal为 false 时,Editor.nodes只会在当前节点的直接子节点中进行匹配。
});
// 先清除选中节点的样式
for (const node of nodeEntries) {
const n = node[0];//{text:xxxx}
const keys = Object.keys(n);
keys.forEach((key) => {
if (key === 'text') {// 保留 text 属性
return;
}
// 其他属性,全部清除
SlateEditor.removeMark(editor, key);
});
}
// 再赋值新样式
for (const key in this.savedMarks) {
if (Object.hasOwnProperty.call(this.savedMarks, key)) {
const value = this.savedMarks[key];
editor.addMark(key, value);
}
}
this.addorRemove('remove');
}
}
}
getValue(editor) {
return 'MyPainter'; // 标识格式刷菜单
}
isActive(editor) {
return false;
}
isDisabled(editor) {//是否禁用
return false;
}
exec(editor) {//当菜单点击后触发
this.editor = editor;
this.domId = editor.id.split('-')[1] ? `w-e-textarea-${editor.id.split('-')[1]}` : undefined;
if (this.isDisabled(editor)) return;
if (editor.isEmpty() || editor.getHtml() == '<p><br></p>' || editor.getSelectionText() == '') return;//这里是对没有选中或者没内容做的处理
this.savedMarks = SlateEditor.marks(editor);// 获取当前选中文本的样式
this.getSelectionParentEle('get', this.getorSetparentStyle);//获取父节点样式并赋值
this.addmouseStyle();//点击之后给鼠标添加样式
this.addorRemove('add');//处理添加和移除事件函数
}
}
ts实现也是大差不差:
import type { IButtonMenu, IDomEditor } from '@wangeditor/editor'
import { SlateEditor, SlateText, SlateTransforms, SlateElement } from '@wangeditor/editor'
export default class MyPainter implements IButtonMenu {
title: string;
tag: string;
editor: null | IDomEditor;
domId: string | undefined;
savedMarks: Record<string, any> | undefined;
iconSvg: string;
parentStyle: Record<string, any> | null;
constructor() {
this.editor = null;
this.title = '格式刷'; // 自定义菜单标题
this.iconSvg = '<svg> ....</svg>'; // 可选 格式刷图标
this.tag = 'button';
this.parentStyle = null; // 储存父节点样式
}
addmouseStyle = () => {
const icon = '<svg> ....</svg>';
const base64String = btoa(icon);
// 生成数据URI
const dataUri = `data:image/svg+xml;base64,${base64String}`;
// 将数据URI应用于鼠标图标
document.body.style.cursor = `url('${dataUri}'), auto`;
};
addorRemove = (type: string) => {
const dom: HTMLElement | undefined = document.getElementById('w-e-textarea-1') || undefined;
if (type === 'add') {
dom?.addEventListener('mousedown', this.changeMouseDown);
dom?.addEventListener('mouseup', this.changeMouseup);
} else {
// 赋值完需要做的清理工作
this.savedMarks = undefined;
dom?.removeEventListener('mousedown', this.changeMouseDown);
dom?.removeEventListener('mouseup', this.changeMouseup);
document.body.style.cursor = 'auto';
}
};
handlerRepeatandNotStyle = (styles: Record<string, any>[]) => {
const addStyles = styles[0];
const notVal: string[] = [];
for (const style of styles) {
for (const key in style) {
const value = style[key];
if (!addStyles.hasOwnProperty(key)) {
addStyles[key] = value;
} else {
if (addStyles[key] !== value) {
notVal.push(key);
}
}
}
}
for (const key of notVal) {
delete addStyles[key];
}
return addStyles;
};
getSelectionParentEle = (type: string, func: (type: string, style: Record<string, any>) => void) => {
if (this.editor) {
const parentEntry = SlateEditor.nodes(this.editor, {
match: (node) => SlateElement.isElement(node),
});
let styles: Record<string, any>[] = [];
for (const [node] of parentEntry) {
styles.push(this.editor.toDOMNode(node).style); // 将node对应的DOM对应的style对象加入到数组
}
styles = styles.map((item) => {
const newItem: Record<string, any> = {};
for (const key in item) {
const val = item[key];
if (val !== '') {
newItem[key] = val;
}
}
return newItem;
});
func(type, this.handlerRepeatandNotStyle(styles));
}
};
getorSetparentStyle = (type: string, style: Record<string, any>) => {
if (this.editor) {
if (type === 'get') {
this.parentStyle = style; // 这里是个样式对象 例如{textAlign:'center'}
} else {
SlateTransforms.setNodes(this.editor, { ...this.parentStyle } as Record<string, any>, {
mode: 'highest', // 针对最高层级的节点
});
}
}
};
changeMouseDown = () => {};
changeMouseup = () => {
if (this.editor) {
const editor = this.editor;
const selectTxt = editor.getSelectionText(); // 获取文本是否为null
if (this.savedMarks && selectTxt) {
// 先改变父节点样式
this.getSelectionParentEle('set', this.getorSetparentStyle);
// 获取所有 text node
const nodeEntries = SlateEditor.nodes(editor, {
match: (n) => SlateText.isText(n),
universal: true,
});
// 先清除选中节点的样式
for (const node of nodeEntries) {
const n = node[0]; // {text:xxxx}
const keys = Object.keys(n);
keys.forEach((key) => {
if (key === 'text') {
return;
}
SlateEditor.removeMark(editor, key);
});
}
// 再赋值新样式
Object.entries(this.savedMarks).forEach(([key, value]) => {
if (key === 'text') {
return;
}
editor.addMark(key, value);
});
this.addorRemove('remove');
}
}
};
getValue(editor: IDomEditor): string | boolean {
return 'MyPainter';
}
isActive(editor: IDomEditor): boolean {
return false;
}
isDisabled(editor: IDomEditor): boolean {
return false;
}
exec(editor: IDomEditor, value: string | boolean) {
if (this.isDisabled(editor)) return;
this.domId = editor.id.split('-')[1] ? `w-e-textarea-${editor.id.split('-')[1]}` : undefined;
if (editor.isEmpty() || editor.getHtml() === '<p><br></p>' || editor.getSelectionText() === '') return;
this.editor = editor;
this.savedMarks = SlateEditor.marks(editor) || undefined;
this.getSelectionParentEle('get', this.getorSetparentStyle);
this.addmouseStyle();
this.addorRemove('add');
}
}
总结:
虽然这是我第一次撰写文章,但我深知文笔有待提升。我将不断努力学习,以便更好地传达技术内容和经验分享。在这个过程中,如果各位读者有任何反馈、建议或疑问,我将非常欢迎。你的意见对于我来说是极其宝贵的,将帮助我不断提高,为读者提供更有价值的内容。
如果你有兴趣了解更多关于这个自定义功能的内容,或者你也在开发类似的功能,我愿意随时交流和分享。我们可以在技术社区中共同成长,相互学习,不断推动技术进步。
最后,感谢你的阅读和支持。让我们共同在编程的道路上越走越远,迎接更多的挑战和机遇。
源码地址
如果有兴趣的的话可参考下wangeditor5和slatejs源码
参考文档
slateJS文档