12.DOM2 和DOM3(3)

这篇博客详细介绍了DOM2级中的遍历和范围概念,包括NodeIterator和TreeWalker两种遍历类型。NodeIterator用于深度优先遍历DOM结构,而TreeWalker则提供了更多的遍历选项。此外,文章还探讨了范围(Range)接口,允许开发者选择文档的特定部分并进行操作。这两个DOM2级接口在Firefox、Safari、Opera和Chrome中得到了支持,但不被IE支持。
摘要由CSDN通过智能技术生成

遍历:“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 遵循相同的取值规则)。
commonAncestorContainerstartContainer 和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();

新创建的范围会带有与原范围完全相同的属性。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值