JS高程:读书摘要(十)DOM2级和DOM3级 - [part2

2019-01-17  本文已影响0人  Upcccz

一、遍历

“DOM2 级遍历和范围”模块定义了两个用于辅助完成顺序遍历DOM结构的类型:NodeIteratorTreeWalker。这两个类型能够基于给定的起点对DOM结构执行深度优先(depth-first)的遍历操作。IE 不支持DOM 遍历。使用下列代码可以检测浏览器对DOM2级遍历能力的支持情况。

var supportsNodeIterator = (typeof document.createNodeIterator == "function");
var supportsTreeWalker = (typeof document.createTreeWalker == "function");
NodeIterator

NodeIterator类型是两者中比较简单的一个,可以使用document.createNodeIterator()方法创建它的新实例。这个方法接受下列4 个参数。

关于whatToShow

whatToShow参数是一个位掩码,通过应用一或多个过滤器(filter)来确定要访问哪些节点。这个参数的值以常量形式在NodeFilter类型中定义

可以使用按位或操作符来组合多个选项

var whatToShow = NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT;
关于filter

createNodeIterator()方法的filter参数来指定自定义的NodeFilter对象(不使用上面说到的常量形式的),每个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;
};

如果不指定过滤器,那么应该在第三个参数的位置上传入null。

var iterator = document.createNodeIterator(document, NodeFilter.SHOW_ALL,null, false);

上面的通过document.createNodeIterator()方法得到的iterator ,就是NodeIterator类型,这个类型拥有nextNode()previousNode()这两个方法来遍历DOM子树,nextNode()方法用于向前前进一步,而previousNode()用于向后后退一步。在刚刚创建的NodeIterator对象中,有一个内部指针指向根节点,因此第一次调用nextNode()会返回根节点。向下遍历nextNode()到最后一个节点时返回null,向上遍历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>
var div = document.getElementById("div1");
var iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT,null, false);
var node = iterator.nextNode(); // 拿到根元素div
while (node !== null) {
    alert(node.tagName); //输出标签名
    node = iterator.nextNode(); 
    // 这里再次使用nextNode拿到p元素 依次向下遍历
}

如果只想遍历其中的p元素,就可以使用第三个参数filter,不再传值null,而是传一个与acceptNode()方法类似的函数

TreeWalker

TreeWalkerNodeIterator的一个更高级的版本。除了包括nextNode()previousNode()在内的相同的功能之外,这个类型还提供了下列用于在不同方向上遍历DOM结构的方法。

创建TreeWalker对象要使用document.createTreeWalker()方法,这个方法接受的4 个参数与document.createNodeIterator()方法相同:作为遍历起点的根节点、要显示的节点类型、过滤器和一个表示是否扩展实体引用的布尔值。

在这里,filter可以返回的值有所不同。除了NodeFilter.FILTER_ACCEPTNodeFilter.FILTER_SKIP 之外,还可以使用NodeFilter.FILTER_REJECT。使用SKIP会跳过当前节点,而使用REJECT会跳过当前节点及该节点的子节点。如果使用REJECT情况下,使用自定义filter过滤的时候,不包含根节点的话,将不会访问任何一个节点,因为nextNode()第一个访问的就是根节点,如果照之前定义的filter过滤只要p元素的,三元表达式第一次就会拿到false,取得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();
}

TreeWalker类型还有一个属性,名叫currentNode,表示任何遍历方法在上一次遍历中返回的节点。通过设置这个属性也可以修改遍历继续进行的起点,如下面的例子所示。

var node = walker.nextNode();
alert(node === walker.currentNode); //true
walker.currentNode = document.body; //修改起点

二、范围

“DOM2 级遍历和范围”模块定义了“范围”(range)接口。通过范围可以选择文档中的一个区域,而不必考虑节点的界限(选择在后台完成,对用户是不可见的)。在常规的DOM 操作不能更有效地修改文档时,使用范围往往可以达到目的。Firefox、Opera、Safari 和Chrome 都支持DOM范围。IE 以专有方式实现了自己的范围特性。

