这篇文章中的不少内容都来自Adam Charron的《Getting to know QuillJS - Part 1》中,文中结合了我本身的一些理解和经验,内容也作了些调整,但愿能帮到准备使用quilljs的你。 javascript
quilljs是一个现代富文本编辑器,它具有良好的兼容性及强大的可扩展性。用户能够很是方便地实现自定义功能。另外一特色是,quilljs自带一套数据系统来支撑内容生产,Parchment 和 Delta。
java
Parchment是抽象的文档模型,是与DOM树相对应的树形结构。Parchment树由 Blot组成,Blot便是DOM Node的对应物,Blot可能包含结构、样式、内容等。打个比方:用户在编辑器中输入了文字“你”,在Parchment树中就会产生一个TextBlot与之对应。node
Delta是一个扁平的JSON数组,用于保存(描述)编辑器中的内容数据。Delta中的每一项表明了一次操做,它的变化会直接影响到编辑器中内容的变化。下面这个delta就表示:git
const delta = new Delta().retain(12)
.delete(4)
.insert('White', { color: '#fff' });
复制代码
经过编辑器的方法getContents
能够获取当前编辑器中的内容的delta数据:
github
Blot是Parchment文档的组成部分,它是quilljs中最重要的抽象。有了Blot,可让用户对编辑器中的内容进行操做,而无需对DOM进行直接操做。每一种Blot都须要实现blot接口规范,quill中的内置Blot都继承自ShadowBlot。
为了方便查找与blot相关的其余blot,因此每一个blot都拥有如下这些引用属性:api
.parent
—父级blot,包含当前blot。若当前blot是顶级blot,则为null
。.prev
—上一个同级blot, 与当前blot拥有同一个parent, 若当前blot为第一个child,则为null
。.next
—下一个同级blot, 与当前blot拥有同一个parent, 若当前blot为最后一个child,则为null
。.scroll
—顶级blot,后面会提供更多关于scroll blot的信息。.domNode
—当前blot的DOM结构,该blot在DOM树中的实际结构。Blot主要经过调用Patchment.create()
建立。Blot拥有几个生命周期方法,你能够经过使用同名方法去覆盖它们,并根据具体状况在你的逻辑代码中经过super去调用被你覆盖的方法,以保证blot的默认行为不被破坏。下面继续介绍这些生命周期方法: 数组
Blot.create()
每个Blot都有static create()
函数,用于根据初始值建立DOM Node。这里也很是适合在node上设置一些与Blot实例无关的初始属性。该函数会返回新建立的DOM Node,但并未插入文档中。此时,Blot也还未实例化成功,由于Blot实例化须要依赖DOM Node。须要注意的是,create()
并非任什么时候候都会在blot实例化前执行,例如:当用户经过 复制/粘贴 建立blot时,blot的建立会直接接受来自剪切板的HTML结构,从而跳过create
方法。缓存
import Block from "quill/blots/block";
class ClickableSpan extends Inline {
// ...
static create(initialValue) {
// Allow the parent create function to give us a DOM Node
// The DOM Node will be based on the provided tagName and className.
// E.G. the Node is currently <code class="ClickableSpan">{initialValue}</code>
const node = super.create();
// Set an attribute on the DOM Node.
node.setAttribute("spellcheck", false);
// Add an additional class
node.classList.add("otherClass")
// Returning <code class="ClickableSpan otherClass">{initialValue}</code>
return node;
}
// ...
}
复制代码
constructor(domNode)
Blot类的构造函数,经过domNode实例化blot。在这里能够作一些一般在class的构造函数中作的事情,好比:事件绑定,缓存引用等。app
class ClickableSpan extends Inline {
// ...
constructor(domNode) {
super(domNode);
// Bind our click handler to the class.
this.clickHandler = this.clickHandler.bind(this);
domNode.addEventListener(this.clickHandler);
}
clickHandler(event) {
console.log("ClickableSpan was clicked. Blot: ", this);
}
// ...
}
复制代码
上面两段代码中,咱们定义了一个简单的Blot,但此时还没法在quilljs中使用它,还须要进行注册,让Parchment认识咱们的Blot。dom
import Quill from "quill";
// Our Blot from earlier
class ClickableSpan extends Inline { /* ... */ }
ClickableSpan.className = "ClickableSpan";
ClickableSpan.blotName = "ClickableSpan";
ClickableSpan.tagName = "span";
Quill.register(ClickableSpan);
复制代码
通常来讲,有两种方式来调用:Parchment.create(blotName)
该方式为Blot实例的主要建立方式,经过传入已经注册的blotName来正确建立blot实例。经过quill.update(Delta)
或者在逻辑中手动调用Patchment.create(blotName)均是此方式。
Parchment.create(domNode)
有时候咱们须要经过传入domNode
来建立blot实例,好比:粘贴/复制的时候,在这种状况中,Blots就须要用到className和tagName来区分。
在定义一个Blot时,须要为它指定blotName、className、tagName。
// Matches to <strong ...>...</strong>
class Bold extends Inline {}
Bold.tagName = "strong";
Bold.blotName = "bold";
// Matches to <em ...>...</em>
class Italic extends Inline {
static tagName = "em";
static blotName = "italic";
}
Bold.tagName = "em";
Bold.blotName = "italic";
// Matches to <em class="italic-alt" ...>...</em>
class AltItalic extends Inline {}
AltItalic.tagName = "em";
AltItalic.blotName = "alt-italic";
AltItalic.className = "italic-alt"
复制代码
上面例子中,HTML结构中的<strong></strong>
和<em></em>
经过tagName区分生成对应的blot,<em></em>
和<em class="italic-alt"></em>
经过className区分能够生成正确的blot。
通过了Blot的定义和建立的过程,咱们还须要将建立好的blot实例插入到quill编辑器的文档树和HTML DOM树中。下面介绍两个api来完成Blot的插入及挂载:
newBlot.insertInto(parentBlot, refBlot)
这是最主要的插入方法,其余几个插入方法都是基于这个方式实现,该方法就是将newBlot
插入到parentBlot
的children中,默认做为最后一个元素插入,若是refBlot也正确传入了,就插入到refBlot
前面。
parentBlot.insertBefore(newBlot, refBlot)
这个方法很经常使用,相似于parentNode.appendChild(domNode)
,默认做为最后一个元素插入,若是refBlot正确传入,就插入refBlot
以前。
注意:本文更关注quilljs底层的Patchment相关知识,在实际应用quilljs时,常常会经过构造Delta实例调用quill.updateContents(Delta)
来改变编辑器内容。
ScrollBlot
是最顶层的ContainerBlot
,它包裹其他全部的blots,而且管理编辑器内的内容变化。ScrollBlot
会建立一个MutationObserver,用于掌控编辑器的内容。ScrollBlot
会追踪MutationRecords,而后调用MutationRecord
的target
中domNode
对应的blot的update
方法。相关的MutationRecords会被做为参数传入。接下来,ScrollBlot
会调用全部受影响的blot的optimize
方法(包括这些blot的child blot)。
update(mutation: MutationRecord[], sharedContext: Object)
Blot发生变化时会被调用,参数mutation的target是blot.domNode。在同一次更新循环中,全部blots收到的sharedContext是相同的。
optimize(context: Object)
更新循环完成后会被调用,避免在optimize方法中改变document的length和value。该方法中很适合作一些下降document复杂度的事。
简单来讲,文档的delta
在optimize执行先后应该是同样的,没发生变化。不然,将引发性能损耗。
remove()
该方法是最经常使用也最简单的彻底移除blot及其domNode的方法。remove
主要是将blot的domNode从DOM树中移除,并调用detach()
。
removeChild(blot)
该方法只有containerBlot及继承自containerBlot的类具有,做用是从该containerBlot的.children
中移除传入的blot。
deleteAt(index, length)
该方法会根据给定的index
及length
来移除调用者的children中对应的blot及内容,若index为0
且length为调用者的children的length, 则移除自身。
detach()
解除一切blot与quill相关的引用关系,从blot的parent上移除自身,同时对children blot调用detach()
。
到这里,Patchment中Blot的主要生命周期已介绍完毕。quilljs的扩展性及其强大,几乎能够在quill编辑器中实现任何的定制化功能。一般的Block/Embed等Blot的定义都比较简单,容易理解,而相对复杂的应该是ContainerBlot如何应用。
后面讲专门写一篇文章,介绍“如何使用Container建立嵌套结构的内容”,有兴趣的朋友能够关注一下。