JS Range HTML文档/文字内容选中、库及应用介绍

这篇文章发布于 2011年04月14日,星期四,01:00,归类于 Web综合。 阅读 290584 次, 今日 7 次 20 条评论

 

一、前面的些话

本文的内容基本上是基于“区域范围对象(Range objects)”这个概念来说的。这个玩意,可以让你选择HTML文档的任意部分,并可以拿这些选择的信息做你想做的事情。其中,最常见的Range是用户用鼠标选择的内容(user selection)。

本文有不少篇幅就是讲如何将用户的这种选择转换为W3C RangeMicrosoft Text Range对象

二、什么是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对象,且你有必要全部理解。先展示详细的兼容性情况表:

支持:支持不支持:不支持部分支持:部分支持

1. W3C Range
W3C 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

2. Mozilla Selection
Mozilla Selection
  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。

3. Microsoft TextRange
Microsoft TextRange
  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。可读/写。

4. 总的兼容性
总的兼容性
  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对象包含用户选择的文本内容,如下操作:

console.log(userSelection.toString())

虽然格式并不是字符串,但是在现代浏览器下还是会弹出类似下面的内容:

泄漏事故等级与苏联切尔诺贝利核电站核泄漏事故等级相同。负责调查切尔诺贝

为了从Microsoft Text Range 对象上获得同样的信息,你需要使用userSelection.text。为了读取旋转的文字,可以使用类似下面代码:

var selectedText = userSelection;
if (userSelection.text) {
    selectedText = userSelection.text;
}

现在selectedText就包含了用户所选中的文字了。

我们组合一下,完整获取用户选择文本代码如下:

var userSelection, selectedText = '';
if (window.getSelection) { //现代浏览器
    userSelection = window.getSelection();
    selectedText = userSelection.toString();
} else if (document.selection) { // 老IE浏览器
    userSelection = document.selection.createRange();
    selectedText = userSelection.text;
}

您可以狠狠地点击这里:获取用户所选文字demo

例如在IE7浏览器下,选中一段文字再点击demo页面上的测试按钮,就会有类似下面的弹出内容:

获取用户选中的文字截图

input输入框IE下选择文本的获取
如果是<input>或者<textarea>输入框,在支持getSelection()方法的浏览器中,selectedText值是空字符串,需要使用其他方法获取。如下:

// input指输入框DOM元素
selectedText = input.value.slice(input.selectionStart, input.selectionEnd);

六、从Selection对象创建Range对象

在IE浏览器下,userSelectionText 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()的两个参数属性为startContainerstartOffsetsetEnd()两个参数属性为endContainerendOffset

以下面这个例子举例:

<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浏览器下就会出现类似下图的结果:

Safari下getRangeAt弹出

所以,在当前环境下,要想将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);
}

七、rangy – JavaScript Range&Selection库

项目地址:http://code.google.com/p/rangy/

就在几天前,rangy更新到了版本1.1,作者还新更新了四五个示意的页面,展示了相关的API,方法和属性等。虽然如此,由于实例较少,还是让人很难知道此JavaScript库如何使用。这里就举几个简单的例子示意下。//zxx:此插件非压缩达115K,个人觉得有些庞大,在实际项目中的应用价值不大

示例1,获取用户选中的文字:

您可以狠狠地点击这里:rangy获取用户选中文字demo

选中部分文字点击按钮,会有如下弹出(截自Firefox3.6):

rangy获取用户选中文字

相关JavaScript代码如下:

var sel = rangy.getSelection();
alert(sel.toString());

示例2,给选中文字添加背景

您可以狠狠地点击这里:文字选中添加背景图demo

选中页面上一段文字,然后失去焦点,就会看到文字后面有了个美女背景图,如下截图,截自IE7浏览器:

完整JavaScript代码如下:

<script src="//www.zhangxinxu.com/study/201104/rangy/rangy-core.js"></script>
<script src="//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。

