前言
由于 Quill 在近期项目中的实践,特此写了这篇文章。从使用场景、基本原理,再到对 Quill 的扩展,跟大家分享 Quill 富文本编辑器的那些事儿。
使用场景
- 文章博客
- 聊天框
- 评论
- ...
基本原理
Quill 有两个核心的概念 Delta 和 Parchment Blot。
Delta
Delta 是一种特定 JSON 格式的数据模型,用来描述内容的变化。所以有了 Delta 数据,就能知道编辑器的内容。可以参考 Quill 文档中 Delta 的描述。
{
"ops": [
{ "insert": "Hello " },
{ "insert": "World", "attributes": { "bold": true } },
{ "insert": "\n" }
]
}
Parchment
Parchment is Quill's document model. It is a parallel tree structure to the DOM tree, and provides functionality useful for content editors, like Quill. A Parchment tree is made up of Blots, which mirror a DOM node counterpart.
Parchment 和 Blot 是对 DOM 的模拟。Blot 相当于 Node,它包含了很多 Quill 富文本操作需要的 API ,这些是原生 DOM API 没有的。
扩展能力
扩展性是编辑器的重要能力。
在使用编辑器时不仅仅有图文,往往还有个性化的需求,比如以下场景。
超链接卡片
比如插入知乎这样的超链接卡片。
插入表情
比如在编辑器中插入表情,类似微信的聊天框。
提及功能
社区评论中常见的提及(@Mention)功能。
扩展实现
扩展 Quill 的方式:
- 通过自定义 Blot 格式来扩展编辑器的内容。
- 通过自定义模块来扩展编辑器的功能。
如何插入表情
我们从如何插入表情入手,一起看看怎么在 Quill 中插入自定义的内容。
- 自定义工具栏按钮
- 自定义 EmojiBlot
- 注册 EmojiBlot
- 调用 Quill 的 API 插入表情
自定义工具栏按钮
参考微信的表情选择面板,所以需要我们自定义 emoji 工具栏按钮。
// emoji-tool.vue
<template>
<span class="editor-tool-emoji">
<el-popover ref="popover">
<span v-for="(item, i) of emojis" :key="i" @click="onSelect(item)">
<img :src="item.src_ios" :alt="item.code_cn" />
</span>
</el-popover>
<img src="../assets/emoji.png" v-popover:popover />
</span>
</template>
<script>
// [{ "code": "[Smile]", "code_cn": "[微笑]", "src_ios": "https://emojipedia-us.s3.amazonaws.com/content/2021/02/14/emojipedia_wechat_ios_802_smile.png", "src_android": "" }]
import emojis from '../assets/wechat-emojis.json'
export default {
data () {
return {
emojis,
}
},
methods: {
onSelect (item) {
this.$emit('select', item)
this.$refs.popover.doClose()
}
}
}
</script>
在自定义的 toolbar 中使用 emoji 工具栏按钮。
<template>
<div id="editor">
<div id="editor-toolbar">
<emoji-tool />
</div>
<div id="editor-container"></div>
</div>
</template>
this.quill = new Quill('#editor-container', {
theme: 'snow',
modules: {
toolbar: '#editor-toolbar',
}
})
效果如下:
自定义 EmojiBlot
Quill 中的 Blot 是一个普通的 ES6 Class,由于表情和图片的差别就在于:
Quill 内置的图片格式不支持自定义宽高,而我们要插入的表情是需要特定的宽高的。
因此我们可以基于 Quill 内置的 ImageBlot 来扩展。
// emoji.js
import Quill from 'quill'
const ImageBlot = Quill.import('formats/image')
// 扩展 Quill 内置的 ImageBlot
export default class EmojiBlot extends ImageBlot {
static tagName = 'img' // 自定义内容的标签名
static blotName = 'emoji' // 自定义 Blot 的名字(必须全局唯一)
static className = 'emoji'
// 创建自定义内容的 DOM 节点
static create(value) {
const node = super.create(value)
node.setAttribute('src', value.src)
node.setAttribute('alt', value.alt)
node.setAttribute('style', 'vertical-align: bottom;pointer-events: none;')
if (value.width !== undefined) {
node.setAttribute('width', value.width)
}
if (value.height !== undefined) {
node.setAttribute('height', value.height)
}
return node
}
// 返回 ops 数据
static value(node) {
return {
src: node.getAttribute('src'),
alt: node.getAttribute('alt'),
width: node.getAttribute('width'),
height: node.getAttribute('height'),
}
}
}
注册 EmojiBlot
有了 EmojiBlot,要将其插入 Quill 编辑器中,还需要将这个类注册到 Quill 中。
// index.vue
import Quill from 'quill'
import EmojiBlot from './formats/emoji'
Quill.register({
'formats/emoji': EmojiBlot
}, true)
调用 Quill 的 API 插入表情
EmojiBlot 注册到 Quill 中之后,Quill 就能认识它了,也就可以调用 Quill 的 API 将其插入到编辑器中。
onSelectEmoji (data) {
const { index } = this.quill.getSelection(true)
this.quill.insertEmbed(index, 'emoji', {
width: '20px',
height: '20px',
src: data.src_ios,
alt: data.code_cn
})
this.quill.setSelection(index + 1)
}
最终效果
demo 源码地址:https://github.com/jiangrubin/quill-demo
结语
最后再说一下对 Quill 使用后的感受。在开箱即用方面 Quill 做的不错,简单易用文档清晰,对一些基本场景都能够覆盖。但在个性化定制扩展方面,需要通过操作 DOM 的方式来实现,对习惯了 Vue 或 React 的开发方式,确实不太友好。
在写这篇文章时,发现了几款不错的富文本编辑器:
- Tiptap 基于 ProseMirror 封装的,专为 vue.js 打造,设计优雅,体验流畅舒服的现代富文本编辑器。
- Editor.js Next generation block styled editor 块样式编辑器 - 知识库内容编辑的未来。