quill

Quill 是一款现代的所见即所得编辑器,具备兼容性和可扩展性

Posted by page on September 21, 2023

Quill

社区优秀实现 quill-awesome

基础

安装:npm install quill -S

使用:

import Quill from "quill";

const quillEditor = new Quill(".editor", {
  placeholder: "此处输入文字",
});

主题:

import Quill from "quill";
import "quill/dist/quill.snow.css";

const quillEditor = new Quill(".editor", {
  theme: "snow",
  placeholder: "此处输入文字",
});

核心

Format

quill.format

对用户选中文本格式化,如果无选区内容(光标),格式将作用于后续输入字符

format(name: String, value: any, source: String = 'api'): Delta

quill.formatText

对编辑器指定范围内容格式化

formatText(
  index: Number,
  length: Number,
  formats: { [String]: any },
  source: String = 'api'
): Delta

Embed

quill.insertEmbed

插入嵌入内容

insertEmbed(index: Number, type: String, value: any, source: String = 'api'): Delta

Delta

quill 通过 delta (json)描述内容和变化

delta操作内容

import Delta from "quill-delta";
const index = quill.getSelection(true).index;
const delta = new Delta().retain(index);
      delta.insert("\n");
      delta.insert(
        { image: dataString },
        { width: 100 }
    );
delta.insert("\n");
quill.updateContents(delta);
quill.setSelection(index + 3, Quill.sources.USER);

链式方法

插入操作:insert(String | Object, attributes)

删除操作:delete(length)

保留操作:retain(length, attribute),可实现跳过n项、批量文字处理

delta更新内容

const delta = quill.getContents();
delta.ops = delta.ops.filter... // 过来部分operation
delta.ops = delta.ops.map... // 修改部分operation
quill.setContents(delta);

配置

配置自定义编辑器

new Quill(".editor", Configurations)

modules.toolbar

工具栏选项

{
  modules: {
    toolbar: ['bold', 'italic', 'underline', 'strike']
  }
}

工具栏布局

{
  modules: {
    toolbar: [['bold', 'italic'], ['link', 'image']]
  }
}

工具关键字值

{
  modules: {
    { 'header': '3' }
    { size: [ 'small', false, 'large', 'huge' ]},
    [{ 'color': [] }, { 'background': [] }], // [] 即主题默认值
  }
}

工具处理方法

{
  modules: {
    handler: {
      'link': customLinkHandler'
      'bold': customBoldHandler
    }
  }
}

定制工具栏

HTML 中手动创建工具栏,Quill 会将适当的处理程序附加到类名的形式为 ql-${format} 元素上

<div id="toolbar">
  <button class="edit-btn ql-bold">加粗</button>
  <button class="edit-btn ql-italic">斜体</button>
  <!-- ql-formats表示分组 -->
  <span class="ql-formats">
    <button class="edit-btn ql-underline">下划线</button>
    <button class="edit-btn ql-strike">删除线</button>
  <span>
</div>

<div id="editor"></div>

<script>
  import "quill/dist/quill.core.css";
  new Quill('#editor', {
    modules: {
      toolbar: '#toolbar'
    }
  });
</script>

对于下拉选择颜色、字号等工具的定制,通过 format API + Attributor 实现

// 字号白名单(支持的字号)
const SizeAttributor = Quill.import("attributors/style/size");
SizeAttributor.whitelist = [
  "12px",
  "14px",
  "15px",
  "16px",
  "17px",
  "18px",
  "20px",
  "24px",
];
Quill.register(SizeAttributor, true);

// 菜单选择调用
editor.format("size", `${size}px`);
editor.container.style.fontSize = `${size}px`; // 设置editor默认字号

主题与CSS

模块(Module)

History

历史记录支持

const quill = new Quill(".editor", {
  modules: {
    toolbar: "#toolbar",
    history: {},
  },
});
// 撤销
quill.history.undo()
// 重做
quill.history.redo()

自定义模块

Parchment

支持基于纯文本自定义 Quill 可识别的内容和格式,或添加全新的内容和格式

基于 Parchment API构造 Blot 声明内容/格式接口,最终通过 formatembed 调用;

blots(继承自parchment): block(block/embed)、break、container、embed、inline、text…(详见quill/blots目录)

format: Italic、Strike、Bold…(详见quill/formats目录)

embed: Image、Video…(详见quill/formats目录)

自定义blots

format

import Quill from 'quill';

// format id
let Inline = Quill.import('blots/inline');

class IdBlot extends Inline {
  static create(value) {
    let node = super.create();
    node.setAttribute('quill-id', value);
    return node;
  }

  static formats(node) {
    return node.getAttribute('quill-id');
  }
}
IdBlot.blotName = 'id';
IdBlot.tagName = 'span';

Quill.register(IdBlot);

// 使用
quill.format(0, 10, { id: '0x12345' })

embed

import Quill from 'quill';
let Embed = Quill.import('blots/embed');

class IdMarker extends Embed {
  static create(value) {
    let node = super.create();
    node.setAttribute('quill-id', value);
    return node;
  }

  static value(node) {
    return node.getAttribute('quill-id');
  }
}
IdMarker.blotName = 'idMarker';
IdMarker.className = 'id-marker'; // 必须声明类名称!!!
IdMarker.tagName = 'span';

Quill.register(IdMarker);


// 使用
quill.insertEmbed(10, 'idMarker', '0x12345' })

其它

操作行为

在editor的 disabled 生效下,需要为每个操作注明 source: api,否则将视为 user 操作被 blocked

功能实现

文章批注修改

批注操作数据,包含操作类型(增删改),操作位置范围

删除和修改操作在正确位置直接执行即可;插入操作,需预留插入占位符,如下:

let baseIndex = 0; // 基础偏移量,即插入占位符数量
for (let item of checkList) {
  if (operationMap.get(item.operation) === '插入') {
    // 预留插入占位符
    quillEditor.insertEmbed(
      item.posStart + baseIndex,
      'idMarker',
      item.id,
    );
    baseIndex++;
  }
  // 删改操作高亮显示
  else {
    quillEditor.formatText(
      item.posStart + baseIndex,
      item.len,
      {
        color: '#f56c6c',
        id: item.id,
      },
    );
  }
}

表情emoji

当文章内容存在emoji字符时,由于表情实际占位字符长大于1,导致后续文字实际位置偏移(从人的理解上),导致读取或操作的范围位置不正确

通过一个文字位置映射解决

const indexDictionary = []; // 包含内容每个文字对应的正确index
[...articleInfo.value.content].reduce((total, chat) => {
  indexDictionary.push(total);
  return total + chat.length;
 }, 0);
string.charAt(indexDictionary[index]) // string的index处字符,读取表情需额外处理