昨天我們新增了一個元件並初始化 Quill 的核心,今天繼續實現 Medium 編輯器的練習。

實作基礎格式

我們之前提到過,Inline 不貢獻任何格式。這是為 Inline 基礎類別所制定的例外,而不是規則。基本的 block Blot 和區塊級的元素 (Block level element) 的運作方式相同。
要實作粗體和斜體,我們只需要繼承 Inline,設定 blotName 和 tagName,並註冊到 Quill 中即可。有關繼承和靜態方法和變數的內容介紹可以參考 Parchment 的介紹。

import Quill from 'quill';

const Inline = Quill.import('blots/inline');

export class BoldBlot extends Inline {
  static blotName = 'myBold';
  static tagName = 'strong';
}

export class ItalicBlot extends Inline {
  static blotName = 'myItalic';
  static tagName = 'em';
}

這裡跟著 Medium 的範例使用 Strong 以及 em 標籤,但我們也可以使用 bi 標籤。Quill 將使用 blot 的名稱當作格式名稱,透過註冊我們的 Blot,我們現在可以在新格式上使用 Quill 的完整 API:

ngAfterViewInit(): void {
  this.registerBasicFormatting();
  this.quillInstance = new Quill(this.editorContainer.nativeElement);
}

registerBasicFormatting() {
  Quill.register(BoldBlot);
  Quill.register(ItalicBlot);
}

insertText() {
  this.quillInstance.insertText(0, 'Test', { myBold: true });
}

formatText() {
  this.quillInstance.formatText(0, 4, 'myItalic', true);
}

接著將按鈕的 click 事件加上,這邊為了示範方便,我們直接寫死一個 true 在程式裡面,這樣就會一直是加上格式的操作。在 App 中,我們可以使用 getFormat() 來尋找指定範圍內的文本格式,來決定是否新增或刪除格式。Toolbar 模組因為 Quill 已經實現了,就不在這重新實作。

兩個按鈕都點擊之後的效果如下:
都點擊之後的效果

與其他格式(如粗體或斜體)不同,Link 需要存入更多資訊,特別是 URL。這主要影響到「Link blot」的兩個方面:建立和格式檢索。

  1. 建立(Creation): 當建立一個 Link 時,除了表示它是一個 Link 外,還需要加上 URL。這通常會以字串的形式來表示。
  2. 格式檢索(Format Retrieval): 當需要找出或修改一個已存在的 Link 格式時,除了知道它是一個Link 外,我們還需要取得或修改 Link 的 URL。

雖然 URL 通常以字串的型別存入,但也可以用其他方式來表示,例如以一個包含 URL Key value 的物件。這樣做可以允許我們加入其他的 Key/Value 來定義一個連結,提供更多自定義的選項。

新增一個 Link Blot:

export class LinkBlot extends Inline {

  static blotName = 'myLink';
  static tagName = 'a';

  static create(value: string) {
    let node = super.create();
    // Sanitize url value if desired
    node.setAttribute('href', value);
    // Okay to set other non-format related attributes
    // These are invisible to Parchment so must be static
    node.setAttribute('target', '_blank');
    return node;
  }

  static formats(node: HTMLElement) {
    // We will only be called with a node already
    // determined to be a Link blot, so we do
    // not need to check ourselves
    return node.getAttribute('href');
  }
}

Component 加入新的註冊和方法:

registerBasicFormatting() {
  Quill.register(BoldBlot);
  Quill.register(ItalicBlot);
  Quill.register(LinkBlot);
}

考慮到安全性,這裡我們可以在 constructor 注入 Angular 提供的 DomSanitizer “消毒” (sanitize)輸入的 URL 避免 XSS 問題發生:

constructor(private sanitizer: DomSanitizer) {}

addLink() {
  const url = prompt('請輸入 URL');
  const safeUrl = this.sanitizer.sanitize(SecurityContext.URL, url);
  this.quillInstance.format('myLink', safeUrl);
}

接著嘗試選取文本內容,並點擊加入連結的按鈕,輸入網址後可以看到效果:

加入連結效果

區塊引用 (Blockquote) 與標題 (Headers)

Blockquotes 繼承自 Block,這是基本的 Block Blot(一種自定義的文本塊)。與 Inline blots 不同的是,Block Blots 不能被嵌套。如果對同一範圍的文字套用多個 Block blots,它們不會互相包裹,而是會相互替換。也就是說,新套用的 Block Blot 會取代原有的 Block Blot。

建立 BlockquoteBlot:

const Block = Quill.import('blots/block');

export class BlockquoteBlot extends Block {
  static blotName = 'myBlockquote';
  static tagName = 'blockquote';
}

註冊 Blot:

Quill.register(BlockquoteBlot);

Header 的實作方式完全相同,只有一處不同:它可以由多個 DOM 元素表示。預設情況下,格式的值將成為 tagName,而不僅僅是 true。我們可以透過擴充 formats() 來自訂,類似於我們對連結所做的那樣:

export class HeaderBlot extends Block {
  static blotName = 'myHeader';
  static tagName = ['H1', 'H2'];
  static formats(node: HTMLElement) {
    return HeaderBlot.tagName.indexOf(node.tagName) + 1;
  }
}

為了方便測試,加入 CSS 的部分:

::ng-deep h1, ::ng-deep h2 {
  margin-top: 0.5em;
  color: purple;
}

::ng-deep blockquote {
  border-left: 4px solid #111;
  padding-left: 1em;
}

最後在 Component 加入 event function,再和 template 的 click 事件綁定:

addBlockquote() {
  this.quillInstance.format('myBlockquote', true);
}

addHeader1() {
  this.quillInstance.format('myHeader', 1);
}

addHeader2() {
  this.quillInstance.format('myHeader', 2);
}

輸入不同段落的內容後,點擊按鈕試試看套用格式效果,可以看到對應的 HTML 元素也被成功加入了,並且套用了設定好的 CSS Style:

套用了設定好的 CSS Style

小結

今天嘗試跟著實現自訂的 Inline Blot 和 Block Blot,實際操作過一遍會比較有感覺,官方文件提供的範例是 JavaScript,那我們就直接以 Angular 的專案當作練習,以 Angular 的方式來實現對應的功能。對於自訂的 Blot 內容有進一步的理解,明天再接著練習後面的其他功能。

雜記

轉眼間就來到第 28 天了,時間真的過的很快,也平安度過試用期(?)。但對於很多細節和產業的觀念還是持續學習中,白天工作內容的轟炸與考古,晚上則持續學習及寫文章做紀錄,上週的連假則是邊出去旅遊,回到住宿的地方後,繼續準備文章內容,腦袋裝了滿滿的東西。生活的節奏也比以往要快了許多,從進辦公室開始工作,回過神來就快下班了,除了充實,還是充實 XD…

Reference

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