DOM2级在Document类型中定义了createRange()方法。在兼容DOM 的浏览器中,这个方法属于document对象。直接检测该方法可以确定浏览器是否支持范围。

var alsoSupportsRange = (typeof document.createRange == "function");

使用createRange()来创建DOM范围

var range = document.createRange();

每个范围由一个Range类型的实例表示,这个实例拥有很多属性和方法。下列属性提供了当前范围在文档中的位置信息。

选择范围

这两个方法都接受一个参数,即一个DOM 节点,然后使用该节点中的信息来填充范围,selectNode()方法选择整个节点,包括其子节点;而selectNodeContents()方法则只选择节点的子节点。

html:
<html>
    <body>
        <p id="p1"><b>Hello</b> world!</p>
    </body>
</html>
js:
var range1 = document.createRange();
var range2 = document.createRange();
var p1 = document.getElementById("p1");
range1.selectNode(p1);
console.log(range1.startContainer); // 传入节点的父节点即document.body
console.log(range1.endContainer);   // 传入节点的父节点即document.body
console.log(range1.commonAncestorContainer); // 传入节点的父节点即document.body
console.log(range1.startOffset); // 1 --> 给定节点p元素在其父节点的childNodes 集合中的索引(body->p之间的空格算作一个文本节点)
console.log(range1.endOffset);  // 2  --> 等于startOffset + 范围中选择中的节点个数(只选择了一个p元素)

range2.selectNodeContents(p1);
console.log(range2.startContainer); // 等于传入的节点即p元素
console.log(range2.endContainer);   // 等于传入的节点即p元素
console.log(range2.commonAncestorContainer); // 等于传入的节点即p元素
console.log(range2.startOffset); // 0 始终为0,因为范围是从给定节点的第一个子节点开始的
console.log(range2.endOffset);  // 2  等于给点节点的子节点数量 node.children.length
range

将范围的起点设置在refNode之前,因此refNode也就是范围选区中的第一个子节点。同时会将startContainer属性设置为refNode.parentNode,将startOffset属性设置为refNode在其父节点的childNodes集合中的索引。

将范围的起点设置在refNode之后,因此refNode也就不在范围之内了,其下一个同辈节点才是范围选区中的第一个子节点。同时会将startContainer属性设置为refNode.parentNode,将startOffset属性设置为refNode在其父节点的childNodes集合中的索引加1。

将范围的终点设置在refNode之前,因此refNode也就不在范围之内了,其上一个同辈节点才是范围选区中的最后一个子节点。同时会将endContainer属性设置为refNode.parentNode,将endOffset属性设置为refNode在其父节点的childNodes集合中的索引。(即最后一个子节点索引加1)

将范围的终点设置在refNode之后,因此refNode也就是范围选区中的最后一个子节点。同时会将endContainer属性设置为refNode.parentNode,将endOffset属性设置为refNode在其父节点的childNodes集合中的索引加1。

复杂选择

这两个方法都接受两个参数:一个参照节点和一个偏移量值,对setStart()来说,参照节点会变成startContainer,而偏移量值会变成startOffset。对于setEnd()来说,参照节点会变成endContainer,而偏移量值会变成endOffset

假设你只想选择前面HTML示例代码中从"Hello""llo""world!""o"llo</b> wo

例1
html:
<p id="p1"><b>Hello</b> world!</p>
js:
var p1 = document.getElementById("p1");
var helloNode = p1.firstChild.firstChild; // p第一个子元素b的第一个子节点(即文本节点hello)
var worldNode = p1.lastChild; // p的第二个子节点(即文本节点 world!)

var range = document.createRange();
range.setStart(helloNode, 2);
rang e.setEnd(worldNode, 3);
range.2

即选择了llo</b> wo,但是,范围知道自身缺少哪些开标签和闭标签,它能够重新构建有效的DOM结构以便我们对其进行操作。DOM就会变成

