介绍

Quill 是一款非常优秀的 Web 富文本编辑器,近半年 npm 周下载量一直在 40w+ ,用户量很大。

Quill 的官网 我有时候打不开,有时候打开很慢。于是,找到一篇中文翻译的文档和当前 npm 版本很接近。

从底层实现上,Quill 也和 slate.js 一样,是 L1 级 Web 编辑器,两者都是非常优秀的作品。只不过 slate.js 提供的是底层能力、供二次开发,而 Quill 提供的就是一个开箱即用的编辑器。

Quill 如此受欢迎,我觉得原因有:

  • 简单易用,文档清晰
  • 默认包含了常用的功能,下载即用
  • 扩展性好,可自定义模块
  • 发布时间比较久了,之前就积累了一大批用户
  • 设计理念非常先进,model view 分离,支持 Operation transformation (可实现多人协同编辑)

不过看 Quill 的最近一次发布是 2019.9 ,离现在(2021.1)也挺久了。不知道以后还会不会频繁更新。

使用

快速体验

Quill 使用特别简单,几行代码即可生成一个编辑器。

<!-- Include stylesheet -->
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
 
<!-- Create the editor container -->
<div id="editor">
  <p>Hello World!</p>
  <p>Some initial <strong>bold</strong> text</p>
  <p><br></p>
</div>
 
<!-- Include the Quill library -->
<script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
 
<!-- Initialize Quill editor -->
<script>
  var quill = new Quill('#editor', {
    theme: 'snow'
    // 其他配置项,可参考 https://quilljs.com/docs/configuration/
  });
</script>

配置工具栏

Quill 可非常灵活的配置工具栏,具体看文档

定义工具栏 UI

可自定义工具栏 DOM 容器,并在初始化时传给 Quill 。也可以自定义菜单 UI ,但需要为 <button><select> 设置 ql-${format} 的 css class。

<!-- Create toolbar container -->
<div id="toolbar">
  <!-- Add font size dropdown -->
  <select class="ql-size">
    <option value="small"></option>
    <!-- Note a missing, thus falsy value, is used to reset to default -->
    <option selected></option>
    <option value="large"></option>
    <option value="huge"></option>
  </select>
  <!-- Add a bold button -->
  <button class="ql-bold"></button>
  <!-- Add subscript and superscript buttons -->
  <button class="ql-script" value="sub"></button>
  <button class="ql-script" value="super"></button>
</div>
<div id="editor"></div>
 
<!-- Initialize editor with toolbar -->
<script>
  var quill = new Quill('#editor', {
    modules: {
      toolbar: '#toolbar'
    }
  });
</script>

分组自定义工具栏菜单

var toolbarOptions = ['bold', 'italic', 'underline', 'strike'];
 
var quill = new Quill('#editor', {
  modules: {
    toolbar: toolbarOptions // 只显示用户需要的菜单
  }
});

handlers

可以自定义 format 行为。

var toolbarOptions = {
  handlers: {
    // handlers object will be merged with default handlers object
    'link': function(value) {
      if (value) {
        var href = prompt('Enter the URL');
        this.quill.format('link', href);
      } else {
        this.quill.format('link', false);
      }
    }
  }
}
 
var quill = new Quill('#editor', {
  modules: {
    toolbar: toolbarOptions
  }
});

内容

即我们常见的获取内容、设置内容、插入文字、删除文字等操作,具体可参考文档

Delta 数据

和传统认知不一样,Quill 设置、获取的内容,都不是常见的 html 或者类似 vnode 的 JSON 数据,而是个 Delta 数据。

Delta 数据也是 JSON ,格式下文会详细介绍,这里先不必过于纠结。你只需要知道,Quill 可以通过 Delta 来表示内容,即可。

insertEmbed

插入图片、视频等卡片,和插入文本不一样,需要指定类型。目前 Quill 支持图片和视频两种,其他的可以自己扩展。

quill.insertEmbed(10, 'image', 'https://quilljs.com/images/cloud.png');

