DEV Community

Cover image for How Quill module works? 1/10
Kagol
Kagol

Posted on • Updated on

How Quill module works? 1/10

The introduction

This post is based on DevUI rich text editor development practice(EditorX) and Quill source code written.

EditorX is a handy, easy-to-use, and powerful rich text editor developed by DevUI. It is based on Quill and has extensive extensions to enhance the editor's power.

Quill is an open source rich text editor for the Web that is API-driven and supports format and module customization. It currently has more than 29K stars on GitHub.

If you haven't had contact with Quill, it is recommended to go to the official website of Quill first to understand its basic concept.

By reading this post, you will learn:

  1. What is Quill module? How to configure Quill module?
  2. Why and how to create a custom Quill module?
  3. How does a Quill module communicate with Quill?
  4. Dive into Quill's modularity mechanism

A preliminary study of Quill module

Anyone who has used Quill to develop rich text applications should be familiar with Quill's modules.

For example, when we need to customize our own toolbar buttons, we will configure the toolbar module:

var quill = new Quill('#editor', {
  theme: 'snow',
  modules: {
    toolbar: [['bold', 'italic'], ['link', 'image']]
  }
});
Enter fullscreen mode Exit fullscreen mode

The modules parameter is used to configure the module.

The toolbar parameter is used to configure the toolbar module and is passed in a two-dimensional array representing the grouped toolbar buttons.

The rendered editor will contain four toolbar buttons:

Alt Text

To see the Demo above, please anger the configuration toolbar module.

The Quill module is a normal JS class

So what is a Quill module?

Why do we need to know and use the Quill module?

A Quill module is just a normal JavaScript class with constructors, member variables, and methods.

The following is the general source structure of the toolbar module:

class Toolbar {
  constructor(quill, options) {
    // Parse the toolbar configuration of the incoming module (that is, the two-dimensional array described earlier) and render the toolbar
  }