<p><b>He</b><b>llo</b> world!</p>

在此例中,会为范围内的llo</b>添加开标签,也会为范围外的<b>He添加闭合标签。

操作DOM 范围

这个方法能够从文档中删除范围所包含的内容,上面的例子中如果在最后调用range.deleteContents(); ,DOM就会变成

<p><b>He</b>rld!</p>

由于范围选区在修改底层DOM结构时能够保证格式良好,因此即使内容被删除了,最终的DOM结构依旧是格式良好的。在此例中就是会为范围外的<b>He添加闭合标签。

这个方法也会从文档中移除范围选区。但区别在于extractContents()会返回范围的文档片段。

// 在例1的基础上添加如下代码
var fragment = range.extractContents();
p1.parentNode.appendChild(fragment);

最终DOM就会变成

<p><b>He</b>rld!</p>
<b>llo</b> wo

创建范围对象的一个副本,然后在文档的其他地方插入该副本。不会移除范围选取

// 在例1的基础上添加如下代码
var fragment = range.cloneContents();
p1.parentNode.appendChild(fragment);

最终DOM就会变成

<p><b>Hello</b> world!</p>
<b>llo</b> wo
// 跟使用删除的方法不同, p元素的选中范围是没有被移除的

可以向范围选区的开始处插入一个节点。

// 在例1的基础上添加如下代码
var span = document.createElement("span");
span.style.color = "red";
span.appendChild(document.createTextNode("Inserted text"));
range.insertNode(span);

最终DOM会变成

<p id="p1"><b>He<span style="color: red">Inserted text</span>llo</b> world</p>

这个方法接受一个参数,即环绕范围内容的节点。在环绕范围插入内容时,后台会执行下列步骤。

  1. 提取出范围中的内容(类似执行extractContent());
  2. 给定节点插入到文档中原来范围所在的位置上
  3. 文档片段的内容添加到给定节点中。

可以使用这种技术来突出显示网页中的某些词句

html:
<p id="p1"><b>Hello</b> world!</p>

js:
var p1 = document.getElementById("p1");
var helloNode = p1.firstChild.firstChild;
range = document.createRange();

range.selectNode(helloNode);
var span = document.createElement("span");
span.style.backgroundColor = "yellow";
range.surroundContents(span)

最终DOM会变成,"Hello"这个单词会变成黄色背景,被标签span包裹。

<p><b><span style="background-color:yellow">Hello</span></b> world!</p>
// 为了插入<span>,范围必须包含整个DOM 选区(不能仅仅包含选中的DOM 节点)。
range.collapse(true); //折叠到起点
alert(range.collapsed); //输出true

检测某个范围是否处于折叠状态,可以帮我们确定范围中的两个节点是否紧密相邻。即这个范围是否被清除,是否什么都没有

html:
<p id="p1">Paragraph 1</p><p id="p2">Paragraph 2</p>

js:
var p1 = document.getElementById("p1"),
var p2 = document.getElementById("p2"),
var range = document.createRange();
range.setStartAfter(p1); // 将起点设置在p1后面
range.setStartBefore(p2); // 将终点设置在p2前面
alert(range.collapsed); //输出true --> p1 的后面和p2 的前面什么也没有。

这个方法接受两个参数:表示比较方式的常量值和要比较的范围。比较方式的常量值如下,

例:A.compareBoundaryPoints(Range.xxxx,B)

compareBoundaryPoints()方法可能的返回值如下:如果第一个范围中的点位于第二个范围中的点之前,返回-1;如果两个点相等,返回0;如果第一个范围中的点位于第二个范围中的点之后,返回1。

这个方法会创建调用它的范围的一个副本。新创建的范围与原来的范围包含相同的属性,而修改它的端点不会影响原来的范围。

var newRange = range.cloneRange();

在使用完范围之后,最好是调用detach()方法,以便从创建范围的文档中分离出该范围。调用detach()之后,就可以放心地解除对范围的引用,从而让垃圾回收机制回收其内存了