text-change

可以通过 text-change 及时获取修改的内容。

quill.on('text-change', function(delta, oldDelta, source) {
  if (source == 'api') {
    console.log("An API call triggered this change.");
  } else if (source == 'user') {
    console.log("A user action triggered this change.");
  }
});

选区

对于富文本编辑器,选区是不可缺少的功能。特别是做插件扩展和二次开发,经常用到选区 API 。

Quill 的选区也是一个 Range 对象,格式非常简单。index 表示选区的开始位置,length 表示选区的长度。

可以通过 range-change 事件监听选区变化。这个也很有用,很多情况选区变了,UI 也需要随之变化的。

quill.on('selection-change', function(range, oldRange, source) {
  if (range) {
    if (range.length == 0) {
      console.log('User cursor is on', range.index);
    } else {
      var text = quill.getText(range.index, range.length);
      console.log('User has highlighted', text);
    }
  } else {
    console.log('Cursor not in the editor');
  }
});

最后,Quill 还支持获取选区的位置和尺寸。这就非常利于我们做诸如 @ 功能,以及在选区位置显示菜单,等功能。

格式操作

富文本编辑器最基本的功能就是文本格式操作,例如加粗、斜体、颜色等。Quill 的格式操作非常清晰,可直接参考 formating 文档 ,其中涉及到的 format 值有哪些,可参考 formats 文档

跟内容处理的 API 一样,格式操作返回的也是 Delta 数据,后面再解释。

// 1. 针对选中的文本,设置格式
quill.format('color', 'red');
quill.format('align', 'right');
 
// 2. 针对某个选区,设置格式
quill.formatText(0, 5, 'bold', true);
quill.formatText(0, 5, {
  'bold': false,
  'color': 'rgb(0, 0, 255)'
});
 
// 3. 设置传入范围内所有行的格式
quill.setText('Hello\nWorld!\n'); // 两行文本
quill.formatLine(1, 2, 'align', 'right');   // 第一行设置 align=right
quill.formatLine(4, 4, 'align', 'center');  // 两行都设置 align=right
 
// 4. 获取一个选区的格式
quill.getFormat(1, 1);   // { bold: true, italic: true }
 
// 5. 移除传入区域所有格式和**嵌入对象**
quill.setContents([
  { insert: 'Hello', { bold: true } },
  { insert: '\n', { align: 'center' } },
  { insert: { formula: 'x^2' } },
  { insert: '\n', { align: 'center' } },
  { insert: 'World', { italic: true }},
  { insert: '\n', { align: 'center' } }
]);
 
quill.removeFormat(3, 7);
// Editor contents are now
// [
//   { insert: 'Hel', { bold: true } },
//   { insert: 'lo\n\nWo' },
//   { insert: 'rld', { italic: true }},
//   { insert: '\n', { align: 'center' } }
// ]

扩展模块

module 模块

模块就类似于插件,用于扩展 Quill 的能力。Quill 已经内置了 toolbar clipboard keyboard syntaxhistory ,这些直接配置使用即可,具体去看文档。

var quill = new Quill('#editor', {
  modules: {
  	toolbar: '#toolbar',
    history: {          // Enable with custom configurations
      'delay': 2500,
      'userOnly': true
    },
    syntax: true        // Enable with default configuration
  }
});

扩展模块

可以使用 register 注册一个模块,然后使用 import 来引入。

register 也可以用于注册新的主题和格式,为了区分,用不同的前缀 modules/ formats/ themes/

Quill.register({
  'formats/custom-format': CustomFormat,
  'modules/custom-module-a': CustomModuleA,
  'modules/custom-module-b': CustomModuleB,
});

也可以去继承和重写现有的模块。先用 import 引入,重写完,再 register 重新注册上、覆盖即可。

var Clipboard = Quill.import('modules/clipboard');
var Delta = Quill.import('delta');
 