  addHandler(format, handler) {
    this.handlers[format] = handler;
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

You can see that the toolbar module is just a normal JS class. The constructor passes in the Quill instance and the options configuration, and the module class gets the Quill instance to control and manipulate the editor.

For example, a toolbar module will construct a toolbar container based on the Options configuration, fill the container with buttons/drop-down boxes, and bind the button/drop-down box handling events. The end result is a toolbar rendered above the editor body that allows you to format elements in the editor or insert new elements in the editor through the toolbar buttons/drop-down boxes.

The Quill module is very powerful, and we can use it to extend the power of the editor to do what we want.

In addition to toolbar modules, Quill also has some useful modules built in. Let's take a look at them.

Quill built-in modules

There are 6 built-in modules in Quill:

  1. Clipboard
  2. History
  3. Keyboard
  4. Syntax
  5. Toolbar
  6. Uploader

Clipboard, History, and Keyboard are the built-in modules required by Quill, which will be automatically opened. They can be configured but not cancelled. Among them:

The Clipboard module handles copy/paste events, matching HTML element nodes, and HTML-to-delta conversions.

The History module maintains a stack of actions that record every editor action, such as inserting/deleting content, formatting content, etc., making it easy to implement functions such as Undo/Redo.

The Keyboard module is used to configure Keyboard events to facilitate the implementation of Keyboard shortcuts.

The Syntax module is used for code Syntax highlighting. It relies on the external library highlight.js, which is turned off by default. To use Syntax highlighting, you must install highlight.js and turn it on manually.

Other modules do not do much introduction, want to know can refer to the Quill module documentation.

Quill module configuration

I just mentioned the Keyboard event module. Let's use another example to understand the configuration of the Quill module.

Keyboard module supports a number of shortcuts by default, such as:

  1. The shortcut for bold is Ctrl+B;
  2. The shortcut key for hyperlinks is Ctrl+K;
  3. The undo/fallback shortcut is Ctrl+Z/Y.

However, it does not support the strikeline shortcut. If we want to customize the strikeline shortcut, let's say Ctrl+Shift+S, we can configure it like this:

modules: {
  keyboard: {
    bindings: {
      strike: {
        key: 'S',
        ctrlKey: true,
        shiftKey: true,
        handler: function(range, context) {
          const format = this.quill.getFormat(range);
          this.quill.format('strike', !format.strike);
        }
      },
    }
  },
  toolbar: [['bold', 'italic', 'strike'], ['link', 'image']]
}
Enter fullscreen mode Exit fullscreen mode

To see the above Demo, please configure the keyboard module.

In the course of developing a rich text editor with Quill, we will encounter various modules and create many custom modules, all of which are configured using the Modules parameter.

Next we will try to create a custom module to deepen our understanding of Quill modules and module configuration.

Create a custom module

From the introduction of the last section, we learned that in fact, the Quill module is a normal JS class, there is nothing special, in the class initialization parameter will pass the Quill instance and the options configuration parameter of the module, then you can control and enhance the functions of the editor.

When Quill's built-in modules failed to meet our needs, we needed to create custom modules to implement the functionality we wanted.

For example, the EditorX rich text component has the ability to count the current word count in the editor. This function is implemented in a custom module. We will show you how to encapsulate this function as a separate Counter module step by step.

Create a Quill module in three steps:

Step 1: Create the module class

Create a new JS file with a normal JavaScript class inside.

class Counter {
  constructor(quill, options) {
    console.log('quill:', quill);
    console.log('options:', options);
  }
}

export default Counter;
Enter fullscreen mode Exit fullscreen mode

This is an empty class with nothing but the options configuration information for the Quill instance and the module printed in the initialization method.

Step 2: Configure module parameters

modules: {
  toolbar: [
    ['bold', 'italic'],
    ['link', 'image']
  ],
  counter: true
}
Enter fullscreen mode Exit fullscreen mode

Instead of passing the configuration data, we simply enabled the module and found that no information was printed.

Step 3: Register the module

To use a module, we need to register the module class by calling the quill-register method before the Quill is initialized (we'll see how this works later), and since we need to extend a module, the prefix needs to start with modules:

import Quill from 'quill';
import Counter from './counter';
Quill.register('modules/counter', Counter);
Enter fullscreen mode Exit fullscreen mode

At this point we can see that the information has been printed.

Add logic to the module

At this point we add logic to the Counter module to count the words in the current editor:

constructor(quill, options) {
  this.container = quill.addContainer('ql-counter');
  quill.on(Quill.events.TEXT_CHANGE, () => {
    const text = quill.getText(); // Gets the plain text content in the editor
    const char = text.replace(/\s/g, ''); // Use regular expressions to remove white space characters
    this.container.innerHTML = `Current char count: ${char.length}`;
  });
}
Enter fullscreen mode Exit fullscreen mode

In the initialization method of the Counter module, we call the addContainer method provided by Quill to add an empty container for the contents of the word count module to the editor, and then bind the content change event of the editor, so that when we enter the content in the editor, the word count can be counted in real time.

In the Text Change event, we call the Quill instance's getText method to get the plain Text content in the editor, then use a regular expression to remove the white space characters, and finally insert the word count information into the character count container.

The general effect of the presentation is as follows:

Alt Text

To see the above Demo, please anger the custom character statistics module.

Module loading mechanism

After we have a preliminary understanding of the Quill module, we will want to know how the Quill module works. Next, we will start from the initialization process of Quill, through the toolbar module example, in-depth discussion of the Quill module loading mechanism.

Tips: This summary involves the Quill source code analysis, welcome to leave a message to discuss if you don't understand the place.

The initialization of the Quill class

When we execute new Quill(), we execute the Quill class's constructor method, which is located in the Quill source code's core/quill.js file.

The approximate source structure of the initialization method is as follows (remove module loading irrelevant code) :

constructor(container, options = {}) {
  this.options = expandConfig(container, options); // Extend configuration data, including adding topic classes, and so on
  ...
  this.theme = new this.options.theme(this, this.options); // 1. Initialize the theme instance using the theme class in Options

  // 2.Add required modules
  this.keyboard = this.theme.addModule('keyboard');
  this.clipboard = this.theme.addModule('clipboard');
  this.history = this.theme.addModule('history');

  this.theme.init(); // 3. Initialize the theme. This method is the core of the module rendering (the actual core is the AddModule method called in it), traversing all configured module classes and rendering them into the DOM
  ... 
}
Enter fullscreen mode Exit fullscreen mode

When Quill is initialized, it will use the expandConfig method to extend the options passed in and add elements such as topic classes to initialize the topic. (A default BaseTheme theme can be found without configuring the theme)

The addModule method of the theme instance is then called to mount the built-in required module into the theme instance.

Finally, the theme instance's init method is called to render all modules into the DOM. (More on how this works later)

If it is a snow theme, you will see a toolbar appear above the editor:

Alt Text

If it is a Bubble theme, then a toolbar float will appear when a text is selected:

Alt Text

Next, we take the toolbar module as an example to introduce the loading and rendering principle of Quill module in detail.

The loading of toolbar modules

Taking the Snow theme as an example, the following parameters are configured when the Quill instance is initialized:

{
  theme: 'snow',
  modules: {
    toolbar: [['bold', 'italic', 'strike'], ['link', 'image']]
  }
}
Enter fullscreen mode Exit fullscreen mode

Quill in the constructor method to get to this. The theme is SnowTheme class instances, execute this.theme.init() method is invoked when its parent class theme of the init method, this method is located in the core/theme.js file.

init() {
  // Iterate through the Modules parameter in Quill Options to mount all the user-configured Modules into the theme class
  Object.keys(this.options.modules).forEach(name => {
    if (this.modules[name] == null) {
      this.addModule(name);
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

It iterates through all the modules in the options.modules parameter and calls the AddModule method of BaseTheme, which is located in the themes/base.js file.

addModule(name) {
  const module = super.addModule(name);
  if (name === 'toolbar') {
    this.extendToolbar(module);
  }
  return module;
}
Enter fullscreen mode Exit fullscreen mode

This method will first execute the AddModule method of its parent class to initialize all the modules. If it is a toolbar module, additional processing will be done to the toolbar module after the initialization of the toolbar module, which is mainly to build the ICONS and bind the shortcut key of hyperlink.

Let's return to the addModule method of BaseTheme, this method is the core of module loading.

This is a method we saw earlier when we introduced the initialization of Quill, and called when we loaded the three built-in required modules. All modules load through this method, so it's worth exploring this method, which is located in core/theme.js.

addModule(name) {
  const ModuleClass = this.quill.constructor.import(`modules/${name}`); // To import a module class, create a custom module by registering the class with Quill. Register the class with Quill
// Initialize the module class
  this.modules[name] = new ModuleClass(
    this.quill,
    this.options.modules[name] || {},
  );
  return this.modules[name];
}
Enter fullscreen mode Exit fullscreen mode

The addModule method imports the module class by calling the Quill.import method (if you have registered it through the Quill.register method).

We then initialize the class, mounting the instance into the Modules member variable of the theme class (which at this point already has an instance of the built-in required module).

In the case of a Toolbar module, the Toolbar class initialized in the addModule method is located in the modules/toolbar.js file.

class Toolbar {
  constructor(quill, options) {
    super(quill, options);

    // Parse the modules.toolbar parameters to generate the toolbar structure
    if (Array.isArray(this.options.container)) {
      const container = document.createElement('div');
      addControls(container, this.options.container);
      quill.container.parentNode.insertBefore(container, quill.container);
      this.container = container;
    } else {
      ...
    }

    this.container.classList.add('ql-toolbar');

    // Bind toolbar events
    this.controls = [];
    this.handlers = {};
    Object.keys(this.options.handlers).forEach(format => {
      this.addHandler(format, this.options.handlers[format]);
    });
    Array.from(this.container.querySelectorAll('button, select')).forEach(
      input => {
        this.attach(input);
      },
    );
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

When a toolbar module is initialized, it parses the modules.toolbar parameters, calls the addControls method to generate the toolbar buttons and drop-down boxes (the basic idea is to iterate through a two-dimensional array and insert them into the toolbar as buttons or drop-down boxes), and binds events to them.

function addControls(container, groups) {
 if (!Array.isArray(groups[0])) {
  groups = [groups];
 }
 groups.forEach(controls => {
  const group = document.createElement('span');
  group.classList.add('ql-formats');
  controls.forEach(control => {
    if (typeof control === 'string') {
      addButton(group, control);
    } else {
      const format = Object.keys(control)[0];
      const value = control[format];
      if (Array.isArray(value)) {
        addSelect(group, format, value);
      } else {
        addButton(group, format, value);
      }
    }
  });
  container.appendChild(group);
 });
}
Enter fullscreen mode Exit fullscreen mode

The toolbar module is then loaded and rendered into the rich text editor to facilitate editor operations.

Now a summary of the module loading process is made:

  1. The starting point for module loading is the init method of the Theme class, which loads all the modules configured in the option.modules parameter into the member variable of the Theme class: modules, and merges them with the built-in required modules.
  2. The addModule method imports the module class through the import method, and then creates an instance of the module through the new keyword.
  3. When creating a module instance, the initialization method of the module is executed, and the specific logic of the module is executed.

Here is a diagram of the relationship between the module and the editor instance:

Alt Text

Conclusion

In this post, We introduced the configuration method of Quill module briefly through two examples, so that we have a intuitive and preliminary impression of the Quill module.

The character statistics module is then used as a simple example to show how to develop a custom Quill module that extends the functionality of the rich text editor.

Finally, through analyzing the initialization process of Quill, the loading mechanism of Quill module is gradually cut into, and the loading process of toolbar module is elaborated in detail.

About DevUI team

DevUI is a team with both design and engineering perspectives, serving for the DevCloud platform of Huawei Cloud and several internal middle and background systems of Huawei, serving designers and front-end engineers.

Official website: devui.design

Ng component library: ng-devui (Welcome to star🌟)

by Kagol

Top comments (0)