公司要作一個筆記模塊,須要用到富文本編輯器。以前有耳聞富文本編輯器是天坑。知乎-爲何說富文本編輯器是個天坑? 在試過了市面上主流的編輯器後,發現或多或少都不符合要求。主要有如下問題:javascript
還好開發時間比較富足,因而決定在vue-html5-editor基礎上二次開發,最後完成上線的做品,呼喚star✨ 🙋 Github:my-vue-editorhtml
web端實現富文本編輯器主要有2個套路:前端
contenteditable
屬性結合document.execCommand API實現,好比國外的CKEditor、百度的UEditor、優秀的後起之秀wangEditor。selection
、視圖渲染等一切。好比Google Doc、有道雲筆記、基於electron
開發的VS Code。這裏咱們很理智的選擇了第一種實現方式。先簡單介紹下編輯器很重要的幾個概念:vue
Range
翻譯過來是範圍,幅度的意思,與數學上的「區間」這以概念相似。瀏覽器提供的Range
對象用來描述DOM樹中的一段連續的範圍。html5
startContainer
,startOffset
描述Range
的起始處,endContainer
,endOffset
描述Range
的結尾處。當一個Range
的起始處和結尾處是同一個位置時,該Range
就處於collapsed
狀態。java
Selection
(選區)管理整個頁面當前的Range
及Range
的繪製。當Selection
中的Range
處於collapsed
狀態時,便是平常所說的光標。光標實際上是Selection
的一種特殊狀態。node
瀏覽器原生爲咱們提供了一些對Range
內節點進行富文本操做的方法,這些方法都是經過document.execCommand
調用。git
bool = document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
複製代碼
好比github
// 向當前插入點插入一個p標籤。
document.execCommand('insertHTML', false, '<p></p>')
// 將框選部分字體變爲綠色,若是是collapsep狀態則接下來輸入的文字爲綠色
document.execCommand('foreColor', false, '#00ff00')
複製代碼
咱們編輯器的架都是圍繞這兩個概念展開的:web
Range
對象,並在須要時能夠調用的機制。document.execCommand
是用來操縱選區HTML結構的,可是原生提供的方法的邏輯大多數都不徹底符合咱們的須要,或者存在兼容性問題。因此咱們封裝咱們本身的構造函數Command
用來操縱富文本,不一樣的按鈕點擊後就會實例化相應的Command
並執行相關操做。對於第一點,只須要定義一個保存,一個設置方法。
// 保存當前Range
function saveCurrentRange () {
// 獲取selection對象
const selection = window.getSelection ? window.getSelection() : document.getSelection()
if (!selection.rangeCount) {
return
}
const content = this.$refs.content
for (let i = 0; i < selection.rangeCount; i++) {
// 從selection中獲取第一個Range對象
const range = selection.getRangeAt(0)
let start = range.startContainer
let end = range.endContainer
// 兼容IE11 node.contains(textNode) 永遠 return false的bug
start = start.nodeType === Node.TEXT_NODE ? start.parentNode : start
end = end.nodeType === Node.TEXT_NODE ? end.parentNode : end
if (content.contains(start) && content.contains(end)) {
// Range對象被保存在this.range
this.range = range
break
}
}
}
// 設置Range對象
function restoreSelection () {
// 首先獲取selection對象並清除當前的Range
const selection = window.getSelection ? window.getSelection() : document.getSelection()
selection.removeAllRanges()
// 從this.range中得到保存的Range設置爲Selection的Range對象
if (this.range) {
selection.addRange(this.range)
} else {
// 若是以前沒有保存Range則新建一個
const content = this.$refs.content
const row = RH.prototype.newRow({br: true})
const range = document.createRange()
content.appendChild(row)
range.setStart(row, 0)
range.setEnd(row, 0)
selection.addRange(range)
this.range = range
}
}
複製代碼
有了這兩個方法,咱們只須要爲編輯器的內容區域註冊mouseup
keyup
mouseout
事件監聽來實時執行saveCurrentRange
,當點擊按鈕後在實例化Command
前執行restoreSelection
。
對於第二點,封裝execCommand
方法很好理解,好比我要實現"縮進indent"的功能,document.execCommand
就提供了indent
這個參數能夠直接使用,當Range處於ul>li,中執行indent
會讓ul嵌套ul,變成ul>ul>li,多個縮進就執行多個嵌套。這知足咱們的須要。
// 縮進前
<ul>
<li>當前光標位置</li>
</ul>
// 縮進後
<ul>
<ul>
<li>當前光標位置</li>
</ul>
</ul>
複製代碼
可是當Range處於通常的塊級元素中,執行indent
會讓塊級元素外面嵌套blockquote
元素,咱們想經過在塊級元素上增長margin-left
來處理通常塊級元素的縮進。
// 縮進前
<p>當前光標位置</p>
// 縮進後
<blockquote>
<p>當前光標位置</p>
</blockquote>
// 咱們但願的狀況
<p style='margin-left: 8%;'>當前光標位置</p>
複製代碼
咱們只須要封裝execCommand
方法,當其參數爲indent
時,執行對應封裝好的indent
方法,判斷Range
是處於列表元素仍是其餘塊級元素中分別對待就行。 這裏之因此要採用構造函數而不是普通函數的形式,是由於全部原生的execCommand
方法,當執行時瀏覽器內部會對該contenteditable區域維護一個undo棧和一個redo棧,使得每個修改行爲能夠撤銷和重作。
咱們封裝的方法覆寫了原生的方法,就會破壞undo/redo棧的連續性,致使撤銷和重作出錯或失效。因此咱們須要在每一個Command
實例上保存執行前編輯器區域的DOM結構(快照)和執行後編輯器區域的DOM結構(快照),並把這個實例推入相應的undo/redo棧。當咱們執行撤銷和重作操做時只須要從相應的棧中取出保存的快照恢復到內容區域便可。 因此你發現啦,undo
和redo
也是兩個須要重寫的Command
。
到這裏一個富文本編輯器的雛形就出來了,咱們只須要在這個基礎上不斷完善咱們的Command
,再處理須要過濾的樣式、多端數據結構同步、各類瀏覽器的兼容性等一個又一個坑就能作出功能豐富的編輯器啦。👏👏👏😄