前言
鑑於各種繁雜的需求,quill.js
編輯器也面臨着各種挑戰,例如我們需要添加“table”佈局樣式以適應郵件發送格式,手動擴展表情符號功能等等。本文將對這些可定製化功能進行講解和實現。
區分 format 和 module
首先需要明確的是,我們應該清楚自己所需的擴展具體是什麼?
比如想要新增一個自定義 emoji, 那麼想象一下步驟:
- 點擊工具欄
- 彈出彈窗或者對應的 popover
- 在 2 中選中 emoji
這些步驟是一種常見的添加流程。
我們需要明確的是,添加自定義表情符號必然需要一個相應的格式。
本文將以 format
爲例,對此進行詳細講解。
quill 的格式類型
說起 quill 的格式類型, 他的常用格式可以分成 3 類:
- Inline
常見的有Bold
,Color
,Font
等等, 不佔據一行的標籤, 類似於 html 裏 span 的特性, 是一個行內樣式,Inline
格式之間可以相互影響 - Block
添加Block
樣式, 必然會佔據一整行, 並且Block
樣式之間不能兼容(共存), 常見的有List
,Header
,Code Block
等等 - Embeds
媒體文件, 常見的有Image
,Video
,Formula
, 這類格式擴展的比較少, 但是本次要加的emoji
但是這種格式
自定義樣式
新增 emoji.ts 文件來存儲格式, 關於他的類型, 我們選擇 Embeds
格式, 使用這種格式有以下原因:
- 他是一種獨特的類型, 不能和顏色, 字體大小等等用在一起
- 需要和字體並列, 所以也不能是
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
的效果:
基礎格式說明
我們的自定義格式都是基於 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
便是創建自定義格式時常用到的
在上文講到了 format
和 value
的作用, 我們也可以對於 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 上的信息
其他格式
上面我們講述了三個常見的格式: Inline
、Embeds
、Block
, 其實在 quill
還有一些特殊的 blot
:
如: TextBlot
、 ContainerBlot
、 ScrollBlot
其中 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
類型時經常會用到的值:
在源碼中, 並沒有默認的子節點配置, 所以導致看上去就像這樣, 但其實 container
的自由度是非常強的
這裏就給出一個我之前創建的信件格式例子:
在富文本中擴展格式生成能兼容大部分信件的外層格式, 格式要求:
格式佔據一定寬度, 如 500px, 需要讓這部分居中, 格式內可以輸入其他的樣式
大家可能覺得簡單, 只需要 div
套上, 再加上一個樣式 width
和 text-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
標籤, 那麼同樣也會需要 tr
和 rd
:
也是類似的創建方法:
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', {})
}
展示下效果:
比較遺憾的是, 同樣作爲 Block
格式, 這兩類是不能兼容的, 也就是說在 width-format
格式中, 不能使用 List
, Header
, Code
這幾項屬性
個人吐槽幾句, 之前嘗試兼容過, 但是在 HTML
和 delta
相互轉換時被卡主了, 感覺轉換的方式沒做好
總結
demo鏈接: 點此查看
本文介紹了 quill.js 在面臨多種需求挑戰時需要添加可定製化功能。quill.js 的常用格式包括 Inline、Block 和 Embeds 三類,而
ContainerBlot 則是創建 Block 類型時常用的值,具有極高的自由度。希望本文能夠幫助讀者更好地瞭解和思考富文本編輯的相關問題。