Introduction to the

Rich text editor, with good configurability and scalability

  • Api-driven design
  • Customize the content and format
  • cross-platform
  • Easy to use

Simple to use

<div id="editor" /> new Quill('#editor', options); // options indicates the configuration itemCopy the code

rendering

Two ways to use the toolbar

  • An array of options
const toolbarOptions = [
    ['bold', 'italic', 'underline', 'strike'],
    ['blockquote', 'code-block'],
    [{ 'header': 1 }, { 'header': 2 }],
    [{ 'list': 'ordered'}, { 'list': 'bullet' }],
    [{ 'script': 'sub'}, { 'script': 'super' }], 
    [{ 'indent': '-1'}, { 'indent': '+1' }],
    [{ 'direction': 'rtl' }],
    [{ 'size': ['small', false, 'large', 'huge'] }], 
    [{ 'header': [1, 2, 3, 4, 5, 6, false] }],
    [{ 'color': [] }, { 'background': [] }], 
    [{ 'font': [] }],
    [{ 'align': [] }],
    ['link', 'image', 'video', 'formula'],
    ['clean'],
];
const quill = new Quill('#editor', {
    modules: { 
        toolbar: toolbarOptions
    },
    theme: 'snow'
});
Copy the code
  • html
<div id="toolbar-container">
    <span class="ql-formats">
        <select class="ql-font"></select>
        <select class="ql-size"></select>
    </span>
    <span class="ql-formats">
        <button class="ql-bold"></button>
        <button class="ql-italic"></button>
        <button class="ql-underline"></button>
        <button class="ql-strike"></button>
    </span>
    <span class="ql-formats">
        <select class="ql-color"></select>
        <select class="ql-background"></select>
    </span>
    <span class="ql-formats">
        <button class="ql-script" value="sub"></button>
        <button class="ql-script" value="super"></button>
    </span>
    <span class="ql-formats">
        <button class="ql-header" value="1"></button>
        <button class="ql-header" value="2"></button>
        <button class="ql-blockquote"></button>
        <button class="ql-code-block"></button>
    </span>
    <span class="ql-formats">
        <button class="ql-list" value="ordered"></button>
        <button class="ql-list" value="bullet"></button>
        <button class="ql-indent" value="-1"></button>
        <button class="ql-indent" value="+1"></button>
    </span>
    <span class="ql-formats">
        <button class="ql-direction" value="rtl"></button>
        <select class="ql-align"></select>
    </span>
    <span class="ql-formats">
        <button class="ql-link"></button>
        <button class="ql-image"></button>
        <button class="ql-video"></button>
        <button class="ql-formula"></button>
    </span>
    <span class="ql-formats">
        <button class="ql-clean"></button>
    </span>
</div>

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

<script>
  const quill = new Quill('#editor', {
    modules: {
      toolbar: '#toolbar'
    }
  });
</script>
Copy the code

The theme

  1. snow

2. bubble

Get rich text HTML

const html = quill.root.innerHTML;
Copy the code

Configuration items

API

The above souce has three values:

  • user
  • api
  • silent

API indicates that the QUill is operated using the API, silent indicates that the quill is operated using the API, and silent indicates that the quill is operated by the user. This parameter affects whether the quill event is triggered, and whether redo/undo logs are recorded.

There are three important events in Quill:

  • Text-change: trigger when Quill contents change (source: silent)
  • Selection-change: triggered when Quill’s cursor selection changes, null means out of focus (silent source does not trigger)
  • Editor-change: The first two events will fire even silent events

Realize the principle of

editable

/ quill – 1.3.7 blots/scroll. Js

enable(enabled = true) {
    this.domNode.setAttribute('contenteditable', enabled);
}
Copy the code

The contenteditable property specifies whether the content of an element is editable. If an element has no contenteditable property set, it inherits the property from its parent.

Toolbar selection -> Editor to show the effects

  1. Defines the click callback for the toolbar image icon

2. Insert ({image: e.target.result})That is, add attributes to the Delta instance

ops:[{insert: {image: 'xxxxxxx......'}}]
Copy the code
  1. UpdateContents inserts images

The Delta data passed in by the quill. Update Ontents application, ApplyDelta reduce the Delta ops by insert (calling scroll.inserAt and scroll.formatAt) and delete(calling scroll.deleteAt) respectively. -> scroll.insertAt -> ScrollBlot.insertAt -> ContainerBlot.insertAt \parchment\src\blot\abstract\container.ts