class PlainClipboard extends Clipboard {
  convert(html = null) {
    if (typeof html === 'string') {
      this.container.innerHTML = html;
    }
    let text = this.container.innerText;
    this.container.innerHTML = '';
    return new Delta().insert(text);
  }
}
 
Quill.register('modules/clipboard', PlainClipboard, true);

核心概念

Quill 将其核心概念,都单独抽离出来,分别写在不同的 github 仓库,这个非常好。

Delta

想了解 Delta 的设计,提前得去了解一下 Operational Transformation ,即 OT 算法。这是目前多人在线协同编辑器的常用方案。Delta 就是参考 OT 算法来设计的,并不是自创的。

Delta 就是一种特定的 JSON 格式,用来描述内容的变化。所以,有了 Delta 数据,就能知道编辑器的内容。可以参考 Quill 文档中 Delta 的描述

如下图,通过阅读 Delta 内容,就能猜出它的意思。就是插入一行一行的文字,其中有换行,某些文字文字还有属性。

Delta 包含三种操作类型(也是 OT 算法里的),不可扩展,不可修改

  • insert 插入文字
  • delete 删除文字
  • retain 保留文字,或者理解为移动光标到某个文字处

insert

默认为插入文本,可以在插入时设置样式、属性。

{
  ops: [
    { insert: 'Gandalf', attributes: { bold: true } },
    { insert: ' the ' },
    { insert: 'Grey', attributes: { color: '#cccccc' } }
  ]
}

当然,除了插入文字之外,还可以插入 embed 数据类型。此时 insert 属性值就不是字符串,而是对象。

{ insert: { image: 'xxx.png' }, attributes: { link: 'xxx.html' } }

还有,如何表示换行呢?普通的就用 \n,会被渲染为 <p> 标签。其他标签,可设置属性,如 { header: 1 }

{
  ops: [
    { insert: 'The Two Towers' },
    { insert: '\n', attributes: { header: 1 } },
    { insert: 'Aragorn sped on up the hill.\n' }
  ]
}
//表示的内容:<h1>The Two Towers</h1><p>Aragorn sped on up the hill.</p>

delete

删除指定长度的文本。如下图,先试用 retain 将选区定位到 30 的位置,然后删除接下来的 5 个字符。

retain

retain 稍微难理解一些。retain 字面意思是“保留”,如 { retain: 10 } 意思就是保留当前选区之后的 10 个字符。其实它的意思就是:移动光标到 10 个字符之后,然后继续做其他操作。这样就比较好理解了。

例如上文的删除操作,是移动光标到 30 个字符串之后,然后再删除掉接下来的 5 个字符。

retain 还有一个重要用途,就是修改属性。如下图,先移动光标到 6 个字符之后,然后对接下来的 5 个字符,设置 color 属性。

Parchment Blot

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 就是 Quill 对于 DOM 节点和操作的模拟。Blot 就相当于 Node,但它包含了很多 Quill 富文本操作需要的 API ,这些是原生 DOM API 没有的。

Quill 提供了一些和 Blot 相关的 API ,不过都是写实验性质的。

// 静态方法,给定一个DOM节点,返回对应的 Quill 或 Blot 实例
Quill.find(someNode)
 
// 给一个 offset (从文档开始位置计算),返回当前选区定位的 blot 和 offset(当前 blot 开始位置计算)
const [blot, offset] = quill.getLine(17); // [Block, 2]
 
// 返回文档开始至给定 blot 位置的距离
quill.getIndex(blot) // 15
 
// getLeaf 根据 offset (文档开始位置),返回叶子节点。
// 叶子节点可能有很多分类,例如 TextNode Image Video 等,请看下图。
 
// 返回选区范围内的所有行的 blot 对象(Block 类型)
quill.getLines(2, 13) // [Block, Block]

总结

Quill 是一个很强大、很易用、设计理念很先进的编辑器。它的模块机制,渲染机制,Delta 操作,很值得我们学习和研究。

本文对 Quill 的使用和概念,做了一个简单的介绍,并未深入。未来我会继续研究 Quill ,会再分享文章。