FED

©FrontEndDev.org
2015 - 2024
web@2.22.0 api@2.20.0

Javascript标准DOM Range操作

2级DOM定义了一个createRange()方法,如果是按照DOM此标准的浏览器(IE并不是支持此标准的,但是IE里的属性或方法却远比标准中定义的多得多),它属于document对象,所以创建一个range对象要这样做:

var oRange = document.createRange();

如果你要检测你的浏览器是否支持此标准Range对象,可以用hasFeature()方法来检测:

var supportsDOMRanges = document.implementation.hasFeature("Range", "2.0");
    if (supportsDOMRange) {
    var oRange = document.createRange();
    //range code here
}

Range对象进行简单的选择

最简单用Range进行选择,用selectNode()或者selectNodeContents()方法,这两个方法只有一个接收参数,一个DOM节点。

selectNode()方法选择全部节点,包括它的孩子,而selectNodeContents()选择的节点只是它的孩子。如

<p id="p1"><b>Hello</b> World</p>
<script>
    var oRange1 = document.createRange();
    var oRange2 = document.createRange();
    var oP1 = document.getElementById("p1");
    oRange1.selectNode(oP1);
    oRange2.selectNodeContents(oP1);
</script>

oRange1和oRange2包含上面所说的两种方法,但是看了下面的示图相信你能很快明白这两个方法的区别:

enter image description here

Range实例就会有以下的属性

当你创建了一个Range对象时,Range实例就会有以下的属性:

  • startContainer — 返回range对象从何开始的节点对象(父节点的第一个节点)
  • startOffset — 返回Range开始的偏移量(offset),如果startContainer是一个文本节点,注释节点,或者是CDATA节点,这个属性返回文本的偏移量,否则返回第一个节点的索引。
  • endCOntainer — 返回Range对象最后一个节点对象(父节点的最后一个节点)
  • endOffset — 返回Range结束时的偏移量(offset)特性与startOffset相同。
  • commonAncestorContainer — 返回第一个包含该Range对象的节点。

注:这些属性均为只读属性(read-only),startOffset和endOffset将在下文中有较详细的解释。

下面这段代码将说明这些属性,请在Mozilla firefox里运行(支持此标准的浏览器——DOM2级,IE里将无效):

