富文本兼容性問題歸納(win)


上周抽空把去年寫的富文本重寫了一下,封裝成基本UI組件,就可以在聊天框之外的地方復用了。個人覺得富文本是個兼容問題最多的模塊之一,尤其是文檔也沒幾個,把mozilla的api文檔和IE的dom api關於selection和range的看了一個遍,一個個試,總算找到勉強能用的方法。

其實之前的富文本代碼太亂,而且還有不少bug,只是產品經理不給時間改,O__O”…

這個富文本沒有用iframe來做輸入框,原因有二:

  1. iframe是所有dom節點中消耗性能最大的,開多幾個ie6/7就會卡到不行了;
  2. 因為webqq是多窗口聊天的,當光標點擊到輸入框時,iframe會捕獲鼠標事件,通知不了聊天窗設置樣式;

所以就用了div,設置contentEditable=”true”,這個屬性基本瀏覽器都支持,除了firefox2.0(不過還真有用戶還在用ff2.0⊙﹏⊙b汗)

這次修改發現了不少蛋疼的兼容性問題,挑幾個歸納一下:

 

1. 光標位置的保存/還原

富文本很大一部分兼容問題在於保存和還原光標的位置。說起光標位置,有個要注意的地方就是不要隨便調用focus方法,連續調用兩次focus會導致光標失去, 跟調用blur的效果一樣,最好的方式就是讓調用方在調用的時候保證光標在輸入框中,內部代碼中不要調用focus。

保存就不說了,keyup/mouseup的時候把當前的range存起來(這里有性能問題,但是blur事件又不能用,產生這個事件的時候,光標已經移到別處了),但是要保證光標在輸入框中,否則range就是document的。

這里要注意到是,ie9支持了window.getSelection方法,但是,它拿到的range對象沒有createContextualFragment方法,這個方法可以傳入一個html字符串,直接生成dom節點,跟pasteHTML有點類似,具體說明可以點擊這里查看。因此自己封裝的getSelection方法,要把document.selection放在前面。

還原光標位置,對於高級瀏覽器,直接把原來的range添加到selection就行,像這樣

雙擊選中源代碼
1
2
selection.removeAllRanges();
selection.addRange( this ._lastRange);

ie則有兩種方法:

  1. getBookmark和moveToBookmark
    雙擊選中源代碼
    1
    2
    3
    var range = RichEditor.getRange();
    range.moveToBookmark( this ._lastBookmark);
    range.select();
  2. setEndPoint
    雙擊選中源代碼
    1
    2
    3
    4
    range.setEndPoint( 'EndToStart' , this ._lastRange);
    range.collapse( false );
    range.setEndPoint( 'EndToEnd' , this ._lastRange);
    range.select();

這里說下setEndPoint的原理:

  1. 先保存lastRange,如”ABCDEFG”中的”CDE”
  2. 把新的range的結尾移動到lastRange的開頭(即”C”的左邊)
  3. 然后調用 collapse(false)把光標的插入點移動到range的結尾,也就是把range的開頭和結尾合並在一起,不這樣處理的話,調用select之后會選中”AB”(即選中”C”之前的所有內容)
  4. 把range的結尾移動到lastRange的結尾(即”E”的右邊)
  5. 選中該range就能把上次保存的選區還原了(即選中”CDE”)

2. 換行處理

當在輸入框按下回車鍵之后,ie會生成一個新的<p></p>段落標簽,ff是<br>,chrome則是<div></div>。這也不是什么大問題,但是會讓后續的處理產生麻煩, 理想的情況就是任何瀏覽器里輸入框的內容都一樣。所以這里要監控輸入框的keydown事件,如果是回車,則阻止瀏覽器的默認行為,使用代碼插入一個換行標簽<br>。

注意1: opera的keydown事件是沒辦法阻止默認行為的,要用keypress事件代替。

注意2:當chrome的光標在一行的末尾的時候,插入一個<br/>並不能讓光標移動到下一行,還需要在<br/>后面插入一個額外的節點才能跳到下一樣。因此可以先插入<br/>&nbsp;,然后把html空格”&nbsp;”刪除即可。

3. 刪除處理(ie)

ie中如果選中一個圖片或input等節點,按下退格鍵的話,會觸發瀏覽器的后退處理,跟調用history.back()一樣的效果,可以在keydown的時候判斷選中內容的類型,如果是control類型,則阻止瀏覽器的行為,使用代碼刪除。

