本文适用于wangeditor5在vue3中的使用:
目前遇到个需求是类似对话的功能,根据用户提出的问题,把答案回显到富文本里。
- 官网文档:www.wangeditor.com
安装
npm install @wangeditor/editor
npm install @wangeditor/editor-for-vue
初始化
页面editor.vue
<template>
<Toolbar :editor="editorRef" :defaultConfig="toolbarConfig" :mode="mode" />
<Editor v-model="valueHtml" :defaultConfig="editorConfig"
:defaultContent="defaultContent" :mode="mode"
@onCreated="handleCreated" />
</template>
<script setup lang="ts">
import '@wangeditor/editor/dist/css/style.css';
import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
const valueHtml = ref('');
const editorRef = shallowRef();
// 配置工具栏,显示哪些菜单,以及菜单的排序、分组
const toolbarConfig = {
toolbarKeys: [],
};
// 编辑器配置
const editorConfig = {
placeholder: '请输入文本',
hoverbarKeys: {
'text': {
menuKeys: [],
}
// 其他配置项...
},
};
const defaultContent = [
{
type: 'paragraph',
lineHeight: '',
children: [
{ text: '', fontFamily: '', fontSize: '' },
]
}
];
//editor初始化成功
const handleCreated = (editor: any) => {
editorRef.value = editor;
console.log('Editor准备就绪:');
};
// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {
if (editorRef.value == null || !editorRef.value) return;
editorRef.value.destroy();
});
</script>
首先是自定义菜单遇到的问题:
- wangeditor的自定义菜单只允许注册一次,重复注册会报错。
- 解决方案:注册的过程前判断一下menus是否存在当前自定义菜单。
在注册菜单的时候
import { Boot } from "@wangeditor/editor";
const allRegisterMenu = editor.getAllMenuKeys();
const allRegisterMenu = editor.getAllMenuKeys();
// 获取所有已注册的菜单
if (allRegisterMenu.indexOf(自定义菜单的key) < 0) {
// 如果未注册,则注册
const menuObj = {
key: item.key,
factory() {
return new 自定义菜单的class名()
}
}
Boot.registerMenu(menuObj);
}
- 在退出当前编辑器页面后重新进入或者多次打开弹窗时,自定义菜单的自定义exec事件调用vue实例的方法,比如emit或者动态事件,会导致菜单事件绑定的是首次实例的方法,访问不到当前页面实例的方法。
- 解决方案:使用editor菜单的emit方法自定义事件,然后编辑器实例监听。
新建一个menu.ts用来定义menu的类,以button类型菜单为例
import type { IButtonMenu, IDomEditor } from "@wangeditor/editor";
class menuButton implements IButtonMenu {
tag: string;
title: string;
constructor() {
this.title = 'menu';
this.tag = 'button';
}
getValue(editor: IDomEditor): string | boolean {
return 'menu';
}
isActive() {
return false;
}
isDisabled(editor: IDomEditor): boolean {
return false;
}
exec(editor: IDomEditor, value: string | boolean) {
if (this.isDisabled(editor)) {
return;
}
editor.emit('menuClick', value); //重点
}
}
export { menuButton }
新建一个register.ts用来注册菜单,如果想添加到toolbarConfig,可以把toolbarConfig传进来
import { menuButton } from "./menu";
import { Boot } from "@wangeditor/editor";
import type { IDomEditor } from "@wangeditor/editor";
const MenusList = [
{
key: 'menuConfig',
class: menuButton,
}
]
const registerMenu = function (editor: IDomEditor, toolbarConfig) {
const allRegisterMenu = editor.getAllMenuKeys();
for (let item of MenusList) {
if (allRegisterMenu.indexOf(item.key) < 0) {
const menuObj = {
key: item.key,
factory() {
return new item.class()
}
}
Boot.registerMenu(menuObj);
}
}
// 如果注册到菜单栏可以在menulist里定义好index插入进去
// toolbarConfig.insertKeys = {
// index: MenusList[0].index,
// keys: keys
// }
}
export default registerMenu;
回到editor.vue
//editor初始化成功
const handleCreated = (editor: any) => {
editorRef.value = editor;
console.log('Editor准备就绪:');
// 注册自定义menu
registerMenu(editor);
// 监听事件
editor.on('menuClick', (value: any) => {
// 执行操作,访问当前vue的emit或者自定义动态事件等等
});
};
这样就可以在重复进入页面后不会重新注册菜单并且可以访问vue的实例
操作内容切换的问题:
Cannot find a descendant at path [3] in node: {"children":[{"type":"paragraph","children":[{"text":""}]}]
这个问题出现的操作有很多种,这是我遇到的几个注意点:
- 首先一定要只创建一个实例,退出时编辑器调用编辑器destory的方法,或者v-if强行重新生成新实例。
- 最好不要使用keep-alive包裹,会缓存对编辑器的操作。
- 进入编辑器在onCreated时候再去渲染数据,使用v-model双向绑定来插入。
- 如果这个报错没有影响到显示或者需求不需要别的操作,使用try catch忽略报错也不是不可以。
- 插入图片视频内容使用insertNode方法。
打字机效果
一般这种情况是使用markdown格式文本,时间原因我们前后端目前没有去处理,后续我们也会去使用markdown,这个目前没有使用markdown的jy可以看看
自己写的打字机效果,时间问题没有去优化,如果各位有更好的方式请忽略
一般插入的数据是一整段html格式文本,我是采用DOMParser解析然后循环插入,为了减少操作,我只处理了两层节点,文本p,标题和列表ul li,然后使用callback进行渲染富文本,sse流接口的话onmessage的时候调用解析文本方法就行。
let nodeList: any = [];
let time = 400;
// 显示的文本
const showHtml = ref<string>('');
// 组合后的文本 html
let assemblyHtml: string = '';
// 元数据下标
let dataIndex = ref<number>(0);
// 段落文本 text
let paraText: string = '';
// 处理中的文本下标
let textIndex = ref<number>(0);
// 处理中的文本 html
let processHtml = ref<string>('');
function handleWriteData(data: any) {
// 创使用DOMParser解析HTML字符串
const parser = new DOMParser();
const doc = parser.parseFromString(data, 'text/html');
const paragraphs: any = Array.from(doc.body.childNodes);
for (let child of paragraphs) {
// 确保我们处理的是元素节点并有子节点(而不是文本节点或其他类型的节点)
if (child.nodeType === Node.ELEMENT_NODE
&& child.childNodes.length && child.childNodes[0])
{
// 输出标签名和文本内容
let node = child.childNodes[0];
if (node.nodeType == 3) {
nodeList.push({
parNode: '',
node: child.tagName.toLowerCase(),
text: child.textContent,
style: child?.style?.cssText || '',
});
} else {
for (let subChild of child.childNodes) {
if (subChild.nodeType === Node.ELEMENT_NODE) {
nodeList.push({
parNode: child.tagName.toLowerCase(),
node: subChild.tagName.toLowerCase(),
text: subChild.textContent,
style: ''
});
};
};
};
};
};
}
// 依次处理文本
function initParaHtml(callback: () => void) {
if (!nodeList.length) return
let dataItem = nodeList[dataIndex.value]
// 重制内容用来停止打字
if (paraText.length >= dataItem.text.length) {
dataIndex.value++
paraText = ''
textIndex.value = 0
processHtml.value = ''
}
handleTimeoutData(nodeList[dataIndex.value], callback)
};
// 打字效果和倒计时,目前只处理到二级元素层级
function handleTimeoutData(item: any, callback: () => void) {
if (!item) return
function getVal() {
let line = item.text[textIndex.value]
if (line) {
paraText += line
if (item.parNode) {
processHtml.value = `
<${item.parNode}>
<${item.node}>${paraText}</${item.node}>
</${item.parNode}>
`
} else {
processHtml.value = `
<${item.node} style='${item.style}'>${paraText}</${item.node}>
`
}
textIndex.value++
setTimeout(getVal, time)
showHtml.value = assemblyHtml + processHtml.value
callback()
} else {
assemblyHtml = assemblyHtml + processHtml.value
initParaHtml(callback)
}
}
getVal()
};
然后页面操作:
let textValue = '<p>你好,我是富文本内容</p>';
handleWriteData(textValue);
// 如果在打字中,停止执行打印方法,可以自定义一个值去判断
if (hasTypeWriter.value) return;
initParaHtml(() => {
valueHtml.value = showHtml.value;
nextTick(() => {
let wscrollBox: Element | null = document.querySelector('.w-e-scroll');
if (wscrollBox && wscrollBox?.scrollHeight > wscrollBox?.clientHeight) {
wscrollBox.scrollTop = wscrollBox.scrollHeight;
};
});
});
滚动的问题:之前使用光标一直置于文本末,但是会操作编辑器api会导致出现不顺畅的问题,所以我建议打字的时候使编辑器失去焦点,然后获取富文本滚动元素,打字的时候设置top值滚动到底部。
结尾
富文本编辑器这个东西确实全是坑,流泪~~~,如果是大公司有自己开发的就方便许多。
使用后的感受,由于作者时间原因这个库已经不维护了,所以后面框架升级了可能会遇到问题,如果大家只是简单的富文本回显内容可以使用,作者在官网也给出了react,vue框架的demo,可以快速应用到项目中(日常够用,而且确实有些内置api很好用,不用二次开发)。如果大家有充足的时间去开发,不建议大家使用这个库,可以去二次开发quill.js,tinymce等等。