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:
- What is Quill module? How to configure Quill module?
- Why and how to create a custom Quill module?
- How does a Quill module communicate with Quill?
- 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']]
}
});
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:
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;
}
...
}
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:
- Clipboard
- History
- Keyboard
- Syntax
- Toolbar
- 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:
- The shortcut for bold is Ctrl+B;
- The shortcut key for hyperlinks is Ctrl+K;
- 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']]
}
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;
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
}
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);
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}`;
});
}
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:
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
...
}
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:
If it is a Bubble theme, then a toolbar float will appear when a text is selected:
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']]
}
}
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);
}
});
}
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;
}
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];
}
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);
},
);
...
}
}
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);
});
}
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:
- The starting point for module loading is the
init
method of theTheme
class, which loads all the modules configured in theoption.modules
parameter into the member variable of the Theme class:modules
, and merges them with the built-in required modules. - The
addModule
method imports the module class through theimport
method, and then creates an instance of the module through thenew
keyword. - 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:
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)