雙擊選中源代碼
1
2
3
4
5
var selection = RichEditor.getSelection();
if (selection.type.toLowerCase() === 'control' ) {
     e.preventDefault();
     selection.clear();
}

PS: 這種情況只存在於使用div做輸入框的情況,iframe沒有。

4. 粘貼&拖拽處理

聊天窗的輸入框跟一般的富文本不太一樣,想發表文章用的富文本,是可以允許粘貼html片段進來的。但是聊天框里貼入html片段會導致樣式很亂,影響體驗。而且里面的圖片都必須先上傳到服務器才能使用。因此要對貼入的內容進行過濾。

之前的處理是直接把所有內容用正則過濾一遍,放過<br>和部分有標識的標簽,其余一概刪掉,然后再重新插入輸入框。這樣處理比較簡單,但是會導致過濾后的光標無法找回原來的地方,體驗不好。

現在是用遍歷dom的方法,遍歷輸入框的直接子節點,把其中的文本提取出來,創建TextNode,並替換掉它的父節點,這里用到兩個比較重要的屬性:

  1. textContent(標准瀏覽器): textContent保存了它所有的子孫節點的文本,去除了所有Element節點
  2. innerText(ie): ie沒有textContent屬性,但是可以用innerText代替

注意: opera沒有onpaste事件,只能捕捉到ctrl+v的粘貼行為,而且很意外的keypress的v鍵keyCode 還是86。右鍵貼入的就沒辦法了,連編輯的div連oninput事件也觸發不了 O__O”…

5. 插入處理

標准瀏覽器(非ie)要在光標處插入內容,可以用range.createContextualFragment創建一個html片段,調用range.insertNode插入。用這種方法插入后,光標會消失,要把光標重新定位顯示。

雙擊選中源代碼
1
2
3
4
5
6
7
8
9
var fragment = range.createContextualFragment(html);
var lastNode = fragment.lastChild;
range.insertNode(fragment);
//插入后把開始和結束位置都放到lastNode后面, 然后添加到selection
range.setEndAfter(lastNode);
range.setStartAfter(lastNode);
var selection = RichEditor.getSelection();
selection.removeAllRanges();
selection.addRange(range);

ie就簡單多了, 雖然也不見得是什么好事

雙擊選中源代碼
1
2
3
range.pasteHTML(html);
range.collapse( false );
range.select();

6. 插入后的光標定位

插入html片段后,如果出現了滾動條,在非ie瀏覽器里,光標已經在可視區下面,而且不會自動滾動到可視區域。解決辦法是插入html片段的時候,在后面添加多一個寬高都是0的圖片,然后計算圖片相對輸入框的位置是否已經超出了輸入框的可視范圍。如果是,將輸入框滾動定位到圖片處,之后將圖片刪除。

這里之所以用圖片,是因為他是display: inline;的元素,不會導致內容換行,又可以設置寬高,讓其對用戶不可見,是在是殺人越貨必備之品。

代碼如下:

JavaScript
1
2
3
4
5
6
7
8
html += '<img class="focus_mark" alt="" />' ;
var fragment = range.createContextualFragment(html);
var lastNode = fragment.lastChild;
//..........
var divArea = this ._divArea;
var pos = $D.getRelativeXY(lastNode, divArea);
divArea.scrollTop = pos[1] < divArea.scrollHeight ? divArea.scrollHeight : pos[1];
document.execCommand( 'Delete' , false , null ); // 刪除附加的節點

這里也可以用lastNode.scrollIntoView()滾動到可視區域的, 只是ff如果打開了firebug, 會導致webqq的樣式錯亂, 其他網站也許可以測試看看.

7. 保證range在輸入框中

前面很多方法的執行前提都是當前焦點在輸入框中,否則如果焦點在document上的話,插入的html會顯示在頁面的左上角,就是一個大bug了。

判斷一個range是否在輸入框中,可以對range的父節點進行判斷,如果其parentNode是輸入框或者在輸入框里面,則是正確的range。 標准瀏覽器可以用range.commonAncestorContainer獲得父節點,ie則是range.parentElement()。比較的方法是compareDocumentPosition(w3c)和contains(ie),具體怎么用就不說了,這里有個說明及封裝好的代碼。

 

以上的問題都是windows平台的,linux上也有問題,但是還沒測,待續…


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM