阅读 3830

Quill 自定义富文本功能

前言

富文本是给输入的内容增加样式,Quill 提供 api 的方式去判断修改 DOM ,而不像很多其他的编辑器通过遍历 DOM 树的方式来做到这点。并且 Quill 支持定制化功能,接下来来了解下如何使用 Quill 及它的相关源码

quickstart

如同 Vue,React 一样,Quill 也需要已存在的 dom 元素作为 Quill 实例挂载的目标

基础配置

参数一:可以是dom,可以选择器(自动去转换成对于的dom)。 参数二:配置对象,关于富文本的一些配置选项。

var editor = new Quill('#editor', options);
复制代码

options 配置对象

var options = {
  modules: {
    toolbar: '#toolbar'
  },
  placeholder: 'Compose an epic...',
  theme: 'snow'
};
复制代码

modules.toolbar

用于配置工具栏
方式一:css标签

var quill = new Quill('#editor', {
  modules: {
    // Equivalent to { toolbar: { container: '#toolbar' }}
    toolbar: '#toolbar'
  }
});
复制代码

方式二:对象

var quill = new Quill('#editor', {
  modules: {
    toolbar: {
        container:'#toolbar'
    }
  }
});
复制代码

方式三:数组

var quill = new Quill('#editor', {
  modules: {
    toolbar: ['bold', 'italic', 'underline', 'strike']
  }
});
复制代码

编辑区和工具栏区

从这些配置可以看出,整个富文本分为:编辑区和工具栏区。 #editor 挂载的元素目标将会被替换成输入框的部分,而#toolbar 挂载的元素目标将会被替换成:工具栏区。

var editor = new Quill('#editor', {
    modules: {
    toolbar: {
        container:'#toolbar'
    }
  }
});
复制代码

定制化功能

通过上面简单的配置,可以使用一些基础的富文本编辑器的功能,来实现Quill 功能的定制化

Quill自带功能如何实现

font 功能实现,分为两步:

  1. 实例化 Parchment.Attributor.Class 类,前面介绍中知道 Parchment.Attributor 属性下有一个基础类Attributor 和三个功能类class,style,store
  2. 我们通过类实例化出一个 blots 节点,之后我们需要 registerd 这个节点
// 步骤1
import Parchment from 'parchment';

let config = {
  scope: Parchment.Scope.INLINE,
  whitelist: ['serif', 'monospace']
};

let FontClass = new Parchment.Attributor.Class('font', 'ql-font', config);

export { FontClass };
// 步骤2 
import { FontClass } from './formats/font';
Quill.register({
'formats/font': FontClass,
},true)
复制代码

自定义行高功能

quill 本身是不支持行高的功能的,需要我们自定义这个功能,模仿Quill本地 font 功能的实现如下:codepen 自定义行高demo

const config = {
        scope: Parchment.Scope.INLINE,
        whitelist: this.lineHeightList
}
const Parchment = Quill.import("parchment");
    // 步骤一实例化 lineHeight 类(自定义的)
    class lineHeightAttributor extends Parchment.Attributor.Class {}
    const lineHeightStyle = new lineHeightAttributor(
      "lineHeight",
      "ql-lineHeight",
       config
    );
    // 注册实例
    Quill.register({ "formats/lineHeight": lineHeightStyle }, true);
复制代码

注册完实例后,该如何使用行高的功能?

  1. 由于我们继承的是 Parchment.Attributor.Class 它是会通过操作节点类名来实现节点的样式的控制,所以可以通过自定义的 ql-lineHeightclass 来控制选中文本的行高
<div id="app">
  <div id="toolbar">
    <select class="ql-lineHeight">
        // 通过 selected 来设置默认选中的行高样式
        <option v-for="(lineHeight,index) in lineHeightList" :key="lineHeight" :value="lineHeight" :selected="index === 3">{{ lineHeight }}</option>
      </select>
  </div>
  <div id="container"></div>
</div>
复制代码
  1. 自定义的dom结构定义好了以后,我们需要自定义这些样式对应的行高,

查看控制台 Elements 可以看到,Quill 会根据我们设定的 select 去生成一个自定样式的下拉框,例如 2.0 的行高样式我们可以设置如下css样式

// 该类名对应的行高
.ql-lineHeight-2 {
  line-height: 2;
}
// 设置 option 下拉框中选项的样式
.ql-picker.ql-lineHeight .ql-picker-label[data-value="2"]::before,
// 设置 选中 option 后显示的样式
.ql-picker.ql-lineHeight .ql-picker-item[data-value="2"]::before {
  content: "2";
}
复制代码

