這篇文章中的不少內容都來自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建立嵌套結構的內容」,有興趣的朋友能夠關注一下。