轉眼已經2020年,飢渴的人類再也不知足於簡單的文本,因而有了花裏胡哨的攜帶各類樣式的文本,然而有文本還不夠,咱們還須要讓用戶在編輯的時候,可以插入各類自定義消息類型,讓咱們發出去的軟文更加好看,所以有了這篇文章。
因爲Quill編輯器自帶的富文本過濾(大部分主流編輯器都會對富文本進行過濾處理),致使開發者想要配置自定義HTML模板時,遇到了很多麻煩。css
爲了自定義Quill中的HTML塊內容,首先須要瞭解Quill內部的渲染流程,這裏有幾個關鍵的概念須要瞭解:html
Delta是Quill內部定義的一個數據格式,用於表示文檔內容以及文檔修改操做,易讀且格式簡單,經過Delta的形式來維護文檔內容,HTML內容和Delta二者能夠相互轉化。node
舉個例子:
這樣一段富文本會被表示成如下的格式:git
{ "ops":[ {"insert":"this is a simple text.\\nbut when "}, {"attributes":{"bold":true},"insert":"it is "}, {"insert":"not bold.\\nlet me try "}, {"attributes":{"italic":true},"insert":"italic "}, {"insert":"haha\\nwhat about "}, {"attributes": {"italic":true,"bold":true},"insert":"both"}, {"insert":" ?\\n"} ] }"
普通的文本會被定義成一個個的insert動做,每一項表明這一個delta,都是對文本內容的描述。github
相似的,若是修改和刪除也會生成對應的delta,以後會將新生成的change delta,與原有的delta進行合併操做,生成新的delta。(delta中一共包含三種操做:insert、delete、retain)api
保留前10個字符,對後續的20個字符進行加粗操做的delta以下:app
{ "ops": [ { "retain": }, { "retain": , "attributes": { "bold": } } ] }
保留前10個字符,對後續的20個字符進行刪除操做以下:xss
{ "ops": [ { "retain": }, { "delete": } ] }
Parchment是抽象的文檔模型,對Blot進行管理。
將Parchment理解成完整的DOM樹結構的話,那麼Blot就是其中一個個單一的節點。而Blot去了Quill中默認的之外,還容許咱們進行自定義,給了更大的擴展空間。編輯器
Blot是Parchment文檔的組成部分,至關於對DOM節點類型的抽象,而一個具體的Blot實例裏仍有其餘的節點信息。ui
全局的根節點Blot是由Quill內部自定義的Scroll類型Blot,管理其下面的全部Blot。
對於Blot的實現定義能夠參照這裏:https://github.com/quilljs/parchment#blots
Quill中默認定義的Blot以下:
這其中常見的包括TextBlot(行內普通文本)、Inline(行內攜帶樣式的普通文本)、Block(塊級行,通常以段落p爲單位)、Break(換行)、Image(圖片IMG插入)、Bold(加粗文本)。
而一段HTML如何構建出Blot?Quill中會根據節點類型優先排除文本節點,若是是元素節點會根據節點的ClassName進行再次判斷,若是仍然沒法找到匹配的BlotName,則默認匹配如下的映射關係,來找到對應的BlotClass。
既然已經有Blot能夠來表示咱們的內容結構了,爲何還須要Delta?Delta自己只是一分內容數據的維護,也就是說HTML的更新,不管是用戶輸入,仍是API操做,都會同步更新到Delta中,而Delta若是不做爲HTML的數據源的話,那麼維護一份Delta數據的意義又在哪裏?
若是HTML => Delta,而不存在Delta=>HTML,那麼不停地去維護一份delta的意義是什麼?
一、由Delta生成HTML實際上是存在的,只不過應用場景只限於初始化文檔的時候,Quill會對傳入的初始化HTML字符串進行解析處理,生成對應的Delta,其次經過applyDelta的方式,生成DOM節點回顯與頁面中。
二、看到這裏你可能還不滿意,爲啥非要走這一步流程,初始化的時候直接一段字符串document.getElementById('container').innerHTML = val不行嗎,是的,能夠,可是Delta的存在讓用戶的文檔變得粒度更細小,變得易維護,變得可追溯。假如A和B同時編輯着一份文檔,A刪除了第二行的10個字符,不須要將文檔內容全量更新,只須要提交action操做,同步本身的行爲,而B這邊也只須要進行衝突處理後merge便可。雖然Delta的維護讓邏輯變得複雜了很多,但它的存在也讓文檔有了更多擴展的可能。
對於內容的修改一共有如下3種方式:
一、初始化編輯器內容:初始化調用quill.pasteHTML,通過HTML過濾和解析回顯到編輯框中。
二、Input Event:用戶輸入和編輯操做,經過MutationObserver監聽處理,更新delta。
三、API調用:調用內部提供API,經過modify方法,然後調用全局Scroll實例的方法去修改。
因爲文章內容愈來愈多樣化,在文章插入地圖、音樂播放器、廣告面板等需求的存在,讓咱們須要對富文本編輯器擴展出更多的功能。可是同時也要作好xss防禦攻擊。
按照第一部分的講述,咱們須要插入一個自定義HTML塊,同時又要Quill可以識別,聰明的你必定想到了,咱們須要自定義一個Blot。經過定義好Blot的方式,讓Quill在初始化的時候可以識別咱們的HTML塊展現,同時也讓咱們在插入HTML塊的時候不會被Quill進行髒HTML過濾。
註冊Blot方法以下:
export default function (Quill) { // 引入源碼中的BlockEmbed const BlockEmbed = Quill.import('blots/block/embed'); // 定義新的blot類型 class AppPanelEmbed extends BlockEmbed { static create(value) { const node = super.create(value); node.setAttribute('contenteditable', 'false'); node.setAttribute('width', '100%'); // 設置自定義html node.innerHTML = this.transformValue(value) return node; } static transformValue(value) { let handleArr = value.split('\n') handleArr = handleArr.map(e => e.replace(/^[\s]+/, '') .replace(/[\s]+$/, '')) return handleArr.join('') } // 返回節點自身的value值 用於撤銷操做 static value(node) { return node.innerHTML } } // blotName AppPanelEmbed.blotName = 'AppPanelEmbed'; // class名將用於匹配blot名稱 AppPanelEmbed.className = 'embed-innerApp'; // 標籤類型自定義 AppPanelEmbed.tagName = 'div'; Quill.register(AppPanelEmbed, true); }
接下來你只須要這樣調用,即可以在編輯器中插入自定義的HTML塊:
quill.insertEmbed(quill.getSelection().index || 0, 'AppPanelEmbed', ` <div class="app_card_header"> 自定義面板標題 </div> <div class="app_card_content"> 自定義面板內容 </div> <div class="app_card_footer"> footer </div> `);
傳參格式要求以下:
insertEmbed(index: Number, type: String, value: any, source: String \= 'api'): Delta
這裏僅僅這是個簡單的示例,若是想豐富自定義Blot的功能,能夠參照:https://github.com/quilljs/parchment#blots
因爲contenteditable屬性放開,爲了防止形成xss攻擊,因此須要咱們對該屬性作特殊的過濾處理,這裏以xss模塊處理爲例:
handleWithXss(content) { const options = { whiteList: { ... div: ['class', 'style', 'data-id','contenteditable'], ... }, css: { whiteList: { color: true, 'background-color': true, 'max-width': true, }, }, stripIgnoreTag: true, onTagAttr: (tag, name, value, isWhiteAttr) => { // 針對div的contenteditable 處理 if (isWhiteAttr && tag === 'div' && name === 'contenteditable') { return 'contenteditable="false"'; } }, } // 自定義規則 const myxss = new xss.FilterXSS(options) return myxss.process(content) }
到這裏,就大功告成啦~
感謝觀看~