wangeditor 是开源 Web 富文本编辑器,开箱即用,配置简单,有以下优势:
- 简洁易用,功能强大
- 支持 JS Vue React
- 踩过 5000+ 坑
以上是官网上的简单介绍,有点王婆卖瓜的感觉,大家看看就好。我们项目上之所以选择wangeditor,主要有以下几点:
- 文档详情,社区活跃,尤其是V5版本(推荐大家使用)
- 功能支持扩展,包括自定义数据节点、自定义菜单(工具栏,操作)
业务驱动需要自定义数据节点,即自定义dom
wangeditor原生支持的扩展功能包括,“网络图片” 和 “上传图片”,如下图
## 一、插入网络图片网络图片即往编辑器中插入一个网络图片。
二、上传图片
上传图片,选择本地图片进行上传,下面是一个自定义图片上传的代码实现功能,不是本文的重点,一笔带过,仅供参考:
async customUpload(file, insertFn) {
// 自定义图片上传
const image: any = await uploadServer({
type: UPLOAD_TYPE.IMAGE,
file,
progressHandler: handleProgress
})
const { url, title } = image
// 插入图片
insertFn(url, title);
// 对选中节点居中显示
SlateTransforms.setNodes(editorRef.value, {
textAlign: 'center'
}, {
mode: 'highest' // 针对最高层级的节点
})
},
对上传的图片进行默认居中显示,是业务上的需求。
三、从素材库选择上传
我们的业务为知识库系统,图片视频等资源会存储在服务器,并通过可视化系统进行管理。如何将素材库运用到编辑器,并将其进行关联。 思路如下:- 创建一个按钮菜单“素材图片”,将其配置到菜单栏中,将按钮“素材图片”与业务系统关联起来。比如点击按钮,弹出选择素材的对话框。
- 如何将与“素材图片”按钮放到图片菜单组中,与“网络图片”、“上传图片”并列现实。
- 自定义图片插入功能,将选择的素材插入到编辑器中。
思路和方向没有问题,接下来就是实现了。
1. 自定义菜单
创建ButtonMenu按钮,创建class
import { IButtonMenu, IDomEditor } from '@wangeditor/editor';
// const svg = require('@/assets/svg/ai-keywords-extract.svg')
const svgIcon = '<svg t="1691112864399" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1825" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M1008.298667 504.149333c0 114.346667-38.912 225.28-110.250667 314.606934l110.250667 106.5984a56.0128 56.0128 0 0 1-79.223467 79.189333l-110.2848-106.496C601.224533 1071.786667 283.989333 1036.288 110.250667 818.722133-63.488 601.224533-27.989333 283.989333 189.508267 110.250667A504.149333 504.149333 0 0 1 504.149333 0c278.4256 0 504.149333 225.723733 504.149334 504.149333zM504.149333 111.991467c-216.541867 0-392.0896 175.547733-392.0896 392.0896 0 216.576 175.547733 392.123733 392.0896 392.123733s392.0896-175.547733 392.0896-392.123733c0-216.541867-175.547733-392.0896-392.0896-392.0896z" fill="#4F4F4F" p-id="1826"></path><path d="M412.5696 682.666667l21.2992-72.6016h154.794667L610.304 682.666667h142.199467L587.1616 242.858667H438.954667L273.681067 682.666667h138.888533z m147.592533-167.697067h-96.904533l48.298667-158.1056 48.605866 158.1056z" fill="#4F4F4F" p-id="1827"></path></svg>'
class SelectImageMaterialMenu implements IButtonMenu { // TS 语法
title: string;
tag: string;
iconSvg: string;
constructor() {
this.title = '选择素材' // 自定义菜单标题
this.tag = 'button'
this.iconSvg = svgIcon // 可选
}
// 获取菜单执行时的 value ,用不到则返回空 字符串或 false
getValue(editor: IDomEditor): string | boolean {
return ''
}
// 菜单是否需要激活(如选中加粗文本,“加粗”菜单会激活),用不到则返回 false
isActive(editor: IDomEditor): boolean {
return false
}
// 菜单是否需要禁用(如选中 H1 ,“引用”菜单被禁用),用不到则返回 false
isDisabled(editor: IDomEditor): boolean {
return false
}
// 点击菜单时触发的函数
exec(editor: IDomEditor, value: string | boolean) {
// 自定义触发事件。
editor.emit('on-select-image-material')
}
}
export default SelectImageMaterialMenu
自定义事件on-select-image-material
,在按钮点击时触发,并在编辑器创建完成后监听,这样就将编辑器按钮与业务进行关联。
触发:editor.emit('on-select-image-material')
监听:
editor.on('on-select-image-material', () => {
ElMessage.success('触发了素材图片按钮!!!')
// 后续的业务操作,如弹出选择素材的对话框。
})
注册菜单到 wangEditor
const selectImageMaterialMenuConf = {
key: 'selectImageMaterial',
factory() {
return new SelectImageMaterial()
},
}
插入菜单到工具栏
import { Boot, } from '@wangeditor/editor';
Boot.registerModule({
editorPlugin: dealEditorPlugin,
menus: [
selectImageMaterialMenuConf,
],
});
2. 配置工具栏菜单。
toolbarConfig
提供的工具栏的配置api包括:
- toolbarKeys: 重新配置工具栏,显示哪些菜单,以及菜单的排序、分组。
- insertKeys: 可以在当前
toolbarKeys
的基础上继续插入新菜单,如自定义扩展的菜单。 - excludeKeys: 如果仅仅想排除掉某些菜单,其他都保留
- modalAppendToBody:将菜单弹出的 modal 添加到 body 下,并自定义 modal 的定位和其他样式
通过提供api,
insertKeys
向工具栏中指定位置插入菜单,仅支持一级菜单,并不支持配置菜单组中二级菜单,图片为菜单组。因此只能通过toolbarKeys
重新配置工具栏。将自定义的菜单插入图片菜单组中。
获取当前编辑器工具栏所有的配置
重新配置工具栏
toolbarConfig.toolbarKeys = [
"headerSelect",
"blockquote",
"|",
"bold",
"underline",
"italic",
{
"key": "group-more-style",
"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",
"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",
"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",
"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",
"selectImageMaterial", // 自定义菜单
]
},
{
"key": "group-video",
"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"
]
3、自定义图片插入方法
wangeditor有插入图片的实现,但是没有对外暴露,因此需要自己试下插入方法。当前实现需要阅读源码。
import { Transforms, Range, Editor } from 'slate'
import { IDomEditor, DomEditor } from '@wangeditor/core'
// 定义数据类型
type EmptyText = {
text: ''
}
export type ImageStyle = {
width?: string
height?: string
}
export type ImageElement = {
type: 'image'
src: string
alt?: string
href?: string
style?: ImageStyle
children: EmptyText[]
}
export function replaceSymbols(str: string) {
return str.replace(/</g, '<').replace(/>/g, '>')
}
async function check(
menuKey: string,
editor: IDomEditor,
src: string,
alt: string = '',
href: string = ''
): Promise<boolean> {
const { checkImage } = editor.getMenuConfig(menuKey)
if (checkImage) {
const res = await checkImage(src, alt, href)
if (typeof res === 'string') {
// 检验未通过,提示信息
editor.alert(res, 'error')
return false
}
if (res == null) {
// 检验未通过,不提示信息
return false
}
}
return true
}
async function parseSrc(menuKey: string, editor: IDomEditor, src: string): Promise<string> {
const { parseImageSrc } = editor.getMenuConfig(menuKey)
if (parseImageSrc) {
const newSrc = await parseImageSrc(src)
return newSrc
}
return src
}
export async function insertImageNode(
editor: IDomEditor,
src: string,
alt: string = '',
href: string = ''
) {
const res = await check('insertImage', editor, src, alt, href)
if (!res) return // 检查失败,终止操作
const parsedSrc = await parseSrc('insertImage', editor, src)
// 新建一个 image node
const image: ImageElement = {
type: 'image',
src: replaceSymbols(parsedSrc),
href,
alt,
style: {},
children: [{ text: '' }], // 【注意】void node 需要一个空 text 作为 children
}
// 如果 blur ,则恢复选区
if (editor.selection === null) editor.restoreSelection()
// 如果当前正好选中了图片,则 move 一下(如:连续上传多张图片时)
if (DomEditor.getSelectedNodeByType(editor, 'image')) {
editor.move(1)
}
if (isInsertImageMenuDisabled(editor)) return
// 插入图片
Transforms.insertNodes(editor, image)
// 回调
const { onInsertedImage } = editor.getMenuConfig('insertImage')
if (onInsertedImage) onInsertedImage(image)
}
export function isInsertImageMenuDisabled(editor: IDomEditor): boolean {
const { selection } = editor
if (selection == null) return true
if (!Range.isCollapsed(selection)) return true // 选区非折叠,禁用
const [match] = Editor.nodes(editor, {
match: n => {
const type = DomEditor.getNodeType(n)
if (type === 'code') return true // 代码块
if (type === 'pre') return true // 代码块
if (type === 'link') return true // 链接
if (type === 'list-item') return true // list
if (type.startsWith('header')) return true // 标题
if (type === 'blockquote') return true // 引用
if (Editor.isVoid(editor, n)) return true // void
return false
},
universal: true,
})
if (match) return true
return false
}
const isDisabled = (editor: IDomEditor) => {
return isInsertImageMenuDisabled(editor)
}
/**
* 自定义插入图片
*/
export const insertImage = (editor: IDomEditor, src: string, alt: string = '', href: string = '') => {
if (!src) return
// 还原选区
editor.restoreSelection()
if (isDisabled(editor)) return
// 插入图片
insertImageNode(editor, src, alt, href)
}
至此大功告成
只需要在我们需要的地方,调用insertImage
方法即可,如:选择素材之后。