自定义扩展新功能
wangEditor 从 V5 开始,源码上就分离了 core editor 还有各个 module 。
core 是核心 API ,editor 负责汇总集成。所有的具体功能,都分布在各个 module 中来实现。
所以,从底层设计就保证了扩展性。
概述
wangEditor 扩展性包括以下部分,你可以来扩展大部分常用的功能。
- 定义新元素(如 todo-list 、链接卡片、
@xxx
功能,插入地图等)- 渲染到编辑器
- 显示时获取 html
- 劫持编辑器的 API 并自定义(如输入
#
之后,切换为 H1 ,实现简单的 markdown 功能) - 扩展菜单
元素的数据结构
如果你需要扩展新元素,则需要先定义数据结构。
如果不需要,则忽略该步骤。
具体可参考 节点数据结构 ,定义自己的节点数据结构。
注意要符合 slate.jsopen in new window 的数据规范。
Render
需要你提前了解 vdom 概念,以及 snabbdom.jsopen in new window 和它的 h
函数。
如果你定义了新元素,则需要把它显示到编辑器内。主要过程是:model -> 生成 vdom -> 渲染 DOM
用到了 vdom 需要安装 snabbdom
,参考上文。
安装 snabbdom.js
yarn add snabbdom --peer
## 安装到 package.json 的 peerDependencies 中即可
renderElem
增加了新的元素,需要渲染到编辑器中。
注意:
- 必须在创建编辑器之前注册
- 全局只能注册一次,不要重复注册
import { h, VNode } from 'snabbdom'
import { Boot, IDomEditor, SlateElement } from '@wangeditor/editor'
// 渲染函数
function renderParagraph(elem: SlateElement, children: VNode[] | null, editor: IDomEditor): VNode {
// elem 即当前节点
// children 是下级节点
// editor 即编辑器实例
const vnode = h('p', {}, children) // type: 'paragraph' 节点,即渲染为 <p> 标签
return vnode
}
// 渲染配置
const renderElemConf = {
type: 'paragraph', // 节点 type ,重要!!!
renderElem: renderParagraph,
}
// 注册到 wangEditor
Boot.registerRenderElem(renderElemConf)
PS:h
函数的使用,请参考 snabbdomopen in new window
renderStyle
渲染 CSS 样式,最基本的一些样式(如加粗、斜体、颜色、对齐方式等)编辑器已经自带了。 如果你需要再自定义新的样式,可以通过以下方式来注册。
注意:
- 必须在创建编辑器之前注册
- 全局只能注册一次,不要重复注册
import { h, VNode, VNodeStyle } from 'snabbdom'
import { Boot, SlateElement, SlateText, SlateDescendant } from '@wangeditor/editor'
/**
* 给 vnode 添加样式
* @param vnode vnode
* @param newStyle { key: val }
*/
function addVnodeStyle(vnode: VNode, newStyle: VNodeStyle) {
if (vnode.data == null) vnode.data = {}
const data = vnode.data
if (data.style == null) data.style = {}
Object.assign(data.style, newStyle)
}
/**
* render style
* @param node slate node
* @param vnode vnode
* @returns new vnode
*/
function renderStyle(node: SlateDescendant, vnode: VNode): VNode {
// 1. 获取样式相关的属性
const { bold, color } = node as SlateText
// const { lineHeight } = node as SlateElement // node 可能是 Text 也可能是 Element
let newVnode = vnode
// 2. 为 vnode 添加样式标签
if (bold) {
newVnode = h('strong', {}, [newVnode])
}
if (color) {
addVnodeStyle(newVnode, { color })
}
// if (lineHeight) {
// addVnodeStyle(newVnode, { lineHeight })
// }
// 3. 返回添加了样式的 vnode
return newVnode
}
// 注册到 wangEditor
Boot.registerRenderStyle(renderStyle)
PS:h
函数的使用,请参考 snabbdomopen in new window
To HTML
在显示编辑器内容时 ,无论什么渲染形式,都需要得到各个元素的 html。所以对于新元素,必须要扩展 toHtml 方法。
elemToHtml
生成元素的 html
注意:
- 必须在创建编辑器之前注册
- 全局只能注册一次,不要重复注册
import { Boot, SlateElement } from '@wangeditor/editor'
// 生成 html 的函数
function paragraphToHtml(elem: SlateElement, childrenHtml: string): string {
if (childrenHtml === '') {
return '<p><br/></p>'
}
return `<p>${childrenHtml}</p>`
}
// 配置
const elemToHtmlConf = {
type: 'paragraph', // 节点 type ,重要!!!
elemToHtml: paragraphToHtml,
}
// 注册到 wangEditor
Boot.registerElemToHtml(elemToHtmlConf)
可参考 wangEditor 源码中 基础模块open in new window 中各个模块的所有 elem-to-html.ts
文件。
styleToHtml
生成 CSS 样式的 html ,如文本的加粗、斜体、颜色等,还有段落的对齐、行高等。
注意:
- 必须在创建编辑器之前注册
- 全局只能注册一次,不要重复注册
import { Boot, SlateText, SlateElement, SlateDescendant } from '@wangeditor/editor'
import $ from 'dom7' // jquery 也可以
/**
* style to html
* @param node slate node
* @param nodeHtml node html
* @returns styled html
*/
function styleToHtml(node: SlateDescendant, nodeHtml: string): string {
// 1. 获取样式相关的属性获取属性
const { color, bgColor } = node as SlateText
// const { lineHeight } = node as SlateElement // node 可能是 Text 也可能是 Element
// 设置 css 样式
const $elem = $(elemHtml)
if (color) $elem.css('color', color)
if (bgColor) $elem.css('background-color', bgColor)
// $elem.css('line-height', lineHeight)
// 输出 html
return $elem[0].outerHTML
}
// 注册到 wangEditor
Boot.registerStyleToHtml(styleToHtml)
可参考 wangEditor 源码中 颜色、背景色open in new window 和 字体字号open in new window 的 style-to-html 。
Parse HTML
上文的 toHtml 是从编辑器获取 html 。得到的 html 还可能再设置回显到编辑器中,这就需要 parse html 。
parseElemHtml
例如编辑器的“链接”,以下函数会把 html '<a href="https://www.baidu.com/" target="_blank">百度<a/>'
转换为 slate element 。
import { Dom7Array } from 'dom7'
import { Boot, IDomEditor, SlateDescendant, SlateText, SlateElement } from '@wangeditor/editor'
/**
* 将 html 转换为 slate elem
* @param $elem 由 html 生成的 DOM 节点(Dom7 封装,类似于 jquery)
* @param children 子节点
* @param editor editor
* @returns slate element
*/
function parseHtml($elem: Dom7Array, children: SlateDescendant[], editor: IDomEditor): SlateElement {
// 过滤 children
children = children.filter(child => {
// child 必须是 text 或者 inline element (不能是 block element)
if (SlateText.isText(child)) return true
if (editor.isInline(child)) return true
return false
})
// 无 children ,则取 $elem 纯文本
if (children.length === 0) {
children = [{ text: $elem.text().replace(/\s+/gm, ' ') }]
}
// 返回 slate elem ,链接类型
return {
type: 'link',
url: $elem.attr('href') || '',
target: $elem.attr('target') || '',
children,
}
}
export const parseHtmlConf = {
selector: 'a', // CSS 选择器,以匹配“链接”的 html tag
parseElemHtml: parseHtml,
}
// 注册
Boot.registerParseElemHtml(parseHtmlConf)
parseStyleHtml
例如编辑器处理颜色,以下代码可识别 html '<span style="color: rgb(231, 95, 51); background-color: rgb(252, 251, 207);">hello</span>'
中的 color
和 background-color
,并添加到 text node 。
import { Dom7Array, SlateText } from 'dom7'
import { Boot, SlateDescendant, SlateText } from '@wangeditor/editor'
/**
* 识别 html 中的颜色,并添加到 text node
* @param $text 由 html 创建的 DOM 节点(Dom7 创建,类似 jquery)
* @param node text node
* @returns text node with color and bgColor
*/
export function parseStyleHtml($text: Dom7Array, node: SlateDescendant): SlateDescendant {
if (!SlateText.isText(node)) return node
const textNode = node as SlateText
const color = getStyleValue($text, 'color') // 获取 style 中的 color 值
if (color) {
textNode.color = color
}
const bgColor = getStyleValue($text, 'background-color') // 获取 style 中的 background-color 值
if (bgColor) {
textNode.bgColor = bgColor
}
return textNode
}
// 注册
Boot.registerParseStyleHtml(parseStyleHtml)
注册插件
wangEditor 是基于 slate.jsopen in new window 内核的,所以可以直接使用 slate.js 插件。所以你先要去了解 slate.js 的 API 和插件机制。
注意:
- 必须在创建编辑器之前注册
- 全局只能注册一次,不要重复注册
import { Boot, IDomEditor } from '@wangeditor/editor'
// 定义 slate 插件
function withBreak<T extends IDomEditor>(editor: T): T {
const { insertBreak } = editor
const newEditor = editor
// 重写 editor insertBreak API
// 例如做一个 ctrl + enter 换行功能
newEditor.insertBreak = () => {
// 判断,如果按键是 ctrl + enter ,则执行 insertBreak
insertBreak()
// 否则,则 return
}
// 还可以重写其他 API
// 返回 editor ,重要!
return newEditor
}
// 注册到 wangEditor
Boot.registerPlugin(withBreak)
可参考 wangEditor 源码 基础模块open in new window 中所有 withXxx.ts
文件源码。
注册菜单
菜单分为几种,都可以扩展
- ButtonMenu 按钮菜单,如加粗、斜体
- SelectMenu 下拉菜单,如标题、字体、行高
- DropPanelMenu 下拉面板菜单,如颜色、创建表格
- ModalMenu 弹出框菜单,如插入链接、插入网络图片
注意,下面代码中的 key
即菜单 key ,要唯一不重复。
注册完菜单之后,即可把这个 key
配置到工具栏中。
安装 @wangeditor/core
如使用 Typescript 需要类型校验,需安装 @wangeditor/core
。
yarn add @wangeditor/core --peer
## 安装到 package.json 的 peerDependencies 中即可
ButtonMenu
代码如下。菜单的详细配置,可参考“引用”菜单源码open in new window。
注意:
- 必须在创建编辑器之前注册
- 全局只能注册一次,不要重复注册
import { IButtonMenu } from '@wangeditor/core'
import { Boot } from '@wangeditor/editor'
// 定义菜单 class
class MyButtonMenu implements IButtonMenu {
// 菜单配置,参考“引用”菜单源码
}
// 定义菜单配置
export const menu1Conf = {
key: 'menu1', // menu key ,唯一。注册之后,可配置到工具栏
factory() {
return new MyButtonMenu()
},
}
// 注册到 wangEditor
Boot.registerMenu(menu1Conf)
SelectMenu
代码如下。菜单的详细配置,可参考“标题”菜单源码open in new window。
注意:
- 必须在创建编辑器之前注册
- 全局只能注册一次,不要重复注册
import { ISelectMenu } from '@wangeditor/core'
import { Boot } from '@wangeditor/editor'
// 定义菜单 class
class MySelectMenu implements ISelectMenu {
// 菜单配置,代码可参考“标题”菜单源码
}
// 定义菜单配置
export const menu2Conf = {
key: 'menu2', // menu key ,唯一。注册之后,可配置到工具栏
factory() {
return new MySelectMenu()
},
}
// 注册到 wangEditor
Boot.registerMenu(menu2Conf)
DropPanelMenu
代码如下。菜单的详细配置,可参考“颜色”菜单源码open in new window。
注意:
- 必须在创建编辑器之前注册
- 全局只能注册一次,不要重复注册
import { IDropPanelMenu } from '@wangeditor/core'
import { Boot } from '@wangeditor/editor'
// 定义菜单 class
class MyDropPanelMenu implements IDropPanelMenu {
// 菜单配置,代码可参考“颜色”菜单源码
}
// 定义菜单配置
export const menu3Conf = {
key: 'menu3', // menu key ,唯一。注册之后,可配置到工具栏
factory() {
return new MyDropPanelMenu()
},
}
// 注册到 wangEditor
Boot.registerMenu(menu3Conf)
ModalMenu
代码如下。菜单配置可参考“插入链接”菜单源码open in new window。
注意:
- 必须在创建编辑器之前注册
- 全局只能注册一次,不要重复注册
import { IModalMenu } from '@wangeditor/core'
import { Boot } from '@wangeditor/editor'
// 定义菜单 class
class MyModalMenu implements IModalMenu {
// 菜单配置,代码可参考“插入链接”菜单源码
}
// 定义菜单配置
export const menu4Conf = {
key: 'menu4', // menu key ,唯一。注册之后,可配置到工具栏
factory() {
return new MyModalMenu()
},
}
// 注册到 wangEditor
Boot.registerMenu(menu4Conf)
封装为模块
可以把上述的 renderElem toHtml parseHtml 插件 菜单等,封装为一个 module ,然后一次性注册。
import { Boot } from '@wangeditor/editor'
import { IModuleConf } from '@wangeditor/core'
const module: Partial<IModuleConf> = {
editorPlugin: withBreak,
renderElems: [renderElemConf],
renderStyle: renderStyle,
elemsToHtml: [elemToHtmlConf],
styleToHtml: styleToHtml,
parseElemsHtml: [parseHtmlConf],
parseStyleHtml: parseStyleHtml,
menus: [menu1Conf, menu2Conf, menu3Conf],
}
Boot.registerModule(module)
总结
一个模块常用代码文件如下,共选择参考(不一定都用到)
- render-elem.ts
- render-style.ts
- elem-to-html.ts
- style-to-html.ts
- parse-elem-html.ts
- parse-style-html.ts
- plugin.ts
- menu/
- Menu1.ts
- Menu2.ts