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

一、遍历

“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 个参数。

  • root:想要作为搜索起点的树中的节点。
  • whatToShow:表示要访问哪些节点的数字代码。
  • filter:是一个NodeFilter对象,或者一个表示应该接受还是拒绝某种特定节点的函数。
  • entityReferenceExpansion:布尔值,表示是否要扩展实体引用。这个参数在HTML页面中没有用,因为其中的实体引用不能扩展。
关于whatToShow

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 页面没有用。

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

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

createNodeIterator()方法的filter参数来指定自定义的NodeFilter对象(不使用上面说到的常量形式的),每个NodeFilter对象只有一个方法,即acceptNode();如果应该访问给定的节点,该方法返回NodeFilter.FILTER_ACCEPT,如果不应该访问给定的节点,该方法返回NodeFilter.FILTER_SKIP

由于NodeFilter是一个抽象的类型,因此不能直接创建它的实例。在必要时,只要创建一个包含acceptNode()方法的对象,然后将这个对象传入createNodeIterator()中即可。

下列代码展示了如何创建一个只显示

元素的节点迭代器。

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

Hello world!

  • List item 1
  • List item 2
  • List item 3
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结构的方法。

  • parentNode():遍历到当前节点的父节点;
  • firstChild():遍历到当前节点的第一个子节点;
  • lastChild():遍历到当前节点的最后一个子节点;
  • nextSibling():遍历到当前节点的下一个同辈节点;
  • previousSibling():遍历到当前节点的上一个同辈节点。

创建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树,即使不定义过滤器,也可以取得所有

  • 元素。

    var div = document.getElementById("div1");
    var walker = document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT, null, false);
    walker.firstChild(); //转到

    walker.nextSibling(); //转到

      var node = walker.firstChild(); //转到第一个
    • 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类型的实例表示,这个实例拥有很多属性和方法。下列属性提供了当前范围在文档中的位置信息。

    • startContainer:包含范围起点的节点(即选区中第一个节点的父节点)。

    • endContainer:包含范围终点的节点(即选区中最后一个节点的父节点)。

    • commonAncestorContainerstartContainerendContainer共同的祖先节点在文档树中位置最深的那个。

    • startOffset:范围在startContainer中起点的偏移量。如果startContainer是文本节点、注释节点或CDATA节点,那么startOffset就是范围起点之前跳过的字符数量。否则,startOffset就是范围中第一个子节点的索引。

    • endOffset:范围在endContainer中终点的偏移量(与startOffset遵循相同的取值规则)。

    选择范围
    • selectNode()selectNodeContents()

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

    html:
    
        
            

    Hello world!

    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
    • 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集合中的索引。(即最后一个子节点索引加1)

    • setEndAfter(refNode)

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

    复杂选择
    • setStart()setEnd()方法

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

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

    例1
    html:
    

    Hello world!

    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 wo,但是,范围知道自身缺少哪些开标签和闭标签,它能够重新构建有效的DOM结构以便我们对其进行操作。DOM就会变成

    Hello world!

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

    操作DOM 范围
    • deleteContents()

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

    Herld!

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

    • extractContents()

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

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

    最终DOM就会变成

    Herld!

    llo wo
    • cloneContents()

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

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

    最终DOM就会变成

    Hello world!

    llo wo // 跟使用删除的方法不同, p元素的选中范围是没有被移除的
    • insertNode()

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

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

    最终DOM会变成

    HeInserted textllo world

    • surroundContents()

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

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

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

    html:
    

    Hello world!

    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包裹。

    Hello world!

    // 为了插入,范围必须包含整个DOM 选区(不能仅仅包含选中的DOM 节点)。
    • collapse()折叠DOM
      使用collapse()方法来折叠范围(可以理解为清除范围,即范围起点终点重合,范围中什么都没有),这个方法接受一个参数,一个布尔值,表示要折叠到范围的哪一端。参数true表示折叠到范围的起点,参数false表示折叠到范围的终点。要确定范围已经折叠完毕,可以检查collapsed属性
    range.collapse(true); //折叠到起点
    alert(range.collapsed); //输出true
    

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

    html:
    

    Paragraph 1

    Paragraph 2

    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 的前面什么也没有。
    • compareBoundaryPoints() 比较范围

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

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

    • Range.START_TO_START:比较A和B的起点;(A头 与 B头 )
    • Range.START_TO_END:比较A的起点和B的终点;(A头 与 B尾)
    • Range.END_TO_END:比较A和B的终点;(A尾 与 B尾)
    • Range.END_TO_START:比较B的终点和A的起点。(A尾 与 B头)

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

    • cloneRange()复制范围

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

    var newRange = range.cloneRange();
    
    • detach() 清理范围

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

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

    三、IE中的范围

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

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

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

    html:
    

    Hello world!

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

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

    • moveToElementText()

    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 个方法以特定的增量向四周移动范围。这些方法都接受两个参数:移动单位和移动单位的数量,移动单位是下列一种字符串值。

    • "character":逐个字符地移动。
    • "word":逐个单词(一系列非空格字符)地移动。
    • "sentence":逐个句子(一系列以句号、问号或叹号结尾的字符)地移动。
    • "textedit":移动到当前范围选区的开始或结束位置。

    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属性

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

    html:
    

    Hello world!

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

    Howdy world!

    • pasteHTML()

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

    html:
    

    Hello world!

    js: var range = document.body.createTextRange(); range.findText("Hello"); range.pasteHTML("Howdy") html会变成:

    Howdy world!

    • collapse()折叠范围

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

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

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

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

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

  • 你可能感兴趣的:(JS高程:读书摘要(十)DOM2级和DOM3级 - [part2])