//  不再是清除范围,而是在文档中直接清理出去。
range.detach(); //从文档中分离 
range = null; //解除引用

三、IE中的范围

IE8 及之前版本不支持DOM范围。不过,IE8 及早期版本支持一种类似的概念,即文本范围(text range),IE专有。

var range = document.body.createTextRange(); // 创建范围
选择范围

使用范围的findText()方法。这个方法会找到第一次出现的给定文本,并将范围移过来以环绕该文本。如果没有找到文本,这个方法返回false;否则返回true

html:
<p id="p1"><b>Hello</b> world!</p>

js:
var range = document.body.createTextRange();
var found = range.findText("Hello");
// 选中"Hello"
alert(found); //true
alert(range.text); //"Hello"

还可以为findText()传入另一个参数,即一个表示向哪个方向继续搜索的数值。负值表示应该从当前位置向后搜索,而正值表示应该从当前位置向前搜索。

moveToElementText(),这个方法接受一个DOM元素,并选择该元素的所有文本,包括HTML标签。类似于selectNode()(selectNode()方法选择整个节点,包括其子节点)

var range = document.body.createTextRange();
var p1 = document.getElementById("p1");
range.moveToElementText(p1);

在文本范围中包含HTML的情况下,可以使用htmlText属性取得范围的全部内容,alert(range.htmlText);

复杂选择

IE 提供了4 个方法以特定的增量向四周移动范围。这些方法都接受两个参数:移动单位和移动单位的数量,移动单位是下列一种字符串值。

moveStart():移动范围的起点,例:range.moveStart("word", 2);,起点移动2 个单词。

moveEnd():移动范围的终点,例:range.moveEnd("character", 1);,终点移动1 个字符。

expand()方法可以将范围规范化。换句话说,expand()方法的作用是将任何部分选择的文本全部选中。例如,当前选择的是一个单词中间的两个字符,调用expand("word")可以将整个单词都包含在范围之内。

move()方法则首先会折叠当前范围(让起点和终点相等),然后再将范围移动指定的单位数量。

range.move("character", 5); //移动5 个字符

调用move()之后,范围的起点和终点相同,因此必须再使用moveStart()moveEnd()创建新的选区。

操作范围

通过text属性可以取得范围中的内容文本;但是,也可以通过这个属性设置范围中的内容文本。

html:
<p id="p1"><b>Hello</b> world!</p>

js:
var range = document.body.createTextRange();
range.findText("Hello");
range.text = "Howdy";

html会变成: 
<p id="p1"><b>Howdy</b> world!</p>

向范围中插入HTML代码,但是在范围本身就包含有HTML代码时,不建议使用。( 可能会产生格式不正确的HTML

html:
<p id="p1"><b>Hello</b> world!</p>

js:
var range = document.body.createTextRange();
range.findText("Hello");
range.pasteHTML("<em>Howdy</em>")

html会变成: 
<p id="p1"><b><em>Howdy</em></b> world!</p>

传入true把范围折叠到起点,传入false把范围折叠到终点。但是没有对应的collapsed属性让我们知道范围是否已经折叠完毕。为此,必须使用boundingWidth属性,该属性返回范围的宽度(以像素为单位)。如果boundingWidth属性等于0,就说明范围已经折叠了。

range.collapse(true); //折叠到起点
var isCollapsed = (range.boundingWidth == 0);
// isCollapsed 为true时则已经折叠了

这个方法接受两个参数:比较的类型和要比较的范围,比较的类型也是以下几个字符串的值,"StartToStart""StartToEnd""EndToEnd""EndToStart",比较方式与调用方式也与之前的compareBoundaryPoints()方法一致。

range1.compareEndPoints("EndToEnd", range2)
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
var newRange = range.duplicate();

可以复制文本范围,结果会创建原范围的一个副本,新创建的范围会带有与原范围完全相同的属性。

上一篇 下一篇

猜你喜欢

热点阅读