JavaScript DOM2和DOM3——“范围”的注意要点

  • A+
所属分类:JavaScript

“DOM2级遍历和范围”模块定义了“范围”接口。通过范围可以选择文档中的一个区域,而不必考虑节点的界限(选择在后台完成,对用户是不可见的)。

DOM中的范围

DOM2级在Document类型中定义了createRange()方法,可以用来创建DOM范围,如下所示:

var range = document.createRange();

每个范围由一个Range类型的实例表示,这个实例有很多属性和方法:

  • startContainer:包含范围起点的节点(选区中第一个节点的父节点);
  • startOffset:范围在startContainer中起点的偏移量;
  • endContainer:包含范围终点的节点(选区中最后一个节点的父节点);
  • endOffset:范围在endContainer中终点的偏移量;
  • commonAncestorContainer:startContainer和endContainer共同的祖先节点在文档树中位置最深的那个;

用DOM范围实现简单选择

selectNode()或selectNodeContents()

前者选择整个节点,包括子节点;后者选择节点的子节点。如:

<body>
    <p id="p1"><b>Hello</b> world!</p>
    <script>
    var range1 = document.createRange(),
        range2 = document.createRange(),
        p1 = document.getElementById("p1");
    range1.selectNode(p1);
    range2.selectNodeContents(p1);
    
    console.log(range1.startContainer); //document.body
    console.log(range1.startOffset); //1 有个空格
    console.log(range1.endContainer); //document.body
    console.log(range1.endOffset); //2
    console.log(range1.commonAncestorContainer); //document.body
    
    console.log(range2.startContainer); //p1
    console.log(range2.startOffset); //0
    console.log(range2.endContainer); //p1
    console.log(range2.endOffset); //2
    console.log(range2.commonAncestorContainer); //p1
    </script>
</body>

在调用selectNode()时,startContainer、endContainer和commonAncestorContainer等都等于传入节点的父节点,也就是其中的document.body。而startOffset属性等于给定节点在其父节点的childNodes集合中的索引。endOffset等于startOffset加上1;

在调用selectNodeContents()时,startContainer、endContainer和commonAncestorContainer等于传入的节点。而startOffset属性始终等于0.最后,endOffset等于子节点的数量(node.childNodes.length);

更精细的选择

为了更精细的控制将哪些节点包含在范围中,还可以使用下列方法:

  • setStartBefore(refNode):将范围的起点设置在refNode之前;
  • setStartAfter(refNode):将范围的起点设置在refNode之后;
  • setEndBefore(refNode):将范围的终点设置在refNode之前;
  • setEndAfter(refNode):将范围的终点设置在refNode之后;

如下html:

<p id="p1">
    <b>Hello</b> world!
</p>
<div>
    <p>hello</p>
</div>

js:

var range1 = document.createRange(),
    range2 = document.createRange(),
    p1 = document.getElementById("p1");
range1.selectNode(p1);
range1.setStartAfter(p1);

console.log(range1.startContainer); //document.body
console.log(range1.startOffset); //2 有1个空格和一个p元素
console.log(range1.endContainer); //document.body
console.log(range1.endOffset); //2 选择了一个元素,所以是startOffset加1
console.log(range1.commonAncestorContainer); //document.body

range2.setStartAfter(p1); //结果与上面的相同
console.log(range2.startContainer); //document.body
console.log(range2.startOffset); //2
console.log(range2.endContainer); //document.body
console.log(range2.endOffset); //2
console.log(range2.commonAncestorContainer); //document.body

用DOM范围实现更加复杂的选择

setStart()和setEnd()方法

这两个方法都接受两个参数:一个参照节点和一个偏移量值。对前者来说,参照节点会变成startContainer,偏移值则会变成startOffset。对于后者来说,参照节点会变成endContainer,偏移值会变成endOffset。

html:

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

js:

var p1 = document.getElementById("p1"),
    helloNode = p1.firstChild.firstChild,
    worldNode = p1.lastChild;
var range = document.createRange();
range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);

这样就完成了对“llo wo”的选择,但仅仅完成对该选区的选择意义不大,重要的是对其进行操作。

操作DOM范围中的内容

deleteContents()删除范围所包含的内容

如:

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

举例:

var p1 = document.getElementById("p1");
var hello = p1.firstChild.firstChild;
var world = p1.lastChild;
var range = document.createRange();

range.setStart(hello, 1);
range.setEnd(world, 2);

console.log(range.toString()); //ello w
console.log(p1.outerHTML); //<p id="p1"><b>Hello</b> world!</p>