<html>
 <head>
 <title>DOM Range Example</title>
 <script type="text/javascript">
 function useRanges() {
 var oRange1 = document.createRange();
 var oRange2 = document.createRange();
 var oP1 = document.getElementById("p1");
 oRange1.selectNode(oP1);
 oRange2.selectNodeContents(oP1);

 document.getElementById("txtStartContainer1").value 
    = oRange1.startContainer.tagName;
 document.getElementById("txtStartOffset1").value = 
    oRange1.startOffset;
 document.getElementById("txtEndContainer1").value = 
    oRange1.endContainer.tagName;
 document.getElementById("txtEndOffset1").value = 
    oRange1.endOffset;
 document.getElementById("txtCommonAncestor1").value = 
    oRange1.commonAncestorContainer.tagName;
 document.getElementById("txtStartContainer2").value = 
    oRange2.startContainer.tagName;
 document.getElementById("txtStartOffset2").value = 
    oRange2.startOffset;
 document.getElementById("txtEndContainer2").value = 
    oRange2.endContainer.tagName;
 document.getElementById("txtEndOffset2").value = 
    oRange2.endOffset;
 document.getElementById("txtCommonAncestor2").value = 
    oRange2.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>oRange1</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>oRange2</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>
 </body>
</html>

上面的代码将不作注释了,有什么问题,在评论中留言。

Range中还有一些其它的方法

  • setStartBefore(node) — 设置Range的相对于node节点的起始位置
  • setStartAfter(node) — 同上
  • setEndBefore — 设置Range的相对于node节点的结束位置
  • setEndAfter — 同上

复杂的DOM Range 建立复杂的DOM range需要使用setStart()和setEnd()两个方法,这两个方法有两个参数:一个是一个节点(node)引用和一个偏移(offset)。

  • setStart方法节点的引用是startContainer,偏移则是startOffset;
  • setEnd()方法时,节点引用为endContainer,偏移就是endOffset。

使用这两个方法与selectNode()和selectNodeContents()方法相似。比如,下面的useRanges()函数的前一个示例,可以使用setStart()和setEnd():

function useRanges() {
     var oRange1 = document.createRange();
     var oRange2 = document.createRange();
     var oP1 = document.getElementById("p1");
     var iP1Index = -1;
     for (var i=0; i < oP1.parentNode.childNodes.length; i++) {
         if (oP1.parentNode.childNodes[i] == oP1) {
         iP1Index = i;
         break;
     }
 }

 oRange1.setStart(oP1.parentNode, iP1Index);
 oRange1.setEnd(oP1.parentNode, iP1Index + 1);
 oRange2.setStart(oP1, 0);
 oRange2.setEnd(oP1, oP1.childNodes.length);
 //textbox assignments here
}

注意这个选择节点时的代码(oRange1),你必须指定oP1父节点里所有childNodes集合里的一个索引。

而选择内容时的代码(oRange2),则不需要额外的考虑,

从刚才的例子来从这段HTML里

<p id="p1"><b>Hello</b> World</p>

选择从hello中的llo开始到从World中的Wo开始的Range,我们用setStart()和setEnd(),很容易就可以做到。

首先,我们必须用常规的DOM方法得到文本节点的引用还有就是容器p1的引用。

var oP1 = document.getElementById("p1");
var oHello = oP1.firstChild.firstChild;
var oWorld = oP1.lastChild;

说明:

文本Hello实际上是容器p1的孙子节点,所以我们可以用oP1.firstChild得到b元素,oP1.firstChild.firstChild也就是Hello文本节点的引用了,而World则就是容器p1的最后一个节点了。

下一步,建立range然后设置偏移(offset):

var oP1 = document.getElementById("p1");
var oHello = oP1.firstChild.firstChild;
var oWorld = oP1.lastChild;
var oRange = document.createRange();
oRange.setStart(oHello, 2);
oRange.setEnd(oWorld, 3);

说明:

对于setStart(),偏移(offset)为2,因为字母l在该文本节点中(即Hello中)的位置是2,(位置是从0开始计算的),设置setEnd()方法中的偏移为3,原因同上,需要注意的是World前面有一个空格,空格也是占位置的。如图:

enter image description here

注意:

(Mozilla DOM Range bug #135928)在 Mozilla低版本浏览器 执行此Range方法时,如果setStart()和setEnd()都指向同一个文本节点会出现异常

用DOM Range做一些操作

当创建一个Range对象时,在Range里的所有对象之上,已经创建了一个文档的fragment节点。在这之前,Range对象必须合格证你选择的这段Range是一个well-formed(格式良好)。 比如以面这段Rangeenter image description here

很明显的,在这里,并不是一个well-formed,上面说过了,当创建一个Range时,会自动产生一个fragment,在这里,framgment自动动态的添加一些元素,以保证Range的正确性:

<p><b>He</b><b>llo</b> World</p>

也就是自动加上了开始标签b,使得整个Range变为

<b>llo</b> Wo

fragment的示意图为:enter image description here

当此fragment创建后,你就可以用Range的一些方法来操作它了。

最简单的一个操作就是:deleteContents()方法,这个方法将删除Range选中的部分,在上面的操作之后进行deleteContents(),那么余下的HTML就为:

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

之所以加上闭合标签b,上面也说了,也是Range为了确保它是well-formed。

extractContents() 方法类似于deleteContents(),但具体操作不同,extractContents()是将选中的Range从DOM树中移到一个 fragment中,并返回此fragment,复制下面这些代码然后在Mozilla Firefox里运行,看看结果你就明白了。——删除的

<b>llo</b> Wo

作为一个fragment被添加到body的末端。

<p id="p1"><b>Hello</b> World</p>
<script>
var oP1 = document.getElementById("p1");
var oHello = oP1.firstChild.firstChild;
var oWorld = oP1.lastChild;
var oRange = document.createRange();
oRange.setStart(oHello, 2);
oRange.setEnd(oWorld, 3);
var oFragment = oRange.extractContents();
document.body.appendChild(oFragment);
</script>

cloneContents()方法可以克隆选中Range的fragment,比如:

<p id="p1"><b>Hello</b> World</p>
<script>
var oP1 = document.getElementById("p1");
var oHello = oP1.firstChild.firstChild;
var oWorld = oP1.lastChild;
var oRange = document.createRange();
oRange.setStart(oHello, 2);
oRange.setEnd(oWorld, 3);
var oFragment = oRange.cloneContents();
document.body.appendChild(oFragment);
</script>

这个方法类似于extractContents(),但是不是删除,而是克隆

从Range中插入一些数据

前一节的几个方法解决了如何移除range中所选中的fragment。现在说明如何添加内容到Range中。 insertNode()方法可以插入一个节点到Range中。假如我想把以下的节点插如Range中,将如何操作呢?

<span style="color: red">Inserted text</span>

看下面的代码:

<p id="p1"><b>Hello</b> World</p>
<script>
var oP1 = document.getElementById("p1");
var oHello = oP1.firstChild.firstChild;
var oWorld = oP1.lastChild;
var oRange = document.createRange();
var oSpan = document.createElement("span");
oSpan.style.color = "red";
oSpan.appendChild(document.createTextNode("Inserted text"));

oRange.setStart(oHello, 2);
oRange.setEnd(oWorld, 3);
oRange.insertNode(oSpan);
</script>

那么原来的HTML将会变成这样:

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

surroundContents()的参数为一个node,它将这个node加入到Range,下面看这个示例。

<p id="p1"><b>Hello</b> World</p>
<script>
var oP1 = document.getElementById("p1");
var oHello = oP1.firstChild.firstChild;
var oWorld = oP1.lastChild;
var oRange = document.createRange();
var oSpan = document.createElement("span");
oSpan.style.backgroundColor = "yellow";

oRange.setStart(oHello, 2);
oRange.setEnd(oWorld, 3);
oRange.surroundContents(oSpan);
</script>

在oRange选取的范围内有一个我们新生成的节点span,因此选取的Range的背景变成了黄色。

collapse()方法:

collapse()方法只有一个布尔型的参数,该参数为可选的,也就是说,可以有,也可以没有,默认为false。 true时折叠到Range边界的首部,为false时折叠到Range尾部。即enter image description here

<p id="p1"><b>Hello</b> World</p>
<script>
var oP1 = document.getElementById("p1");
var oHello = oP1.firstChild.firstChild;
var oWorld = oP1.lastChild;
var oRange = document.createRange();
oRange.setStart(oHello, 2);
oRange.setEnd(oWorld, 3);
oRange.collapse(true); 
</script>

如果你想知道该Range是否已经折叠,可以用collapsed属性来得到true或者false。看下面的例子。

<p id="p1">Paragraph 1</p><p id="p2">Paragraph 2</p>
<script>
var oP1 = document.getElementById("p1");
var oP2 = document.getElementById("p2");
var oRange = document.createRange();
oRange.setStartAfter(oP1);
oRange.setStartBefore(oP2);
alert(oRange.collapsed); //outputs "true"
</script>

上面的代码输为true。虽然我们没有用collapse方法,但是由于我们的Range设置开始为1末端到p2的首端,没有任何元素。即</p>(Range开始)(Range结束)<p id="p2">,所以显示的是true。

Range边界的比较

compareBoundaryPoints()方法,语法形式如下:

compare = comparerange.compareBoundaryPoints(how,sourceRange)

参数含义:

  • compare —— 返回1, 0, -1.(0为相等,1为时,comparerange在sourceRange之后,-1为comparerange在sourceRange之前)
  • how —— 为Range常数:END_TO_END|END_TO_START|START_TO_END|START_TO_START sourceRange —— 一个Range对象的边界。

看下面的例子:

<p id="p1"><b>Hello</b> World</p>
<script>
var oRange1 = document.createRange();
var oRange2 = document.createRange();
var oP1 = document.getElementById("p1");
oRange1.selectNodeContents(oP1);
oRange2.selectNodeContents(oP1);
oRange2.setEndBefore(oP1.lastChild);
alert(oRange1.compareBoundaryPoints(Range.START_TO_START, oRange2)); 
//outputs 0
alert(oRange1.compareBoundaryPoints(Range.END_TO_END, oRange2)); 
//outputs 1;
</script>

下图为这两个Range的示意图,结合代码和上面的说明,可以清晰的分析出结果了。

enter image description here

克隆(clone)Range

这个操作很简单,只需要一句语句即可:

var oNewRange = oRange.cloneRange();

cloneRange()方法将返回一个当前Range的副本,当然,它也是Range对象。

清除Range所占的系统资源

当你创建了Range对象最好用detach()方法来清除它所占的系统资源。虽然不清除,GC(垃圾收集器)也会将其收集,但用detach()释放是一个好习惯。语法为:

oRange.detach();

下面一个示例在Mozilla中,利用Range可以模拟出IE中的element.insertAdjacentHTML()方法,

if (browser.isMozilla) {
 HTMLElement.prototype.insertAdjacentHTML = function (sWhere, sHTML) {
 var df; var r = this.ownerDocument.createRange();
 switch (String(sWhere).toLowerCase()) {
     case "beforebegin":
     r.setStartBefore(this);
     df = r.createContextualFragment(sHTML);
     this.parentNode.insertBefore(df, this);
     break;
     case "afterbegin":
     r.selectNodeContents(this);
     r.collapse(true);
     df = r.createContextualFragment(sHTML);
     this.insertBefore(df, this.firstChild);
     break;
     case "beforeend":
     r.selectNodeContents(this);
     r.collapse(false);
     df = r.createContextualFragment(sHTML);
     this.appendChild(df);
     break;
     case "afterend":
     r.setStartAfter(this);
     df = r.createContextualFragment(sHTML);
     this.parentNode.insertBefore(df, this.nextSibling);
     break;
   }
 };
}