本文的内容基本上是基于“区域范围对象(Range objects)”这个概念来说的。这个玩意,可以让你选择HTML文档的任意部分,并可以拿这些选择的信息做你想做的事情。其中,最常见的Range
是用户用鼠标选择的内容(user selection)。
本文有不少篇幅就是讲如何将用户的这种选择转换为W3C Range
或Microsoft Text Range对象
。
所谓"Range"
,是指HTML文档中任意一段内容。一个Range
的起始点和结束点位置任意,甚至起始点和结束点可以是一样的(也就是空Range
)。最常见的Range
是用户文本选择范围(user text selection)。当用户选择了页面上的某一段文字后,你就可以把这个选择转为Range
。当然,你也可以直接用程序定义Range
。
例如下面这个模样的例子:
2011-04-12
负责调查切尔诺贝利核事故对人与环境造成影响的俄科学家亚布罗科夫博士指出,因福岛核电站使用的燃料较切尔诺贝利核电站多,且有反应堆使用了含有高毒性的钚的燃料,因此"福岛核电站事故可能会比切尔诺贝利带来更严重的后果"。
上面选中状态的那些文字就可以转换成Range对象
(下面会详细讲述)。通过Range对象
你可以找到Range
的起始点和结束点,如果你实在有心,还可以删除或是复制这些内容,或是用其他文字替换,甚至是简单的HTML。
上面的例子可以说是最简单的Range对象
的例子,因为其只包含了文字。而实际上,Range对象
也是可以包含HTML代码内容的,例如下面这个示例:
<time>2011-04-12</time>
<p>据日本广播协会电视台12日报道,日本经济产业省原子能安全保安院决定将福岛第一核电站核泄漏事故等级提高至7级。这使日本核泄漏事故等级与苏联切尔诺贝利核电站核泄漏事故等级相同。</p>
<p>负责调查切尔诺贝利核事故对人与环境造成影响的俄科学家亚布罗科夫博士指出,因福岛核电站使用的燃料较切尔诺贝利核电站多,且有反应堆使用了含有高毒性的钚的燃料,因此"福岛核电站事故可能会比切尔诺贝利带来更严重的后果"。</p>
同样的,Range对象
被创建,且包含HTML,现在的问题是选择的内容正好跨过了楚河和汉界(跨标签),如果就单纯的论选择的内容的话,应该如下:
泄漏事故等级与苏联切尔诺贝利核电站核泄漏事故等级相同。</p> <p>负责调查切尔诺贝
显然,上面的HTML属于1级残废,基本无效。然而幸运的是,所有的浏览器都会自动调整HTML片段使其有效,就像变成下面这样:
<p>泄漏事故等级与苏联切尔诺贝利核电站核泄漏事故等级相同。</p> <p>负责调查切尔诺贝</p>
可以看到,浏览器自动补全了一定数目的HTML来让Range
有效。如果你复制或是移动Range
,你所复制或移动的HTML内容一定是有效的。
在真正操刀JavaScript之前我们需要大致知道Range对象
的浏览器兼容性情况。实际上,问题是比较麻烦的,因为至少有3种类似Range对象
,且你有必要全部理解。先展示详细的兼容性情况表:
支持:不支持:部分支持:
Explorer 6/7 | Firefox 2 | Safari 1.3 | Opera 9 | |
---|---|---|---|---|
cloneContents() | ||||
cloneRange() | ||||
collapse() | tbd | tbd | tbd | tbd |
collapsed | ||||
commonAncestorContainer | ||||
compareBoundaryPoints() | ||||
comparePoint() – Mozilla 扩展 | ||||
createContextualFragment() – Mozilla 扩展 | ||||
deleteContents() | ||||
detach() | ||||
endContainer | ||||
endOffset | ||||
extractContents() | ||||
insertNode() | ||||
isPointInRange() – Mozilla 扩展 | ||||
selectNode() | ||||
selectNodeContents() | ||||
setEnd() | ||||
setEndAfter() | ||||
setEndBefore() | ||||
setStart() | ||||
setStartAfter() | ||||
setStartBefore() | ||||
startContainer | ||||
startOffset | ||||
surroundContents() |
说明:
cloneContents()
的用法类似docFrag = rangeObject.cloneContents()
,Range对象
内容被克隆同时被添加到文档片段上,并返回自身。但是在Safari下有个问题,即如果选择范围是空,将会返回null
而不是空的文档片段。可以通过类似docFrag = rangeObject.cloneContents() || document.createDocumentFragment()
这样的代码修复。
deleteContents()
处,Range
内容会被永久删除,无返回值。
endContainer
指用户选择内容结尾处的容器节点。通常是文本节点。
extractContents()
用法docFrag = rangeObject.extractContents()
。从DOM树上剪切Range对象
并返回文档片段。该片段可以粘贴到页面上。
startContainer
指用户选择内容起始处的容器节点。通常是文本节点。
startOffset
在Opera浏览器下,在选择内容为空的时候返回0
。
Explorer 6/7 | Firefox 2 | Safari 1.3 | Opera 9 | |
---|---|---|---|---|
addRange() | ||||
anchorNode | ||||
anchorOffset | ||||
collapse() | tbd | tbd | tbd | tbd |
collapseToEnd() | ||||
collapseToStart() | ||||
containsNode() | ||||
deleteFromDocument() | ||||
extend() | ||||
focusNode | ||||
focusOffset | ||||
getRangeAt() | ||||
isCollapsed | ||||
rangeCount | ||||
removeAllRanges() | ||||
removeRange() | ||||
selectAllChildren() | ||||
selectionLanguageChange() |
说明:
anchorNode
用法为userSelection.anchorNode
。指用户选择内容起始处的容器节点。通常是文本节点。
anchorNode
在Opera浏览器下,在选择内容为空的时候返回0
。
focusNode
用法为userSelection.focusNode
。指用户选择内容结尾处的容器节点。通常是文本节点。
focusOffset
在Opera浏览器下,在选择内容为空的时候返回0
。
getRangeAt()
用法为rangeObject = userSelection.getRangeAt(0),作用是将
Mozilla Selection
转换为W3C Range
。
Explorer 6/7 | Firefox 2 | Safari 1.3 | Opera 9 | |
---|---|---|---|---|
boundingHeight | ||||
boundingLeft | ||||
boundingTop | ||||
boundingWidth | ||||
collapse() | tbd | tbd | tbd | tbd |
compareEndPoints() | ||||
duplicate() | ||||
expand() | ||||
findText() | ||||
htmlText | ||||
move() | ||||
moveEnd() | ||||
moveStart() | ||||
moveToElementText() | ||||
moveToPoint() | ||||
offsetLeft | ||||
offsetTop | ||||
parentElement() | ||||
pasteHTML() | ||||
scrollIntoView() | ||||
select() | ||||
text |
说明:
htmlText
用法为htmlString = userSelection.htmlText
。返回字符串,为TextRange
的HTML内容,相当于innerHTML
。只读。
pasteHTML()
,当粘贴HTML到一个文本节点时,该文本节点自动分隔。
text
用法为string = userSelection.text
。返回字符串,为TextRange
的文本内容,相当于innerText
。可读/写。
Explorer 6/7 | Firefox 2 | Safari 1.3 | Opera 9 | |
---|---|---|---|---|
W3C Range 详述 |
||||
Mozilla Selection 详述 |
||||
Microsoft Text Range 详述 |
说明:
W3C Range对象
是唯一官方指定。基本上其是将Range
作为包含DOM的文档片段。Mozilla Selection对象
显得有些多余,其存在是为了向后兼容Netscape 4。其类似于W3C Range对象
,也是基于DOM树的。Microsoft Text Range对象
跟上面两个就是郭德纲和玄彬的区别了,因为其是基于字符串的。事实上,Text Range
包含的字符串是很难一下子跳变成DOM节点的。 总的来说,Mozilla Selection对象
就是个打酱油的命,仅有的闪光点能够直接将用户选择任何内容变成完全Range对象
以及一些额外的方法或是属性可以向后兼容Netscape 4。但是不幸的是除了IE浏览器外的其他浏览器都支持此Selection对象
。
婆婆妈妈的解释就免了,直接看相关代码:
var userSelection; if (window.getSelection) { //现代浏览器 userSelection = window.getSelection(); } else if (document.selection) { //IE浏览器 考虑到Opera,应该放在后面 userSelection = document.selection.createRange(); }
由于兼容性的问题,IE浏览器吃IE的包子,其他浏览器吃Mozilla的馒头。
在Mozilla、Safari、Opera下上面的userSelection
是个Selection对象
,而在IE下则是Text Range对象
。这种差异会影响到你后面的脚本:Internet Explorer的Text Ranges
完全不同于Mozilla的Selection
或是W3C的Range对象
,你需要分别为IE和其他浏览器写两套不同的脚本。
需要注意脚本书写的顺序:Mozilla Selection
需放在前面。原因在于Opera支持两种对象,如果你使用window.getSelection()
去读取用户选择的内容,Opera会创建一个Selection对象
;而使用document.selection
则会创建一个Text Range对象
。
因为Opera支持Mozilla Selection
和W3C Range
非常好,但是其对Microsoft Text Range
的支持却差强人意。所以显然优先考虑标准浏览器,即使用window.getSelection()
。
userSelection
的内容 userSelection
变量现在的内容要么是Mozilla Selection
要么就是Microsoft Text Range对象
。因此它允许访问定义在对象上的全部方法和属性。
Mozilla Selection对象包含用户选择的文本内容,如下操作:
alert(userSelection)
虽然格式并不是字符串,但是在现代浏览器下还是会弹出类似下面的内容:
泄漏事故等级与苏联切尔诺贝利核电站核泄漏事故等级相同。负责调查切尔诺贝
为了从Microsoft Text Range 对象
上获得同样的信息,你需要使用userSelection.text
。为了读取旋转的文字,可以使用类似下面代码:
var selectedText = userSelection; if (userSelection.text) { selectedText = userSelection.text; }
现在selectedText
就包含了用户所选中的文字了。
您可以狠狠地点击这里:获取用户所选文字demo
例如在IE7浏览器下,选中一段文字再点击demo页面上的测试按钮,就会有类似下面的弹出内容:
在IE浏览器下,userSelection
是Text Range
,在现代浏览器下,userSelection
仍然是Selection对象
,要想同样创建和Selection对象
内容一样的Range对象
可以使用类似下面代码:
var getRangeObject = function(selectionObject) {
if (selectionObject.getRangeAt)
return selectionObject.getRangeAt(0);
else { // 较老版本Safari!
var range = document.createRange();
range.setStart(selectionObject.anchorNode,selectionObject.anchorOffset);
range.setEnd(selectionObject.focusNode,selectionObject.focusOffset);
return range;
}
}
var rangeObject = getRangeObject(userSelection);
理想情况下,我们通过Selection
对象的getRangeAt()
方法就可以得到W3C Range对象
。此方法可以返回给定索引值的range对象
。通常情况下,在JavaScript中第一个Range
的索引值是0
。
使用程序创建Range
Safari 1.3不支持getRangeAt()
,因此我们要想兼顾此浏览器,需要使用其他的方法创建新的Range对象
。显然,显示创建一个对象:
var range = document.createRange();
上面的一行代码创建了一个空的Range
,为了插入内容,我们需要通过setStart()
和setEnd()
方法定义起止点。
这两个方法需要两个参数:
1. Range
起止的DOM节点
2. Range
起止的文本偏移。该偏移指选中文字第一个字符和最后一个字符在文本节点中的位置。
setStart()
的两个参数属性为startContainer
和startOffset
;setEnd()
两个参数属性为endContainer
和endOffset
。
以下面这个例子举例:
<p>男人,即使到了50岁,也千万不要碰超过26岁还没有结婚的女人。她可以是离婚,丧偶等等的,但是绝对不能是没有结婚。超过了26岁没有结婚,这种女人一般心理变态,不然就是有严重问题。市场很少犯错。即使它犯了错,那被你捡到宝的概率也很小。</p>
<p>婚姻市场未来的变化将会是很有趣的问题,而且对未来大陆经济的走势也有举足轻重的影响,对于行业的分布,经济的整体效率有决定性的影响。</p>
<ol>
<li>为什么是26这个准确的数字?</li>
<li>找骂帖</li>
<li>言论是对的,在100年前,lz穿越了而已。</li>
</ol>
此处Range
开始于第二个<p>
节点,结束与第一个<li>
节点。(通常文本节点的第一个字符的索引是0
。)
由于<p>
节点处的文字偏移值是8, <li>
节点处的偏移是5,因而有:
var startP = [the p node]; var endLi = [the second li node]; range.setStart(startP, 8); range.setEnd(endLi, 5);
读取起止选中内容
上面提到了setStart(startContainer, startOffset)
以及setEnd(endContainer, endOffset)
。考虑到实际情况,你很难准确知道用户选择的文字的起始位置,所以,上面一板一眼赋予偏移值的方法显然有很大的局限性。好在任何(看参见上面的兼容性表格)Range对象
有4个属性是用来定义选择内容起止点的,这4个属性与Selection对象相似
,但是却是不同的名称:anchorNode/anchorOffset
定义选择的起始,focusNode/focusOffset
定义结束。
因此,上面的脚本创建选区可以使用如下代码实现:
range.setStart(selectionObject.anchorNode,selectionObject.anchorOffset); range.setEnd(selectionObject.focusNode,selectionObject.focusOffset);
Safari的多虑
现在已经是2011年了,释小龙都有绯闻了,Safari 5已经出来好些日子了。所以,如果仅仅是为了兼顾低版本的Safari而去使用程序创建Range
,我觉得是一点必要都没有。尤其在我们这个神奇的国度上,首先使用Safari就少,低版本的就少之又少,Safari老早就已经支持getRangeAt()
了,Chrome浏览器也是如此。
您可以狠狠地点击这里:Safari下getRangeAt测试demo
选择demo页面中的任意一部分文字,然后点击测试按钮,在较新版本的Safari浏览器下就会出现类似下图的结果:
所以,在当前环境下,要想将Selection对象
转换成Range对象
,直接如下代码就OK了(完整版):
var userSelection, rangeObject; if (window.getSelection) { //现代浏览器 userSelection = window.getSelection(); } else if (document.selection) { //IE浏览器 考虑到Opera,应该放在后面 userSelection = document.selection.createRange(); } //Range对象 rangeObject = userSelection; if (userSelection.getRangeAt) { //现代浏览器 rangeObject = userSelection.getRangeAt(0); }
项目地址:http://code.google.com/p/rangy/
就在几天前,rangy更新到了版本1.1,作者还新更新了四五个示意的页面,展示了相关的API,方法和属性等。虽然如此,由于实例较少,还是让人很难知道此JavaScript库如何使用。这里就举几个简单的例子示意下。//zxx:此插件非压缩达115K,个人觉得有些庞大,在实际项目中的应用价值不大
示例1,获取用户选中的文字:
您可以狠狠地点击这里:rangy获取用户选中文字demo
选中部分文字点击按钮,会有如下弹出(截自Firefox3.6):
相关JavaScript代码如下:
var sel = rangy.getSelection(); alert(sel.toString());
示例2,给选中文字添加背景
您可以狠狠地点击这里:文字选中添加背景图demo
选中页面上一段文字,然后失去焦点,就会看到文字后面有了个美女背景图,如下截图,截自IE7浏览器:
完整JavaScript代码如下:
<script type="text/javascript" src="http://www.zhangxinxu.com/study/201104/rangy/rangy-core.js"></script> <script type="text/javascript" src="http://www.zhangxinxu.com/study/201104/rangy/rangy-cssclassapplier.js"></script> <script> var cssApplier; window.onload = function() { rangy.init(); cssApplier = rangy.createCssClassApplier("selectClass", true); document.body.onmouseup = function() { cssApplier.toggleSelection(); }; }; </script>
微博之插入话题
差不多去年这个时候,自己折腾过JS 文本域光标处添加文字并选中的内容,也是拿的新浪微博示例的,文章是“新浪微博插入话题后部分文字选中的js实现”,但是去年这篇文章多实现的话题插入效果是比较弱的:
1. 选中普通文字不能作为话题插入
2. 话题只能插在文本域最后二不是光标处
3. 默认文字的话题可以重复插入
所以,趁这个机会,正好把微博之插入话题这个功能完善下。
您可以狠狠地点击这里:微博插入话题的效果实现demo
欢迎输入内容,点击测试。大致会有类似下面的效果(截自Chrome):
源代码有些高度,为了节约篇幅,这里就不展示出来了,您可以在demo页面中看到完整的CSS/HTML/JS代码。不过JS部分半封装,您要是有兴趣可以在外面包裹一个函数使其插件化,我是懒得再去折腾了。
对于Range
相关的知识即使到现在都是半生不熟的,所以文章的内容更多的算是翻译性质的内容。自己并没有从深入理解的基础上很浅显地剖析相关知识点,文章很多地方会显得不怎么通俗易懂。
文中多展示的Range
等兼容性表格的数据都是N年前的,还是Safari 1.3时代的数据,老的牙都掉了,实用价值大打折扣,不过可以告知的是先前现代浏览器所不支持的个别属性现早就支持了。
跌跌撞撞,滚滚爬爬。文章难免有表述不准确的地方,欢迎指正。也欢迎提交相关的脚本的bug。