昨天體驗了基本的行內格式 Blot 以及區塊格式 Blot,今天繼續實現類似 Medium 編輯器的最後四個部分,分別為分隔線、圖片、影片、以及推文的自訂功能實現。

分隔線 (Dividers)

接下來的步驟中,我們將實作第一個所謂的「葉子 Blot (Leaf Blot) 」。不同於先前我們練習過的 Blot,這些主要是負責文本格式化—例如定義文字的外觀或調整排列,並實作format()方法。Leaf Blot 的主要職責則是提供特定的內容,並透過實作 value() 方法來達成。

Leaf Blot 可以是文本 (Text) 型態或嵌入 (Embed) 型態的 Blot。在本例中,我們會實作一個屬於嵌入型態的 Blot,即分隔線 (Divider)。值得注意的是,一旦 Embed Blot 建立,其內含的值將會是不可變的 (Immutable) 。因此,如果你需要變更這個 Blot 的內容,則必須先將其從文本中刪除,再重新插入新的內容。

首先我們新增一個 TS 檔當作 Leaf Blot 的練習,並加入 Divider 的 Blot:

import Quill from 'quill';

const BlockEmbed = Quill.import('blots/block/embed');
export class DividerBlot extends BlockEmbed {
  static blotName = 'myDivider';
  static tagName = 'hr';
}

我們的 click handler 呼叫了 insertEmbed() 方法,這個方法不像 format() 那麼方便可以確定、保存和恢復使用者的選擇區域。因此我們需要自行做一些額外的工作來維護這個選擇區域。此外,當我們嘗試在一個 Block 的中間插入一個 Block Embed 時,Quill 會自動為我們將該 Block 分割開來。為了讓這個行為更為明確,我們會在插入分隔線之前明確地插入一個換行符,以自行分割該 Block。

建立 DividerBlot 之後,回到 Component 註冊 DividerBlot 並新增 addDivider 方法:

registerBasicFormatting() {
  // ...
  // Leaf blot
  Quill.register(DividerBlot);
}

addDivider() {
  const range = this.quillInstance.getSelection(true);
  this.quillInstance.insertText(range.index, '\n', Quill.sources.USER)
  this.quillInstance.insertEmbed(
    range.index + 1,
    'myDivider',
    true,
    Quill.sources.USER
  );
  
  this.quillInstance.setSelection(
    { index: range.index + 2, length: 0 },
    Quill.sources.SILENT
  );
}

接著將對應的 button 加上事件綁定:

  <button
    type="button"
    title="divider"
    id="divider-button"
    (click)="addDivider()"
  >
    <i class="fa fa-minus"></i>
  </button>

輸入兩行 Hello World 之後,游標停留在第一行的 Hello 後面,並點擊加入分隔線,可以看到 HTML 被強制換行後加入分隔線:

強制換行後加入分隔線

圖片

圖片的處理可以使用我們在建立 Link 和 Divider blots 時所學到的概念來新增。我們會使用一個物件作為圖片的值來展示如何被支援的。我們用於插入圖像的 click handler 直接帶入 hardcode 的內容來專注在插入圖片 Blot 的實現。

建立 ImageBlot,分別有 create 以及 value 兩個靜態方法:

export class ImageBlot extends BlockEmbed {
  static blotName = 'myImage';
  static tagName = 'img';
  
  static create(value: { alt: string; url: string }) {
    const node = super.create();
    node.setAttribute('alt', value.alt);
    node.setAttribute('src', value.url);
    return node;
  }

  static value(node: HTMLImageElement) {
    return {
      alt: node.getAttribute('alt'),
      url: node.getAttribute('src'),
    };
  }
}

接著在 Component 加上插入圖片的 handler,並綁定到對應的 button:

<button type="button" title="image" id="image-button" (click)="addImage()">
  <i class="fa fa-camera"></i>
</button>
addImage() {
    const range = this.quillInstance.getSelection(true);
    this.quillInstance.insertText(range.index, '\n', Quill.sources.USER);
    this.quillInstance.insertEmbed(
      range.index + 1,
      'image',
      {
        alt: 'Quill Cloud',
        url: 'https://quilljs.com/0.20/assets/images/cloud.png',
      },
      Quill.sources.USER
    );

    this.quillInstance.setSelection(
      { index: range.index + 2, length: range.length },
      Quill.sources.SILENT
    );
  }

看一下加入圖片後的效果:

加入圖片後的效果

小結

今天主要就兩個 Embed Blot 的實現,我們透過繼承 Quill 底下的 Parchment Embed Blot 來建立自定義的 Blot,對於 Quill 的方法及應用有比較深入的理解。 整體的實現上都是與 DOM 去做對應在編輯器中加入內容,因此都會經過 Create() 方法來新增 DOM,如果是簡單的 HTML,沒有太多的加工處理,則直接帶上 blotNmaetagName 即可,按照官網文件的說明,Quill 的確也讓編輯器的內容與結構盡可能的單純易懂。

雜記

這個週末是魔物獵人 Now 的社群日,有期間限定的櫻火龍,貌似對拿弓箭的玩家來說是不錯的裝備材料收集,準備好今天的文章之後,等等就要出去晃晃,希望不會太快就把藥水喝完 XD

Reference

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