背景:需要做一个富文本生成H5页面的功能,就简单使用@wangeditor/editor-for-vue库做了个功能
问题:需要支持一个设置内边距的功能,支持自定义边距
思考:把组件库支持的所有菜单过了一遍,好像是不支持这个功能,看到文档说可以自定义新功能,那么应该怎么扩展一个新功能呢,文档上只能看到一些基础的样例和api,并且比较零碎,所以基于源码写是最高效的方法,以引用功能为例。
效果图:

定义新元素-pagePad
定义一个新元素需要包含几个方法:
- pagePadMenu: 注册新菜单,定义菜单的形式
- renderElem: 渲染“页边距”元素到编辑器,让编辑器认识“页边距”
- elem-to-html: 生成“页边距”元素的 HTML,让编辑器知道怎么输出html(否则getHtml不认识“页边距”)
- parse-elem-html: 解析 HTML 字符串,生成“页边距”元素的数据结构,定义解析 HTML 的逻辑(否则setHtml无效)
- plugin: 劫持编辑器事件和操作,比如:敲回车时执行什么操作
我最终的代码结构长这样:

首先需要定义元素的数据结构,这里我只需要设置个左右margin,所以除了type和children外加了一个margin属性,后面转换结构时要按照这个格式
const Resume = {
type: 'pagePad',
margin: '20px',
children: [{ text: '' }]
}
index.js 作为元素的入口文件,统一导出元素相关配置
import { menuConfig } from './menu'
import { elemToHtmlConfig } from './elem-to-html'
import { renderToElemConfig } from './render-elem'
import { parseHtmlConfig } from './parse-elem-html'
import withPagePad from './plugin'
import { MENU_KEY } from './menu'
const PAGEPAD_TYPE = 'pagePad'
// 用来注册自定义的方法
export const module = {
menus: [menuConfig],
renderElems: [renderToElemConfig],
elemsToHtml: [elemToHtmlConfig],
parseElemsHtml: [parseHtmlConfig],
editorPlugin: withPagePad
}
// 菜单配置
export const INSERTKEYS = {
index: 4, // 菜单顺序
keys: [MENU_KEY]
}
render-elem.js用于定义元素在编辑器中的渲染效果,wangeditor中对blockquote标签写了一些默认样式,这里就借用blockquote标签实现“内边距”功能
import { h } from 'snabbdom'
import { PAGEPAD_TYPE } from './index'
/**
* @param elem 内边距元素
* @param children 元素子节点
* @param editor 编辑器实例
* @returns vnode 节点(通过 snabbdom.js 的 h 函数生成)
*/
const renderToELem = (elem, children, editor) => {
const vnode = h(
'blockquote',
{
style: {
borderLeft: `${elem.margin} solid var(--w-e-textarea-selected-border-color)`,
borderRight: `${elem.margin} solid var(--w-e-textarea-selected-border-color)`
}
},
children
)
return vnode
}
// 编辑器渲染配置
export const renderToElemConfig = {
type: PAGEPAD_TYPE,
renderElem: renderToELem
}
elem-to-html.js 负责定义怎么输出 html,也就是 getHtml() 的返回值,这里
import { PAGEPAD_TYPE } from './index'
/**
* @param elem 元素的数据结构,如上文的 Resume
* @param childrenHtml 子节点的 HTML 代码
* @returns 元素的 HTML 字符串
*/
const elemToHtml = (elem, childrenHtml) => {
const { margin = '' } = elem
// 生成 HTML 代码,data-w-e-type用于标记元素类型,后面会用
const html = `<blockquote data-w-e-type="${PAGEPAD_TYPE}" data-margin="${margin}" style="margin: 0px ${margin}">${childrenHtml}</blockquote>`
return html
}
// 编辑器输出html配置
export const elemToHtmlConfig = {
type: PAGEPAD_TYPE,
elemToHtml: elemToHtml
}
parse-elem-html.js 负责定义怎么解析 html 字符串
import { PAGEPAD_TYPE } from './index'
/**
* @param domElem HTML 对应的 DOM Element
* @param children 子节点
* @param editor editor 实例
* @returns 元素数据结构,如上文的 Resume
*/
const parseHtml = (domElem, children, editor) => {
const margin = domElem.getAttribute('data-margin') || '0px'
// 生成元素数据结构
const myResume = {
type: PAGEPAD_TYPE,
margin,
children
}
return myResume
}
// 编辑器解析html配置
export const parseHtmlConfig = {
selector: 'blockquote[data-w-e-type="pagePad"]', // CSS 选择器,匹配特定的 HTML 标签
parseElemHtml: parseHtml
}
menu.js 用户定义菜单的交互,这里交互是:点击图标 -> 弹窗输入数字 -> 插入元素
import { SlateEditor, SlateElement, SlateTransforms } from '@wangeditor/editor'
import { PADDING_SVG } from '../icon-svg'
import { PAGEPAD_TYPE } from './index'
// 菜单 key
export const MENU_KEY = 'pagePad'
// 菜单名
export const MENU_NAME = '页边距'
class menu {
constructor () {
this.title = MENU_NAME
this.iconSvg = PADDING_SVG // 在工具栏展示的图标,不设置图标会显示title
this.tag = 'button'
this.showModal = true
this.modalWidth = 300
this.margin = '0px'
this.modalDom = null
}
// 菜单是否需要激活(如选中加粗文本,“加粗”菜单会激活),用不到则返回 false
isActive (editor) {
return false
}
// 获取菜单执行时的 value ,用不到则返回空 字符串或 false
getValue (editor) {
return ''
}
// 菜单是否需要禁用(如选中 H1 ,“引用”菜单被禁用),用不到则返回 false
isDisabled (editor) {
return false
}
// 弹出框 modal 的定位:1. 返回某一个 SlateNode; 2. 返回 null (根据当前选区自动定位)
getModalPositionNode (editor) {
return null // modal 依据选区定位
}
// 定义 modal 内部的 DOM Element
getModalContentElem (editor) {
if (this.modalDom) {
return this.modalDom
}
// 创建一个div元素
const divElement = document.createElement('div')
divElement.style.marginBottom = '30px'
divElement.style.marginTop = '10px'
// 创建页边距输入框
const marginInput = document.createElement('input')
marginInput.type = 'text'
marginInput.placeholder = '请输入页边距'
marginInput.style.marginBottom = '10px'
marginInput.addEventListener('change', e => {
const val = e.target.value
this.padding = val.includes('px') ? val : val + 'px'
})
// 创建确定按钮
const confirmButton = document.createElement('button')
confirmButton.textContent = '确定'
confirmButton.style.marginRight = '20px'
confirmButton.addEventListener('click', () => {
// 恢复最近一次非 null 选区。如编辑器 blur 之后,再重新恢复选区,否则“基于当前选区”的操作无效
editor.restoreSelection()
const nodeEntries = SlateEditor.nodes(editor, {
match: (node) => {
if (SlateElement.isElement(node)) {
return true
}
return false
},
universal: true
})
const children = []
if (nodeEntries == null) {
children.push({ text: '' })
} else {
for (const nodeEntry of nodeEntries) {
const [node] = nodeEntry
children.push(node)
}
}
const targetNode = {
type: PAGEPAD_TYPE,
padding: this.margin,
children
}
SlateTransforms.setNodes(editor, targetNode, { mode: 'highest' })
})
// 将页边距输入框、确定按钮和取消按钮添加到div元素中
divElement.appendChild(marginInput)
divElement.appendChild(confirmButton)
// 缓存 modal
this.modalDom = divElement
return divElement
}
}
// 自定义菜单配置
export const menuConfig = {
key: MENU_KEY,
factory () {
return new menu()
}
}
上面的已经完成一个新元素的定义了,可以在组件中使用了,插件看个人需求需不需要定义,如果不需要可跳过
plugin.js 用于劫持编辑器事件和操作,自定义事件行为
/**
* @description editor 插件,重写 editor API
*/
import { SlateEditor, SlateTransforms, SlatePoint, SlateNode, DomEditor } from '@wangeditor/editor'
import { PAGEPAD_TYPE } from './index'
function withPagePad (editor) {
const { insertBreak, insertText } = editor
const newEditor = editor
// 重写 insertBreak - 换行时插入 p
newEditor.insertBreak = () => {
const { selection } = newEditor
if (selection == null) return insertBreak()
const [nodeEntry] = SlateEditor.nodes(editor, {
match: n => DomEditor.checkNodeType(n, PAGEPAD_TYPE),
universal: true
})
if (!nodeEntry) return insertBreak()
const quoteElem = nodeEntry[0]
const quotePath = DomEditor.findPath(editor, quoteElem)
const quoteEndLocation = SlateEditor.end(editor, quotePath)
if (SlatePoint.equals(quoteEndLocation, selection.focus)) {
// 光标位于 元素的 最后
const str = SlateNode.string(quoteElem)
if (str && str.slice(-1) === '\n') {
// blockquote 文本最后一个是 \n,就跳出边距节点
editor.deleteBackward('character') // 删除最后一个 \n
// 则插入一个 paragraph
const p = { type: 'paragraph', children: [{ text: '' }] }
SlateTransforms.insertNodes(newEditor, p, { mode: 'highest' })
return
}
}
// 情况情况,插入换行符
insertText('\n')
}
return newEditor
}
export default withPagePad
到这里新元素‘页边距’就定义成功了,可以在组件中使用了
<script>
import { Boot } from '@wangeditor/editor'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { module, INSERTKEYS } from './config/richText'
// 注册新元素
Boot.registerModule(module)
export default {
...
data () {
return {
toolbarConfig: {
...
insertKeys: INSERTKEYS
}
}
}
}
</script>
<template>
<Toolbar
...
:default-config="toolbarConfig"
/>
</template>