自定义字体大小

  • whitelist 子项类型必须为 String 类型,否则会导致选中子项后没有应用上对应都类名
data(){
    return {
        // whitelist 子项类型必须为 String 类型,否则会导致选中子项后没有应用上对应都类名
        sizeList:Array.from(Array(58),(item,index)=>String(index+12)),
    }
}
const Parchment = Quill.import("parchment")
      class Font extends Parchment.Attributor.Class{}
      const FontStyle = new Font('size','ql-size',{
        scope:Parchment.Scope.INLINE,
        whitelist:this.sizeList
      })
      Quill.register({
        'formats/size':FontStyle
      },true)
// 使用 less 中 range 和 each 方法来减少重复 css 代码
@list:range(11,70,1);
each(@list,{
  .ql-size-@{value}{
    font-size:@value*1px;
  }
})
复制代码

更多demo中细节代码可以点击查看

事件

text-change 事件

this.quill.on('text-change',function(delta,oldDelta,source){
    console.log('delata',delta,oldDelta,source);
})
复制代码

  • 可以看到 delta 数据对象中包含 :

    1. retain:保留之前多少位数的数据
    2. insert:插入的数据
  • source 表示事件触发来源,如果是用户触发的为 user 如果是api操作的为 api

其他事件

事件类型:可以通过 on(name: String, handler: Function): Quill quill.on()来注册事件,如:text-change,editor-change
添加自定义事件:

// 获取到 toolbar 操作对象
let toolbar = quill.getModule('toolbar');
toolbar.addHandler('image', ()=>{
      // 添加点击图片触发的逻辑
    });
复制代码

更多事件查看

Quill 源码

了解了如何 Quill的基础使用方法,以及如何定制一些功能后,接着来查看下 Quill的源码结构

构造函数

路径为 /core/quill.js ,可以看到构造函数上挂载了很多静态方法和原型上的方法,来了解下一些常用的方法的实现。

class Quill {
    static debug(limit) {
    if (limit === true) {
      limit = 'log';
    }
    logger.level(limit);
  }

  static find(node) {
    return node.__quill || Parchment.find(node);
  }

  static import(name) {
    if (this.imports[name] == null) {
      debug.error(`Cannot import ${name}. Are you sure it was registered?`);
    }
    return this.imports[name];
  }

  static register(path, target, overwrite = false) {
  }
  // ...
}
复制代码

Quill.import

比如我们可以通过 Quill.import去调用静态方法 import,去执行 this.imports[name] Quill.imports 对象上默认挂载来以下四个模块

Quill.imports = {
  'delta'       : Delta,
  'parchment'   : Parchment,
  'core/module' : Module,
  'core/theme'  : Theme
};
复制代码

Quill.register

如果要添加增模块,可以通过 register 方法来注册新的路径及对应的模块, 我们使用时代码通常如下

class LinkBlot extends Inline {}
LinkBlot.blotName = 'link';
LinkBlot.tagName = 'a';
Quill.register(LinkBlot);
复制代码

来看看 Quill 源码中的执行,下面方法中执行顺序

  1. this.register('formats/' + 'link', LinkBlot, undefined);
  2. this.imports['formats/link'] = LinkBlot
  3. 最终通过 Parchment.register(LinkBlot) 注册了模块
class Quill{
    static register(path, target, overwrite = false) {
        if (typeof path !== 'string') {
          let name = path.attrName || path.blotName;
          if (typeof name === 'string') {
            // register(Blot | Attributor, overwrite)
            this.register('formats/' + name, path, target);
          } else {
            Object.keys(path).forEach((key) => {
              this.register(key, path[key], target);
            });
          }
        }else {
          if (this.imports[path] != null && !overwrite) {
            debug.warn(`Overwriting ${path} with`, target);
          }
          this.imports[path] = target;
          if ((path.startsWith('blots/') || path.startsWith('formats/')) &&
              target.blotName !== 'abstract') {
            Parchment.register(target);
          } else if (path.startsWith('modules') && typeof target.register === 'function') {
            target.register();
          }
        }
    }
 }
复制代码

Quill font 功能内部实现

分为两步:

  1. 实例化 Parchment.Attributor.Class 类,前面介绍中知道 Parchment.Attributor 属性下有一个基础类Attributor 和三个功能类class,style,store
  2. 使用 Quill.register() 注册实例
