富文本編輯器quill.js開(kāi)發(fā)之自定義格式擴(kuò)展
前言
鑒于各種繁雜的需求,quill.js 編輯器也面臨著各種挑戰(zhàn),例如我們需要添加“table”布局樣式以適應(yīng)郵件發(fā)送格式,手動(dòng)擴(kuò)展表情符號(hào)功能等等。本文將對(duì)這些可定制化功能進(jìn)行講解和實(shí)現(xiàn)。
區(qū)分 format 和 module
首先需要明確的是,我們應(yīng)該清楚自己所需的擴(kuò)展具體是什么?
比如想要新增一個(gè)自定義 emoji, 那么想象一下步驟:
- 點(diǎn)擊工具欄
- 彈出彈窗或者對(duì)應(yīng)的 popover
- 在 2 中選中 emoji
這些步驟是一種常見(jiàn)的添加流程。
我們需要明確的是,添加自定義表情符號(hào)必然需要一個(gè)相應(yīng)的格式。
本文將以 format 為例,對(duì)此進(jìn)行詳細(xì)講解。
quill 的格式類(lèi)型
說(shuō)起 quill 的格式類(lèi)型, 他的常用格式可以分成 3 類(lèi):
- Inline
常見(jiàn)的有 Bold, Color, Font 等等, 不占據(jù)一行的標(biāo)簽, 類(lèi)似于 html 里 span 的特性, 是一個(gè)行內(nèi)樣式, Inline格式之間可以相互影響
- Block
添加 Block 樣式, 必然會(huì)占據(jù)一整行, 并且 Block 樣式之間不能兼容(共存), 常見(jiàn)的有 List, Header, Code Block 等等
- Embeds
媒體文件, 常見(jiàn)的有 Image, Video, Formula, 這類(lèi)格式擴(kuò)展的比較少, 但是本次要加的 emoji 但是這種格式
自定義樣式
新增 emoji.ts 文件來(lái)存儲(chǔ)格式, 關(guān)于他的類(lèi)型, 我們選擇 Embeds 格式, 使用這種格式有以下原因:
- 他是一種獨(dú)特的類(lèi)型, 不能和顏色, 字體大小等等用在一起
- 需要和字體并列, 所以也不能是
Block類(lèi)型
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) {
// 主要在有初始值時(shí)起作用
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;因?yàn)檫€有正常的圖片類(lèi)型會(huì)使用 img, 這里就需要加上 className, 來(lái)消除歧義
一般來(lái)說(shuō), 新開(kāi)發(fā)的擴(kuò)展性類(lèi)型, 盡量都加上 className
這樣一個(gè) emoji 類(lèi)型就創(chuàng)建完成了!
最后我們注冊(cè)到 Quill 上即可:
import EmojiBlot from "./formats/emoji"; Quill.register(EmojiBlot);
這里我們?cè)诩由献远x的 popover, 用來(lái)點(diǎn)擊獲取 emoji:
<Popover content={<div className={'emoji-popover'} onClick={proxyEmojiClick}>
<img alt={'圖片說(shuō)明'} width={32} height={32} src="https://grewer.github.io/dataSave/emoji/img.png"/>
<img alt={'圖片說(shuō)明'} width={32} height={32} src="https://grewer.github.io/dataSave/emoji/img_1.png"/>
</div>}>
<button className="ql-emoji">emoji</button>
</Popover>通過(guò)代理的方式, 來(lái)獲取 dom 上的具體屬性:
const proxyEmojiClick = ev => {
const img = ev.target
if (img?.nodeName === 'IMG') {
const quill = getEditor();
const range = quill.getSelection();
// 這里可以用 img 的屬性, 也可以通過(guò) data-* 來(lái)傳遞一些數(shù)據(jù)
quill.insertEmbed(range.index, 'emoji', {
alt: img.alt,
src: img.src,
width: img.width,
height: img.height,
});
quill.setSelection(range.index + 1);
}
}展示下新增 emoji 的效果:

基礎(chǔ)格式說(shuō)明
我們的自定義格式都是基于 quill 的基礎(chǔ)庫(kù): parchment
這里我們就介紹下他的幾個(gè)重要 API:
class Blot {
// 在手動(dòng)創(chuàng)建/初始值時(shí), 都會(huì)觸發(fā) create 函數(shù)
static create(value?: any): Node;
// 從 domNode 上獲取想要的數(shù)據(jù)
static formats(domNode: Node);
// static formats 返回的數(shù)據(jù)會(huì)被傳遞給 format
// 此函數(shù)的作用是將數(shù)據(jù)設(shè)置到 domNode
// 如果 name 是 quill 里的格式走默認(rèn)邏輯是會(huì)被正確使用的
// 如果是特殊的name, 不處理就不會(huì)起效
format(format: name, value: any);
// 返回一個(gè)值, 通常在初始化的時(shí)候傳給 static create
// 通常實(shí)現(xiàn)一個(gè)自定義格式, value 和 format 使用一個(gè)即可達(dá)到目標(biāo)
value(): any;
}上述幾個(gè) API 便是創(chuàng)建自定義格式時(shí)常用到的
詳情可參考: https://www.npmjs.com/package/parchment#blots
在上文講到了 format 和 value 的作用, 我們也可以對(duì)于 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);
}
}
}目前來(lái)說(shuō), 這兩種方案都能實(shí)現(xiàn)我們的 EmojiBlot
當(dāng)然 format 的作用, 并不僅僅在于 新增屬性到 dom 上, 也可以針對(duì)某些屬性, 修改、刪除 dom 上的信息
其他格式
上面我們講述了三個(gè)常見(jiàn)的格式: Inline 、Embeds 、Block, 其實(shí)在 quill 還有一些特殊的 blot:
如: TextBlot 、 ContainerBlot 、 ScrollBlot
其中 ScrollBlot 屬于是所有 blot 的根節(jié)點(diǎn):
class Scroll extends ScrollBlot {
// ...
}
Scroll.blotName = 'scroll';
Scroll.className = 'ql-editor';
Scroll.tagName = 'DIV';
Scroll.defaultChild = Block;
Scroll.allowedChildren = [Block, BlockEmbed, Container];至于 TextBlot, 是在定義一些屬性時(shí)常用到的值:
例如源碼中 CodeBlock 的部分:
CodeBlock.allowedChildren = [TextBlot, Break, Cursor];
意味著 CodeBlock 的格式下, 他的子節(jié)點(diǎn), 只能是文本, 換行, 光標(biāo)
(換行符和光標(biāo)都屬于 EmbedBlot)
這樣就控制住了子節(jié)點(diǎn)的類(lèi)型, 避免結(jié)構(gòu)錯(cuò)亂
ContainerBlot
最后要說(shuō)一下 ContainerBlot, 這是一個(gè)在自定義節(jié)點(diǎn)時(shí), 創(chuàng)建 Block 類(lèi)型時(shí)經(jīng)常會(huì)用到的值:

在源碼中, 并沒(méi)有默認(rèn)的子節(jié)點(diǎn)配置, 所以導(dǎo)致看上去就像這樣, 但其實(shí) container 的自由度是非常強(qiáng)的
這里就給出一個(gè)我之前創(chuàng)建的信件格式例子:
在富文本中擴(kuò)展格式生成能兼容大部分信件的外層格式, 格式要求:
格式占據(jù)一定寬度, 如 500px, 需要讓這部分居中, 格式內(nèi)可以輸入其他的樣式
大家可能覺(jué)得簡(jiǎn)單, 只需要 div 套上, 再加上一個(gè)樣式 width 和 text-align 即可
但是這種方案不太適合郵件的場(chǎng)景, 在桌面和移動(dòng)端渲染電子郵件大約有上百萬(wàn)種不同的組合方式。
所以最穩(wěn)定的布局方案只有 table 布局
所以我們開(kāi)始創(chuàng)建一個(gè) 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 標(biāo)簽, 那么同樣也會(huì)需要 tr 和 rd:
也是類(lèi)似的創(chuàng)建方法:
class WidthFormatTR extends Container {
}
class WidthFormatTD extends Container {
}最后通過(guò) API 將其關(guān)聯(lián)起來(lái):
WidthFormatTable.allowedChildren = [WidthFormatTR]; WidthFormatTR.allowedChildren = [WidthFormatTD]; WidthFormatTR.requiredContainer = WidthFormatTable; WidthFormatTD.requiredContainer = WidthFormatTR; WidthFormatTD.allowedChildren = [WidthFormat]; WidthFormat.requiredContainer = WidthFormatTD;
這一段的含義就是, 保證各個(gè)格式的父元素與子元素分別是什么, 不會(huì)出現(xiàn)亂套的情況
格式中最后的主體:
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 函數(shù)的作用就是在注冊(cè)當(dāng)前的 WidthFormat 格式時(shí), 自動(dòng)注冊(cè)其他的依賴格式; 避免人多注冊(cè)多次
最后我們新增一個(gè)按鈕, 來(lái)格式化編輯器內(nèi)容:
const widthFormatHandle = () => {
const editor = getEditor();
editor.format('width-format', {})
}展示下效果:

