JavaScript DOM2和DOM3——“范围”的注意要点 - 前端学习笔记
“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.bodyconsole.log(range1.startOffset); //2 有1个空格和一个p元素console.log(range1.endContainer); //document.bodyconsole.log(range1.endOffset); //2 选择了一个元素,所以是startOffset加1console.log(range1.commonAncestorContainer); //document.bodyrange2.setStartAfter(p1); //结果与上面的相同console.log(range2.startContainer); //document.bodyconsole.log(range2.startOffset); //2console.log(range2.endContainer); //document.bodyconsole.log(range2.endOffset); //2console.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 wconsole.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 的边界点。但事实上,
常量 Range.START_TO_END 指定与当前范围的 end 点和 sourceRange 的 start 点进行比较。
常量 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()); //Helloconsole.log(range1.compareBoundaryPoints(Range.START_TO_START, range2)); //0console.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)); //1console.log(range1.compareBoundaryPoints(Range.END_TO_END, range2)); //-1console.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及更早版本中的范围