上周抽空把去年写的富文本重写了一下,封装成基本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上也有问题,可是还没测,待续…