最近业务上需要使用富文本编辑器,因为所使用的组件库内没有现成的富文本编辑器组件,纯手写的话工程量巨大,想着用开源库封装一个,经过多家对比选用了Quill。
因为自己之前也使用过很多富文本编辑器(emmm…在当码农之前,不幸玩过一年多的新媒体运营,被各种微信编辑器逼疯最后转行…),所以深知作为一款合格的富文本编辑器,它最好具有以下特点:
- 易于扩展。可以方便地添加想要的功能。
- 可自定义样式。某些富文本编辑器样式简直丑到天理难容还不好改。
- 干净。这个有两方面,第一是样式干净,从别处复制过来的文字最好不带难以清除的样式,第二是DOM结构干净。
所幸,Quill完全符合。
接下来是一堆遇到难以解决的问题后应该访问的地方:
- Document:https://quilljs.com/docs/quickstart/ – 基本使用
- API文档:https://quilljs.com/docs/api/ – 查看API方法
- Guides:https://quilljs.com/guides/why-quill/ – 如何定制化开发,讲解得很详细
- github:https://github.com/quilljs/quill – download一份源码看永远最稳
- issues:https://github.com/quilljs/quill/issues – 很多你以为的“不能”这里都有答案
- awesome:https://github.com/quilljs/awesome-quill – 其实上面的一些资源也没有很好,但是可以看看别人的思路,能用则用,不能用可以根据这个思路自己写一下
本文无意科普Quill的使用,文档已经很详细了,只是开发过程中一些常见问题的解决和一些Quill中的基本概念(其实也都来源于以上链接)。
DELTA VS DOM结构
Quill最吸睛的地方在于,它不是直接生成DOM结构的,而是生成了一棵语法树,再通过解析这颗语法树生成DOM节点。这就保持DOM节点和样式上的统一,也就是上面所说的“干净”。只有在样式白名单上的属性会生成对应样式,而复制来的元素,不管之前它的DOM结构有多乱七八糟,格式化成语法树后就只剩元素名称和样式属性,再生成DOM结构就很清爽了。
这棵语法树在Quill中叫DELTA,我们甚至可以直接对它进行增删改查以修改生成的DOM结构。接下来简单罗列下最基本的获取DELTA并转换成DOM结构,高端玩法请查看文档。
获取DELTA直接调用quill实例上的getContents()方法即可,大致会生成这样一个结构:1
2
3
4
5
6
7
8{
ops:[{
attributes:{
size: '26px',
},
insert: 'text'
}]
}
ops数组中的每个元素都对应一个DOM节点,换行符也是。其中包含两个属性,insert为DOM元素,如果纯文本的话直接为{insert:[TEXT]}
格式,如果是图片为{insert:{image:[content]}}
。attributes中的属性值对应相应的formats,key对应formats类型,value对应formats中的白名单值(见下文)。
如果想把这一格式转成DOM结构的话,只需调用setContents(delta)方法即可。
自定义样式
场景一:修改默认样式
Quill给富文本编辑中的元素加上样式是通过两种方式,一是内联样式,二是通过加上相应Class。最简单的一个场景,如果我们想修改一个元素的样式,比如字体大小选择器,选中16px的按钮把文字变成14px,虽然这个例子选的比较蠢也比较不现实…这种情况如果是加上class的话直接覆盖对应css就可以了,就像刚刚举的例子,因为富文本编辑器的样式一般比较固定,什么颜色就是什么颜色,什么尺寸就是什么尺寸,好像没有什么值得修改的地方。但是仔细想想,还是有的,就拿刚刚的字体大小选择器来说,默认有四种,small、normal、large、huge,分别对应,10px、16px、18px、32px,这时候比如我们想添加第五种字体大小,或者将large的尺寸修改成34px,再或者我们觉得这四个单词不太明确,想将label修改成真实的字体大小应该怎么办?
我们来看一下这部分源码:
formats/size1
2
3
4
5
6
7
8
9
10
11
12import { ClassAttributor, Scope, StyleAttributor } from 'parchment';
const SizeClass = new ClassAttributor('size', 'ql-size', {
scope: Scope.INLINE,
whitelist: ['small', 'large', 'huge'],
});
const SizeStyle = new StyleAttributor('size', 'font-size', {
scope: Scope.INLINE,
whitelist: ['10px', '18px', '32px'],
});
export { SizeClass, SizeStyle };
可以参考这篇文档:https://quilljs.com/guides/cloning-medium-with-parchment/ 。在Quill中有几种基本的元素,它们被定义在blots文件夹中,它们是没有样式的,原文:
Like the DOM, a Parchment document is a tree. Its nodes, called Blots, are an abstraction over DOM Nodes. A few blots are already defined for us: Scroll, Block, Inline, Text and Break. As you type, a Text blot is synchronized with the corresponding DOM Text node; enters are handled by creating a new Block blot. In Parchment, Blots that can have children must have at least one child, so empty Blocks are filled with a Break blot. This makes handling leaves simple and predictable. All this is organized under a root Scroll blot.
如果想拥有丰富多彩的样式,则需要继承相应节点,并添加样式相关的配置逻辑,比如size和bold,它们都继承Inline,一个实现字体大小,一个实现字体是否加粗。以上代码抛出了两个实例,SizeClass和SizeStyle,SizeClass是说对于Inline元素,如果size的值是small/large/huge,则加上对应class(ql-size-[value]), SizeStyle的意思是对于Inline元素,如果值为10px、18px、32px,则在style中应用相应的font-size:[value],比如选huge,生成的Delta都是{size:32px}
,但是生成的DOM结构是不一样的。Quill也是通过ClassAttributor和StyleAttrbutor两个类控制样式设置的,具体可以看parchment
包。
回到刚刚的问题,如果我们想添加第5种字体怎么办,首先,在对应whitelist中加入这个字体,比如whitelist: ['small', 'large', 'huge', 'hugehuge']
,这里只是设置了字体大小的白名单,表示设置了这个值的话不会被忽略,接下来只要在toolbarOptions中设置{size:['small', 'large', 'huge', 'hugehuge']}
就可以了,这时候选择hugehuge选项会生成一个class名为ql-size-hugehuge
的DOM节点,然后我们在CSS中写出对应样式即可。
理解了这个流程,第二、三个问题就好解决了,通过修改css和whitelist都可以解决。不过还有一个问题是,我们可以写出这样的代码,但是如何应用呢?这就不得不祭出一个很厉害的方法register了。也是可以在源码中找到痕迹的。在quill.js中就是formats内文件的使用,我们只需要将attributors/style/size
ORattributors/class/size
指向我们修改过后的对象,并修改formats/size
的指向即可。
场景二:内联样式 VS CLASS样式
Quill默认是为元素添加class名的,如果想改成内联样式,也是在刚刚所说的quill.js中,将formats/[module]
改成内联样式的样式实例即可,这个看下源文件的写法基本就能理解了,官方也特意给了个实例。
自定义工具栏
默认的工具栏功能毕竟有限,虽然它提供了很多配置项,如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41[
['bold', 'italic', 'underline', 'strike',
{
'script': 'sub'
}, {
'script': 'super'
},
{
'color': []
}, {
'background': []
}, 'code'
],
[{
'font': []
}, {
'size': ['12px', '14px', '16px', '18px'
, '20px', '22px', '24px', '26px', '28px', '30px', '32px']
}],
['link', 'image', 'video', 'formula'],
['blockquote', 'code-block', {
'header': 6
},
{
'list': 'ordered'
}, {
'list': 'bullet'
}
],
[{
'indent': '-1'
}, {
'indent': '+1'
}, {
'direction': 'rtl'
}, {
'align': []
}],
['clean']
]
但保不齐就想有些新花样,比如觉得按钮图标不好看啦,比如想要新功能啦,等等等等。再次所幸,Quill提供了toolbar的配置,大致有三种,
- toolbar 设置为false,完全自己自定义方法和DOM结构,并绑定相应事件
- toolbar传入DOM结构,这种方式可以使用一部分自带的toolbar按钮(使用对应class名即可),也可以自己添加新的按钮,但是必须以
ql-${format}
命名。 - 传入如上的配置项,这种方式理论上来说也是可以自己添加toolbar按钮的,但是目前还没有研究过,暂不赘述。
还有一个问题比如图片上传,默认是上传后直接转Base64了,但是这样的话DOM结构会增大很多,可不可以上传我们自己的服务器取到地址呢?当然是可以的。1
2const toolbar = this.data.quill.getModule('toolbar');
toolbar.addHandler('image', this.imageHandler.bind(this));
获取到toolbar模块,为image添加事件处理方法即可。1
2
3
4
5
6
7
8
9
10
11
12imageHandler() {
const input = document.createElement('input');
const { nosParams } = this.data;
input.setAttribute('type', 'file');
input.setAttribute('accept', 'image/*');
input.click();
input.onchange = async function () {
const url = await upload(); // 上传逻辑,取到上传后的URL即可
const range = this.data.quill.getSelection();
this.data.quill.insertEmbed(range.index, 'image', link);
}.bind(this);
}
关键是inserEmbed()
方法, 将嵌入的内容插入编辑器,返回代表更改的Delta。
自定义模块
暂时还没有开发新的模块,不过有一个对image的扩展。
Quill默认的图片是很简单的,就是一个图片传上去,大小、位置也不可以调整。作为一个有追求的富文本编辑器,我们当然不能止步于此,首先,我们要可以调整图片大小,其它,可以调整图片位置。在awesome-quill中找到quill-image-resize-module,可以很好地达到预期效果,见demo。然而不幸的是这个库是有问题的,可以看下它的源码,自始至终它用的都是window.Quill,而我们的Quill并不是挂载在window上的,所以引入会报错,更不幸的是瞄了眼issues作者并没有修复的意愿,貌似已经放任它自生自灭了。那就自己来吧,改了下代码重新发了个包quill-image-resize。还有个问题,就是发现这个包并不会改变delta,只是在富文本编辑器中加上了相应样式,那当然是不行的啦,怎么改呢,也很简单,这些样式为什么会对富文本编辑器中的元素有效呢?因为加了内联样式。还有一个细节,虽然style属性不会保存到delta中,但是同样是html上的属性,width可以,看来又是formats的作用,打开源码一看果然是,源码中’alt’、’width’、‘height’属性都在白名单中,那么简单粗暴一些,把style加入就可以了,代码(ImageFormat.js):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39import Quill from 'quill';
const BaseImageFormat = Quill.import('formats/image');
// const Parchment = Quill.import('parchment');
// allow image alignment styles
const ImageFormatAttributesList = [
'alt',
'height',
'width',
'style'
];
class ImageFormat extends BaseImageFormat {
static formats(domNode) {
return ImageFormatAttributesList
.reduce(function (formats, attribute) {
if (domNode.hasAttribute(attribute)) {
formats[attribute]
= domNode.getAttribute(attribute);
}
return formats;
}, {});
}
format(name, value) {
if (ImageFormatAttributesList.indexOf(name) > -1) {
if (value) {
this.domNode.setAttribute(name, value);
} else {
this.domNode.removeAttribute(name);
}
} else {
super.format(name, value);
}
}
}
export default ImageFormat;
然后将’formats/image’指向ImageFormat就可以了。至于刚刚加的组件模块,需要注册到modules
上:1
2
3
4import ImageResize from 'quill-image-resize';
Quill.register({
'modules/imageResize': ImageResize
}, true);
在配置项的modules中加入imageResize即可,具体见组件文档。
Other
其实使用Quill的代码还是很值得学习的,在不断踩坑的过程中看了很多它的源码,哪怕自己之后想从0开始写一个富文本编辑器,也有了一些思路和灵感。
- 很多细小的配置项直接写在CSS中,一些配置项真的很细微,如果都写在config中的额话难免繁琐并且不一定会面面俱到,作者的CSS写得很聪明,充分利用了伪元素的content属性和HTML的[data-]属性,这样修改或覆盖的话会方便很多。
- 工程目录划分清晰。(TODO)
- …