手把手實現富文字編輯器

發表於2015-10-15

前言

最近嘗試去寫一個富文字編輯器,覺得應該也不難,但沒想到還是花了不少時間去寫前期的主要邏輯,其間太多的邊角邏輯是沒有考慮到的。原因是前期走了 很多彎路,單純的一點一點的去實現功能,有分支功能出現就一點一點的修補,到最後發現程式碼量很多,邏輯很複雜。最後痛下決心,靜下心來分析了一下,思考用 理論邏輯去鋪墊根基,才算是構建了一個還算滿意的基礎邏輯。真心覺得,理論才是一切事情的開始點,縝密的理論邏輯才能建造基乎無bug的程式碼。

前面的都是些廢話,由於該文字編器實現了execCommaond的部分方法,下面的內容相對比較複雜,對實現不感興趣的可以忽略,如果是妹子可以直接聯絡筆者交流交流

功能

富文字編輯器實現如下的功能

  • 實現fake原生execCommand的能力
  • 修改style的能力

其實很多能力,原生的execCommand已經幫我們做好了,但我們的編輯器要有與execCommand相同的能力,以備原生無法實現的時候,我們的編輯器還是可以實現。

API能力

  • 標籤插入與刪除
  • style修改

便籤的修改能力
比如一段程式碼

如果abc被選中之後,執行execCommand之後的程式碼是在abc外包裹一個strong標籤
變成

如果b再次選中執行execCommand(‘bold’)命令後,會變成

可以看到原來的strong被分離了,變成單獨的兩個,還有一些更復雜的情況,如下

像這個串,如果沒有良好的理論基礎與抽象建模,靠手動的程式碼去處理基乎是不可能的。

第一步

串的正則化

如上那個複雜的串,其樹結構如下
最上面的是根結點,葉子結點都是文字節點(textNode)

我們可以看到規律

葉子結點向上回溯的過程中,通過的結點會給我們賦予不同的功能,但我們要操作的節點,如果像strong、underline一定程度是與樣式有關的,我們把它們稱為樣式結點
如果我們直接刪掉葉子結點最近的祖先中的樣式結點,很有可能會影響到其他的葉子結點,這時候我們就要把樣式結點的影響最小化,就要先進行正則處理

正則化的過程就是要把樣式結點轉化到葉子結點的上面,每個樣式結點只控制一個我們想要直接操作的葉子結點
如下的轉化過程

正則化的過程的同時,我們還要修剪一些無用的結點,比如空的span結點,比如空的textNode,轉化完了之後的樹,我們處理起來變得很簡單

正則化的實現也很簡單,從ROOT結點進行中序遍歷的演算法,即先訪問根結點,再從左向右依次訪問子結點,然後一直到葉子結點,找出葉子結點的樣式結點次續,並刪除經過的樣式結點,然後把它們插入到每個葉子結點的上面

如下程式碼實現刪除樣式結點的過程

經過這樣處理後的樹,沒有了任何樣式結點,這也是去除樣式的一個方法
下面的程式碼展示將樣式結點最小化到textNode之上的程式碼

經過了這一輪的正則化處理之後, 我們主要做了以下三件事情

  • 最小化樣式結點到文字結點上
  • 去除無用的結點
  • 去除重複的結點

其實這一輪正則化之後理應要再進行更多的優化,比如

  • 合併結點

正則化完成之後使我們修減樣式結點變得容易很多了,為了實現execCommand的功能,我們要實現樹的如下兩個方法
葉子結點某個樣式結點的刪除
葉子結點某個樣式結點的增加

增加的實現

先向上檢查是否有該樣式結點存在,如果有了就不進行新增了,如果沒有,要進行新增
新增的實現了很簡單

刪除的實現也是如此,先向上檢查是否有label存在,有的話才刪除

樹的抽象功能已經建成,但後面提取最選中的葉子結點也並非一件容易的事情,主要是瀏覽器提供的API坑點太多,太不好用

Selection & Range物件

使用這兩個父子物件,能幫我們獲取被選中的區域

Selection

就是藍色被選中的可視區域

Range

就是邏輯裡面的一塊區域,與視覺化無關

獲取Range

從一個藍色選中的區域中獲取到Range物件,用如下的方法

拿到Range,我們還要分析被選中的葉子節點,這裡有很多坑點
首先Range物件,我們常用的五個屬性
startContainer 開始選中的元素
startOffset 開始選中的偏移
endContainer
endOffset
commonAncestorContainer 共同的最近的祖先結點

startContainer有兩種情況,一種是元素,但這時被選中的是其子結點, 這時候startOffset代表的是被選中的子結點的index,子結點可能是葉子結點,也可能是element結點,另外一種可能是葉子結點 (textNode),startOffset代表的是text的偏移位置,比較坑的是,有可能偏移位置是不存在的str,什麼意思呢,就是比如
startContainer對應葉子結點abcd
startOffset如果是1,那就被選中的開始就是從第1個字元開始(下標從0算)即bcd
但startOffset還可能是4,這時就坑了,開始什麼都沒有
同樣endContainer也是如此

我們的任務是要找出被選中的葉子結點,然後進行增刪樣式處理,當前,前提是保證已經正則化過了
找出被選中的葉子結點並非一件容易的事情
文章比較長,請待後續更新

相關文章