import Parchment from 'parchment';

let config = {
  scope: Parchment.Scope.INLINE,
  whitelist: ['serif', 'monospace']
};

let FontClass = new Parchment.Attributor.Class('font', 'ql-font', config);

export { FontClass };
复制代码
  1. 首先实例化 Parchment.Attributor.Class 构造函数,之前查看过 src/attributor/class.ts 实例化改类就是执行 constructor 构造函数
class ClassAttributor extends Attributor {}
复制代码
  1. 由于 class 类继承自 Attributor 所以查看 src/attributor/attributor.ts
export default class Attributor {
  constructor(attrName: string, keyName: string, options: AttributorOptions = {}) {}
 }
复制代码

所以我们执行 let FontClass = new Parchment.Attributor.Class('font', 'ql-font', config);时,就是想这个构造函数中传入这三个参数。
3. 这行代码做了什么

  constructor(attrName: string, keyName: string, options: AttributorOptions = {}) {
    // 记录属性名和类名
    this.attrName = attrName;
    this.keyName = keyName;
    let attributeBit = Registry.Scope.TYPE & Registry.Scope.ATTRIBUTE;
    // 判断是否定义 scope 属性
    if (options.scope != null) {
      // 由于它是 scope 默认是通过二进制来表示的,所以,这里采用位运算来判断
      this.scope = (options.scope & Registry.Scope.LEVEL) | attributeBit;
    } else {
      // 如果没有设置scope,则默认使用 ATTRIBUTE 二进制
      this.scope = Registry.Scope.ATTRIBUTE;
    }
    if (options.whitelist != null) this.whitelist = options.whitelist;
  }
复制代码

可以查看 registry.ts 源码 路径 src/registry.ts 定义了 Scope的可选值如下,scope 决定了 Blot 的类型是行内还是块级元素 关于哪些操作是行内,块级,Embeds 可以点击此处查看

export enum Scope {
 TYPE = (1 << 2) - 1, // 0011 Lower two bits
 LEVEL = ((1 << 2) - 1) << 2, // 1100 Higher two bits

 ATTRIBUTE = (1 << 0) | LEVEL, // 1101
 BLOT = (1 << 1) | LEVEL, // 1110
 INLINE = (1 << 2) | TYPE, // 0111
 BLOCK = (1 << 3) | TYPE, // 1011

 BLOCK_BLOT = BLOCK & BLOT, // 1010
 INLINE_BLOT = INLINE & BLOT, // 0110
 BLOCK_ATTRIBUTE = BLOCK & ATTRIBUTE, // 1001
 INLINE_ATTRIBUTE = INLINE & ATTRIBUTE, // 0101

 ANY = TYPE | LEVEL,
}
复制代码

Quill 其他 api 方法

点击官方文档查看 这些方法都可以在 core/quill.js 文件中查看到,查看它如何实现

parchment 和 Deltas

在 Quill 中操作文档模型和描述富文本内容 分别基于 Parchment 和 Delta,基于这两者,Quill才能够通过 API 来操作富文本样式,定制化和扩展富文本功能。两者功能如下:

  • Parchment 使用 Blots 来代替 dom 描述文档,Parchment 主要作用就是操作文档模型,我们可以通过其提供的接口进行 DOM 初始化,返回指定格式或者指定标签和作用域等等。
  • 通过实例化 delta 会将我们传入的参数配置项挂载到 ops 下,并且这个实例原型上挂载了很多可用的方法,来操作文档

parchment 源码

主文件

路径:src/Parchment.ts ,

let Parchment = {
  Scope: Registry.Scope,
  create: Registry.create,
  register: Registry.register,

  Container: ContainerBlot,
  Format: FormatBlot,
  Embed: EmbedBlot,

  Scroll: ScrollBlot,
  Block: BlockBlot,
  Inline: InlineBlot,
  Text: TextBlot,
  Attributor: {
    Attribute: Attributor,
    Class: ClassAttributor,
    Style: StyleAttributor,
    Store: AttributorStore,
  },
}
复制代码

更多参数属性查看

样式管理模块

attributor 文件夹下放一些节点属性的设置方法。路径为:src/Parchment.ts文件暴露出的对象包含了所有 parchment 提供所有的方法。