比較遺憾的是, 同樣作為 Block 格式, 這兩類(lèi)是不能兼容的, 也就是說(shuō)在 width-format 格式中, 不能使用 List , Header , Code 這幾項(xiàng)屬性
個(gè)人吐槽幾句, 之前嘗試兼容過(guò), 但是在 HTML 和 delta 相互轉(zhuǎn)換時(shí)被卡主了, 感覺(jué)轉(zhuǎn)換的方式?jīng)]做好
總結(jié)
demo鏈接: 點(diǎn)此查看
本文介紹了 quill.js 在面臨多種需求挑戰(zhàn)時(shí)需要添加可定制化功能。quill.js 的常用格式包括 Inline、Block 和 Embeds 三類(lèi),而ContainerBlot 則是創(chuàng)建 Block 類(lèi)型時(shí)常用的值,具有極高的自由度。希望本文能夠幫助讀者更好地了解和思考富文本編輯的相關(guān)問(wèn)題。
以上就是富文本編輯器quill.js開(kāi)發(fā)之自定義格式擴(kuò)展的詳細(xì)內(nèi)容,更多關(guān)于富文本編輯器quill.js格式擴(kuò)展的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue通過(guò)tailwindcss實(shí)現(xiàn)class動(dòng)態(tài)綁定
這篇文章主要介紹了vue通過(guò)tailwindcss實(shí)現(xiàn)class動(dòng)態(tài)綁定,文中給大家介紹了一些常用類(lèi)名語(yǔ)法記錄,對(duì)vue動(dòng)態(tài)綁定class相關(guān)知識(shí)感興趣的朋友一起看看吧2023-07-07
基于vue-cli創(chuàng)建的項(xiàng)目的目錄結(jié)構(gòu)及說(shuō)明介紹
下面小編就為大家分享一篇基于vue-cli創(chuàng)建的項(xiàng)目的目錄結(jié)構(gòu)及說(shuō)明介紹,具有很好的參考價(jià)值,希望對(duì)大家有所幫助2017-11-11
詳解Vue學(xué)習(xí)筆記入門(mén)篇之組件的內(nèi)容分發(fā)(slot)
這篇文章主要介紹了詳解Vue學(xué)習(xí)筆記入門(mén)篇之組件的內(nèi)容分發(fā)(slot),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07
vue 需求 data中的數(shù)據(jù)之間的調(diào)用操作
這篇文章主要介紹了vue 需求 data中的數(shù)據(jù)之間的調(diào)用操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-08-08
vue的注意規(guī)范之v-if 與 v-for 一起使用教程
這篇文章主要介紹了vue的注意規(guī)范之v-if 與 v-for 一起使用方法,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-08-08
使用Vue開(kāi)發(fā)自己的Chrome擴(kuò)展程序過(guò)程詳解
這篇文章主要介紹了使用Vue開(kāi)發(fā)自己的Chrome擴(kuò)展程序過(guò)程詳解,瀏覽器擴(kuò)展程序是可以修改和增強(qiáng) Web 瀏覽器功能的小程序。它們可用于各種任務(wù),例如阻止廣告,管理密碼,組織標(biāo)簽,改變網(wǎng)頁(yè)的外觀和行為等等。,需要的朋友可以參考下2019-06-06
如何配置vue.config.js 處理static文件夾下的靜態(tài)文件
這篇文章主要介紹了如何配置vue.config.js 處理static文件夾下的靜態(tài)文件,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06

