第十二章:DOM2 和 DOM3(范围)

DOM2和DOM3

范围

  • 为了让开发人员更方便的控制页面,DOM Level 2 Traversal and Range 模块定义了“范围”接口。通过范围可以选择文档的一个区域,而不必考虑节点的界限。这种行为是在后台完成的,对用户是不可见的。IE以专有方式实现了自己的范围特性。

DOM中的范围

  • DOM2级在Document类型中定义了createRange()方法。使用hasFeature()或者直接检测该方法,都可以确定浏览器是否支持范围。
    var supportsRange = document.implementation.hasFeature("Range", "2.0");
    var alsoSupportsRange = typeof document.createRange == "function";

    var range = document.createRange();
  • 每个范围由一个Range 类型的实例表示,下面是它的属性和方法
    1. startContainer: 包含范围起点的节点(书上说就是选区中第一个节点的父节点,但这句话不够严谨,如果起点在文本节点(注释节点、CDATA节点)文本内部,则startContainer指代该文本节点(注释节点、CDATA节点)。)
    2. startOffset:范围在startContainer中起点的偏移量。如果startContainer是文本节点、注释节点或者CDATA节点,那么startOffset就是范围起点之前跳过的字符数量。否则,startOffset就是范围中第一个子节点的索引。(这句话看不懂,startContainer不是第一个节点的父节点吗?为什么有可能是文本节点?后来我得知了所谓的包含范围起点的节点中的包含不一定指代父子节点关系,他也可能代表的是字符串和子串的关系!)
    3. endContainer:包含范围终点的节点(即选区中最后一个节点的父节点,同上,也可能是文本节点
    4. endOffset:范围在endContainer中终点的偏移量
    5. commonAncestorContainer:startContainer和endContainer共同的祖先节点在文档树中位置最深的那个。(按照我的理解就是两个节点的“最小公倍数”)
  • 一开始我一直有个误区,那就是我以为这里所说的起点或者终点是指一个节点。其实并不是这样,所谓的起点就是范围中第一个节点的左侧位置,终点就是范围中最后一个节点的右侧位置。起点和终点形成的一个区域叫范围。
  • 后来我知道了范围的边界不一定是节点,也可能是字符串的子串
用DOM范围实现简单选择
  • 一般使用selectNode()selectNodeContents()来填充范围,这两个方法都接收一个参数,即一个DOM节点。其中,前者方法选择整个节点,包括其子节点。而后者方法则只选择节点的子节点。看看书上给的例子:
<!DOCTYPE html>
<html>
    <head>
        <title>DOM Range Example</title>
        <script type="text/javascript">
            function useRanges() {
                var range1 = document.createRange();
                var range2 = document.createRange();
                var p1 = document.getElementById("p1");
                range1.selectNode(p1);
                range2.selectNodeContents(p1);

                document.getElementById("txtStartContainer1").value = range1.startContainer.tagName;
                document.getElementById("txtStartOffset1").value = range1.startOffset;
                document.getElementById("txtEndContainer1").value = range1.endContainer.tagName;
                document.getElementById("txtEndOffset1").value = range1.endOffset;
                document.getElementById("txtCommonAncestor1").value = range1.commonAncestorContainer.tagName;

                document.getElementById("txtStartContainer2").value = range2.startContainer.tagName;
                document.getElementById("txtStartOffset2").value = range2.startOffset;
                document.getElementById("txtEndContainer2").value = range2.endContainer.tagName;
                document.getElementById("txtEndOffset2").value = range2.endOffset;
                document.getElementById("txtCommonAncestor2").value = range2.commonAncestorContainer.tagName;
            }
        </script>
    </head>
    <body>
        <p id="p1"><b>Hello</b> world!</p>
        <input type="button" value="Use Ranges" onclick="useRanges()" />        
        <table border="0">
        <tr>
            <td>
                <fieldset>
                    <legend>range1</legend>
                    Start Container: <input type="text" id="txtStartContainer1" /><br />
                    Start Offset: <input type="text" id="txtStartOffset1" /><br />
                    End Container: <input type="text" id="txtEndContainer1" /><br />
                    End Offset: <input type="text" id="txtEndOffset1" /><br />
                    Common Ancestor: <input type="text" id="txtCommonAncestor1" /><br />    
                </fieldset>
            </td>
            <td>
                <fieldset>
                    <legend>range2</legend>
                    Start Container: <input type="text" id="txtStartContainer2" /><br />
                    Start Offset: <input type="text" id="txtStartOffset2" /><br />
                    End Container: <input type="text" id="txtEndContainer2" /><br />
                    End Offset: <input type="text" id="txtEndOffset2" /><br />
                    Common Ancestor: <input type="text" id="txtCommonAncestor2" /><br />
                </fieldset>
            </td>
        </tr>
        </table>
        <p><strong>Note:</strong> This example uses DOM ranges and will only work in browsers that support DOM ranges. This example will fail in Internet Explorer &lt; 9.</p>

    </body>
</html>
  • 范围这几个属性和方法并不难,这里就不展开了。
  • 此外,为了更精细的控制将哪些节点包含在范围内,还可以使用下列方法:
    1. setStartBefort(refNode):在范围的起点设置在refNode之前,因此refNode就是范围中的第一个子节点。同时会将startContainer属性设置为refNode.parentNode,将startOffset设置为refNode在父节点的childNodes集合中的索引。(都挺好理解的)
    2. setStartAfter(refNode):将范围的起点设置在refNode之后,其下一个同辈节点才是范围选区中的第一个节点(这里有个疑问,如果refNode === refNode.parentNode.lastChild会发生什么情况?)。同时会将startContainer属性设置为refNode.parentNode,将startOffset设置为refNode在父节点的childNodes集合中的索引加1。
    3. setEndBefore(refNode):(这里有个疑问,如果refNode === refNode.parentNode.firstChild会发生什么情况?)
    4. setEndAfter(refNode)
<!DOCTYPE html>
<html>
    <head>
        <title>DOM Range Example</title>
        <script type="text/javascript">

            function useRanges() {
                var range1 = document.createRange();
                var range2 = document.createRange();
                var p1 = document.getElementById("p1");
                var b1 = document.getElementById("b1");
                var p2 = document.getElementById("p2");
                var b2 = document.getElementById("b2");
                //设置range1为b到文本节点"world!"
                range1.setStartBefore(b1);
                range1.setEndAfter(p1.lastChild);

                range2.setStartAfter(p1.lastChild);
                range2.setEndBefore(p2.firstChild);

                document.getElementById("txtStartContainer1").value = range1.startContainer.id;
                document.getElementById("txtStartOffset1").value = range1.startOffset;
                document.getElementById("txtEndContainer1").value = range1.endContainer.id;
                document.getElementById("txtEndOffset1").value = range1.endOffset;
                document.getElementById("txtCommonAncestor1").value = range1.commonAncestorContainer.tagName;

                document.getElementById("txtStartContainer2").value = range2.startContainer.id;
                document.getElementById("txtStartOffset2").value = range2.startOffset;
                document.getElementById("txtEndContainer2").value = range2.endContainer.id;
                document.getElementById("txtEndOffset2").value = range2.endOffset;
                document.getElementById("txtCommonAncestor2").value = range2.commonAncestorContainer.tagName;
            }
        </script>
    </head>
    <body>
        <p id="p1"><b id="b1">Hello</b> world!</p>
        <p id="p2"><b id="b2">Hello</b> world!</p>
        <input type="button" value="Use Ranges" onclick="useRanges()" />
        <table border="0">
        <tr>
            <td>
                <fieldset>
                    <legend>range1</legend>
                    Start Container: <input type="text" id="txtStartContainer1" /><br />
                    Start Offset: <input type="text" id="txtStartOffset1" /><br />
                    End Container: <input type="text" id="txtEndContainer1" /><br />
                    End Offset: <input type="text" id="txtEndOffset1" /><br />
                    Common Ancestor: <input type="text" id="txtCommonAncestor1" /><br />    
                </fieldset>
            </td>
            <td>
                <fieldset>
                    <legend>range2</legend>
                    Start Container: <input type="text" id="txtStartContainer2" /><br />
                    Start Offset: <input type="text" id="txtStartOffset2" /><br />
                    End Container: <input type="text" id="txtEndContainer2" /><br />
                    End Offset: <input type="text" id="txtEndOffset2" /><br />
                    Common Ancestor: <input type="text" id="txtCommonAncestor2" /><br />
                </fieldset>
            </td>
        </tr>
        </table>
        <p><strong>Note:</strong> This example uses DOM ranges and will only work in browsers that support DOM ranges. This example will fail in Internet Explorer &lt; 9.</p>

    </body>
</html>
  • 虽然我能用下面的图解释range2的现象,但是我突然发现了一件事情,我设置这么一个范围有什么用呢?只能获得startOffset这些没什么用的属性吗?
    这里写图片描述
用DOM范围实现复杂选择
  • 要创建复杂的范围就得使用setStart()和setEnd()方法,这两个方法都接收2个参数:一个参照节点和一个偏移量值。对setStart()而言,参照节点会变成startContainer,偏移值会变成startOffset。对setEnd()参照节点会变成endContainer,偏移值会变成endOffset。我们可以用这两个方法模仿selectNode()和selectNodeContents():
    Range.prototype.selectNode2 = function(refNode) {
        var index;
        for (i=0, len=refNode.parentNode.childNodes.length; i < len; i++) {
            if (refNode.parentNode.childNodes[i] == refNode) {
                index = i;
                break;
            }
        }
        this.setStart(refNode.parentNode, index);
        this.setEnd(refNode.parentNode, index + 1);
        //Range.prototype.setStart.call(this, refNode.parentNode, index);
        //Range.prototype.setEnd.call(this, refNode.parentNode, index + 1);
    }

    Range.prototype.selectNodeContents2 = function(refNode) {
        var index;
        this.setStart(refNode, 0);
        this.setEnd(refNode, refNode.childNodes.length);
        //Range.prototype.setStart.call(this, refNode, 0);
        //Range.prototype.setEnd.call(this, refNode.childNodes.length);
    }
  • 如果想选择<p id="p1"><b>Hello</b> world!</p>代码中“hello”的llo到“world”的“o”,则可以用下面的代码:
    var p1 = document.getElementById("p1"),
        helloNode = p1.firstChild.firstChild,
        worldNode = p1.lastChild,
        range = document.createRange();

    range.setStart(helloNode, 2);
    range.setEnd(worldNode, 3);//示例代码中world前面有一个空格,所以o的右侧位置是3
操作DOM范围中的内容
  • 在创建范围时,内部会为这个范围创建一个文档片段,范围所属的全部节点都被添加到了这个文档片段中。为了创建这个片段,范围内容的格式必须正确有效。所以上面的例子开始结束于文本内部不能算是良好的DOM结构,也就无法通过DOM来表示。还有前面一个例子,范围开始于p(id为p1)节点的内部,结束却在p(id为p2)节点的内部(p1,p2同级)。这样会导致这段范围缺少p的开标签闭标签。不过范围能够自动重新构建有效的DOM结构。
  • 对于上面的例子而言,范围会重新构建如下的DOM结构(不影响原文档,可以认为是一种缓存):
    <!--下面的竖杆指代左边和右边是两个独立的文本节点 -->
    <p id="p1"><b>He</b><b>llo</b> wor|ld!</p>
  • 接下来介绍一个方法:deleteContents()。这个方法能够从文档中删除范围内包含的内容。比如对上面的range执行deleteContents(),最终原文的会变成:
    <p id="p1"><b>He</b>ld!</p>
  • extractContents()deleteContents()类似,也会从文档中移除范围选区。不过该方法会返回范围的文档片段。利用这个返回值,我们可以将范围的内容插入到文档的其他地方。这里就简单举例:
    p1.appendChild(range.extractContents());//用法和documentFragment类似

    页面显示:Herld!llo wo
  • 还有cloneContents(),功能就是创建范围对象的一个副本。利用返回值,我们可以将范围副本的内容插入到文档的其他地方。
插入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(),
        span = document.createElement("span");

    span.style.color = "red";
    span.appendChild(document.createTextNode("Inserted text"));

    range.setStart(helloNode, 2);
    range.setEnd(worldNode, 3);
    range.insertNode(span);
--------------------------------注意不会添加<b></b>
    <p id="p1"><b>He<span style="color: red;">Inserted text</span>llo</b> world!</p>
  • surroundContents():环绕范围插入内容。这个方法接收一个参数,即环绕范围内容的节点。在环绕范围插入内容时,后台会执行下列步骤:
    1. 提取出范围中的内容(类似执行extractContents()
    2. 将给定节点插入到文档中原来范围的位置上
    3. 文档片段(documentFragment)的内容添加到给定节点中。
  • 见下面的例子:
    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);
----------------------------------
    <p id="p1"><b><span style="background-color: yellow;">Hello</span></b> World</p>
折叠DOM 范围
  • 所谓折叠范围,就是指范围中未选择文档的任何部分。按照我的理解就是,你本来选择了一个范围,接下来你可以将这个范围的起点和终点设置在同一个位置。这样的一个过程就叫折叠(个人理解)。而最后那个范围就叫折叠范围
  • 使用collapse()方法来折叠范围,这个方法接收一个参数,一个布尔值。传入true表示折叠到范围的起点,传入false表示折叠到范围的终点。通过range.collapsed属性可以判断范围是否处于折叠状态。
    这里写图片描述
  • 检测某个范围是否处于折叠状态,可以帮我们确定范围中的两个节点是否紧密相邻。例如两个相邻的p1、p2节点,我们通过range.setStartAfter(p1)和range.setEndBefore(p2),然后再获取collapsed属性的值即可判断。但是这样的方法很无语,还得知道p1和p2的顺序(不然这样得到false,只能得出p1不在p2的前面,并不能得到p2不在p1的前面,也就不是紧凑的充要条件)
比较DOM范围
  • 在有多个范围的情况下,可以使用compareBoundaryPoints()来确定这些范围是否有公共的边界。这个方法接收两个参数:表示比较方式的常量值和要比较的范围。常量值如下:
    1. Range.START_TO_START(0):比较两个范围的起点
    2. Range.START_TO_END(1)
    3. Range.END_TO_END(2)
    4. Range.END_TO_START(3)
  • 和java中的compareTo()方法类似,当比较的两个位置,第一个位置在第二个位置的前面时(或者说前者在树的位置深度小于后者),则返回-1。两者相等返回0,在后面就返回1。这里不做过多介绍了。
复制范围
  • 利用cloneRange()方法可以复制范围,var r2 = range.cloneRange(),该方法创建的副本包含原来范围中所有属性,且属于深拷贝(和原来的范围互相独立)。
清理范围
  • 使用完范围后,最好是调用detach()方法,以便从创建范围的文档中分离出该范围。

IE8及更早版本的范围

  • 由于范围本身就很少用,这部分就更别提了,这部分内容我就省略了,真要用了再去看书呗。IE8早期不支持范围,仅支持文本范围。通过一些元素(body button textarea input等)调用createTextRange()可以创建一个文本范围。具体请看p340。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值