今天我們將一同探討 Quill 背後的兩個關鍵概念:Blot 和 Parchment。雖然這些名詞可能聽起來有些陌生,但它們在 Quill 的運作中扮演了重要的角色。讓我們來了解一下,究竟 Blot 和 Parchment 是什麼,以及它們如何影響著 Quill 的編輯內容。
什麼是 Parchment?
Parchment 是 Quill 的文件模型,它是與 DOM (Document Object Model) 並行的樹狀結構,並提供了對內容編輯(例如 Quill)的功能。一個 Parchment 樹是由多個 Blot 組成的,這些 Blot 會反射對應的 DOM 節點。Blot 如同剛才提到的,它可以提供結構、格式與內容。換句話說,它們扮演著為文件元素提供實質性、樣式和功能的角色。除了 Blot 之外,Attributors 也可以提供輕量的格式化資訊,可定義如何將某些簡單的格式套用到文本或其他元素。
什麼是 Blot?
Blot 是構成 Parchment 文件的基本區塊,有幾個基本的實現例如:Block
, Inline
, 以及 Embed
。大部分的情況下,我們不會從頭開始實現 Blot,而是從其中一個基本實現來建立自訂功能。一個最基本的 Blot 必須使用一個靜態的 blotName
來命名,並且有一個相關聯的 tagName
或 className
。如果 Blot 是透過標籤和 Class 定義的,Class 會是第一個優先,標籤則會作為備用。Blot 也需要有一個範圍,作為確認是行內 (inline) 還是區塊 (block)。
class Blot {
static blotName: string;
static className: string;
static tagName: string | string[];
static scope: Scope;
domNode: Node;
prev: Blot | null;
next: Blot | null;
parent: Blot;
// 建立對應的 DOM 節點
static create(value?: any): Node;
constructor(domNode: Node, value?: any);
// 對於子集來說,是 Blot 的長度
// 對於父節點來說,是所有子節點的總和
length(): Number;
// 如果適用,則按照給定的 index 和 length 進行處理
// 經常會把響應轉移到合適的子節點上
deleteAt(index: number, length: number);
formatAt(index: number, length: number, format: string, value: any);
insertAt(index: number, text: string);
insertAt(index: number, embed: string, value: any);
// 回傳當下 Blot 與父節點之間的偏移量
offset(ancestor: Blot = this.parent): number;
// 在更新的生命週期結束後被呼叫
// 不能修改文件的值和長度,並且任何 DOM 的操作必須降低 DOM 樹的複雜度
// 共用的 context 物件會被傳到所有的 Blot
optimize(context: {[key: string]: any}): void;
// 當 blot 發生變化時呼叫,並帶著其變化的紀錄。
// blot 值得內部紀錄可以被更新,並允許修改 Blot 本身。
// 可以透過使用者操作或 API 呼叫觸發
// 共用 context 物件並傳給所有的 Blot
update(mutations: MutationRecord[], context: {[key: string]: any});
/** Leaf Blots only **/
// 如果是 Blot 的類型,則回傳由 domNode 表示的值
// 本身沒有對 domNode 的類型校驗,需要應用程式在呼叫前進行外部校驗
static value(domNode): any;
// 給定一個 node 和 DOM 選擇範圍內的偏移量,回傳一個該位置的 index
index(node: Node, offset: number): number;
// 給定一個 Blot 的座標位置,回傳目前節點在 DOM 可以選範圍的偏移量
position(index: number, inclusive: boolean): [Node, number];
// 回傳目前 Blot 代表的值
// 除了來自 API 或透過 update 可檢測的使用者變更,否則不應該被改變
value(): any;
/** Parent blots only **/
// Blots 的白名單陣列,可以是直接的子節點
static allowedChildren: Blot[];
// 預設節點,當節點為空時會被插入
static defaultChild: string;
children: LinkedList<Blot>;
// 在建構時呼叫,應該填入其子節點的 LinkedList
build();
// 對後代有用的搜尋功能,不應修改
descendant(type: BlotClass, index: number, inclusive): Blot
descendents(type: BlotClass, index: number, length: number): Blot[];
/** Formattable blots only **/
// 如果是 Blot 的類型,則回傳 domNode 格式化後的值
// 不需要檢查 domNode 是否為 blot 類型
static formats(domNode: Node);
// 套用格式到 blot,不應該傳到子節點或其他的 Blot
format(format: name, value: any);
// 回傳代表 Blot 的格式,包括來自 Attributors
formats(): Object;
}
自訂 Blot
我們可以透過自訂 Blot 的屬性和行為,來實現各種自訂的功能和外觀。這讓我們能夠按照專案的需求來建立符合特定用途的編輯器元素。例如我們可以建立一個紅色的 span
元素:
import Quill from 'quill';
// 自訂一個紅色的 span 元素
export class RedTextBlot extends Quill.import('blots/inline') {
static blotName = 'red-text';
static tagName = 'span';
static create(value: string) {
const node: HTMLElement = super.create() as HTMLElement;
if (value) {
node.style.color = 'red';
}
return node;
}
}
// 初始化 Quill
const quillEditor = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: true,
},
});
// 插入自訂的紅色文字
quillEditor.insertText(0, '這是自訂的', 'red-text', true);
小結
剛開始研究的時候一頭霧水,需要多看幾次搭配練習才逐漸有感覺。Blot、Parchment 和 Delta 在 Quill Editor 中是密切相關的三個核心概念,它們共同形成了 Quill Editor 的基礎架構和功能。
Delta: Delta 是 Quill 使用的資料結構,專門用來描述編輯器內容或其變更。Delta 包含一個稱為
ops
的物件陣列,其中每個物件都是一個操作,這些操作可以是插入文字、刪除內容或套用格式等。Blot: Blot 是 Quill 文件模型 Parchment 中的基本建構單位,代表編輯器中的各種元素,例如文字、圖片和樣式。每個 Blot 都具有特定的屬性和行為,能夠包含或被其他 Blots 包含,形成一個層次化的樹狀結構。
Parchment: Parchment 是 Quill 的文件模型,它存在於 DOM 樹結構的平行層面。Parchment 提供了一系列對內容編輯有用的功能和接口。一個 Parchment 主要由多個 Blot 組成,這些 Blot 對應到 DOM 樹中的特定節點。
今天初步了解了 Quill 其中的核心概念:Blot 和 Parchment。透過理解這些概念,讓我們能對於 Quill 的功能與機制有所掌握,並在在實際專案開發中較能得心應手。
雜記
昨天參加了教師節聚餐,意外發現有兩個學妹在同公司,但我現在要叫學姊 XD 難得有這樣的機會可以跟學弟妹們交流。吃完飯之後還到了咖啡廳邊喝咖啡邊聊天,聊了很多,無論是技術或是職涯規劃,都有很棒的收穫。老師還加碼續攤請我們吃晚餐,體驗到某家美墨餐廳美味餐點的強大。我覺得每年盡可能的來參加這樣的活動,除了敘舊之外,更多的是互相同步一下近況,也能看到很多厲害的學弟妹們卓越的成就。期待下一次的聚餐!
Reference:
文章同步發表於2023 iThome 鐵人賽