1. 程式人生 > >線上文字編輯器實現原理

線上文字編輯器實現原理

from: https://io-meter.com/2014/09/01/contenteditable-and-selection/

最近研究了一下在瀏覽器中實現的 WYSIWYG 文字編輯器的原理, 在瞭解基本原理並瀏覽了 zenpen 這個相對簡單的線上編輯器的原始碼後, 在這方面有種豁然開朗的感覺。

說來讓人驚訝,最初在瀏覽器中使之變為可能的瀏覽器是 IE5。在那個時代, IE 的確也算是非常先進的瀏覽器了,現在廣為使用的 AJAX 技術,不也是 IE5 最早提供的麼? 不過這裡就不再討論當初 IE 那套陳舊的 API 了,而主要來討論 HTML5 之後被各個瀏覽器廣泛支援的一些技術方法。

進行 WYSIWYG 的文字編輯,需要的幾個基礎是

  1. 使得某一部分 DOM 可以被編輯
  2. 可以獲取和操作使用者選中的區域
  3. 可以在編輯的同時對所編輯的部分 DOM 進行修改,實現如新增樣式等功能

這些功能都非常方便實現。

使 DOM 可編輯

使一部分 HTML 的 DOM 作為容器進入編輯狀態,只需要為這個 DOM 新增一個 contentEditable 屬性。 一般來講,是使用div或者article元素作為這樣的容器。

被新增 contentEditable 屬性的容器元素的子元素都就可以由使用者修改了, 如果想在這個容器下面巢狀一個不可修改的子元素,需要顯式地在這個子元素中新增 contentEditable='false'這樣的宣告。

獲取和操作使用者選擇

操作和獲取使用者選擇是一個非常有用的功能,它不但可以用來實現這裡提出的編輯器的功能, 還可以用來實現在游標位置顯示提示選單等多種功能,在後面對所編輯部分進行樣式修改的時候也常用到。

想要獲取一個 Selection 物件非常簡單:

var selection = window.getSelection();

Selection 物件有anchorNodefocusNode兩個屬性,可以用來獲得選中部分的開始和結束元素, 不過實用不多(一般實用 Range 代替)。此外還有一個isCollapsed 屬性值得注意,當其為true時,代表選擇區域開始和結束在相同的點,也就是沒有選中內容時游標閃爍的模式。

selection 物件還有諸多方法可以用來修改選擇範圍,主要就是對 Range 物件的編輯。

首先可以通過

var range = selection.getRangeAt(0);

來獲取被選中的第一個區塊,以此類推還可以獲得第二個、第三個。簡單起見我們只討論選中一個區塊的情況。 獲得了 Range 物件,我們就可以方便地進行獲得選中區域內容了。 Range 物件有一對屬性分別用來獲得選擇區域的開始和結束點。

range.startContainer // 開始點所在的容器(元素)
range.endOffset // 開始點在其容器內的偏移
range.endContainer // 結束點所在的容器(元素)
range.endOffset // 結束點在其容器內的偏移

除了這兩對屬性,還有一個非常有用的屬性,那就是

range.commonAncestorContainer // 選擇範圍的共同父元素

上面這個屬性常用於檢測選擇範圍的型別/樣式,比如檢測到選中範圍的公共父元素是一個 h1 元素, 那麼可以在工具欄中將代表 h1 元素的按鈕設為啟用狀態。

需要注意的是,返回的 Range 物件是一類可變物件。簡單來說,如果使用者的選區改變了, 那麼 Range 物件的內容也會改變。因此要記錄某一時刻的 Range ,就要記錄上面提到了的兩對屬性。 此外,Range 物件的屬性都是隻讀的,需要通過對應的函式方法來修改。

Range 還定義了各種獲取內容和修改內容的函式,詳細引數和方法可以參見文件, 這裡對幾個常見的 Use Case 說明一下。

獲得選取的座標範圍

我們可以獲得游標在網頁上的精確位置,對於選區還能得到其矩形邊框的幾何表示, 這為我們顯示提示選單提供了方便。獲得游標的位置或者選區的矩形邊框可以使用

var rect = range.getBoundingClientRect();

rect物件包括的屬性包括矩形的topleftrightbottom座標。 如果選取是collapsed的話,這四個屬性就可以用來計算游標的位置了。

儲存選區並恢復

如果對選擇區域內的元素進行了修改,比如新增新元素、改變元素型別等等, 那麼原來的選區會失效。因此一個比較有用的技巧就是在修改元素之前,先儲存選區 Range, 待修改完成後再恢復。

一個完整的例子如下:

var selection = window.getSelection();var range = selection.getRangeAt(0);// 儲存所有 Range 的屬性var startContainer = range.startContainer;var startOffset = range.startOffset;var endContainer = range.endContainer;var endOffset = range.endOffset;// 進行元素修改操作// ......// 構造新的 Rangevar newRange = document.createRange();// 注意,此處必須建立一個新的選區,在原來的 range 上修改無效
newRange.setStart(startContainer, startOffset);
newRange.setEnd(endContainer, endOffset);// 恢復選區
selection.removeAllRanges();
selection.addRange(newRange);

需要注意的是,有些操作可能會自動修改選區,那麼使用上面方法就不能達到恢復選區的目的了。 一個常用的技巧是為恢復選區新增一個延遲,也就是在上面將addRange呼叫放入setTimeout當中。

setTimeout(function(){
  selection.addRange(newRange);},50);

編輯 DOM,修改樣式

文字編輯器的一個很重要的功能就是修改內容的樣式,比如將文字加粗、傾斜、加下劃線等。 還包括將段落修改為標題、塊引用等。一個比較直觀的方法是按照上述介紹的儲存和恢復選區的方法, 按照需求修改元素新增樣式即可。但是這種方法其實細想起來比較複雜,尤其是段落中, 存在混雜多種樣式,以至於存在樣式可以巢狀的情況(比如一段即是加粗又是傾斜的文字), 維護節點關係和清理空白節點會很繁瑣。

好在瀏覽器為我們提供了一個方便的介面來實現這樣的功能。那就是document.execCommand, 這個介面將各種操作抽象成命令的形式。下面展示了實現一些基本功能的方法。

document.execCommand('bold');// 加粗所選
document.execCommand('italic');// 傾斜所選
document.execCommand('underline');// 下劃線所選

document.execCommand('createLink',true,'http://io-meter.com');// 為所選文字加連結
document.execCommand('unlink');// 取消連結

document.execCommand('formatBlock',true,'h1');// 將游標所在段落修改為一級標題
document.execCommand('formatBlock',true,'p');// 將游標所在塊修改為段落

除此之外,瀏覽器還提供了一些編輯命令,如copycutpaste等。 完整的命令列表可以參見這個文件

需要注意的是,各種瀏覽器對這些命令的支援也有些不同,因此需要格外注意。 瞭解了這些命令,就具備實現編輯器中修改樣式等功能的基本知識了。

偵聽修改

在此還需要說一點題外話,在以往要偵聽使用者對文字的修改,一般是繫結keydown事件, 此外考慮到使用者還可能選取並拖拽來改變內容,可能還要新增mouseup事件, 這種方法是低效且繁瑣的,還對元素樣式的修改無能為力。

總結

OK,瞭解了這些知識,實現一個簡單的 Web 文字編輯器是不是也顯得不那麼難了呢? 雖然在這些 API 上不同瀏覽器還有些差異,但是已經被廣泛應用在實現線上文字編輯功能了。

其實如果只專注於現代瀏覽器,那麼實現如同 Medium 那樣漂亮又實用的編輯和寫作工具也不是非常困難的事!