遍历:“DOM2 级遍历和范围”模块定义了两个用于辅助完成顺序遍历DOM 结构的类型:NodeIterator和TreeWalker。这两个类型能够基于给定的起点对DOM 结构执行深度优先(depth-first)的遍历操作。在与DOM 兼容的浏览器中(Firefox 1 及更高版本、Safari 1.3 及更高版本、Opera 7.6 及更高版本、Chrome0.2 及更高版本),都可以访问到这些类型的对象。IE 不支持DOM 遍历。使用下列代码可以检测浏览器对DOM2 级遍历能力的支持情况。
var supportsTraversals = document.implementation.hasFeature("Traversal", "2.0");
var supportsNodeIterator = (typeof document.createNodeIterator == "function");
var supportsTreeWalker = (typeof document.createTreeWalker == "function");
如前所述,DOM 遍历是深度优先的DOM 结构遍历,也就是说,移动的方向至少有两个(取决于使用的遍历类型)。遍历以给定节点为根,不可能向上超出DOM 树的根节点。以下面的HTML 页面为例。
<!DOCTYPE html>
<html>
<head>
<title>Example</title>
</head>
<body>
<p><b>Hello</b> world!</p>
</body>
</html>
任何节点都可以作为遍历的根节点。如果假设<body>元素为根节点,那么遍历的第一步就是访问<p>元素,然后再访问同为<body>元素后代的两个文本节点。不过,这次遍历永远不会到达<html>、<head>元素,也不会到达不属于<body>元素子树的任何节点。而以document 为根节点的遍历则可以访问到文档中的全部节点。图12-5 展示了对以document 为根节点的DOM树进行深度优先遍历的先后顺序。
从document 开始依序向前,访问的第一个节点是document,访问的最后一个节点是包含"world!"的文本节点。从文档最后的文本节点开始,遍历可以反向移动到DOM 树的顶端。此时,访问的第一个节点是包含"Hello"的文本节点,访问的最后一个节点是document 节点。NodeIterator和TreeWalker 都以这种方式执行遍历。
- NodeIterator:NodeIterator 类型是两者中比较简单的一个,可以使用document.createNodeIterator()方法创建它的新实例。这个方法接受下列4 个参数。
root | 想要作为搜索起点的树中的节点。 |
whatToShow | 表示要访问哪些节点的数字代码。 |
filter | 是一个NodeFilter 对象,或者一个表示应该接受还是拒绝某种特定节点的函数。 |
entityReferenceExpansion | 布尔值,表示是否要扩展实体引用。这个参数在HTML 页面中没有用,因为其中的实体引用不能扩展。 |
whatToShow 参数是一个位掩码,通过应用一或多个过滤器(filter)来确定要访问哪些节点。这个参数的值以常量形式在NodeFilter 类型中定义,如下所示。
NodeFilter.SHOW_ALL | 显示所有类型的节点。 |
NodeFilter.SHOW_ELEMENT | 显示元素节点。 |
NodeFilter.SHOW_ATTRIBUTE | 显示特性节点。由于DOM结构原因,实际上不能使用这个值。 |
NodeFilter.SHOW_TEXT | 显示文本节点。 |
NodeFilter.SHOW_CDATA_SECTION | 显示CDATA 节点。对HTML 页面没有用。 |
NodeFilter.SHOW_ENTITY_REFERENCE | 显示实体引用节点。对HTML 页面没有用。 |
NodeFilter.SHOW_ENTITYE | 显示实体节点。对HTML 页面没有用。 |
NodeFilter.SHOW_PROCESSING_INSTRUCTION | 显示处理指令节点。对HTML 页面没有用。 |
NodeFilter.SHOW_COMMENT | 显示注释节点。 |
NodeFilter.SHOW_DOCUMENT | 显示文档节点。 |
NodeFilter.SHOW_DOCUMENT_TYPE | 显示文档类型节点。 |
NodeFilter.SHOW_DOCUMENT_FRAGMENT | 显示文档片段节点。对HTML 页面没有用。 |
NodeFilter.SHOW_NOTATION | 显示符号节点。对HTML 页面没有用。 |
除了NodeFilter.SHOW_ALL 之外,可以使用按位或操作符来组合多个选项,如下面的例子所示:
var whatToShow = NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT;
可以通过createNodeIterator()方法的filter 参数来指定自定义的NodeFilter 对象,或者指定一个功能类似节点过滤器(node filter)的函数。每个NodeFilter 对象只有一个方法,即acceptNode();如果应该访问给定的节点,该方法返回NodeFilter.FILTER_ACCEPT,如果不应该访问给定的节点,该方法返回NodeFilter.FILTER_SKIP。由于NodeFilter 是一个抽象的类型,因此不能直接创建它的实例。在必要时,只要创建一个包含acceptNode()方法的对象,然后将这个对象传入createNodeIterator()中即可。例如,下列代码展示了如何创建一个只显示<p>元素的节点迭代器。
var filter = {
acceptNode: function(node){
return node.tagName.toLowerCase() == "p" ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_SKIP;
}
};
var iterator = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT, filter, false);
第三个参数也可以是一个与acceptNode()方法类似的函数,如下所示。
var filter = function(node){
return node.tagName.toLowerCase() == "p" ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_SKIP;
};
var iterator = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT, filter, false);
一般来说,这就是在JavaScript 中使用这个方法的形式,这种形式比较简单,而且也跟其他的JavaScript 代码很相似。如果不指定过滤器,那么应该在第三个参数的位置上传入null。
下面的代码创建了一个能够访问所有类型节点的简单的NodeIterator。
var iterator = document.createNodeIterator(document, NodeFilter.SHOW_ALL, null, false);
NodeIterator 类型的两个主要方法是nextNode()和previousNode()。顾名思义,在深度优先的DOM 子树遍历中,nextNode()方法用于向前前进一步,而previousNode()用于向后后退一步。在刚刚创建的NodeIterator 对象中,有一个内部指针指向根节点,因此第一次调用nextNode()会返回根节点。当遍历到DOM 子树的最后一个节点时,nextNode()返回null。previousNode()方法的工作机制类似。当遍历到DOM 子树的最后一个节点,且previousNode()返回根节点之后,再次调用它就会返回null。
以下面的HTML 片段为例。
<div id="div1">
<p><b>Hello</b> world!</p>
<ul>
<li>List item 1</li>
<li>List item 2</li>
<li>List item 3</li>
</ul>
</div>
假设我们想要遍历<div>元素中的所有元素,那么可以使用下列代码。
var div = document.getElementById("div1");
var iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, null, false);
var node = iterator.nextNode();
while (node !== null) {
alert(node.tagName); //输出标签名
node = iterator.nextNode();
}
//在这个例子中,第一次调用nextNode()返回<p>元素。因为在到达DOM子树末端时nextNode()返回null,
//所以这里使用了while 语句在每次循环时检查对nextNode()的调用是否返回了null。
//执行上面的代码会显示如下标签名:
DIV
P
B
UL
LI
LI
LI
也许用不着显示那么多信息,你只想返回遍历中遇到的<li>元素。很简单,只要使用一个过滤器即可,如下面的例子所示。
var div = document.getElementById("div1");
var filter = function(node){
return node.tagName.toLowerCase() == "li" ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_SKIP;
};
var iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, filter, false);
var node = iterator.nextNode();
while (node !== null) {
alert(node.tagName); //输出标签名
node = iterator.nextNode();
}
在这个例子中,迭代器只会返回<li>元素。
由于nextNode()和previousNode()方法都基于NodeIterator 在DOM 结构中的内部指针工作,所以DOM 结构的变化会反映在遍历的结果中。
Firefox 3.5 之前的版本没有实现createNodeIterator()方法,但却支持下一节要讨论的createTreeWalker()方法。
-
TreeWalker:TreeWalker 是NodeIterator 的一个更高级的版本。除了包括nextNode()和previousNode()在内的相同的功能之外,这个类型还提供了下列用于在不同方向上遍历DOM 结构的方法。
parentNode() | 遍历到当前节点的父节点; |
firstChild() | 遍历到当前节点的第一个子节点; |
lastChild() | 遍历到当前节点的最后一个子节点; |
nextSibling() | 遍历到当前节点的下一个同辈节点; |
previousSibling() | 遍历到当前节点的上一个同辈节点。 |
创建TreeWalker 对象要使用document.createTreeWalker()方法,这个方法接受的4 个参数与document.createNodeIterator()方法相同:作为遍历起点的根节点、要显示的节点类型、过滤器和一个表示是否扩展实体引用的布尔值。由于这两个创建方法很相似,所以很容易用TreeWalker来代替NodeIterator,如下面的例子所示。
var div = document.getElementById("div1");
var filter = function(node){
return node.tagName.toLowerCase() == "li"?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_SKIP;
};
var walker= document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT, filter, false);
var node = iterator.nextNode();
while (node !== null) {
alert(node.tagName); //输出标签名
node = iterator.nextNode();
}
在这里,filter 可以返回的值有所不同。除了NodeFilter.FILTER_ACCEPT 和NodeFilter.FILTER_SKIP 之外,还可以使用NodeFilter.FILTER_REJECT。在使用NodeIterator 对象时,NodeFilter.FILTER_SKIP 与NodeFilter.FILTER_REJECT 的作用相同:跳过指定的节点。但在使用TreeWalker 对象时,NodeFilter.FILTER_SKIP 会跳过相应节点继续前进到子树中的下一个节点,而NodeFilter.FILTER_REJECT 则会跳过相应节点及该节点的整个子树。例如,将前面例子中的NodeFilter.FILTER_SKIP 修改成NodeFilter.FILTER_REJECT,结果就是不会访问任何节点。这是因为第一个返回的节点是<div>,它的标签名不是"li",于是就会返回NodeFilter.FILTER_REJECT,这意味着遍历会跳过整个子树。在这个例子中,<div>元素是遍历的根节点,于是结果就会停止遍历。
当然,TreeWalker 真正强大的地方在于能够在DOM结构中沿任何方向移动。使用TreeWalker遍历DOM 树,即使不定义过滤器,也可以取得所有<li>元素,如下面的代码所示。
var div = document.getElementById("div1");
var walker = document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT, null, false);
walker.firstChild(); //转到<p>
walker.nextSibling(); //转到<ul>
var node = walker.firstChild(); //转到第一个<li>
while (node !== null) {
alert(node.tagName);
node = walker.nextSibling();
}
因为我们知道<li>元素在文档结构中的位置,所以可以直接定位到那里,即使用firstChild()转到<p>元素,使用nextSibling()转到<ul>元素,然后再使用firstChild()转到第一个<li>元素。注意,此处TreeWalker 只返回元素(由传入到createTreeWalker()的第二个参数决定)。因此,可以放心地使用nextSibling()访问每一个<li>元素,直至这个方法最后返回null。
TreeWalker 类型还有一个属性,名叫currentNode,表示任何遍历方法在上一次遍历中返回的节点。通过设置这个属性也可以修改遍历继续进行的起点,如下面的例子所示。
var node = walker.nextNode();
alert(node === walker.currentNode); //true
walker.currentNode = document.body; //修改起点
与NodeIterator 相比,TreeWalker 类型在遍历DOM时拥有更大的灵活性。由于IE 中没有对应的类型和方法,所以使用遍历的跨浏览器解决方案非常少见。
范围:
- DOM中的范围:为了让开发人员更方便地控制页面,“DOM2 级遍历和范围”模块定义了“范围”(range)接口。通过范围可以选择文档中的一个区域,而不必考虑节点的界限(选择在后台完成,对用户是不可见的)。在常规的DOM 操作不能更有效地修改文档时,使用范围往往可以达到目的。Firefox、Opera、Safari 和Chrome 都支持DOM 范围。IE 以专有方式实现了自己的范围特性。
DOM2 级在Document 类型中定义了createRange()方法。在兼容DOM 的浏览器中,这个方法属于document 对象。使用hasFeature()或者直接检测该方法,都可以确定浏览器是否支持范围。
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 共同的祖先节点在文档树中位置最深的那个。 |
在把范围放到文档中特定的位置时,这些属性都会被赋值。
1. 用DOM 范围实现简单选择:要使用范围来选择文档中的一部分,最简的方式就是使用selectNode()或selectNodeContents()。这两个方法都接受一个参数,即一个DOM 节点,然后使用该节点中的信息来填充范围。其中,selectNode()方法选择整个节点,包括其子节点;而selectNodeContents()方法则只选择节点的子节点。以下面的HTML 代码为例。
<!DOCTYPE html>
<html>
<body>
<p id="p1"><b>Hello</b> world!</p>
</body>
</html>
//我们可以使用下列代码来创建范围:
var range1 = document.createRange();
range2 = document.createRange();
p1 = document.getElementById("p1");
range1.selectNode(p1);
range2.selectNodeContents(p1);
这里创建的两个范围包含文档中不同的部分:rang1 包含<p/>元素及其所有子元素,而rang2 包含<b/>元素、文本节点"Hello"和文本节点"world!"。
在调用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。 |
在调用这些方法时,所有属性都会自动为你设置好。不过,要想创建复杂的范围选区,也可以直接指定这些属性的值。
2. 用DOM 范围实现复杂选择:要创建复杂的范围就得使用setStart()和setEnd()方法。这两个方法都接受两个参数:一个参照节点和一个偏移量值。对setStart()来说,参照节点会变成startContainer,而偏移量值会变成startOffset。对于setEnd()来说,参照节点会变成endContainer,而偏移量值会变成endOffset。
可以使用这两个方法来模仿selectNode()和selectNodeContents()。来看下面的例子:
var range1 = document.createRange();
range2 = document.createRange();
p1 = document.getElementById("p1");
p1Index = -1;
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");
helloNode = p1.firstChild.firstChild;
worldNode = p1.lastChild;
实际上,"Hello"文本节点是<p>元素的孙子节点,因为它本身是<b>元素的一个子节点。因此,p1.firstChild 取得的是<b>,而p1.firstChild.firstChild 取得的才是这个文本节点。"world!"文本节点是<p>元素的第二个子节点(也是最后一个子节点),因此可以使用p1.lastChild 取得该节点。然后,必须在创建范围时指定相应的起点和终点,如下面的例子所示。
var range = document.createRange();
range.setStart(helloNode, 2);
rang e.setEnd(worldNode, 3);
因为这个范围的选区应该从"Hello"中"e"的后面开始,所以在setStart()中传入helloNode的同时,传入了偏移量2(即"e"的下一个位置;"H"的位置是0)。设置选区的终点时,在setEnd()中传入worldNode 的同时传入了偏移量3,表示选区之外的第一个字符的位置,这个字符是"r",它的位置是3(位置0 上还有一个空格)。
由于helloNode 和worldNode 都是文本节点,因此它们分别变成了新建范围的startContainer和endContainer。此时startOffset 和endOffset 分别用以确定两个节点所包含的文本中的位置,而不是用以确定子节点的位置(就像传入的参数为元素节点时那样)。此时的commonAncestorContainer 是<p>元素,也就是同时包含这两个节点的第一个祖先元素。
当然,仅仅是选择了文档中的某一部分用处并不大。但重要的是,选择之后才可以对选区进行操作。
3. 操作DOM 范围中的内容:在创建范围时 ,内部会为这个范围创建一个文档片段,范围所属的全部节点都被添加到了这个文档片段中。为了创建这个文档片段,范围内容的格式必须正确有效。在前面的例子中,我们创建的选区分别开始和结束于两个文本节点的内部,因此不能算是格式良好的DOM结构,也就无法通过DOM来表示。但是,范围知道自身缺少哪些开标签和闭标签,它能够重新构建有效的DOM结构以便我们对其进行操作。
对于前面的例子而言,范围经过计算知道选区中缺少一个开始的<b>标签,因此就会在后台动态加入一个该标签,同时还会在前面加入一个表示结束的</b>标签以结束"He"。于是,修改后的DOM 就变成了如下所示。
<p><b>He</b><b>llo</b> world!</p>
另外,文本节点"world!"也被拆分为两个文本节点,一个包含"wo",另一个包含"rld!"。
像这样创建了范围之后,就可以使用各种方法对范围的内容进行操作了(注意,表示范围的内部文档片段中的所有节点,都只是指向文档中相应节点的指针)。
第一个方法,也是最容易理解的方法,就是deleteContents()。这个方法能够从文档中删除范围所包含的内容。
var p1 = document.getElementById("p1");
helloNode = p1.firstChild.firstChild;
worldNode = p1.lastChild;
range = document.createRange();
range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);
range.deleteContents();
执行以上代码后,页面中会显示如下HTML 代码:
<p><b>He</b>rld!</p>
由于范围选区在修改底层DOM 结构时能够保证格式良好,因此即使内容被删除了,最终的DOM结构依旧是格式良好的。
与deleteContents()方法相似,extractContents()也会从文档中移除范围选区。但这两个方法的区别在于,extractContents()会返回范围的文档片段。利用这个返回的值,可以将范围的内容插入到文档中的其他地方。如下面的例子所示:
与deleteContents()方法相似,extractContents()也会从文档中移除范围选区。但这两个方法的区别在于,extractContents()会返回范围的文档片段。利用这个返回的值,可以将范围的内容插入到文档中的其他地方。如下面的例子所示:
var p1 = document.getElementById("p1");
helloNode = p1.firstChild.firstChild;
worldNode = p1.lastChild;
range = document.createRange();
range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);
var fragment = range.extractContents();
p1.parentNode.appendChild(fragment);
在这个例子中,我们将提取出来的文档片段添加到了文档<body>元素的末尾。(记住,在将文档片段传入appendChild()方法中时,添加到文档中的只是片段的子节点,而非片段本身。)结果得到如下HTML 代码:
<p><b>He</b>rld!</p>
<b>llo</b> wo
还一种做法,即使用cloneContents()创建范围对象的一个副本,然后在文档的其他地方插入该副本。如下面的例子所示:
var p1 = document.getElementById("p1"),
helloNode = p1.firstChild.firstChild,
worldNode = p1.lastChild,
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
有一点请读者注意,那就是在调用上面介绍的方法之前,拆分的节点并不会产生格式良好的文档片段。换句话说,原始的HTML 在DOM 被修改之前会始终保持不变。
4. 插入DOM 范围中的内容:利用范围,可以删除或复制内容,还可以像前面介绍的那样操作范围中的内容。使用insertNode()方法可以向范围选区的开始处插入一个节点。假设我们想在前面例子中的HTML 前面插入以下HTML代码:
<span style="color: red">Inserted text</span>
那么,就可以使用下列代码:
var p1 = document.getElementById("p1");
helloNode = p1.firstChild.firstChild;
worldNode = p1.lastChild;
range = document.createRange();
range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);
var span = document.createElement("span");
span.style.color = "red";
span.appendChild(document.createTextNode("Inserted text"));
range.insertNode(span);
运行以上JavaScript 代码,就会得到如下HTML 代码:
<p id="p1"><b>He<span style="color: red">Inserted text</span>llo</b> world</p>
注意,<span>正好被插入到了"Hello"中的"llo"前面,而该位置就是范围选区的开始位置。还要注意的是,由于这里没有使用上一节介绍的方法,结果原始的HTML 并没有添加或删除<b>元素。使用这种技术可以插入一些帮助提示信息,例如在打开新窗口的链接旁边插入一幅图像。
除了向范围内部插入内容之外,还可以环绕范围插入内容,此时就要使用surroundContents()方法。这个方法接受一个参数,即环绕范围内容的节点。在环绕范围插入内容时,后台会执行下列步骤。
- (1) 提取出范围中的内容(类似执行extractContent());
- (2) 将给定节点插入到文档中原来范围所在的位置上;
- (3) 将文档片段的内容添加到给定节点中。
可以使用这种技术来突出显示网页中的某些词句,例如下列代码:
var p1 = document.getElementById("p1");
helloNode = p1.firstChild.firstChild;
worldNode = p1.lastChild;
range = document.createRange();
range.selectNode(helloNode);
var span = document.createElement("span");
span.style.backgroundColor = "yellow";
range.surroundContents(span);
会给范围选区加上一个黄色的背景。得到的HTML 代码如下所示:
<p><b><span style="background-color:yellow">Hello</span></b> world!</p>
为了插入<span>,范围必须包含整个DOM 选区(不能仅仅包含选中的DOM 节点)。
5. 折叠DOM 范围:所谓折叠范围,就是指范围中未选择文档的任何部分。可以用文本框来描述折叠范围的过程。假设文本框中有一行文本,你用鼠标选择了其中一个完整的单词。然后,你单击鼠标左键,选区消失,而光标则落在了其中两个字母之间。同样,在折叠范围时,其位置会落在文档中的两个部分之间,可能是范围选区的开始位置,也可能是结束位置。图12-9 展示了折叠范围时发生的情形。
使用collapse()方法来折叠范围,这个方法接受一个参数,一个布尔值,表示要折叠到范围的哪一端。参数true 表示折叠到范围的起点,参数false 表示折叠到范围的终点。要确定范围已经折叠完毕,可以检查collapsed 属性,如下所示:
range.collapse(true); //折叠到起点
alert(range.collapsed); //输出true
检测某个范围是否处于折叠状态,可以帮我们确定范围中的两个节点是否紧密相邻。例如,对于下面的HTML 代码:
<p id="p1">Paragraph 1</p><p id="p2">Paragraph 2</p>
如果我们不知道其实际构成(比如说,这行代码是动态生成的),那么可以像下面这样创建一个范围。
var p1 = document.getElementById("p1"),
p2 = document.getElementById("p2"),
range = document.createRange();
range.setStartAfter(p1);
range.setStartBefore(p2);
alert(range.collapsed); //输出true
在这个例子中,新创建的范围是折叠的,因为p1 的后面和p2 的前面什么也没有。
6. 比较DOM 范围:在有多个范围的情况下,可以使用compareBoundaryPoints()方法来确定这些范围是否有公共的边界(起点或终点)。这个方法接受两个参数:表示比较方式的常量值和要比较的范围。表示比较方式的常量值如下所示。
- Range.START_TO_START(0):比较第一个范围和第二个范围的起点;
- Range.START_TO_END(1):比较第一个范围的起点和第二个范围的终点;
- Range.END_TO_END(2):比较第一个范围和第二个范围的终点;
- Range.END_TO_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)); //0
alert(range1.compareBoundaryPoints(Range.END_TO_END, range2)); //1
在这个例子中,两个范围的起点实际上是相同的,因为它们的起点都是由selectNodeContents()方法设置的默认值来指定的。因此,第一次比较返回0。但是,range2 的终点由于调用setEndBefore()已经改变了,结果是range1 的终点位于range2 的终点后面(见图12-10),因此第二次比较返回1。
7. 复制DOM 范围:可以使用cloneRange()方法复制范围。这个方法会创建调用它的范围的一个副本。
var newRange = range.cloneRange();
新创建的范围与原来的范围包含相同的属性,而修改它的端点不会影响原来的范围。
8. 清理DOM 范围:
在使用完范围之后,最好是调用detach()方法,以便从创建范围的文档中分离出该范围。调用detach()之后,就可以放心地解除对范围的引用,从而让垃圾回收机制回收其内存了。来看下面的例子。
range.detach(); //从文档中分离
range = null; //解除引用
在使用范围的最后再执行这两个步骤是我们推荐的方式。一旦分离范围,就不能再恢复使用了。
- IE8 及更早版本中的范围:虽然IE9 支持DOM 范围,但IE8 及之前版本不支持DOM范围。不过,IE8 及早期版本支持一种类似的概念,即文本范围(text range)。文本范围是IE 专有的特性,其他浏览器都不支持。顾名思义,文本范围处理的主要是文本(不一定是DOM 节点)。通过<body>、<button>、<input>和<textarea>等这几个元素,可以调用createTextRange()方法来创建文本范围。以下是一个例子:
var range = document.body.createTextRange();
像这样通过document 创建的范围可以在页面中的任何地方使用(通过其他元素创建的范围则只能在相应的元素中使用)。与DOM 范围类似,使用IE 文本范围的方式也有很多种。
1. 用IE 范围实现简单的选择:选择页面中某一区域的最简单方式,就是使用范围的findText()方法。这个方法会找到第一次出现的给定文本,并将范围移过来以环绕该文本。如果没有找到文本,这个方法返回false;否则返回true。同样,仍然以下面的HTML 代码为例。
<p id="p1"><b>Hello</b> world!</p>
要选择"Hello",可以使用下列代码。
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);
IE 中与DOM 中的selectNode()方法最接近的方法是moveToElementText(),这个方法接受一个DOM 元素,并选择该元素的所有文本,包括HTML 标签。下面是一个例子。
var range = document.body.createTextRange();
var p1 = document.getElementById("p1");
range.moveToElementText(p1);
在文本范围中包含HTML 的情况下,可以使用htmlText 属性取得范围的全部内容,包括HTML和文本,如下面的例子所示。
alert(range.htmlText);
IE 的范围没有任何属性可以随着范围选区的变化而动态更新。不过,其parentElement()方法倒是与DOM 的commonAncestorContainer 属性类似。
var ancestor = range.parentElement();
这样得到的父元素始终都可以反映文本选区的父节点。
2. 使用IE 范围实现复杂的选择:在IE 中创建复杂范围的方法,就是以特定的增量向四周移动范围。为此,IE 提供了4 个方法:move()、moveStart()、moveEnd()和expand()。这些方法都接受两个参数:移动单位和移动单位的数量。其中,移动单位是下列一种字符串值。
- "character":逐个字符地移动。
- "word":逐个单词(一系列非空格字符)地移动。
- "sentence":逐个句子(一系列以句号、问号或叹号结尾的字符)地移动。
- "textedit":移动到当前范围选区的开始或结束位置。
通过moveStart()方法可以移动范围的起点,通过moveEnd()方法可以移动范围的终点,移动的幅度由单位数量指定,如下面的例子所示。
range.moveStart("word", 2); //起点移动2 个单词
range.moveEnd("character", 1); //终点移动1 个字符
使用expand()方法可以将范围规范化。换句话说,expand()方法的作用是将任何部分选择的文本全部选中。例如,当前选择的是一个单词中间的两个字符,调用expand("word")可以将整个单词都包含在范围之内。
而move()方法则首先会折叠当前范围(让起点和终点相等),然后再将范围移动指定的单位数量,如下面的例子所示。
range.move("character", 5); //移动5 个字符
调用move()之后,范围的起点和终点相同,因此必须再使用moveStart()或moveEnd()创建新的选区。
3. 操作IE 范围中的内容:在IE 中操作范围中的内容可以使用text 属性或pasteHTML()方法。如前所述,通过text 属性可以取得范围中的内容文本;但是,也可以通过这个属性设置范围中的内容文本。来看一个例子。
var range = document.body.createTextRange();
range.findText("Hello");
range.text = "Howdy";
如果仍以前面的Hello World 代码为例,执行以上代码后的HTML 代码如下。
<p id="p1"><b>Howdy</b> world!</p>
注意,在设置text 属性的情况下,HTML 标签保持不变。
要向范围中插入HTML 代码,就得使用pasteHTML()方法,如下面的例子所示。
var range = document.body.createTextRange();
range.findText("Hello");
range.pasteHTML("<em>Howdy</em>");
执行这些代码后,会得到如下HTML。
<p id="p1"><b><em>Howdy</em></b> world!</p>
不过,在范围中包含HTML 代码时,不应该使用pasteHTML(),因为这样很容易导致不可预料的结果——很可能是格式不正确的HTML。
4. 折叠IE 范围:IE 为范围提供的collapse()方法与相应的DOM 方法用法一样:传入true 把范围折叠到起点,传入false 把范围折叠到终点。例如:
range.collapse(true); //折叠到起点
可惜的是,没有对应的collapsed 属性让我们知道范围是否已经折叠完毕。为此,必须使用boundingWidth 属性,该属性返回范围的宽度(以像素为单位)。如果boundingWidth 属性等于0,就说明范围已经折叠了:
var isCollapsed = (range.boundingWidth == 0);
此外,还有boundingHeight、boundingLeft 和boundingTop 等属性,虽然它们都不像boundingWidth 那么有用,但也可以提供一些有关范围位置的信息。
5. 比较IE 范围:IE 中的compareEndPoints()方法与DOM范围的compareBoundaryPoints()方法类似。这个方法接受两个参数:比较的类型和要比较的范围。比较类型的取值范围是下列几个字符串值:"StartToStart"、"StartToEnd"、"EndToEnd"和"EndToStart"。这几种比较类型与比较DOM范围时使用的几个值是相同的。
同样与DOM类似的是,compareEndPoints()方法也会按照相同的规则返回值,即如果第一个范围的边界位于第二个范围的边界前面,返回-1;如果二者边界相同,返回0;如果第一个范围的边界位于第二个范围的边界后面,返回1。仍以前面的Hello World 代码为例,下列代码将创建两个范围,一个选择"Hello world!"(包括<b>标签),另一个选择"Hello"。
var range1 = document.body.createTextRange();
var range2 = document.body.createTextRange();
range1.findText("Hello world!");
range2.findText("Hello");
alert(range1.compareEndPoints("StartToStart", range2)); //0
alert(range1.compareEndPoints("EndToEnd", range2)); //1
由于这两个范围共享同一个起点,所以使用compareEndPoints()比较起点返回0。而range1的终点在range2 的终点后面,所以compareEndPoints()返回1。
IE 中还有两个方法,也是用于比较范围的:isEqual()用于确定两个范围是否相等,inRange()用于确定一个范围是否包含另一个范围。下面是相应的示例。
var range1 = document.body.createTextRange();
var range2 = document.body.createTextRange();
range1.findText("Hello World");
range2.findText("Hello");
alert("range1.isEqual(range2): " + range1.isEqual(range2)); //false
alert("range1.inRange(range2):" + range1.inRange(range2)); //true
这个例子使用了与前面相同的范围来示范这两个方法。由于这两个范围的终点不同,所以它们不相等,调用isEqual()返回false。由于range2 实际位于range1 内部,它的终点位于后者的终点之前、起点之后,所以range2 被包含在range1 内部,调用inRange()返回true。
6. 复制IE 范围:在IE 中使用duplicate()方法可以复制文本范围,结果会创建原范围的一个副本,如下面的例子所示。
var newRange = range.duplicate();
新创建的范围会带有与原范围完全相同的属性。