insertAt(index: number, value: string, def?: any): void {
    let [child, offset] = this.children.find(index);
    if (child) {
      child.insertAt(offset, value, def);
    } else {
      let blot = def == null ? Registry.create('text', value) : Registry.create(value, def);
      this.appendChild(blot);
    }
  }
Copy the code

The above Registry. The create

export function create(input: Node | string | Scope, value? : any): Blot { let match = query(input); if (match == null) { throw new ParchmentError(`Unable to create ${input} blot`); } let BlotClass = <BlotConstructor>match; let node = // @ts-ignore input instanceof Node || input['nodeType'] === Node.TEXT_NODE ? input : BlotClass.create(value); return new BlotClass(<Node>node, value); }Copy the code

Query to find the corresponding BlotClass

export function query(
  query: string | Node | Scope,
  scope: Scope = Scope.ANY,
): Attributor | BlotConstructor | null {
  let match;
  if (typeof query === 'string') {
    match = types[query] || attributes[query];
    // @ts-ignore
  } else if (query instanceof Text || query['nodeType'] === Node.TEXT_NODE) {
    match = types['text'];
  } else if (typeof query === 'number') {
    if (query & Scope.LEVEL & Scope.BLOCK) {
      match = types['block'];
    } else if (query & Scope.LEVEL & Scope.INLINE) {
      match = types['inline'];
    }
  } else if (query instanceof HTMLElement) {
    let names = (query.getAttribute('class') || '').split(/\s+/);
    for (let i in names) {
      match = classes[names[i]];
      if (match) break;
    }
    match = match || tags[query.tagName];
  }
  if (match == null) return null;
  // @ts-ignore
  if (scope & Scope.LEVEL & match.scope && scope & Scope.TYPE & match.scope) return match;
  return null;
}
Copy the code

Look in types or Attributes, which are registered into types via quill initialization. So please register yourself before using Parchment.

Find the corresponding: formats/image.js

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

To customize the render of an image, you can write a JAVASCRIPT file similar to the above, and initialize it with the formats/image.js, image tags, and SRC:

Quill.register(Image, true); // Image customCopy the code

Above, returning Image Blot -> also needs to be converted to Dom via appendChild

appendChild(other: Blot): void { // ContainerBlot
    this.insertBefore(other);
}
Copy the code

->

insertBefore(childBlot: Blot, refBlot? : Blot): void { // ContainerBlot if ( this.statics.allowedChildren ! = null && ! this.statics.allowedChildren.some(function(child: Registry.BlotConstructor) { return childBlot instanceof child; }) ) { throw new Registry.ParchmentError( `Cannot insert ${(<ShadowBlot>childBlot).statics.blotName} into ${this.statics.blotName}`, ); } childBlot.insertInto(this, refBlot); }Copy the code

->

ShadowBlot

/parchment-master/src/blot/abstract/shadow.ts

insertInto(parentBlot: Parent, refBlot: Blot | null = null): void { if (this.parent ! = null) { this.parent.children.remove(this); } let refDomNode: Node | null = null; parentBlot.children.insertBefore(this, refBlot); if (refBlot ! = null) { refDomNode = refBlot.domNode; } if (this.domNode.parentNode ! = parentBlot.domNode || this.domNode.nextSibling ! = refDomNode) { parentBlot.domNode.insertBefore(this.domNode, refDomNode); } this.parent = parentBlot; this.attach(); }Copy the code

Domnode. insertBefore converts to the actual DOM operation

User input triggers data model updates

Scroll key class parent ScrollBlot/parchment – master/SRC/blot/Scroll. Ts

constructor(node: HTMLDivElement) {
    super(node);
    this.scroll = this;
    this.observer = new MutationObserver((mutations: MutationRecord[]) => {
      this.update(mutations);
    });
    this.observer.observe(this.domNode, OBSERVER_CONFIG);
    this.attach();
  }
Copy the code

Listen for DOM changes through the MutationObserver

update(mutations? : MutationRecord[], context: { [key: string]: any } = {}): void { mutations = mutations || this.observer.takeRecords(); // TODO use WeakMap mutations .map(function(mutation: MutationRecord) { let blot = Registry.find(mutation.target, true); if (blot == null) return null; // @ts-ignore if (blot.domNode[Registry.DATA_KEY].mutations == null) { // @ts-ignore blot.domNode[Registry.DATA_KEY].mutations = [mutation]; return blot; } else { // @ts-ignore blot.domNode[Registry.DATA_KEY].mutations.push(mutation); return null; } }) .forEach((blot: Blot | null) => { if ( blot == null || blot === this || //@ts-ignore blot.domNode[Registry.DATA_KEY] == null ) return; // @ts-ignore blot.update(blot.domNode[Registry.DATA_KEY].mutations || [], context); }); // @ts-ignore if (this.domNode[Registry.DATA_KEY].mutations ! = null) { // @ts-ignore super.update(this.domNode[Registry.DATA_KEY].mutations, context); } this.optimize(mutations, context); }Copy the code

Transform the changes into blot update, and call the corresponding blot update method, such as: /parchment-master/ SRC /blot/text.ts

update(mutations: MutationRecord[], context: { [key: string]: any }): void { if ( mutations.some(mutation => { return mutation.type === 'characterData' && mutation.target === this.domNode; }) ) { this.text = this.statics.value(this.domNode); }}Copy the code

Optimize is called after each call to Update. It’s worth noting that the Optimize call should never change the final render visual. However, the main purpose of this approach is to reduce document complexity and maintain consistency in Delta rendering results.

Delta and blot are used above, which are independently maintained as third-party libraries. What are they?

Delta

Delta is used to describe the contents and changes of the Quill editor, and is a simple but expressive data format. This format is essentially a JSON format

Note: Manual construction of the Delta format is not recommended. Instead, chain operations insert(), delete(), retain() are recommended to create Delta objects.

{ ops: [ { insert: 'Gandalf', attributes: { bold: true } }, { insert: ' the ' }, { // An image link insert: { image: 'https://quilljs.com/assets/images/icon.png' }, attributes: { link: 'https://quilljs.com' } }, { delete: // Unbold and italicize "Gandalf" {retain: 7, attributes: {bold: null, italic: true}},]}Copy the code

These include several operations:

  1. insert
  2. delete
  3. Retain retention

The retain operation simply preserves the next specified number of strings without modification. With attributes, it means that only the strings are kept but the format defined by Attributes is applied. If the format value defined by attributes is null, the original format of the text is removed.

Parchment

Quill’s document model, DOM tree abstraction.

A Parchment tree is composed of Blots which reflect a DOM corresponding node. Blots can provide structure, format, and content, or just content. Attributors can provide lightweight formatting information.

Blue represents interface and orange represents class

As you can see from the above example of inserting images, quill. Scroll is crucial, and many of the APIS for manipulating blot are called through scroll.

  1. Through the Quill. Register ({‘ blots/scroll: scroll,}); registered
  2. Scroll to scroll via Parchment. Creat in the Quill constructor
  3. Scroll inherits from ScrollBlot above

There are two types of Blot:

  • Non-atomic nodes (ScrollBlot, InlineBlot, BlockBlot)
  • Atomic nodes (EmbedBlot, TextBlot)

Every Blot needs to implement the Blot interface specification \ Parchment \ SRC \ Blot \abstract\blot.ts

export interface Blot extends LinkedNode { scroll: Parent; // top blot parent: parent; // Parent Blot, if the Blot is a top-level Blot, parent is null prev: Blot; // The previous sibling blot has the same parent as the current blot, null if the current blot is the first child. // The next sibling blot has the same parent as the current blot. If the current blot is the last child, it is nu domNode: Node; // Attach (): void; // Set the scroll clone of blot as the parent of blot (): blot; // Copy the current domNode to generate a new blot detach(): void; InsertInto (parentBlot: Parent, refBlot? : Blot): void; isolate(index: number, length: number): Blot; // Find blot offset(root? : Blot): number; Remove (): void; // Delete domNode replace(target: Blot): void; replaceWith(name: string, value: any): Blot; replaceWith(replacement: Blot): Blot; split(index: number, force? : boolean): Blot; wrap(name: string, value: any): Parent; // name generate parent blot wrap, insert current blot, insert wrap into parent wrap(wrapper: parent): parent; deleteAt(index: number, length: number): void; formatAt(index: number, length: number, name: string, value: any): void; insertAt(index: number, value: string, def? : any): void; optimize(context: { [key: string]: any }): void; optimize(mutations: MutationRecord[], context: { [key: string]: any }): void; update(mutations: MutationRecord[], context: { [key: string]: any }): void; }Copy the code

Parchment \ SRC \blot\abstract\shadow.ts ShadowBlot

  • Create () creates a real element based on the tagName and adds className to return the node.
  • FormatAt (index,length,name,value) formatAt(index,length,name,value)
  • Optimize () Blot Any change to the optimize() Blot will trigger the optimize method, which is override enabled, and leaf also supports this method. It is common to do some final validation or qualification operations, such as the table after inserting data, to rewrite the calculation of the grid size. For example, after the update, the data results are saved in real time. Avoid changing the length and value of the document in the optimize method. This method is a good place to do things that reduce document complexity.
import { Blot, Parent, Formattable } from './blot'; import * as Registry from '.. /.. /registry'; class ShadowBlot implements Blot { static blotName = 'abstract'; static className: string; static scope: Registry.Scope; static tagName: string; // @ts-ignore prev: Blot; // @ts-ignore next: Blot; // @ts-ignore parent: Parent; // @ts-ignore scroll: Parent; // Hack for accessing inherited static methods get statics(): any { return this.constructor; } static create(value: any): Node { if (this.tagName == null) { throw new Registry.ParchmentError('Blot definition missing tagName'); } let node; if (Array.isArray(this.tagName)) { if (typeof value === 'string') { value = value.toUpperCase(); if (parseInt(value).toString() === value) { value = parseInt(value); } } if (typeof value === 'number') { node = document.createElement(this.tagName[value - 1]); } else if (this.tagName.indexOf(value) > -1) { node = document.createElement(value); } else { node = document.createElement(this.tagName[0]); } } else { node = document.createElement(this.tagName); } if (this.className) { node.classList.add(this.className); } return node; } constructor(public domNode: Node) { // @ts-ignore this.domNode[Registry.DATA_KEY] = { blot: this }; } attach(): void { if (this.parent ! = null) { this.scroll = this.parent.scroll; } } clone(): Blot { let domNode = this.domNode.cloneNode(false); return Registry.create(domNode); } detach() { if (this.parent ! = null) this.parent.removeChild(this); // @ts-ignore delete this.domNode[Registry.DATA_KEY]; } deleteAt(index: number, length: number): void { let blot = this.isolate(index, length); blot.remove(); } formatAt(index: number, length: number, name: string, value: any): void { let blot = this.isolate(index, length); if (Registry.query(name, Registry.Scope.BLOT) ! = null && value) { blot.wrap(name, value); } else if (Registry.query(name, Registry.Scope.ATTRIBUTE) ! = null) { let parent = <Parent & Formattable>Registry.create(this.statics.scope); blot.wrap(parent); parent.format(name, value); } } insertAt(index: number, value: string, def? : any): void { let blot = def == null ? Registry.create('text', value) : Registry.create(value, def); let ref = this.split(index); this.parent.insertBefore(blot, ref); } insertInto(parentBlot: Parent, refBlot: Blot | null = null): void { if (this.parent ! = null) { this.parent.children.remove(this); } let refDomNode: Node | null = null; parentBlot.children.insertBefore(this, refBlot); if (refBlot ! = null) { refDomNode = refBlot.domNode; } if (this.domNode.parentNode ! = parentBlot.domNode || this.domNode.nextSibling ! = refDomNode) { parentBlot.domNode.insertBefore(this.domNode, refDomNode); } this.parent = parentBlot; this.attach(); } isolate(index: number, length: number): Blot { let target = this.split(index); target.split(length); return target; } length(): number { return 1; } offset(root: Blot = this.parent): number { if (this.parent == null || this == root) return 0; return this.parent.children.offset(this) + this.parent.offset(root); } optimize(context: { [key: string]: any }): void { // TODO clean up once we use WeakMap // @ts-ignore if (this.domNode[Registry.DATA_KEY] ! = null) { // @ts-ignore delete this.domNode[Registry.DATA_KEY].mutations; } } remove(): void { if (this.domNode.parentNode ! = null) { this.domNode.parentNode.removeChild(this.domNode); } this.detach(); } replace(target: Blot): void { if (target.parent == null) return; target.parent.insertBefore(this, target.next); target.remove(); } replaceWith(name: string | Blot, value? : any): Blot { let replacement = typeof name === 'string' ? Registry.create(name, value) : name; replacement.replace(this); return replacement; } split(index: number, force? : boolean): Blot { return index === 0 ? this : this.next; } update(mutations: MutationRecord[], context: { [key: string]: any }): void { // Nothing to do by default } wrap(name: string | Parent, value? : any): Parent { let wrapper = typeof name === 'string' ? <Parent>Registry.create(name, value) : name; if (this.parent ! = null) { this.parent.insertBefore(wrapper, this.next); } wrapper.appendChild(this); return wrapper; } } export default ShadowBlot;Copy the code

Parchment Life Cycle

  1. register

Parchment registration is required before use

The source code

The toolbar

  1. Initialize the

Initialize each instance of modules

The toolbar:

class Toolbar extends Module { constructor(quill, options) { super(quill, options); // this.options.container: Module.tools. container // Pass an array, such as ['bold', 'italic', 'underline', 'strike'], Dom if (array.isarray (this.options.container)) {let container = document.createElement("div"); // Generate toolbar dom addControls(container, this.options.container); Containers/insert/edit first quill. Container. ParentNode. The insertBefore (container, quill. Container); this.container = container; } else if (typeof this.options.container === "string") {// Pass in string and use the dom as the toolbar dom this.container = document.querySelector(this.options.container); } else { this.container = this.options.container; } if (! (this.container instanceof HTMLElement)) { return debug.error("Container required for toolbar", this.options); } this.container.classList.add("ql-toolbar"); this.controls = []; this.handlers = {}; Handlers object.keys (this.options.handlers).foreach ((format) => {this.addHandler(format, this.options.handlers[format]); }); / / to binding toolbar button click event [] forEach. Call (this. Container. QuerySelectorAll (" button, select "), (input) = > {this. Attach (input); }); This.quill. on(quill.events.editor_change, (type, range) => { if (type === Quill.events.SELECTION_CHANGE) { this.update(range); }}); // There's a trigger in scroll optimize /** * optimize Blot any change that happens will trigger the optimize method, which is override enabled, and leaf supports that as well. It is common to do some final validation or qualification operations, such as the table after inserting data, to rewrite the calculation of the grid size. * For example, after the update is completed, the data results are saved in real time. Avoid changing the length and value of the document in the optimize method. This method is a good place to do things that reduce document complexity. */ this.quill.on(Quill.events.SCROLL_OPTIMIZE, () => { let [range] = this.quill.selection.getRange(); // quill.getSelection triggers update this.update(range); }); }... }Copy the code

custom

Configurations in general, common customizations are set by configurations, user interfaces are implemented by Themes and CSS, functions are implemented by modules, and editor content is implemented by Parchment.

Custom modules

Register ('modules/counter', function(Quill, options) { var container = document.querySelector('#counter'); quill.on('text-change', function() { var text = quill.getText(); if (options.unit === 'word') { container.innerText = text.split(/\s+/).length + ' words'; } else { container.innerText = text.length + ' characters'; }}); }); Var Quill = new Quill('#editor', {modules: {counter: true}});Copy the code

Custom blots

var Bold = Quill.import('formats/bold');
Bold.tagName = 'B';   // Quill uses <strong> by default
Quill.register(Bold, true);
Copy the code

Custom font whitelist

var FontAttributor = Quill.import('attributors/class/font'); FontAttributor.whitelist = [ 'sofia', 'slabo', 'roboto', 'inconsolata', 'ubuntu' ]; Quill.register(FontAttributor, true); TODO also needs to add the styles of these classes to the Css fileCopy the code

Replace toolbar icon

const icons = Quill.import('ui/icons'); icons.image = '<svg>... </svg>';Copy the code

New progress bar and close button for picture uploading

  1. Modules -> Toolbar -> Handlers Customize image click callback
  2. Trigger the input file click
  3. After selecting the image, insert the local image into the editor with insertEmbed
  4. Run addContainer to add absolute positioning DOM to qL-Container to display the image uploading progress bar and close the button
  5. The absolute location of the DOM is obtained by getBounds
  6. Upload, update progress
  7. Delete the progress bar and display the upload result
  8. Listen for editor-change to update the position of the absolute dom above

Support for insert emoticons

Blog.csdn.net/kagol/artic…

reference

www.shuzhiduo.com/A/QW5Y61Bqd… Juejin. Cn/post / 684490… Juejin. Cn/post / 698233… Blog.csdn.net/kagol/artic…