import Attributor from './attributor/attributor';
import ClassAttributor from './attributor/class';
import StyleAttributor from './attributor/style';
import AttributorStore from './attributor/store';
let Parchment = {
    Attributor: {
        Attribute: Attributor,
        Class: ClassAttributor,
        Style: StyleAttributor,
        Store: AttributorStore,
    },
}
复制代码

那style举例,可以看下 Attributor 如何实现 节点的 style 管理。

class StyleAttributor extends Attributor {
  add(node: HTMLElement, value: string): boolean {
    if (!this.canAdd(node, value)) return false;
    // @ts-ignore
    node.style[camelize(this.keyName)] = value;
    return true;
  }
  remove(node: HTMLElement): void {
    // @ts-ignore
    node.style[camelize(this.keyName)] = '';
    if (!node.getAttribute('style')) {
      node.removeAttribute('style');
    }
  }
  value(node: HTMLElement): string {
    // @ts-ignore
    let value = node.style[camelize(this.keyName)];
    return this.canAdd(node, value) ? value : '';
  }
}
复制代码

实现方式其实也很简单,就是通过 element.style.color = '#f00'这样的格式给元素设置样式。

  • class.ts 用于设置 class
  • store.ts 用于设置 attribute 记录样式格式如下,attributes 记录了样式。如下结构中的 attributes用于操作属性的变化
{
  ops: [
    { insert: 'Gandalf', attributes: { bold: true } },
    { insert: 'Grey', attributes: { color: '#cccccc' } }
  ]
}
复制代码
  • attributor.ts 相当于其他三个类的基础类,定义了一些公共的属性和方法。
export default class Attributor {
  attrName: string;
  keyName: string;
  scope: Registry.Scope;
  whitelist: string[] | undefined;
 }
复制代码

元素管理模块

路径为:src/blot/abstract 文件夹下的结构如下

这些文件中分别定义了很多基础通用的方法,如format.ts 文件中定义了一些格式化的方法如下

class FormatBlot extends ContainerBlot implements Formattable {
    format(){}
    formats(){}
    replaceWith(){}
    update(){}
    wrap(){}
}
复制代码

然后在路径为 src/blot/blot.ts中引入了 format.ts 文件使用,通过继承来实现 FormatBlot 中逻辑的复用

import FormatBlot from './abstract/format';
class BlockBlot extends FormatBlot {
  static blotName = 'block';
  static scope = Registry.Scope.BLOCK_BLOT;
  static tagName = 'P';
  format(name: string, value: any) {
    if (Registry.query(name, Registry.Scope.BLOCK) == null) {
      return;
    } else if (name === this.statics.blotName && !value) {
      this.replaceWith(BlockBlot.blotName);
    } else {
      super.format(name, value);
    }
  }
}
复制代码

Blots

Parchment 来代替 dom 来描述文档,Blots 就相当于元素,下面是关于 Blots 的定义

// 拥有以下字段和静态方法,以及这些属性的类型
class Blot {
  static blotName: string;
  static className: string;
  static tagName: string;
  //  inline or block
  static scope: Scope;
  domNode: Node;
  prev: Blot;
  next: Blot;
  parent: Blot;
  // Creates corresponding DOM node
  static create(value?: any): Node;
  // Apply format to blot. Should not pass onto child or other blot.
  format(format: name, value: any);
  insertAt(index: number, text: string);
  // ... 
}
复制代码

Deltas

var delta = new Delta([
  { insert: 'Gandalf', attributes: { bold: true } }
]);
复制代码

通过实例化 delta 会将我们传入的参数配置项挂载到 ops 下,并且这个实例原型上挂载了很多可用的方法。

生成文本

var delta = new Delta([
  { insert: 'Gandalf', attributes: { bold: true } },
  { insert: ' the ' },
  { insert: 'Grey', attributes: { color: '#ccc' } }
]);
复制代码

Delta 是用来描述富文本内容的一种简单的 JSON 格式, 上面的实例表示:会生成一个Gandalf the Grey 字符串,并且 Gandalf 字样为 bold , Grey 的字体颜色为 #ccc

api 修改文本

上面我们通过 Deltas 提供的 api 来修改文档 :

var death = new Delta().retain(12).delete(4).insert('White', { color: '#fff' });
复制代码

描述上面行为的 json 如下,delta 保留原先字符串的前12位,在此之上删除后四位,然后插入 White 字符串字体样式为白色

{
  ops: [
    { retain: 12 },
    { delete: 4 },
    { insert: 'White', attributes: { color: '#fff' } }
  ]
}
复制代码
文章分类
前端
文章标签