上週抽空把去年寫的富文本重寫了一下,封裝成基本UI組件,就能夠在聊天框以外的地方複用了。我的以爲富文本是個兼容問題最多的模塊之一,尤爲是文檔也沒幾個,把mozilla的api文檔和IE的dom api關於selection和range的看了一個遍,一個個試,總算找到勉強能用的方法。html
其實以前的富文本代碼太亂,並且還有很多bug,只是產品經理不給時間改,O__O」…linux
這個富文本沒有用iframe來作輸入框,緣由有二:web
因此就用了div,設置contentEditable=」true」,這個屬性基本瀏覽器都支持,除了firefox2.0(不過還真有用戶還在用ff2.0⊙﹏⊙b汗)chrome
此次修改發現了很多蛋疼的兼容性問題,挑幾個概括一下:windows
富文本很大一部分兼容問題在於保存和還原光標的位置。提及光標位置,有個要注意的地方就是不要隨便調用focus方法,連續調用兩次focus會致使光標失去, 跟調用blur的效果同樣,最好的方式就是讓調用方在調用的時候保證光標在輸入框中,內部代碼中不要調用focus。api
保存就不說了,keyup/mouseup的時候把當前的range存起來(這裏有性能問題,可是blur事件又不能用,產生這個事件的時候,光標已經移到別處了),可是要保證光標在輸入框中,不然range就是document的。瀏覽器
這裏要注意到是,ie9支持了window.getSelection方法,可是,它拿到的range對象沒有createContextualFragment方法,這個方法能夠傳入一個html字符串,直接生成dom節點,跟pasteHTML有點相似,具體說明能夠點擊這裏查看。所以本身封裝的getSelection方法,要把document.selection放在前面。服務器
還原光標位置,對於高級瀏覽器,直接把原來的range添加到selection就行,像這樣dom
1
2
|
selection.removeAllRanges();
selection.addRange(
this
._lastRange);
|
ie則有兩種方法:post
1
2
3
|
var
range = RichEditor.getRange();
range.moveToBookmark(
this
._lastBookmark);
range.select();
|
1
2
3
4
|
range.setEndPoint(
'EndToStart'
,
this
._lastRange);
range.collapse(
false
);
range.setEndPoint(
'EndToEnd'
,
this
._lastRange);
range.select();
|
這裏說下setEndPoint的原理:
當在輸入框按下回車鍵以後,ie會生成一個新的<p></p>段落標籤,ff是<br>,chrome則是<div></div>。這也不是什麼大問題,可是會讓後續的處理產生麻煩, 理想的狀況就是任何瀏覽器裏輸入框的內容都同樣。因此這裏要監控輸入框的keydown事件,若是是回車,則阻止瀏覽器的默認行爲,使用代碼插入一個換行標籤<br>。
注意1: opera的keydown事件是沒辦法阻止默認行爲的,要用keypress事件代替。
注意2:當chrome的光標在一行的末尾的時候,插入一個<br/>並不能讓光標移動到下一行,還須要在<br/>後面插入一個額外的節點才能跳到下同樣。所以能夠先插入<br/> ,而後把html空格」 」刪除便可。
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沒有。
聊天窗的輸入框跟通常的富文本不太同樣,想發表文章用的富文本,是能夠容許粘貼html片斷進來的。可是聊天框裏貼入html片斷會致使樣式很亂,影響體驗。並且裏面的圖片都必須先上傳到服務器才能使用。所以要對貼入的內容進行過濾。
以前的處理是直接把全部內容用正則過濾一遍,放過<br>和部分有標識的標籤,其他一律刪掉,而後再從新插入輸入框。這樣處理比較簡單,可是會致使過濾後的光標沒法找回原來的地方,體驗很差。
如今是用遍歷dom的方法,遍歷輸入框的直接子節點,把其中的文本提取出來,建立TextNode,並替換掉它的父節點,這裏用到兩個比較重要的屬性:
注意: opera沒有onpaste事件,只能捕捉到ctrl+v的粘貼行爲,並且很意外的keypress的v鍵keyCode 仍是86。右鍵貼入的就沒辦法了,連編輯的div連oninput事件也觸發不了 O__O」…
標準瀏覽器(非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();
|
插入html片斷後,若是出現了滾動條,在非ie瀏覽器裏,光標已經在可視區下面,並且不會自動滾動到可視區域。解決辦法是插入html片斷的時候,在後面添加多一個寬高都是0的圖片,而後計算圖片相對輸入框的位置是否已經超出了輸入框的可視範圍。若是是,將輸入框滾動定位到圖片處,以後將圖片刪除。
這裏之因此用圖片,是由於他是display: inline;的元素,不會致使內容換行,又能夠設置寬高,讓其對用戶不可見,是在是殺人越貨必備之品。
代碼以下:
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的樣式錯亂, 其餘網站也許能夠測試看看.
前面不少方法的執行前提都是當前焦點在輸入框中,不然若是焦點在document上的話,插入的html會顯示在頁面的左上角,就是一個大bug了。
判斷一個range是否在輸入框中,能夠對range的父節點進行判斷,若是其parentNode是輸入框或者在輸入框裏面,則是正確的range。 標準瀏覽器能夠用range.commonAncestorContainer得到父節點,ie則是range.parentElement()。比較的方法是compareDocumentPosition(w3c)和contains(ie),具體怎麼用就不說了,這裏有個說明及封裝好的代碼。
以上的問題都是windows平臺的,linux上也有問題,可是還沒測,待續…