preface

Rich text styles the input, and Quill provides an API for judging how to modify the DOM, unlike many other editors that traverse the DOM tree. And Quill supports customization, so let’s learn how to use Quill and its source code.

quickstart

Just like Vue and React, Quill requires an existing DOM element as the target to mount the Quill instance

Basic configuration

Parameter 1: can be DOM, can selectors (automatically to convert to the appropriate DOM). Parameter 2: Configuration object, some configuration options for rich text.

var editor = new Quill('#editor', options);
Copy the code

Options Configuration object

var options = {
  modules: {
    toolbar: '#toolbar'
  },
  placeholder: 'Compose an epic... ',
  theme: 'snow'
};
Copy the code

modules.toolbar

Toolbar configuration Method 1: CSS labels

var quill = new Quill('#editor', {
  modules: {
    // Equivalent to { toolbar: { container: '#toolbar' }}
    toolbar: '#toolbar'}});Copy the code

Method 2: Object

var quill = new Quill('#editor', {
  modules: {
    toolbar: {
        container:'#toolbar'}}});Copy the code

Method 3: array

var quill = new Quill('#editor', {
  modules: {
    toolbar: ['bold'.'italic'.'underline'.'strike']}});Copy the code

Edit area and toolbar area

As you can see from these configurations, rich text is divided into an edit area and a toolbar area. The #editor mounted element target will be replaced with the part of the input box, and the #toolbar mounted element target will be replaced with the toolbar area.

var editor = new Quill('#editor', {
    modules: {
    toolbar: {
        container:'#toolbar'}}});Copy the code

Customization function

With the simple configuration above, you can use some of the basic rich text editor features to customize the Quill functionality

How to implement the built-in features of Quill

Font function realization, divided into two steps:

  1. Instantiate the Parchment. Attributor. Class Class, mentioned earlier that Parchment. Attributor properties have a base ClassAttributorAnd three function classesclass.style.store
  2. We instantiate a blots node through the class, and then we need registerd
// Step 1 Import Parchment from Parchment'parchment';

let config = {
  scope: Parchment.Scope.INLINE,
  whitelist: ['serif'.'monospace']};let FontClass = new Parchment.Attributor.Class('font'.'ql-font', config);

export{ FontClass }; // Step 2 import {FontClass} from'./formats/font';
Quill.register({
'formats/font': FontClass,
},true)
Copy the code

Custom line height function

Quill itself does not support the line height function, we need to customize this function, imitate quill native font function implementation as follows: codepen custom line height demo

const config = {
        scope: Parchment.Scope.INLINE,
        whitelist: this.lineHeightList
}
const Parchment = Quill.import("parchment"); / / step one instantiation lineHeight class (custom) class lineHeightAttributor extends Parchment. Attributor. Class {} const lineHeightStyle = new lineHeightAttributor("lineHeight"."ql-lineHeight", config ); // register instance quill.register ({"formats/lineHeight": lineHeightStyle }, true);
Copy the code

After registering an instance, how do I use the line height feature?

  1. Because we inheritedParchment.Attributor.ClassIt controls the style of the node by manipulating the node class name, so it can be customizedql-lineHeightThe class to controlSelect the line height of the text
<div id="app">
  <div id="toolbar">
    <select class="ql-lineHeight"> // Set the default line height with selected <option v-for="(lineHeight,index) in lineHeightList" :key="lineHeight" :value="lineHeight" :selected="index === 3">{{ lineHeight }}</option>
      </select>
  </div>
  <div id="container"></div>
</div>
Copy the code
  1. Once we’ve defined our custom DOM structure, we need to customize the row heights for those styles,

Ql-lineheight-2 {line-height: 2; } // Set the style of the options in the option dropdown. Ql-picker. ql-lineheight. ql-picker-label[data-value="2"]::before, // Sets the style displayed after option is selected. Ql-picker.ql-lineheight. ql-picker-item[data-value="2"]::before {
  content: "2";
}
Copy the code

Customize font size

  • The type of the whitelist subitem must be String. Otherwise, the whitelist subitem will not have the corresponding class name applied
data() {returnSizeList: 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@list:range(11,70,1); @list:range(11,70,1); each(@list,{ .ql-size-@{value}{ font-size:@value*1px; }})Copy the code

For more details on the demo code, click here

The event

The text – change event

this.quill.on('text-change'.function(delta,oldDelta,source){
    console.log('delata',delta,oldDelta,source);
})
Copy the code

  • You can see that the Delta data object contains:

    1. Retain: Retain the previous number of bits of data
    2. Insert: indicates the data to be inserted
  • Source indicates the event trigger source. If the event is triggered by a user, it is user. If the event is triggered by an API, it is API

Other events

Event types: You can register events with on(name: String, handler: Function): Quill quill.on(), for example, text-change,editor-change to add custom events:

// Get the Toolbar action objectlet toolbar = quill.getModule('toolbar');
toolbar.addHandler('image', ()=>{// add click on the picture trigger logic});Copy the code

More events view

Quill source

Now that you know the basics of how to use Quill and how to customize some of its features, take a look at the source structure of Quill

The constructor

The path is /core/quill.js. You can see that there are many static methods and prototype methods mounted on the constructor.

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{} //... }Copy the code

Quill.import

Imports [name] quill. imports, for example, can be used to call the static import method and perform the following four modules mounted by default on the this.imports[name] Quill

Quill.imports = {
  'delta'       : Delta,
  'parchment'   : Parchment,
  'core/module' : Module,
  'core/theme'  : Theme
};
Copy the code

Quill.register

If you want to add additional modules, you can use the register method to register the new path and the corresponding module. The code we use is usually as follows

class LinkBlot extends Inline {}
LinkBlot.blotName = 'link';
LinkBlot.tagName = 'a';
Quill.register(LinkBlot);
Copy the code

Take a look at the Quill source code for the execution sequence in the following methods

  1. this.register('formats/' + 'link', LinkBlot, undefined);
  2. this.imports[‘formats/link’] = LinkBlot
  3. Finally, the module was registered through 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(); }}}}Copy the code

Quill font function internal implementation

There are two steps:

  1. Instantiate the Parchment. Attributor. Class Class, mentioned earlier that Parchment. Attributor properties have a base ClassAttributorAnd three function classesclass.style.store
  2. Use quill.register () to register the instance
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 };
Copy the code
  1. First instantiation Parchment. Attributor. Class constructor, view beforesrc/attributor/class.tsInstantiating a class executes the constructor constructor
class ClassAttributor extends Attributor {}
Copy the code
  1. View it because the class class inherits from Attributorsrc/attributor/attributor.ts
export default class Attributor {
  constructor(attrName: string, keyName: string, options: AttributorOptions = {}) {}
 }
Copy the code

So we perform the let FontClass = new Parchment. Attributor. Class (‘ font ‘, ‘ql – the font, the config); You want to pass these three arguments to the constructor. What does this line of code do

Constructor (attrName: string, keyName: string, options: AttributorOptions = {}) {// Record the name of the attribute and class. this.keyName = keyName;letattributeBit = Registry.Scope.TYPE & Registry.Scope.ATTRIBUTE; // Determine whether the scope attribute is definedif(options.scope ! = null) {/ / because it is the default scope is represented by binary, so the bit arithmetic is used to judge this. Here the scope = (options. The scope & Registry. Scope. LEVEL) | attributeBit; }else{// If scope is not set, the ATTRIBUTE binary this.scope = register.scope.attribute is used by default; }if(options.whitelist ! = null) this.whitelist = options.whitelist; }Copy the code

Ts defines the Scope as follows. Scope determines whether Blot is an inline or block-level element. For details on which operations are inline, block-level, Embeds, click here

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,
}
Copy the code

Quill other API methods

All of these methods are available in the core/quill.js file. How is it implemented


Parchment and Deltas

Manipulating document models and describing rich text content in Quill were Parchment and Delta, respectively, which enabled Quill to manipulate rich text styles, customize and extend rich text functionality through the API. Both functions are as follows:

  • Parchment uses Blots instead of DOM to describe documents. Parchment is mainly used to manipulate document models and can be initialized to DOM via the interface provided by Parchment to return to specified formats, labels and scopes, etc.
  • Delta will pass us in by instantiatingParameter Configuration Item Mount to OPSAnd the instance prototype has mounted a number of available methods to manipulate documents

Parchment source

Master file

Path: 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,
  },
}
Copy the code

More parameter properties to view

Style management module

The Attributor folder lays down some node attributes. The SRC /Parchment. Ts file exposed objects that contained all the methods Parchment provided.

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,
    },
}
Copy the code

For style, see how Attributor implements style management for nodes.

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 : ' '; }}Copy the code

The implementation is simple: style the element with element.style.color = ‘#f00’.

  • class.tsUsed to set class
  • store.tsThe format used to set the attribute record style is as follows, and attributes records the style. In the following structureattributesUsed to manipulate changes to properties
{
  ops: [
    { insert: 'Gandalf', attributes: { bold: true } },
    { insert: 'Grey', attributes: { color: '#cccccc'}}}]Copy the code
  • attributor.tsEquivalent to the base class of the other three classes, it defines some common properties and methods.
export default class Attributor {
  attrName: string;
  keyName: string;
  scope: Registry.Scope;
  whitelist: string[] | undefined;
 }
Copy the code

Element Management module

The path is as follows: the structure of the SRC /blot/abstract folder is as follows

These files define many basic common methods respectively. For example, the format.ts file defines some formatting methods as follows

class FormatBlot extends ContainerBlot implements Formattable {
    format() {}formats() {}replaceWith() {}update() {}wrap(){}
}
Copy the code

src/blot/blot.ts

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); }}}Copy the code

Blots

Parchment is used instead of DOM to describe documents and Blots are like elements. Here is the definition of Blots

Class Blot {static blotName: string; // Have the following fields and static methods, and the types of these properties. 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); / /... }Copy the code

Deltas

var delta = new Delta([
  { insert: 'Gandalf', attributes: { bold: true}}]);Copy the code

The generation of textual

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

Delta is a simple JSON format used to describe rich text content, as shown in the example above: a string of Gandalf the Grey is generated with Gandalf bold and Grey’s font color # CCC

API modify text

Above we use the API provided by Deltas to modify the document:

var death = new Delta().retain(12).delete(4).insert('White', { color: '#fff' });
Copy the code

The JSON that describes the above behavior is as follows, with delta preserving the first 12 bits of the original string, deleting the last four bits above it, and then inserting the White string font style as White

{
  ops: [
    { retain: 12 },
    { delete: 4 },
    { insert: 'White', attributes: { color: '#fff'}}}]Copy the code