range.deleteContents(); //刪除範圍內的內容
console.log(p1.outerHTML); //<p id="p1"><b>H</b>orld!</p> 

又如:

<ul id="list">
    <li>thi is a list No.1</li>
    <li>thi is a list No.2</li>
    <li>thi is a list No.3</li>
    <li>thi is a list No.4</li>
    <li>thi is a list No.5</li>
</ul>

举例:

var list = document.getElementById("list");
var starting = list.getElementsByTagName("li")[1];
var ending = list.getElementsByTagName("li")[3];
var range = document.createRange();
range.setStart(starting, 0);
range.setEnd(ending, 0);
console.log(range.toString());
console.log(list.outerHTML);
range.deleteContents();
console.log(list.outerHTML);

extractContents()移除范围所包含的内容并返回文档片段

如:

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

举例:

var p1 = document.querySelector("#p1");
var helloNode = p1.firstChild.firstChild,
    worldNode = p1.lastChild;
var range = document.createRange();
range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);
var fragment = range.extractContents();
p1.parentNode.insertBefore(fragment, p1);

又如:

<ul id="list">
    <li>thi is a list No.1</li>
    <li>thi is a list No.2</li>
    <li>thi is a list No.3</li>
    <li>thi is a list No.4</li>
    <li>thi is a list No.5</li>
</ul>

举例:

var list = document.getElementById("list");
var range = document.createRange();

var starting = list.getElementsByTagName("li")[1];
var ending = list.getElementsByTagName("li")[4];

range.selectNode(list);
range.setStartBefore(starting);
range.setEndBefore(ending);

var fragment = range.extractContents();
document.body.appendChild(fragment);

console.log(document.body.innerHTML);

// <li>thi is a list No.2</li>
// <li>thi is a list No.3</li>
// <li>thi is a list No.4</li>

console.log(list.innerHTML);

// <li>thi is a list No.1</li>
// <li>thi is a list No.5</li>

cloneContents()创建范围对象的一个副本

如:

var list = document.getElementById("list");
var range = document.createRange();

var starting = list.getElementsByTagName("li")[1];
var ending = list.getElementsByTagName("li")[4];

range.selectNode(list);
range.setStartBefore(starting);
range.setEndBefore(ending);

var fragment = range.cloneContents();
document.body.appendChild(fragment);

console.log(document.body.innerHTML);

// <li>thi is a list No.2</li>
// <li>thi is a list No.3</li>
// <li>thi is a list No.4</li>

console.log(list.innerHTML);

// <li>thi is a list No.1</li>
// <li>thi is a list No.2</li>
// <li>thi is a list No.3</li>
// <li>thi is a list No.4</li>
// <li>thi is a list No.5</li>

插入DOM范围中的内容

insertNode()向范围选区的开始处插入一个节点(范围内部插入内容)

如:

<ul id="list">
    <li>thi is a list No.1</li>
    <li>thi is a list No.2</li>
    <li>thi is a list No.3</li>
    <li>thi is a list No.4</li>
    <li>thi is a list No.5</li>
</ul>

代码:

<script>
var list = document.getElementById("list");
var range = document.createRange();

var starting = list.getElementsByTagName("li")[1];
var ending = list.getElementsByTagName("li")[4];

range.selectNode(list);
range.setStartBefore(starting);
range.setEndBefore(ending);

var newLi = document.createElement("li");
newLi.appendChild(document.createTextNode("data"));
range.insertNode(newLi);

console.log(list.innerHTML);
// <li>thi is a list No.1</li>
// <li>data</li><li>thi is a list No.2</li>
// <li>thi is a list No.3</li>
// <li>thi is a list No.4</li>
// <li>thi is a list No.5</li>
</script>

又如:

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

代码:

var p = document.getElementById("p1");
var range = document.createRange();
var starting = p.firstChild.firstChild;
var ending = p.lastChild;

range.setStart(starting, 5);
range.setEnd(ending, 0);

var span = document.createElement("span");
span.appendChild(document.createTextNode(" there in this"));

range.insertNode(span);

console.log(p1.innerText); //Hello there in this world!
console.log(p1.innerHTML); //<b>Hello<span> there in this</span></b> world!

surroundContents()向范围选区周围插入一个节点(围绕范围插入内容)

通常与selectNode()配合,因为范围必须包含整个DOM选区,不能仅仅包含选中的DOM节点。

如:

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

代码:

var p = document.getElementById("p1");
var range = document.createRange();

range.selectNode(p.firstChild);

