昨天探討了 Delta,初步有了一些概念,今天就來嘗試練習看看,為了方便查看,先新增一個 quill-editor.service,並將 Delta 相關的練習內容都放到這裡面。

使用 new Delta() 操作

在上一篇文章中,官方不建議手動建立 Delta 物件,應該要透過可連結 Deltas 物件的方法像是:insert()delete(),和 keep() 等方法來建立新的 Delta。

因此使用的操作方式會是每次都直接 new 一個 Delta,並把要進行的文本操作透過鏈式呼叫(Method Chaining) 的方式處理,最後再呼叫 quill.updateContents 方法並帶入新增的 helloWorldDelta

const helloWorldDelta = new Delta().insert('Hello World!');
quill.updateContents(helloWorldDelta as any); // types 問題,暫時 as any

從上面看到我在 helloWorldDelta 後面加了 as any,如果沒加的話會導致型別的錯誤,因為 @types/quill 並不是官方的類型定義庫,因此在後來的版本有修改了 Op 這個 interface,導致在編譯的時候會發生型別錯誤,目前暫解就是 as any,我也提個一個 PR,主要是變更 quill-delta 的版本號,還不確定能不能過 XD 先等看看 reviewer 有沒有什麼回應了。

鏈式呼叫 (Method Chaining)

如果直接執行上方的範例,應該會看到編輯器出現了 Hello World!,但當你先輸入一些內容之後再執行這個方法,就會看到 Hello World! 並不是從游標後面接著進去的,原因是 delta 加入內容的操作都是從頭開始塞進去的,所以這裡我們需要使用鏈式呼叫的方式,在插入新內容之前計算一下目前的游標位置,並在這個位置後面加上內容:

// 使用 `getSelection()` 取得選取狀態
const currentIndex = quill.getSelection()?.index;
if (typeof currentIndex === 'number') {
// 將內容插入
    const insertContent = 'Hello World!';
    const helloWorldDelta = new Delta()
        .retain(currentIndex)
        .insert(insertContent);
      quill.updateContents(helloWorldDelta);
}

這時我們就能跟著游標位置插入內容,但又注意到另一個問題,插入內容之後游標卻還是在原地,印象中好的操作體驗應該是插入內容後,游標也應該跟著移動到新增的內容後面才對。這時我們還需要呼叫一個方法來更新游標的位置。

更新游標位置

使用 setSelection 更新編輯器游標位置,新的 index 可以用 currentIndex + insertContent.length 來獲得:

// 使用 `getSelection()` 取得選取狀態
const currentIndex = quill.getSelection()?.index;
if (typeof currentIndex === 'number') {
// 將內容插入
    const insertContent = 'Hello World!';
    const helloWorldDelta = new Delta()
        .retain(currentIndex) // 保留到游標前的內容
        .insert(insertContent); // 插入內容
    quill.updateContents(helloWorldDelta); // 帶入 Delta 更新內容
    quill.setSelection(currentIndex + insertContent.length, 0); // 更新游標位置
}

Line Formatting

除了內容的輸入,有時候需要加入整行的內容並加上文字格式,例如我們可以插入一個 H1 級別的 Header 內容,為了能夠套用到整行,必須再加上一個換行的 Delta 並加上 Header 的 attribute:

const currentLength = quill.getLength();
const currentIndex = quill.getSelection()?.index;
if (typeof currentIndex === 'number') {
    const headerContent = 'This is Header';
    const headerDelta = new Delta()
        .retain(currentLength)
        .insert(headerContent)
        .insert('\n', { header: 1 });

    quill.updateContents(headerDelta);
    quill.setSelection(currentIndex + headerContent.length + 1, 0);
}

上面這個範例可以看到除了 insert(headerContent) 將 header 的內容加上之外,後面還 insert('\n', { header:1 }) 表示 header 的樣式是加在換行符號上的。即使最後一行沒有套用格式,所有 Quill 文件都必須以換行符號結尾,這樣我們就始終能有一個字元位置來套用行格式。

從內容變化時看 Delta 內容

我們可以註冊 Quill 的 text-change 事件,這個事件會提供 Delta 作為文本內容發生變化時的描述。先在 quill-editor.service.ts 新增一個 updateQuillChanges 方法來處理 Quill 事件註冊:

@Injectable({
    providedIn: 'root',
})
export class QuillEditorService {
    quillUpdateSubject$ = new Subject<Delta>();
    // ...
    
    updateQuillChanges(quill: Quill) {
        quill.on('text-change', (delta) => this.quillUpdateSubject$.next(delta));
    }
}

updateQuillChanges 底下註冊 text-change 事件,並把獲得帶有內容變更的 delta 使用 subejct 方式打出去。這個方法可以在 Quill 初始化後,接著加入事件監聽並訂閱。

小結

Delta 的操作其實還滿單純的,我們可以透過監聽事件觀察 Delta 如何描述內容變更,同時我們也可以使用 quill.getContent() 取得完整描述文本的 Delta 狀態,透過這樣的觀察可以回推當有特定需求的時候,我們可以如何實現較正確的方式來變更編輯器的內容。搭配 Angular 實現 Quill 相關的機制在專案管理及維護上都能有所幫助。

雜記

今天室友發了一個很酷的 30 天鐵人賽,是關於桃園青埔的建案描述,雖然跟 IT 沒有太大的關聯,但也是滿有趣的挑…戰? XD 這讓我想到有一個說法是要培養一個習慣需要花 30 天來練習。希望寫文章的習慣可以在這次挑戰之後,對於寫文章就比較不會太卡,往往都是面對一頁空白的時候不知道從何下筆,但都用電腦設備打文章了,先寫一點東西就對了 XD

Reference

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