今天我們將一同探討 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 來命名,並且有一個相關聯的 tagNameclassName。如果 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 的基礎架構和功能。

  1. Delta: Delta 是 Quill 使用的資料結構,專門用來描述編輯器內容或其變更。Delta 包含一個稱為 ops 的物件陣列,其中每個物件都是一個操作,這些操作可以是插入文字、刪除內容或套用格式等。

  2. Blot: Blot 是 Quill 文件模型 Parchment 中的基本建構單位,代表編輯器中的各種元素,例如文字、圖片和樣式。每個 Blot 都具有特定的屬性和行為,能夠包含或被其他 Blots 包含,形成一個層次化的樹狀結構。

  3. Parchment: Parchment 是 Quill 的文件模型,它存在於 DOM 樹結構的平行層面。Parchment 提供了一系列對內容編輯有用的功能和接口。一個 Parchment 主要由多個 Blot 組成,這些 Blot 對應到 DOM 樹中的特定節點。

今天初步了解了 Quill 其中的核心概念:Blot 和 Parchment。透過理解這些概念,讓我們能對於 Quill 的功能與機制有所掌握,並在在實際專案開發中較能得心應手。

雜記

昨天參加了教師節聚餐,意外發現有兩個學妹在同公司,但我現在要叫學姊 XD 難得有這樣的機會可以跟學弟妹們交流。吃完飯之後還到了咖啡廳邊喝咖啡邊聊天,聊了很多,無論是技術或是職涯規劃,都有很棒的收穫。老師還加碼續攤請我們吃晚餐,體驗到某家美墨餐廳美味餐點的強大。我覺得每年盡可能的來參加這樣的活動,除了敘舊之外,更多的是互相同步一下近況,也能看到很多厲害的學弟妹們卓越的成就。期待下一次的聚餐!

Reference:

文章同步發表於2023 iThome 鐵人賽