DOM范围
DOM2在Document类型上定义了一个****createRange()方法,暴露在document对象上。使用这个方法可以创建一个DOM范围对象,
let range = document.createRange();
PowerShell
这样创建了一个范围对象,关键属性有:
startContainer:范围起点所在的节点(选区中第一个子节点的父节点)。
startOffset:范围起点在startContainer中的偏移量。如果startContainer是文本节点、注释节点或CData区块节点,则*startOffset: 指范围起点之前跳过的字符数;否则,表示范围中第一个节点的索引。
endContainer:范围终点所在的节点(选区中最后一个子节点的父节点)。
endOffset:范围起点在endContainer中的偏移量(与startOffset中偏移量的含义相同)。
commonAncestorContainer:文档中以startContainer和endContainer为后代的最深的节点。
简单选择
通过范围选择文档中某个部分最简单的方式,就是使用selectNode()或selectNodeContents()方法。这两个方法都接收一个节点作为参数,并将该节点的信息添加到调用它的范围。selectNode()方法选择整个节点,包括其后代节点,而selectNodeContents()只选择节点的后代。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<br><p id="p1"><b>Hello</b> world! </p>
<script>
let range1 = document.createRange(),
range2 = document.createRange(),
p1 = document.getElementById("p1");
range1.selectNode(p1);
range2.selectNodeContents(p1);
console.log(range1);
console.log(range2);
console.log(document.body.childNodes);
console.log(p1.childNodes);
</script>
</body>
</html>
HTML, XML
可以很明显的观察到差异
打印的数据有一个知识点,我们的html文档中一个或多个空格都会变成一个文本节点,除非我们把标签全部粘着写,这里p1的startOffset为2是因为前面有一个连写的br标签,同时br和父节点body中间有空格
endOffset的计算偏移量有个加1的概念,因为自身本身是一个节点(从父节点开始偏移,偏移了三个节点),
同样的p1.childNodes也能看出来,以自己为起点startOffset是0,endOffset是2(它从自身开始偏移,偏移了两个子节点).
在像上面这样选定节点或节点后代之后,还可以在范围上调用相应的方法,实现对范围中选区的更精细控制。
❑ 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集合中的索引。
❑ setEndAfter(refNode),把范围的终点设置到refNode之后,从而让refNode成为选区的最后一个子节点。endContainer属性被设置为refNode.parentNode, endOffset属性被设置为refNode在其父节点childNodes集合中的索引加1。
调用这些方法时,所有属性都会自动重新赋值。不过,为了实现复杂的选区,也可以直接修改这些属性的值。
复杂选择
要创建复杂的范围,需要使用setStart()和setEnd()方法。这两个方法都接收两个参数:参照节点和偏移量。对setStart()来说,参照节点会成为startContainer,而偏移量会赋值给startOffset。对setEnd()而言,参照节点会成为endContainer,而偏移量会赋值给endOffset。
使用这两个方法,可以模拟selectNode()和selectNodeContents()的行为。比如:
let range1 = document.createRange(),
range2 = document.createRange(),
p1 = document.getElementById("p1"),
p1Index = Array.from(p1.parentNode.childNodes).findIndex(node => node === p1);
range1.setStart(p1.parentNode, p1Index);
range1.setEnd(p1.parentNode, p1Index + 1);
range2.setStart(p1, 0);
range2.setEnd(p1, p1.childNodes.length);
JavaScript
虽然可以模拟selectNode()和selectNodeContents(),但setStart()和setEnd()真正的威力还是选择节点中的某个部分。假设我们想通过范围从前面示例中选择从"Hello"中的"llo"到" world! "中的"o"的部分。很简单,第一步是取得所有相关节点的引用,如下面的代码所示:
let p1 = document.getElementById("p1"),
helloNode = p1.firstChild.firstChild,
worldNode = p1.lastChild;
let range = document.createRange();
range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);
JavaScript
因为选区起点在"Hello"中的字母"e"之后,所以要给setStart()传入helloNode和偏移量2("e"后面的位置,"H"的位置是0)。要设置选区终点,则要给setEnd()传入worldNode和偏移量3,即不属于选区的第一个字符的位置,也就是"r"的位置3(位置0是一个空格)。
操作范围
创建范围之后,浏览器会在内部创建一个文档片段节点,用于包含范围选区中的节点。为操作范围的内容,选区中的内容必须格式完好。在前面的例子中,因为范围的起点和终点都在文本节点内部,并不是完好的DOM结构,所以无法在DOM中表示。不过,范围能够确定缺失的开始和结束标签,从而可以重构出有效的DOM结构,以便后续操作。
仍以前面例子中的范围来说,范围发现选区中缺少一个开始的标签,于是会在后台动态补上这个标签,同时还需要补上封闭"He"的结束标签,结果会把DOM修改为这样:
<p><b>He</b><b>llo</b> world! </p>
HTML, XML
而且," world! "文本节点会被拆分成两个文本节点,一个包含" wo",另一个包含"rld! "。最终的DOM树,以及范围对应的文档片段如图所示。
这样创建了范围之后,就可以使用很多方法来操作范围的内容。(注意,范围对应文档片段中的所有节点,都是文档中相应节点的指针。)
deleteContents():这个方法会从文档中删除范围包含的节点。
let p1 = document.getElementById("p1"),
helloNode = p1.firstChild.firstChild,
worldNode = p1.lastChild,
range = document.createRange();
range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);
range.deleteContents();
JavaScript
执行结束后dom会变化
<p><b>He</b>rld! </p>
HTML, XML
extractContents():从文档中剪切出范围对应的文档片段并返回
let p1 = document.getElementById("p1"),
helloNode = p1.firstChild.firstChild,
worldNode = p1.lastChild,
range = document.createRange();
range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);
let fragment=range.extractContents();
p1.parentNode.appendChild(fragment);
JavaScript
fragment 常用场景便是做批量的dom插入,由于他的特性只会插入自己的子树,所以最后的html如下
<p><b>He</b>rld! </p>
<b>llo</b> wo
HTML, XML
如果不想把范围从文档中移除,也可以使用cloneContents()创建一个副本,然后把这个副本插入到文档其他地方。
let p1 = document.getElementById("p1"),
helloNode = p1.firstChild.firstChild,
worldNode = p1.lastChild,
range = document.createRange();
range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);
letfragment=range.cloneContents();
p1.parentNode.appendChild(fragment);
JavaScript
这个方法跟extractContents()很相似,因为它们都返回文档片段。主要区别是cloneContents()返回的文档片段包含范围中节点的副本,而非实际的节点。执行上面操作之后,HTML页面会变成这样:
<p><b>Hello</b> world! </p>
<b>llo</b> wo
HTML, XML
此时关键是要知道,为保持结构完好而拆分节点的操作,只有在调用前述方法时才会发生。在DOM被修改之前,原始HTML会一直保持不变(之前只是各种选中操作,真正操作范围后才会影响dom)。
范围插入
insertNode():可以在范围选区的开始位置插入一个节点
例如,假设我们想在前面例子中的HTML中插入如下HTML:
<span style="color: red">Inserted text</span>
HTML, XML
let p1 = document.getElementById("p1"),
helloNode = p1.firstChild.firstChild,
worldNode = p1.lastChild,
range = document.createRange();
range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);
letspan=document.createElement("span");
span.style.color="red";
span.appendChild(document.createTextNode("Insertedtext"));
range.insertNode(span);
JavaScript
结果:
<p id="p1"><b>He<span style="color: red">Inserted text</span>llo</b> world</p>
HTML, XML
注意,正好插入到"Hello"中的"llo"之前,也就是范围选区的前面。同时,也要注意原始的HTML并没有添加或删除元素,因为这里并没有使用之前提到的方法。使用这个技术可以插入有用的信息,比如在外部链接旁边插入一个小图标。
除了向范围中插入内容,还可以使用surroundContents()方法插入包含范围的内容。这个方法接收一个参数,即包含范围内容的节点。调用这个方法时,后台会执行如下操作:
(1)提取出范围的内容;
(2)在原始文档中范围之前所在的位置插入给定的节点;
(3)将范围对应文档片段的内容添加到给定节点。
这种功能适合在网页中高亮显示某些关键词,做富文本的时候常用比如:
let p1 = document.getElementById("p1"),
helloNode = p1.firstChild.firstChild,
worldNode = p1.lastChild,
range = document.createRange();
range.selectNode(helloNode);
letspan=document.createElement("span");
span.style.backgroundColor="yellow";
range.surroundContents(span);
JavaScript
<p><b><span style="background-color:yellow">Hello</span></b> world! </p>
HTML, XML
这里有一个关键点: range.extractContents() 或 range.cloneContents()方法去操作范围浏览器都会自动对范围片段进行补全,但是surroundContents()的规范是dom树中预期位置完全兼容,那么 为了插入元素,范围中必须包含完整的DOM结构。如果范围中包含部分选择的非文节点,这个操作会失败并报错。另外,如果给定的节点是Document、DocumentType或DocumentFragment类型,也会导致抛出错误。
范围折叠
如果范围并没有选择文档的任何部分,则称为折叠(collapsed)。折叠范围有点类似文本框:如果文本框中有文本,那么可以用鼠标选中以高亮显示全部文本。这时候,如果再单击鼠标,则选区会被移除,光标会落在某两个字符中间。而在折叠范围时,位置会被设置为范围与文档交界的地方,可能是范围选区的开始处,也可能是结尾处。下图展示了范围折叠时会发生什么。
折叠范围可以使用collapse()方法,这个方法接收一个参数:布尔值,表示折叠到范围哪一端。true表示折叠到起点,false表示折叠到终点。要确定范围是否已经被折叠,可以检测范围的collapsed属性:
range.collapse(true); // 折叠到起点
console.log(range.collapsed); // 输出true
JavaScript
测试范围是否被折叠,能够帮助确定范围中的两个节点是否相邻。例如有以下HTML代码:
<p id="p1">Paragraph 1</p><p
id="p2">Paragraph 2</p>
HTML, XML
如果事先并不知道标记的结构(比如自动生成的标记),则可以像下面这样创建一个范围:
let p1 = document.getElementById("p1"),
p2 = document.getElementById("p2"),
range = document.createRange();
range.setStartAfter(p1);
range.setStartBefore(p2);
console.log(range.collapsed); // true
JavaScript
在这种情况下,创建的范围是折叠的,因为p1后面和p2前面没有任何内容。
范围比较
如果有多个范围,则可以使用compareBoundaryPoints()方法确定范围之间是否存在公共的边界(起点或终点)。这个方法接收两个参数:要比较的范围和一个常量值,表示比较的方式。这个常量参数包括:
❑ Range.START_TO_START(0),比较两个范围的起点;
❑ Range.START_TO_END(1),比较第一个范围的起点和第二个范围的终点;
❑ Range.END_TO_END(2),比较两个范围的终点;
❑ Range.END_TO_START(3),比较第一个范围的终点和第二个范围的起点。
compareBoundaryPoints()方法在第一个范围的边界点位于第二个范围的边界点之前时返回-1,在两个范围的边界点相等时返回0,在第一个范围的边界点位于第二个范围的边界点之后时返回1。来看下面的例子:
let range1 = document.createRange();
let range2 = document.createRange();
let p1 = document.getElementById("p1");
range1.selectNodeContents(p1);
range2.selectNodeContents(p1);
range2.setEndBefore(p1.lastChild);
console.log(range1.compareBoundaryPoints(Range.START_TO_START, range2)); // 0
console.log(range1.compareBoundaryPoints(Range.END_TO_END, range2)); // 1
JavaScript
在这段代码中,两个范围的起点是相等的,因为它们都是selectNodeContents()默认返回的值。因此,比较二者起点的方法返回0。不过,因为range2的终点被使用setEndBefore()修改了,所以导致range1的终点位于range2的终点之后(见下图),结果这个方法返回了1。
复制范围
调用范围cloneRange()方法可以复制范围。这个方法会创建调用它的范围的副本:调用范围的cloneRange()方法可以复制范围。这个方法会创建调用它的范围的副本:
清理
在使用完范围之后,最好调用detach()方法把范围从创建它的文档中剥离。调用detach()之后,就可以放心解除对范围的引用,以便垃圾回收程序释放它所占用的内存。下面是一个例子:
range.detach(); // 从文档中剥离范围
range = null; // 解除引用
JavaScript
这两步是最合理的结束使用范围的方式。剥离之后的范围就不能再使用了。