var span = document.createElement("span");
span.style.border = "1px solid orange";
span.style.borderRadius = "3px";

range.surroundContents(span);

console.log(p1.innerHTML); //<span style="border: 1px solid orange; border-radius: 3px;"><b>Hello</b></span> world! 

为了插入span标签,范围必须包含整个DOM选区,所以推荐使用selectNode()配合。

折叠DOM范围

collapse()方法

折叠就是指范围中未选择文档的任何部分。该函数接收一个参数,一个布尔值。true表示折叠到范围的起点,参数false表示折叠到范围的终点。要确定范围已经折叠完毕,可以检查collapsed属性:

如:

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

代码:

var p = document.getElementById("p1");
var range = document.createRange();

range.selectNode(p);
range.collapse(true);
console.log(range.collapsed); //True

又如:

<ul id="list">
    <li>thi is a list No.1</li>
    <li>thi is a list No.2</li>
    <li>thi is a list No.3</li>
    <li>thi is a list No.4</li><li>thi is a list No.5</li>
</ul>

代码:

var list = document.querySelector("#list");
var range = document.createRange();
range.setStartAfter(list.getElementsByTagName("li")[1]);
range.setEndBefore(list.getElementsByTagName("li")[2]);
console.log(range.collapsed); //False 因为还有一个空白节点在这里

var range2 = document.createRange();
range2.setStartAfter(list.getElementsByTagName("li")[3]);
range2.setEndBefore(list.getElementsByTagName("li")[4]);
console.log(range2.collapsed); //True 因为范围没有选中任何部分

比较DOM范围

comopareBoundaryPoints()方法来比较

该方法涌来比较这些范围是否有公共的边界。接收两个参数:表示比较方式的常量值和要比较的范围。如:

  • Range.START_TO_START - 比较两个 Range 节点的开始点
  • Range.END_TO_END - 比较两个 Range 节点的结束点
  • Range.START_TO_END - 用 sourceRange 的开始点与当前范围的结束点比较
  • Range.END_TO_START - 用 sourceRange 的结束点与当前范围的开始点比较

注意:《高级程序设计》一书中,对后两个的说明太模糊(不正确?);下面是w3school的解释:

您可能认为,首先需要用参数 how 的范围常量指定当前范围的边界点,然后再用它指定 sourceRange 的边界点。但事实上,

  1. 常量 Range.START_TO_END 指定与当前范围的 end 点和 sourceRange 的 start 点进行比较。
  2. 常量 Range.END_TO_START 指定比较当前范围的 start 点和指定范围的 end 点。

如果第一个范围中的点位于第二个范围中的点之前,返回-1;如果相等返回0;如果第一个范围中的点位于第二个范围中的点之后,返回1

如:

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

代码:

var p = document.getElementById("p1");
var range1 = document.createRange();
var range2 = document.createRange();

range1.selectNodeContents(p);
range2.selectNodeContents(p);
range2.setEndBefore(p.lastChild);

console.log(range1.toString()); //Hello world!
console.log(range2.toString()); //Hello

console.log(range1.compareBoundaryPoints(Range.START_TO_START, range2)); //0
console.log(range1.compareBoundaryPoints(Range.END_TO_END, range2)); //1

又如:

<ul id="list">
    <li>thi is a list No.1</li>
    <li>thi is a list No.2</li>
    <li>thi is a list No.3</li>
    <li>thi is a list No.4</li><li>thi is a list No.5</li>
</ul>

代码:

var list = document.querySelector("#list");
var range1 = document.createRange();
var range2 = document.createRange();

range1.selectNodeContents(list);
range1.setStartAfter(list.firstChild.nextSibling);
range1.setEndBefore(list.lastChild.previousSibling);

range2.selectNodeContents(list);

console.log(range1.compareBoundaryPoints(Range.START_TO_START, range2)); //1
console.log(range1.compareBoundaryPoints(Range.END_TO_END, range2)); //-1
console.log(range1.compareBoundaryPoints(Range.START_TO_END, range2)); //1 注意这里是range1的END与range2的start对比;
console.log(range1.compareBoundaryPoints(Range.END_TO_START, range2)); //-1 注意这里是range1的START与range2的END对比;

复制DOM范围

cloneRange()方法复制范围;

var newRange = range.cloneRange();

清理DOM范围

detach()方法清理范围;

range.detach();
range = null;

上面的是从文档中分离范围;下面的是解除引用。

下节再讨论IE8及更早版本中的范围

weinxin
我的微信公众号
我的微信公众号扫一扫

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: