富文本編輯器 quill.js 開發(四): 自定義格式擴展

前言

鑑於各種繁雜的需求,quill.js 編輯器也面臨着各種挑戰,例如我們需要添加“table”佈局樣式以適應郵件發送格式,手動擴展表情符號功能等等。本文將對這些可定製化功能進行講解和實現。

區分 format 和 module

首先需要明確的是,我們應該清楚自己所需的擴展具體是什麼?

比如想要新增一個自定義 emoji, 那麼想象一下步驟:

  1. 點擊工具欄
  2. 彈出彈窗或者對應的 popover
  3. 在 2 中選中 emoji

這些步驟是一種常見的添加流程。

我們需要明確的是,添加自定義表情符號必然需要一個相應的格式。

本文將以 format 爲例,對此進行詳細講解。

quill 的格式類型

說起 quill 的格式類型, 他的常用格式可以分成 3 類:

  1. Inline
    常見的有 Bold, Color, Font 等等, 不佔據一行的標籤, 類似於 html 裏 span 的特性, 是一個行內樣式, Inline
    格式之間可以相互影響
  2. Block
    添加 Block 樣式, 必然會佔據一整行, 並且 Block 樣式之間不能兼容(共存), 常見的有 List, Header, Code Block 等等
  3. Embeds
    媒體文件, 常見的有 Image, Video, Formula, 這類格式擴展的比較少, 但是本次要加的 emoji 但是這種格式

自定義樣式

新增 emoji.ts 文件來存儲格式, 關於他的類型, 我們選擇 Embeds 格式, 使用這種格式有以下原因:

  1. 他是一種獨特的類型, 不能和顏色, 字體大小等等用在一起
  2. 需要和字體並列, 所以也不能是 Block 類型
import Quill from 'quill';

const Embed = Quill.import('blots/embed');

class EmojiBlot extends Embed {
  static blotName: string;
  static tagName: string;

  static create(value: HTMLImageElement) {
    const node = super.create();
    node.setAttribute('alt', value.alt);
    node.setAttribute('src', value.src);
    node.setAttribute('width', value.width);
    node.setAttribute('height', value.height);
    return node;
  }

  static formats(node: HTMLImageElement) {
    return {
      alt: node.getAttribute('alt'),
      src: node.getAttribute('src'),
      width: node.getAttribute('width'),
      height: node.getAttribute('height'),
    };
  }

  static value(node: HTMLImageElement) {
    // 主要在有初始值時起作用
    return {
      alt: node.getAttribute('alt'),
      src: node.getAttribute('src'),
      width: node.getAttribute('width'),
      height: node.getAttribute('height'),
    };
  }
}

EmojiBlot.blotName = 'emoji';
EmojiBlot.tagName = 'img';
EmojiBlot.className = 'emoji_icon'

export default EmojiBlot;

因爲還有正常的圖片類型會使用 img, 這裏就需要加上 className, 來消除歧義
一般來說, 新開發的擴展性類型, 儘量都加上 className

這樣一個 emoji 類型就創建完成了!

最後我們註冊到 Quill 上即可:

import EmojiBlot from "./formats/emoji";

Quill.register(EmojiBlot);

這裏我們在加上自定義的 popover, 用來點擊獲取 emoji:

    <Popover content={<div className={'emoji-popover'} onClick={proxyEmojiClick}>
  <img alt={'圖片說明'} width={32} height={32} src="https://grewer.github.io/dataSave/emoji/img.png"/>
  <img alt={'圖片說明'} width={32} height={32} src="https://grewer.github.io/dataSave/emoji/img_1.png"/>
</div>}>
  <button className="ql-emoji">emoji</button>
</Popover>

通過代理的方式, 來獲取 dom 上的具體屬性:

  const proxyEmojiClick = ev => {
  const img = ev.target
  if (img?.nodeName === 'IMG') {
    const quill = getEditor();
    const range = quill.getSelection();
    // 這裏可以用 img 的屬性, 也可以通過 data-* 來傳遞一些數據
    quill.insertEmbed(range.index, 'emoji', {
      alt: img.alt,
      src: img.src,
      width: img.width,
      height: img.height,
    });
    quill.setSelection(range.index + 1);
  }
}

展示下新增 emoji 的效果:

image

基礎格式說明

我們的自定義格式都是基於 quill 的基礎庫: parchment

這裏我們就介紹下他的幾個重要 API:

class Blot {
  // 在手動創建/初始值時, 都會觸發 create 函數
  static create(value?: any): Node;

  // 從 domNode 上獲取想要的數據
  static formats(domNode: Node);

  // static formats 返回的數據會被傳遞給 format
  // 此函數的作用是將數據設置到 domNode
  // 如果 name 是 quill 裏的格式走默認邏輯是會被正確使用的
  // 如果是特殊的name, 不處理就不會起效
  format(format: name, value: any);

  // 返回一個值, 通常在初始化的時候傳給 static create
  // 通常實現一個自定義格式, value 和 format 使用一個即可達到目標
  value(): any;
}

上述幾個 API 便是創建自定義格式時常用到的

詳情可參考: https://www.npmjs.com/package/parchment#blots

在上文講到了 formatvalue 的作用, 我們也可以對於 EmojiBlot 做出一些改造:

class EmojiBlot extends Embed {
  static blotName: string;
  static tagName: string;

  static create(value: HTMLImageElement) {
    const node = super.create();
    node.setAttribute('alt', value.alt);
    node.setAttribute('src', value.src);
    node.setAttribute('width', value.width);
    node.setAttribute('height', value.height);
    return node;
  }

  static formats(node: HTMLImageElement) {
    return {
      alt: node.getAttribute('alt'),
      src: node.getAttribute('src'),
      width: node.getAttribute('width'),
      height: node.getAttribute('height'),
    };
  }


  format(name, value) {
    if (['alt', 'src', 'width', 'height'].includes(name)) {
      this.domNode.setAttribute(name, value);
    } else {
      super.format(name, value);
    }
  }
}

目前來說, 這兩種方案都能實現我們的 EmojiBlot

當然 format 的作用, 並不僅僅在於 新增屬性到 dom 上, 也可以針對某些屬性, 修改、刪除 dom 上的信息

其他格式

上面我們講述了三個常見的格式: InlineEmbedsBlock, 其實在 quill 還有一些特殊的 blot:
如: TextBlotContainerBlotScrollBlot

其中 ScrollBlot 屬於是所有 blot 的根節點:

class Scroll extends ScrollBlot {
  // ...
}

Scroll.blotName = 'scroll';
Scroll.className = 'ql-editor';
Scroll.tagName = 'DIV';
Scroll.defaultChild = Block;
Scroll.allowedChildren = [Block, BlockEmbed, Container];

至於 TextBlot, 是在定義一些屬性時常用到的值:

例如源碼中 CodeBlock 的部分:

CodeBlock.allowedChildren = [TextBlot, Break, Cursor];

意味着 CodeBlock 的格式下, 他的子節點, 只能是文本, 換行, 光標
(換行符和光標都屬於 EmbedBlot)

這樣就控制住了子節點的類型, 避免結構錯亂

ContainerBlot

最後要說一下 ContainerBlot, 這是一個在自定義節點時, 創建 Block 類型時經常會用到的值:

image

在源碼中, 並沒有默認的子節點配置, 所以導致看上去就像這樣, 但其實 container 的自由度是非常強的

這裏就給出一個我之前創建的信件格式例子:

在富文本中擴展格式生成能兼容大部分信件的外層格式, 格式要求:
格式佔據一定寬度, 如 500px, 需要讓這部分居中, 格式內可以輸入其他的樣式

大家可能覺得簡單, 只需要 div 套上, 再加上一個樣式 widthtext-align 即可

但是這種方案不太適合郵件的場景, 在桌面和移動端渲染電子郵件大約有上百萬種不同的組合方式。

所以最穩定的佈局方案只有 table 佈局

所以我們開始創建一個 table 佈局的外殼:

class WidthFormatTable extends Container {
  static create() {
    const node = super.create();
    node.setAttribute('cellspacing', 0);
    node.setAttribute('align', 'center');
    return node;
  }
}

WidthFormatTable.blotName = 'width-format-table';
WidthFormatTable.className = 'width-format-table';
WidthFormatTable.tagName = 'table';

有了 table 標籤, 那麼同樣也會需要 trrd:

也是類似的創建方法:

class WidthFormatTR extends Container {
}

class WidthFormatTD extends Container {
}

最後通過 API 將其關聯起來:

WidthFormatTable.allowedChildren = [WidthFormatTR];

WidthFormatTR.allowedChildren = [WidthFormatTD];
WidthFormatTR.requiredContainer = WidthFormatTable;

WidthFormatTD.requiredContainer = WidthFormatTR;
WidthFormatTD.allowedChildren = [WidthFormat];

WidthFormat.requiredContainer = WidthFormatTD;

這一段的含義就是, 保證各個格式的父元素與子元素分別是什麼, 不會出現亂套的情況

格式中最後的主體:

class WidthFormat extends Block {
  static register() {
    Quill.register(WidthFormatTable);
    Quill.register(WidthFormatTR);
    Quill.register(WidthFormatTD);
  }
}


WidthFormat.blotName = 'width-format';
WidthFormat.className = 'width-format';
WidthFormat.tagName = 'div';

register 函數的作用就是在註冊當前的 WidthFormat 格式時, 自動註冊其他的依賴格式; 避免人多註冊多次

最後我們新增一個按鈕, 來格式化編輯器內容:

  const widthFormatHandle = () => {
  const editor = getEditor();
  editor.format('width-format', {})
}

展示下效果:

image

比較遺憾的是, 同樣作爲 Block 格式, 這兩類是不能兼容的, 也就是說在 width-format 格式中, 不能使用 List , Header , Code 這幾項屬性
個人吐槽幾句, 之前嘗試兼容過, 但是在 HTMLdelta 相互轉換時被卡主了, 感覺轉換的方式沒做好

總結

demo鏈接: 點此查看

本文介紹了 quill.js 在面臨多種需求挑戰時需要添加可定製化功能。quill.js 的常用格式包括 Inline、Block 和 Embeds 三類,而
ContainerBlot 則是創建 Block 類型時常用的值,具有極高的自由度。希望本文能夠幫助讀者更好地瞭解和思考富文本編輯的相關問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章