详解Javascript中DOM的范围
前言
为了让开发人员更方便地控制页面,DOM定义了“范围”(range)接口。通过范围可以选择文档中的一个区域,而不必考虑节点的界限(选择在后台完成,对用户是不可见的)。在常规的DOM操作不能更有效地修改文挡时,使用范围往往可以达到目的。本文将详细介绍DOM范围,下面来一起看看吧。
创建范围
Document类型中定义了createRange()
方法。在兼容DOM的浏览器中,这个方法属于document对象。使用hasFeature()
或者直接检测该方法,都可以确定浏览器是否支持范围
[注意]IE8-浏览器不支持
var supportsRange = document.implementation.hasFeature("Range", "2.0"); var alsoSupportsRange =(typeof document.createRange == "function"); |
createRange()
来创建DOM范围,如下所示
var range = document.createRange(); |
与节点类似,新创建的范围也直接与创建它的文档关联在一起,不能用于其他文档。创建了范围之后,接下来就可以使用它在后台选择文档中的特定部分。而创建范围并设置了其位置之后,还可以针对范围的内容执行很多种操作,从而实现对底层DOM树的更精细的控制
每个范围由一个Range类型的实例表示,这个实例拥有很多属性和方法。
下列属性提供了当前范围在文档中的位置信息
- startContainer:包含范围起点的节点(即选区中第一个节点的父节点)
- startoffset:范围在startContainer中起点的偏移量。如果startContainer是文本节点、注释节点或CDATA节点,那么startoffset就是范围起点之前跳过的字符数量。否则,startoffset就是范围中第一个子节点的索引
- endContainer:包含范围终点的节点(即选区中最后一个节点的父节点)
- endOffset:范围在endContainer中终点的偏移量(与startoffset遵循相同的取值规则)
- commonAncestorContainer:startContainer和endContainer共同的祖先节点在文档树中位置最深的那个
在把范围放到文档中特定的位置时,这些属性都会被赋值
简单选择
要使用范围来选择文档中的一部分,最简单的方式就是使用selectNode()
或selectNodeContents()
。这两个方法都接受一个参数,即一个DOM节点,然后使用该节点中的信息来填充范围。其中,selectNode()
方法选择整个节点,包括其子节点;而selectNodeContents()
方法则只选择节点的子节点
<!DOCTYPE html> <html> <body> <p id="p1"><b>Hello</b> world!</p> </body> </html> |
var range1 = document.createRange(); var range2 = document.createRange(); var p1 = document.getElementById("p1"); //Range {startContainer: body, startOffset: 1, endContainer: body, endOffset: 2, collapsed: false…} range1.selectNode(p1); //Range {startContainer: p#p1, startOffset: 0, endContainer: p#p1, endOffset: 2, collapsed: false…} range2.selectNodeContents(p1); |
在调用selectNode()
时,startContainer、endContainer和commonAncestorContainer都等于传入节点的父节点,也就是这个例子中的document.body
。而startoffset属性等于给定节点在其父节点的childNodes集合中的索引(在这个例子中是1——因为兼容DOM的浏览器将空格算作一个文本节点),endOffset等于startoffset加1(因为只选择了一个节点)
在调用selectNodeContents()
时,startContainer、endContainer和commonAncestorContainer等于传入的节点,即这个例子中的<p>元素。而startoffset属性始终等于0,因为范围从给定节点的第一个子节点开始。最后,endOffset等于子节点的数量(node.childNodes.length
),在这个例子中是2
此外,为了更精细地控制将哪些节点包含在范围中,还可以使用下列方法
-
setStartBefore(refNode)
:将范围的起点设置在refNode之前,因此refNode也就是范围选区中的第一个子节点。同时会将startContainer属性设置为refNode.parentNode,将startoffset属性设置为refNode在其父节点的childNodes集合中的索引 -
setStartAfter(refNode)
:将范围的起点设置在refNode之后,因此refNode也就不在范围之内了,其下一个同辈节点才是范围选区中的第一个子节点。同时会将startContainer属性设置为refNode.parentNode,将startoffset属性设置为refNode在其父节点的childNodes集合中的索引加1 -
setEndBefore(refNode)
:将范围的终点设置在refNode之前,因此refNode也就不在范围之内了,其上一个同辈节点才是范围选区中的最后一个子节点。同时会将endContainer属性设置为refNode.parentNode,将endOffset属性设置为refNode在其父节点的childNodes集合中的索引 -
setEndAfter(refNode)
:将范围的终点设置在refNode之后,因此refNode也就是范围选区中的最后一个子节点。同时会将endContainer属性设置为refNode.parentNode,将endOffset属性设置为refNode在其父节点的childNodes集合中的索引加1
调用这些方法时,所有属性会自动设置好。不过,要想创建复杂的范围选区,也可以直接指定这些属性的值
复杂选择
要创建复杂的范围就得使用setStart()
和setEnd()
方法。这两个方法都接受两个参数:一个参照节点和一个偏移量值。对setStart()
来说,参照节点会变成startContainer。而偏移量值会变成startoffset。对于setEnd()
来说,参照节点会变成endContainer,而偏移量值会变成endOffset。可以使用这两个方法来模仿selectNode()
和selectNodeContents()
。
来看下面的例子
var range1 = document.createRange(); var range2 = document.createRange(); var p1 = document.getElementById("p1"); var p1Index = -1; var i, len; for (i=0, len=p1.parentNode.childNodes.length; i < len; i++) { if (p1.parentNode.childNodes[i] == p1) { p1Index = i; break; } } range1.setStart(p1.parentNode, p1Index); range1.setEnd(p1.parentNode, p1Index + 1); range2.setStart(p1, 0); range2.setEnd(p1, p1.childNodes.length); |
显然,要选择这个节点(使用range1),就必须确定当前节点(p1)在其父节点的childNodes集合中的索引。而要选择这个节点的内容(使用range2),也不必计算什么;只要通过setStart()和setEnd()设置默认值即可。模仿selectNode()
和selectNodeContents()
并不是setStart()
和setEnd()
的主要用途,它们更胜一筹的地方在于能够选择节点的一部分
假设只想选择前面HTML示例代码中从“Hello"的"llo"到"world!"的"o"——很容易做到
第一步是取得所有节点的引用,如下所示:
var p1 = document.getElementById("p1"); var helloNode = p1.firstChild.firstChild; var worldNode = p1.lastChild; |
实际上,"Hello”文本节点是<p>元素的孙子节点,因为它本身是<b>元素的一个子节点。因此,p1.firstchild
取得的是<b>,而p1.firstchild.firstchild
取得的才是这个文本节点。"world!"文本节点是<p>元素的第二个子节点(也是最后一个子节点),因此可以使用p1.lastChild
取得该节点。
然后,必须在创建范围时指定相应的起点和终点,如下所示
var range = document.createRange(); range.setStart(helloNode, 2); range.setEnd(worldNode, 3); |
setStart()
中传入helloNode的同时,传入了偏移量2(即"e"的下一个位置;"H"的位置是0)。设置选区的终点时,在setEnd()
中传入worldNode的同时传入了偏移量3,表示选区之外的第一个字符的位置,这个字符是”r",它的位置是3(位置0上还有一个空格)。由于helloNode和worldNode都是文本节点,因此它们分别变成了新建范围的startContainer和endContainer。此时startoffset和endOffset分别用以确定两个节点所包含的文本中的位置,而不是用以确定子节点的位置(就像传入的参数为元素节点时那样)。此时的commonAncestorContainer是<p>元素,也就是同时包含这两个节点的第一个祖先元素
当然,仅仅是选择了文档中的某一部分用处并不大。但重要的是,选择之后才可以对选区进行操作
操作范围内容
在创建范围时,内部会为这个范围创建一个文档片段,范围所属的全部节点都被添加到了这个文档片段中。为了创建这个文档片段,范围内容的格式必须正确有效。在前面的例子中,创建的选区分别开始和结束于两个文本节点的内部,因此不能算是格式良好的DOM结构,也就无法通过DOM来表示。但是,范围知道自身缺少哪些开标签和闭标签,它能够重新构建有效的DOM结构以便对其进行操作
对于前面的例子而言,范围经过计算知道选区中缺少一个开始的<b>标签,因此就会在后台动态加入一个该标签,同时还会在前面加入一个表示结束的</b>标签以结束"He"。于是,修改后的DOM就变成了如下所示
<p><b>He</b><b>llo</b> world!</p> |
像这样创建了范围之后,就可以使用各种方法对范围的内容进行操作了
[注意]表示范围的内部文档片段中的所有节点,都只是指向文档中相应节点的指针
【deleteContents()】
操作范围内容的第一个方法是deleteContents()
,这个方法能够从文档中删除范围所包含的内容
var p1 = document.getElementById("p1"); var helloNode = p1.firstChild.firstChild; var worldNode = p1.lastChild; var range = document.createRange(); range.setStart(helloNode, 2); range.setEnd(worldNode, 3); range.deleteContents(); |
<p><b>He</b>rld!</p> |
由于范围选区在修改底层DOM结构时能够保证格式良好,因此即使内容被删除了,最终的DOM结构依旧是格式良好的
【extractContents()】
与deleteContents()
方法相似,extractContents()
方法也会从文档中移除范围选区。但区别在于,extractContents()
会返回范围的文档片段。利用这个返回的值,可以将范围的内容插入到文档中的其他地方。如下所示:
var p1 = document.getElementById("p1"); var helloNode = p1.firstChild.firstChild; var worldNode = p1.lastChild; var range = document.createRange(); range.setStart(helloNode, 2); range.setEnd(worldNode, 3); var fragment = range.extractContents(); p1.parentNode.appendChild(fragment); |
在这个例子中,将提取出来的文档片段添加到了文档<body>元素的末尾
[注意]在将文档片段传入appendChild()方法中时,添加到文档中的只是片段的子节点,而非片段本身
<p><b>He</b>rld!</p> <b>llo</b> wo |
【cloneContents】
还有一种做法是使用cloneContents()
创建范围对象的一个副本,然后在文档其他地方插入该副本
var p1 = document.getElementById("p1"); var helloNode = p1.firstChild.firstChild; var worldNode = p1.lastChild; var range = document.createRange(); range.setStart(helloNode, 2); range.setEnd(worldNode, 3); var fragment = range.cloneContents(); p1.parentNode.appendChild(fragment); |
extractContents()
非常类似,因为它们都返回文档片段。它们的主要区别在于,cloneContents()
返回的文档片段包含的是范围中节点的副本,而不是实际的节点。执行上面的操作后,页面中的HTML代码如下所示:
<p><b>Hello</b> world!</p> <b>llo</b> wo |
[注意]在调用cloneContents()方法之前,拆分的节点并不会产生格式良好的文档片段。换句话说,原始的HTML在DOM被修改之前会始终保持不变
插入范围内容
利用范围,可以删除或复制内容,还可以像前面介绍的那样操作范围中的内容。使用insertNode()
方法可以向范围选区的开始处插入一个节点。假设在前面例子中的HTML前面插入以下HTML代码
<span style="color: red">Inserted text</span> |
var p1 = document.getElementById("p1"); var helloNode = p1.firstChild.firstChild; var worldNode = p1.lastChild; var range = document.createRange(); var span = document.createElement("span"); range.setStart(helloNode, 2); range.setEnd(worldNode, 3); span.style.color = "red"; span.appendChild(document.createTextNode("Inserted text")); range.insertNode(span); |
<p id="p1"><b>He<span styie="color:red">Inserted text</span>llo</b> world</p> |
[注意]<span>正好被插入到了"Hello"中的"llo"前面,而该位置就是范围选区的开始位置。使用这种技术可以插入一些帮助提示信息,例如在打开新窗口的链接旁边插入一幅图像
【surroundContents()】
除了向范围内部插入内容之外,还可以环绕范围插入内容,此时就要使用surroundContents()
方法。这个方法接受一个参数,即环绕范围内容的节点。在环绕范围插入内容时,后台会执行下列步骤
1、提取出范围中的内容(类似执行extractContents()
)
2、将给定节点插入到文档中原来范围所在的位置上
3、将文档片段的内容添加到给定节点中
可以使用这种技术来突出显示网页中的某些词句,例如下列代码
var p1 = document.getElementById("p1"); var helloNode = p1.firstChild.firstChild; var worldNode = p1.lastChild; var range = document.createRange(); range.selectNode(helloNode); var span = document.createElement("span"); span.style.backgroundColor = "yellow"; range.surroundContents(span); |
<p><b><span style="background-color:yellow">Hello</b> world!</p> |
为了插入<span>,范围必须包含整个DOM选区,而不能仅仅包含选中的DOM节点
折叠范围
所谓折叠范围,就是指范围中未选择文档的任何部分。可以用文本框来描述折叠范围的过程。假设文本框中有一行文本,用鼠标选择了其中一个完整的单词。然后,单击鼠标左键,选区消失,而光标则落在了其中两个字母之间。同样,在折叠范围时,其位置会落在文档中的两个部分之间,可能是范围选区的开始位置,也可能是结束位置
【collapse()】
使用collapse()
方法来折叠范围,这个方法接受一个参数,该参数是一个布尔值,表示要折叠到范围的哪一端。参数true表示折叠到范围的起点,参数false表示折叠到范围的终点。要确定范围已经折叠完毕,可以检査collapsed属性,如下所示:
range.collapse(true);//折叠到起点 console.log(range.collapsed);//输出true |
<p id="p1">Paragraph 1</p><p id="p2">Paragraph 2</p> |
var p1 = document.getElementById("p1"); var p2 = document.getElementById("p2"); var range = document.createRange(); range.setStartAfter(p1); range.setStartBefore(p2); console.log(range.collapsed);//输出true |
在这个例子中,新创建的范围是折叠的,因为p1的后面和p2的前面什么也没有
比较范围
【compareBoundaryPoints()】
在有多个范围的情况下,可以使用compareBoundaryPoints()
方法来确定这些范围是否有公共的边界(起点或终点)。这个方法接受两个参数:表示比较方式的常量值和要比较的范围。表示比较方式的常量值如下所示
-
Range.START_TO_START(0)
:比较第一个范围和第二个范围的起点 -
Range.START_TO_END(1)
:比较第一个范围的起点和第二个范围的终点 -
Range.END_TO_END⑵
:比较第一个范围和第二个范围的终点 -
Range.END_T0_START(3)
:比较第一个范围的终点和第一个范围的起点
compareBoundaryPoints()
方法可能的返回值如下:如果第一个范围中的点位于第二个范围中的点之前,返回-1;如果两个点相等,返回0;如果第一个范围中的点位于第二个范围中的点之后,返回1。
来看下面的例子
var range1 = document.createRange(); var range2 = document.createRange(); var p1 = document.getElementById("p1"); range1.selectNodeContents(p1); range2.selectNodeContents(p1); range2.setEndBefore(p1.lastChild); alert(range1.compareBoundaryPoints(Range.START_TO_START, range2)); //outputs 0 alert(range1.compareBoundaryPoints(Range.END_TO_END, range2)); //outputs 1 |
在这个例子中,两个范围的起点实际上是相同的,因为它们的起点都是由selectNodeContents()
方法设置的默认值来指定的。因此,第一次比较返回0。但是,range2的终点由于调用setEndBefore()
已经改变了,结果是range1的终点位于range2的终点后面,因此第二次比较返回1
复制范围
【cloneRange】
可以使用cloneRange()
方法复制范围。这个方法会创建调用它的范围的一个副本
var newRange = range.cloneRange(); |
新创建的范围与原来的范围包含相同的属性,而修改它的端点不会影响原来的范围
清理范围
【detach()】
在使用完范围之后,最好是调用detach()
方法,以便从创建范围的文档中分离出该范围。调用detach()
之后,就可以放心地解除对范围的引用,从而让垃圾回收机制回收其内存了。来看下面的例子
range.detach();//从文档中分离 range = null;//解除引用 |
在使用范围的最后再执行这两个步骤是推荐的方式。一且分离范围,就不能再恢复使用了
IE兼容
IE8-浏览器不支持DOM范围。但支持一种类似的概念,即文本范围(textrange)。文本范围是IE专有的特性,其他浏览器都不支持。顾名思义,文本范围处理的主要是文本(不一定是DOM节点)。通过<body>、<button>、<input>和<textarea>等这几个元素,可以调用createTextRange()
方法来创建文本范围。以下是一个例子
var range = document.body.createTextRange(); |
像这样通过document创建的范围可以在页面中的任何地方使用(通过其他元素创建的范围则只能在相应的元素中使用)。与DOM范围类似,使用IE文本范围的方式也有很多种
一、简单选择
【findText()】
选择页面中某一区域的最简单方式,就是使用范围的findText()
方法。这个方法会找到第一次出现的给定文本,并将范围移过来以环绕该文本。如果没有找到文本,这个方法返回false;否则返回true。同样,仍然以下面的HTML代码为例
<p id="p1"><b>Hello</b> world!</p> |
var range = document.body.createTextRange(); var found = range.findText("Hello"); |
执行完第二行代码后,文本"Hello"就被包围在范围之内了。为此,可以检査范围的text属性来确认(这个属性返回范围中包含的文本),或者也可以检查findText()
的返回值——在找到了文本的情况下返回值为true。
例如:
alert(found); //true alert(range.text); //"Hello" |
findText()
传入另一个参数,即一个表示向哪个方向继续搜索的数值。负值表示应该从当前位置向后搜索,而正值表示应该从当前位置向前搜索。因此,要査找文档中前两个"Hello"的实例,应该使用下列代码
var found = range.findText("Hello"); var foundAgain = range.findText("Hello", 1); |
【moveToElementText()】
IE中与DOM中的selectNode()方法最接近的方法是moveToElementText()
,这个方法接受一个DOM元素,并选择该元素的所有文本,包括HTML标签。下面是一个例子
var range = document.body.createTextRange(); var p1 = document.getElementById("p1") range.moveToElementText(p1); |
alert(range.htmlText); |
parentElement()
方法倒是与DOM的 commonAncestorContainer属性类似
var ancestor = range.parentElement(); |