上周抽空把去年写的富文本重写了一下,封装成基本UI组件,就可以在聊天框之外的地方复用了。个人觉得富文本是个兼容问题最多的模块之一,尤其是文档也没几个,把mozilla的api文档和IE的dom api关于selection和range的看了一个遍,一个个试,总算找到勉强能用的方法。
其实之前的富文本代码太乱,而且还有不少bug,只是产品经理不给时间改,O__O"…
这个富文本没有用iframe来做输入框,原因有二:
- iframe是所有dom节点中消耗性能最大的,开多几个ie6/7就会卡到不行了;
- 因为webqq是多窗口聊天的,当光标点击到输入框时,iframe会捕获鼠标事件,通知不了聊天窗设置样式;
所以就用了div,设置contentEditable="true",这个属性基本浏览器都支持,除了firefox2.0(不过还真有用户还在用ff2.0⊙﹏⊙b汗)
这次修改发现了不少蛋疼的兼容性问题,挑几个归纳一下:
more
1. 光标位置的保存/还原
富文本很大一部分兼容问题在于保存和还原光标的位置。说起光标位置,有个要注意的地方就是不要随便调用focus方法,连续调用两次focus会导致光标失去, 跟调用blur的效果一样,最好的方式就是让调用方在调用的时候保证光标在输入框中,内部代码中不要调用focus。
保存就不说了,keyup/mouseup的时候把当前的range存起来(_这里有性能问题,但是blur事件又不能用,产生这个事件的时候,光标已经移到别处了_),但是要保证光标在输入框中,否则range就是document的。
这里要注意到是,ie9支持了window.getSelection方法,但是,它拿到的range对象没有createContextualFragment方法,这个方法可以传入一个html字符串,直接生成dom节点,跟pasteHTML有点类似,具体说明可以点击这里查看。因此自己封装的getSelection方法,要把document.selection放在前面。
还原光标位置,对于高级浏览器,直接把原来的range添加到selection就行,像这样
selection.removeAllRanges();
selection.addRange(this._lastRange);
ie则有两种方法:
1. getBookmark和moveToBookmark
var range = RichEditor.getRange();
range.moveToBookmark(this._lastBookmark);
range.select();
2. setEndPoint
range.setEndPoint('EndToStart', this._lastRange);
range.collapse(false);
range.setEndPoint('EndToEnd', this._lastRange);
range.select();
这里说下setEndPoint的原理:
- 先保存lastRange,如"ABCDEFG"中的"CDE"
- 把新的range的结尾移动到lastRange的开头(即"C"的左边)
- 然后调用 collapse(false)把光标的插入点移动到range的结尾,也就是把range的开头和结尾合并在一起,不这样处理的话,调用select之后会选中"AB"(即选中"C"之前的所有内容)
- 把range的结尾移动到lastRange的结尾(即"E"的右边)
- 选中该range就能把上次保存的选区还原了(即选中"CDE")
2. 换行处理
当在输入框按下回车键之后,ie会生成一个新的段落标签,ff是
,chrome则是。这也不是什么大问题,但是会让后续的处理产生麻烦, 理想的情况就是任何浏览器里输入框的内容都一样。所以这里要监控输入框的keydown事件,如果是回车,则阻止浏览器的默认行为,使用代码插入一个换行标签
。
注意1: opera的keydown事件是没办法阻止默认行为的,要用keypress事件代替。
注意2:当chrome的光标在一行的末尾的时候,插入一个
并不能让光标移动到下一行,还需要在
后面插入一个额外的节点才能跳到下一样。因此可以先插入
,然后把html空格" "删除即可。
3. 删除处理(ie)
ie中如果选中一个图片或input等节点,按下退格键的话,会触发浏览器的后退处理,跟调用history.back()一样的效果,可以在keydown的时候判断选中内容的类型,如果是control类型,则阻止浏览器的行为,使用代码删除。
var selection = RichEditor.getSelection();
if (selection.type.toLowerCase() === 'control') {
e.preventDefault();
selection.clear();
}
PS: 这种情况只存在于使用div做输入框的情况,iframe没有。
4. 粘贴&拖拽处理
聊天窗的输入框跟一般的富文本不太一样,想发表文章用的富文本,是可以允许粘贴html片段进来的。但是聊天框里贴入html片段会导致样式很乱,影响体验。而且里面的图片都必须先上传到服务器才能使用。因此要对贴入的内容进行过滤。
之前的处理是直接把所有内容用正则过滤一遍,放过
和部分有标识的标签,其余一概删掉,然后再重新插入输入框。这样处理比较简单,但是会导致过滤后的光标无法找回原来的地方,体验不好。
现在是用遍历dom的方法,遍历输入框的直接子节点,把其中的文本提取出来,创建TextNode,并替换掉它的父节点,这里用到两个比较重要的属性:
- textContent(标准浏览器): textContent保存了它所有的子孙节点的文本,去除了所有Element节点
- innerText(ie): ie没有textContent属性,但是可以用innerText代替
注意: opera没有onpaste事件,只能捕捉到ctrl+v的粘贴行为,而且很意外的keypress的v键keyCode_ _还是86。右键贴入的就没办法了,连编辑的div连oninput事件也触发不了 O__O"…
5. 插入处理
标准浏览器(非ie)要在光标处插入内容,可以用range.createContextualFragment创建一个html片段,调用range.insertNode插入。用这种方法插入后,光标会消失,要把光标重新定位显示。
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就简单多了, 虽然也不见得是什么好事
range.pasteHTML(html);
range.collapse(false);
range.select();
6. 插入后的光标定位
插入html片段后,如果出现了滚动条,在非ie浏览器里,光标已经在可视区下面,而且不会自动滚动到可视区域。解决办法是插入html片段的时候,在后面添加多一个宽高都是0的图片,然后计算图片相对输入框的位置是否已经超出了输入框的可视范围。如果是,将输入框滚动定位到图片处,之后将图片删除。
这里之所以用图片,是因为他是display: inline;的元素,不会导致内容换行,又可以设置宽高,让其对用户不可见,是在是杀人越货必备之品。
代码如下:
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上也有问题,但是还没测,待续...
关于ie下 web editor 光标的问题,谢谢!具体代码请看<br><a href='http://www.iteye.com/problems/91534' rel='nofollow noopener'>http://www.iteye.com/proble...</a>测试ie的range,问题描述(如果需要重现,请先刷新页面): <br>光标放在'初始'之间,使用ctrl+v 快捷键粘贴后,粘贴的内容不在editorPasteElm里面,具体执行后的效果可以使用ie“F12开发者工具”来查看 <br>怎么样才能使粘贴的内容在editorPasteElm里面?请不要使用 document.execCommand('paste',false,null);因为这个会导致ie提示 是否允许程序访问 粘贴板
请问火狐等其他浏览器怎么临时保存光标?求教啊!!!
@无影脚 var lastRange = widnow.getSelection().getRangeAt(0);<br>把lastRange存起来就行啦
@iAzrael 你好<br>怎么getRangeAt(0)是null或是是空呢?
@iAzrael 你好<br>我这样写还原光标位置对吗?<br>window.getSelection().AddRange(lastRange)
@iAzrael 你好~<br>已经可以保存光标基本上可以<br>但是内容插入后你所说的非IE光标到了最下面.请问你这里<br>var divArea = this._divArea;<br>var pos = $D.getRelativeXY(lastNode, divArea);<br>divArea.scrollTop = pos[1] < divArea.scrollHeight ? divArea.scrollHeight : pos[1];<br>document.execCommand(&#039Delete&#039, false, null);// 删除附加的节点可以说明白一点吗?$D.getRelativeXY不太懂什么意思?
@无影脚 addRange之前, 需要先把原来的选区清理掉<br>var sel = window.getSelection();<br>sel.removeAllRanges();<br>sel.addRange(lastRange);
@无影脚 这里主要就是在插入的时候额外加多一个<b>临时节点A</b>, 然后判断A的位置(Y坐标)是否已经div输入框的可视范围, 也就是说光标是不是已经看不到了, 这时就把scrollTop设置为临时节点A的Y坐标, 滚动条就滚下去了. <br>最后把临时节点删掉就行了
@无影脚 $D.getRelativeXY是我用的一个库的方法, 作用是获取lastNode相对于divArea的偏移位置(left, top), 如果divArea是body, 就相当于获取lastNode在页面中的x,y
谢谢了<br>弄好感谢感谢阿
你确定addRange在chrome可以?还有虽然getRangeAt(0)可以获得当前的Rang,但是chorme是不支持多个Range的,楼主检查下
@ljl 虽然它只支持单个range, 但是addRange也是可以工作的, 我在WebQQ里面已经验证过了
你好,问个问题<br>var frag = range.createContextualFragment('<strong></strong>');<br>var lastNode = frag.lastChild;<br>range.insertNode(frag);range.setStartAfter(lastNode);<br>...<br>这里添加的strong标签是空,它的lastChild是null,这样怎么设置setStartAfter呢
你好,问个问题<br>var frag = range.createContextualFragment('<strong></strong>');<br>var lastNode = frag.lastChild;<br>range.insertNode(frag);<br>range.setStartAfter(lastNode);<br>...<br>这里添加的strong标签是空,它的lastChild是null,这样怎么设置setStartAfter呢
@Keboy 你是在什么浏览器上测试的呢? 只要frag里面有一个节点, frag.lastChild就不会是null.<br>你的本意是想把光标放到strong标签内部?
请问ie8光标在文本最后就保存不到光标呢?<br>我的代码类似这样<br><a href='http://www.zhujianfeng.info/?p=122' rel='nofollow noopener'>http://www.zhujianfeng.info...</a><br>请教什么原因呢 :)
@一粒糖 你可以试一下保存和恢复Range来实现光标的还原, ie的bookmark是有些问题的, 偶尔会还原失败
@iAzrael '保存和恢复Range来实现光标的还原'<br>这不太明白<br>我试过<br>range.setEndPoint('EndToStart', this._lastRange);<br>range.collapse(false);<br>range.setEndPoint('EndToEnd', this._lastRange);<br><a href='http://range.select' rel='nofollow noopener'>range.select</a>()<br>还是有问题你之前做的时候没有这个问题吗?
@一粒糖 有可能是你的 this._lastRange本身就是null或者取的位置不对, 我是监听了输入区域的mouseup和keyup事件, 每次触发的时候都取一下当前的range赋值给 this._lastRange, 这样就能保证 this._lastRange一定是在输入区内的
@iAzrael 您好:<br>Ie8以下的可以保存光标了监听keyup时就可以了,但是Ie8以下版本富文本框没有输入东西的时候,插入图片光标不能在图片的前面用鼠标也不能控制光标到前面,只有添加一个空的节点才可以,请问怎么判断光标前面没有节点
@一粒糖 你可以在初始化输入框的时候默认填一个<br>标签, 然后在监听到删除事件的时候检查一下输入框还有没有内容, 没有内容就在删除之后再补上<br>
@iAzrael IE8基本解决.但是还有一点bug<br>我是判断是否IE8以下<br>是的话就<br>document.selection.createRange().pasteHTML(HTML + ' '); <br>加个span<br>然后<br>var cursor = document.selection.createRange();<br> cursor.moveStart('character', -1);<br> cursor.collapse(true);<br> <a href='http://cursor.select' rel='nofollow noopener'>cursor.select</a>();<br> cursor.parentElement().removeNode(cursor.parentElement().lastChild.previousSibling);光标自然定位到图片前面,接着如果用户删除最前面的空格就判断最后的节点是不是,是空就加空格<br> var cursor = document.selection.createRange();<br> if (!(cursor.parentElement().lastChild.nodeValue)) {<br> var _moveStartValue = 0;<br> if (cursor.parentElement().childNodes.length <= 1) {<br> _moveStartValue = -1;<br> }<br> var div = document.createTextNode(' ');<br> cursor.parentElement().appendChild(div)<br> var cursor = document.selection.createRange();<br> cursor.moveStart(&#039character&#039, _moveStartValue);<br> cursor.collapse(true);<br> <a href='http://cursor.select' rel='nofollow noopener'>cursor.select</a>(); } 键盘按了删除野是这个方法.用鼠标在图片前面没有空格光标不能定位在图片前面就难想,百度和腾讯也没有还好解决这个问题
@一粒糖 因为富文本的兼容问题太多了, 特别是ie有很多相当奇怪的问题, 所以都是fix到一个可接受的程度就不会投人力了. 如果不自己做的话, 我推荐用 ACEditor, 他是用dom编辑来实现的富文本, 而不是用 contentEditable 做的, 缺点就是代码有点多
@iAzrael 没有听说过百度也找不到可以发一份我吗253857346@qq.com
@一粒糖 网站是这个 <a href='http://ace.c9.io/' rel='nofollow noopener'>http://ace.c9.io/</a>
IE 11 取document.getSelection() 得到的结果和Window.getSelection() 一样但是 addRange()方法IE11无效
@yanchaoren 是的, ie浏览器好恶心
ie8 range.setEndPoint 方法报参数无效的错误