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
- 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
- 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
- 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:
- insert
- delete
- 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.
- Through the Quill. Register ({‘ blots/scroll: scroll,}); registered
- Scroll to scroll via Parchment. Creat in the Quill constructor
- 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
- register
Parchment registration is required before use
The source code
The toolbar
- 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
- Modules -> Toolbar -> Handlers Customize image click callback
- Trigger the input file click
- After selecting the image, insert the local image into the editor with insertEmbed
- Run addContainer to add absolute positioning DOM to qL-Container to display the image uploading progress bar and close the button
- The absolute location of the DOM is obtained by getBounds
- Upload, update progress
- Delete the progress bar and display the upload result
- 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…