参考文章及相关页面:
  1. Introduction to Range
  2. W3C DOM Compatibility – Range
  3. rangy – A cross-browser JavaScript range and selection library
  4. Reveal a Background Image upon Text Selection

(本篇完)

分享到:


发表评论(目前20 条评论)

  1. a说道:

    您好 我怎么找不到rangy.js架包呀

  2. cyclone77说道:

    婚姻市场未来的变化将会是很有趣的问题,而且对未来大陆经济的走势也有举足轻重的影响,对于行业的分布,经济的整体效率有决定性的影响。

    上面这段话如何选中:“婚姻市场未来的变化将会是很有趣的问题,而且对未来大陆经济的走势也有举足轻重的影响” 这段文字呢? 因为p元素里面嵌套了span,没办法偏移。

    请问您有什么思路?

    • cyclone77说道:

      #p婚姻市场未来的变化#span将会是很有趣的问题#span,而且对未来大陆经济的走势也有举足轻重的影响,对于行业的分布,经济的整体效率有决定性的影响。#p

      评论没显示html。

  3. bu说道:

    这个选取文本的 在移动端是怎么实现?这些在PC端是可以实现的,但是在移动端的话出不来

  4. isLishude说道:

    微博插入主题那个代码冗余好多。。我改了一下。
    “`js
    var oButton = document.getElementById(‘button’),
    oTextarea = document.getElementById(‘textarea’);
    var TOPIC = ‘插入话题’;

    oButton.onclick = function () {
    var topic = ‘#’ + TOPIC + ‘#’,
    value = oTextarea.value,
    index = value.indexOf(topic);
    if (!value || index === -1) {
    oTextarea.value += topic;
    }
    value = oTextarea.value;
    index = value.indexOf(topic);
    oTextarea.setSelectionRange(index + 1, index + 1 + TOPIC.length);
    oTextarea.focus();
    };
    “`

  5. 豆浆说道:

    选择在搜狗浏览器 有问题
    http://www.zhangxinxu.com/study/201104/get-selection-text.html
    这个演示地址搜狗浏览器无法获取选择的文字

  6. 小梦说道:

    如何获取选择的是图片?

  7. 梦想说道:

    在 微博插入话题的效果实现demo 这个例子里面,IE8以下,虽然能用,但是有点小瑕疵。在文本域里面,每次插入话题后,键盘上↑方向键,将光标定位到上面一行,回车,在插入话题,就不是很精准了。

  8. crazyx说道:

    移动端如何获取选中内容呢,谢谢了

  9. y说道:

    在研究怎么把这个抽象得平易近人,攻关中,谢谢了.
    顺便说下,其实用iframe做输入框就是因为window.getSelection其实是给个引用,是保存不了的,只要下一个元素有焦点马上覆盖(所有引用跟着变,就底层一点的说法是delegate),只能多一个window就是iframe.但是ie的document.selection是可以保存为对象的.用div来输入结果就是firefox等高级浏览器,反而不能做到一些事,当然用iframe就ok了,然后ie9以上都可以用标准形式了

  10. wubai说道:

    大神,希望有空讲讲 div可以编辑 contentEditable=”true” 如何获取和设置光标位置,类似于百度贴吧的回复功能

  11. Michael说道:

    如果你实在有心,还可以删除或是复制这些内容,或是用其他文字替换,甚至是简单的HTML。

    或是用其他文字替换?

    这个用法可以讲下吗?

  12. Michael说道:

    这篇文章写得很好,但我有一个问题,就是文章中没有说到把选中内容替换?

  13. emma说道:

    请教下:选中普通文字作为话题插入以后,怎么把光标focus到话题后面呢

  14. skywoo说道:

    图片那个DEMO看不了。

  15. yoyo说道:

    楼主辛苦 非常实用 的文章

  16. tcdona说道:

    看了您的文章,深深的感觉到英文资料的丰富啊,外语水平一直是我的瓶颈要加强学习饿