自定义扩展新功能

wangEditor 从 V5 开始,源码上就分离了 core editor 还有各个 module 。
core 是核心 API ,editor 负责汇总集成。所有的具体功能,都分布在各个 module 中来实现。

所以,从底层设计就保证了扩展性。

自定义扩展新功能 - 图1

概述

wangEditor 扩展性包括以下部分,你可以来扩展大部分常用的功能。

  • 定义新元素(如 todo-list 、链接卡片、@xxx 功能,插入地图等)
    • 渲染到编辑器
    • 显示时获取 html
  • 劫持编辑器的 API 并自定义(如输入 # 之后,切换为 H1 ,实现简单的 markdown 功能)
  • 扩展菜单

元素的数据结构

如果你需要扩展新元素,则需要先定义数据结构。
如果不需要,则忽略该步骤。

具体可参考 节点数据结构 ,定义自己的节点数据结构。
注意要符合 slate.js自定义扩展新功能 - 图2open in new window 的数据规范。

Render

需要你提前了解 vdom 概念,以及 snabbdom.js自定义扩展新功能 - 图3open in new window 和它的 h 函数。

如果你定义了新元素,则需要把它显示到编辑器内。主要过程是:model -> 生成 vdom -> 渲染 DOM
用到了 vdom 需要安装 snabbdom,参考上文。

自定义扩展新功能 - 图4

安装 snabbdom.js

  1. yarn add snabbdom --peer
  2. ## 安装到 package.json 的 peerDependencies 中即可

renderElem

增加了新的元素,需要渲染到编辑器中。

注意:

  • 必须在创建编辑器之前注册
  • 全局只能注册一次,不要重复注册
  1. import { h, VNode } from 'snabbdom'
  2. import { Boot, IDomEditor, SlateElement } from '@wangeditor/editor'
  3. // 渲染函数
  4. function renderParagraph(elem: SlateElement, children: VNode[] | null, editor: IDomEditor): VNode {
  5. // elem 即当前节点
  6. // children 是下级节点
  7. // editor 即编辑器实例
  8. const vnode = h('p', {}, children) // type: 'paragraph' 节点,即渲染为 <p> 标签
  9. return vnode
  10. }
  11. // 渲染配置
  12. const renderElemConf = {
  13. type: 'paragraph', // 节点 type ,重要!!!
  14. renderElem: renderParagraph,
  15. }
  16. // 注册到 wangEditor
  17. Boot.registerRenderElem(renderElemConf)

PS:h 函数的使用,请参考 snabbdom自定义扩展新功能 - 图5open in new window

renderStyle

渲染 CSS 样式,最基本的一些样式(如加粗、斜体、颜色、对齐方式等)编辑器已经自带了。 如果你需要再自定义新的样式,可以通过以下方式来注册。

注意:

  • 必须在创建编辑器之前注册
  • 全局只能注册一次,不要重复注册
  1. import { h, VNode, VNodeStyle } from 'snabbdom'
  2. import { Boot, SlateElement, SlateText, SlateDescendant } from '@wangeditor/editor'
  3. /**
  4. * 给 vnode 添加样式
  5. * @param vnode vnode
  6. * @param newStyle { key: val }
  7. */
  8. function addVnodeStyle(vnode: VNode, newStyle: VNodeStyle) {
  9. if (vnode.data == null) vnode.data = {}
  10. const data = vnode.data
  11. if (data.style == null) data.style = {}
  12. Object.assign(data.style, newStyle)
  13. }
  14. /**
  15. * render style
  16. * @param node slate node
  17. * @param vnode vnode
  18. * @returns new vnode
  19. */
  20. function renderStyle(node: SlateDescendant, vnode: VNode): VNode {
  21. // 1. 获取样式相关的属性
  22. const { bold, color } = node as SlateText
  23. // const { lineHeight } = node as SlateElement // node 可能是 Text 也可能是 Element
  24. let newVnode = vnode
  25. // 2. 为 vnode 添加样式标签
  26. if (bold) {
  27. newVnode = h('strong', {}, [newVnode])
  28. }
  29. if (color) {
  30. addVnodeStyle(newVnode, { color })
  31. }
  32. // if (lineHeight) {
  33. // addVnodeStyle(newVnode, { lineHeight })
  34. // }
  35. // 3. 返回添加了样式的 vnode
  36. return newVnode
  37. }
  38. // 注册到 wangEditor
  39. Boot.registerRenderStyle(renderStyle)

PS:h 函数的使用,请参考 snabbdom自定义扩展新功能 - 图6open in new window

To HTML

在显示编辑器内容时 ,无论什么渲染形式,都需要得到各个元素的 html。所以对于新元素,必须要扩展 toHtml 方法。

elemToHtml

生成元素的 html

注意:

  • 必须在创建编辑器之前注册
  • 全局只能注册一次,不要重复注册
  1. import { Boot, SlateElement } from '@wangeditor/editor'
  2. // 生成 html 的函数
  3. function paragraphToHtml(elem: SlateElement, childrenHtml: string): string {
  4. if (childrenHtml === '') {
  5. return '<p><br/></p>'
  6. }
  7. return `<p>${childrenHtml}</p>`
  8. }
  9. // 配置
  10. const elemToHtmlConf = {
  11. type: 'paragraph', // 节点 type ,重要!!!
  12. elemToHtml: paragraphToHtml,
  13. }
  14. // 注册到 wangEditor
  15. Boot.registerElemToHtml(elemToHtmlConf)

可参考 wangEditor 源码中 基础模块自定义扩展新功能 - 图7open in new window 中各个模块的所有 elem-to-html.ts 文件。

styleToHtml

生成 CSS 样式的 html ,如文本的加粗、斜体、颜色等,还有段落的对齐、行高等。

注意:

  • 必须在创建编辑器之前注册
  • 全局只能注册一次,不要重复注册
  1. import { Boot, SlateText, SlateElement, SlateDescendant } from '@wangeditor/editor'
  2. import $ from 'dom7' // jquery 也可以
  3. /**
  4. * style to html
  5. * @param node slate node
  6. * @param nodeHtml node html
  7. * @returns styled html
  8. */
  9. function styleToHtml(node: SlateDescendant, nodeHtml: string): string {
  10. // 1. 获取样式相关的属性获取属性
  11. const { color, bgColor } = node as SlateText
  12. // const { lineHeight } = node as SlateElement // node 可能是 Text 也可能是 Element
  13. // 设置 css 样式
  14. const $elem = $(elemHtml)
  15. if (color) $elem.css('color', color)
  16. if (bgColor) $elem.css('background-color', bgColor)
  17. // $elem.css('line-height', lineHeight)
  18. // 输出 html
  19. return $elem[0].outerHTML
  20. }
  21. // 注册到 wangEditor
  22. Boot.registerStyleToHtml(styleToHtml)

可参考 wangEditor 源码中 颜色、背景色自定义扩展新功能 - 图8open in new window字体字号自定义扩展新功能 - 图9open 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 。

  1. import { Dom7Array } from 'dom7'
  2. import { Boot, IDomEditor, SlateDescendant, SlateText, SlateElement } from '@wangeditor/editor'
  3. /**
  4. * 将 html 转换为 slate elem
  5. * @param $elem 由 html 生成的 DOM 节点(Dom7 封装,类似于 jquery)
  6. * @param children 子节点
  7. * @param editor editor
  8. * @returns slate element
  9. */
  10. function parseHtml($elem: Dom7Array, children: SlateDescendant[], editor: IDomEditor): SlateElement {
  11. // 过滤 children
  12. children = children.filter(child => {
  13. // child 必须是 text 或者 inline element (不能是 block element)
  14. if (SlateText.isText(child)) return true
  15. if (editor.isInline(child)) return true
  16. return false
  17. })
  18. // 无 children ,则取 $elem 纯文本
  19. if (children.length === 0) {
  20. children = [{ text: $elem.text().replace(/\s+/gm, ' ') }]
  21. }
  22. // 返回 slate elem ,链接类型
  23. return {
  24. type: 'link',
  25. url: $elem.attr('href') || '',
  26. target: $elem.attr('target') || '',
  27. children,
  28. }
  29. }
  30. export const parseHtmlConf = {
  31. selector: 'a', // CSS 选择器,以匹配“链接”的 html tag
  32. parseElemHtml: parseHtml,
  33. }
  34. // 注册
  35. Boot.registerParseElemHtml(parseHtmlConf)

parseStyleHtml

例如编辑器处理颜色,以下代码可识别 html '<span style="color: rgb(231, 95, 51); background-color: rgb(252, 251, 207);">hello</span>' 中的 colorbackground-color ,并添加到 text node 。

  1. import { Dom7Array, SlateText } from 'dom7'
  2. import { Boot, SlateDescendant, SlateText } from '@wangeditor/editor'
  3. /**
  4. * 识别 html 中的颜色,并添加到 text node
  5. * @param $text 由 html 创建的 DOM 节点(Dom7 创建,类似 jquery)
  6. * @param node text node
  7. * @returns text node with color and bgColor
  8. */
  9. export function parseStyleHtml($text: Dom7Array, node: SlateDescendant): SlateDescendant {
  10. if (!SlateText.isText(node)) return node
  11. const textNode = node as SlateText
  12. const color = getStyleValue($text, 'color') // 获取 style 中的 color 值
  13. if (color) {
  14. textNode.color = color
  15. }
  16. const bgColor = getStyleValue($text, 'background-color') // 获取 style 中的 background-color 值
  17. if (bgColor) {
  18. textNode.bgColor = bgColor
  19. }
  20. return textNode
  21. }
  22. // 注册
  23. Boot.registerParseStyleHtml(parseStyleHtml)

注册插件

wangEditor 是基于 slate.js自定义扩展新功能 - 图10open in new window 内核的,所以可以直接使用 slate.js 插件。所以你先要去了解 slate.js 的 API 和插件机制。

注意:

  • 必须在创建编辑器之前注册
  • 全局只能注册一次,不要重复注册
  1. import { Boot, IDomEditor } from '@wangeditor/editor'
  2. // 定义 slate 插件
  3. function withBreak<T extends IDomEditor>(editor: T): T {
  4. const { insertBreak } = editor
  5. const newEditor = editor
  6. // 重写 editor insertBreak API
  7. // 例如做一个 ctrl + enter 换行功能
  8. newEditor.insertBreak = () => {
  9. // 判断,如果按键是 ctrl + enter ,则执行 insertBreak
  10. insertBreak()
  11. // 否则,则 return
  12. }
  13. // 还可以重写其他 API
  14. // 返回 editor ,重要!
  15. return newEditor
  16. }
  17. // 注册到 wangEditor
  18. Boot.registerPlugin(withBreak)

可参考 wangEditor 源码 基础模块自定义扩展新功能 - 图11open in new window 中所有 withXxx.ts 文件源码。

注册菜单

菜单分为几种,都可以扩展

  • ButtonMenu 按钮菜单,如加粗、斜体
  • SelectMenu 下拉菜单,如标题、字体、行高
  • DropPanelMenu 下拉面板菜单,如颜色、创建表格
  • ModalMenu 弹出框菜单,如插入链接、插入网络图片

注意,下面代码中的 key 即菜单 key ,要唯一不重复。
注册完菜单之后,即可把这个 key 配置到工具栏中。

安装 @wangeditor/core

如使用 Typescript 需要类型校验,需安装 @wangeditor/core

  1. yarn add @wangeditor/core --peer
  2. ## 安装到 package.json 的 peerDependencies 中即可

ButtonMenu

代码如下。菜单的详细配置,可参考“引用”菜单源码自定义扩展新功能 - 图12open in new window

注意:

  • 必须在创建编辑器之前注册
  • 全局只能注册一次,不要重复注册
  1. import { IButtonMenu } from '@wangeditor/core'
  2. import { Boot } from '@wangeditor/editor'
  3. // 定义菜单 class
  4. class MyButtonMenu implements IButtonMenu {
  5. // 菜单配置,参考“引用”菜单源码
  6. }
  7. // 定义菜单配置
  8. export const menu1Conf = {
  9. key: 'menu1', // menu key ,唯一。注册之后,可配置到工具栏
  10. factory() {
  11. return new MyButtonMenu()
  12. },
  13. }
  14. // 注册到 wangEditor
  15. Boot.registerMenu(menu1Conf)

SelectMenu

代码如下。菜单的详细配置,可参考“标题”菜单源码自定义扩展新功能 - 图13open in new window

注意:

  • 必须在创建编辑器之前注册
  • 全局只能注册一次,不要重复注册
  1. import { ISelectMenu } from '@wangeditor/core'
  2. import { Boot } from '@wangeditor/editor'
  3. // 定义菜单 class
  4. class MySelectMenu implements ISelectMenu {
  5. // 菜单配置,代码可参考“标题”菜单源码
  6. }
  7. // 定义菜单配置
  8. export const menu2Conf = {
  9. key: 'menu2', // menu key ,唯一。注册之后,可配置到工具栏
  10. factory() {
  11. return new MySelectMenu()
  12. },
  13. }
  14. // 注册到 wangEditor
  15. Boot.registerMenu(menu2Conf)

DropPanelMenu

代码如下。菜单的详细配置,可参考“颜色”菜单源码自定义扩展新功能 - 图14open in new window

注意:

  • 必须在创建编辑器之前注册
  • 全局只能注册一次,不要重复注册
  1. import { IDropPanelMenu } from '@wangeditor/core'
  2. import { Boot } from '@wangeditor/editor'
  3. // 定义菜单 class
  4. class MyDropPanelMenu implements IDropPanelMenu {
  5. // 菜单配置,代码可参考“颜色”菜单源码
  6. }
  7. // 定义菜单配置
  8. export const menu3Conf = {
  9. key: 'menu3', // menu key ,唯一。注册之后,可配置到工具栏
  10. factory() {
  11. return new MyDropPanelMenu()
  12. },
  13. }
  14. // 注册到 wangEditor
  15. Boot.registerMenu(menu3Conf)

ModalMenu

代码如下。菜单配置可参考“插入链接”菜单源码自定义扩展新功能 - 图15open in new window

注意:

  • 必须在创建编辑器之前注册
  • 全局只能注册一次,不要重复注册
  1. import { IModalMenu } from '@wangeditor/core'
  2. import { Boot } from '@wangeditor/editor'
  3. // 定义菜单 class
  4. class MyModalMenu implements IModalMenu {
  5. // 菜单配置,代码可参考“插入链接”菜单源码
  6. }
  7. // 定义菜单配置
  8. export const menu4Conf = {
  9. key: 'menu4', // menu key ,唯一。注册之后,可配置到工具栏
  10. factory() {
  11. return new MyModalMenu()
  12. },
  13. }
  14. // 注册到 wangEditor
  15. Boot.registerMenu(menu4Conf)

封装为模块

可以把上述的 renderElem toHtml parseHtml 插件 菜单等,封装为一个 module ,然后一次性注册。

  1. import { Boot } from '@wangeditor/editor'
  2. import { IModuleConf } from '@wangeditor/core'
  3. const module: Partial<IModuleConf> = {
  4. editorPlugin: withBreak,
  5. renderElems: [renderElemConf],
  6. renderStyle: renderStyle,
  7. elemsToHtml: [elemToHtmlConf],
  8. styleToHtml: styleToHtml,
  9. parseElemsHtml: [parseHtmlConf],
  10. parseStyleHtml: parseStyleHtml,
  11. menus: [menu1Conf, menu2Conf, menu3Conf],
  12. }
  13. 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