DOM范围

前面的话

  为了让开发人员更方便地控制页面,DOM定义了“范围”(range)接口。经过范围能够选择文档中的一个区域,而没必要考虑节点的界限(选择在后台完成,对用户是不可见的)。在常规的DOM操做不能更有效地修改文挡时,使用范围每每能够达到目的。本文将详细介绍DOM范围javascript

 

建立范围

  Document类型中定义了createRange()方法。在兼容DOM的浏览器中,这个方法属于document对象。使用hasFeature()或者直接检测该方法,均可以肯定浏览器是否支持范围html

  [注意]IE8-浏览器不支持java

var supportsRange = document.implementation.hasFeature("Range", "2.0");
var alsoSupportsRange =(typeof document.createRange == "function");

  若是浏览器支持范围,那么就可使用createRange()来建立DOM范围,以下所示node

var range = document.createRange();

  与节点相似,新建立的范围也直接与建立它的文档关联在一块儿,不能用于其余文档。建立了范围以后,接下来就可使用它在后台选择文档中的特定部分。而建立范围并设置了其位置以后,还能够针对范围的内容执行不少种操做,从而实现对底层DOM树的更精细的控制浏览器

  每一个范围由一个Range类型的实例表示,这个实例拥有不少属性和方法。下列属性提供了当前范围在文档中的位置信息app

startContainer:包含范围起点的节点(即选区中第一个节点的父节点)
startoffset:范围在startContainer中起点的偏移量。若是startContainer是文本节点、注释节点或CDATA节点,那么startoffset就是范围起点以前跳过的字符数量。不然,startoffset就是范围中第一个子节点的索引
endContainer:包含范围终点的节点(即选区中最后一个节点的父节点)
endOffset:范围在endContainer中终点的偏移量(与startoffset遵循相同的取值规则)
commonAncestorContainer:startContainer和endContainer共同的祖先节点在文档树中位置最深的那个

  在把范围放到文档中特定的位置时,这些属性都会被赋值spa

 

简单选择

  要使用范围来选择文档中的一部分,最简单的方式就是使用selectNode()或selectNodeContents()。这两个方法都接受一个参数,即一个DOM节点,而后使用该节点中的信息来填充范围。其中,selectNode()方法选择整个节点,包括其子节点;而selectNodeContents()方法则只选择节点的子节点指针

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

  咱们可使用下列代码来建立范围code

var range1 = document.createRange();
var range2 = document.createRange();
var p1 = document.getElementById("p1");
//Range {startContainer: body, startOffset: 1, endContainer: body, endOffset: 2, collapsed: false…}
range1.selectNode(p1);
//Range {startContainer: p#p1, startOffset: 0, endContainer: p#p1, endOffset: 2, collapsed: false…}
range2.selectNodeContents(p1);

  这里建立的两个范围包含文档中不一样的部分:rang1包含<p>元素及其全部子元素,而rang2包含<b>元素、文本节点"Hello"和文本节点"world!"htm

  在调用selectNode()时,startContainer、endContainer和commonAncestorContainer都等于传入节点的父节点,也就是这个例子中的document.body。而startoffset属性等于给定节点在其父节点的childNodes集合中的索引(在这个例子中是1——由于兼容DOM的浏览器将空格算做一个文本节点),endOffset等于startoffset加1(由于只选择了一个节点)

  在调用selectNodeContents()时,startContainer、endContainer和commonAncestorContainer等于传入的节点,即这个例子中的<p>元素。而startoffset属性始终等于0,由于范围从给定节点的第一个子节点开始。最后,endOffset等于子节点的数量(node.childNodes.length),在这个例子中是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()。来看下面的例子 

var range1 = document.createRange();
var range2 = document.createRange();
var p1 = document.getElementById("p1");                
var p1Index = -1;
var i, len;
    
for (i=0, len=p1.parentNode.childNodes.length; i < len; i++) {
    if (p1.parentNode.childNodes[i] == p1) {
        p1Index = i;
        break;
    }
}
        
range1.setStart(p1.parentNode, p1Index);
range1.setEnd(p1.parentNode, p1Index + 1);
range2.setStart(p1, 0);
range2.setEnd(p1, p1.childNodes.length);

  显然,要选择这个节点(使用range1),就必须肯定当前节点(p1)在其父节点的childNodes集合中的索引。而要选择这个节点的内容(使用range2),也没必要计算什么;只要经过setStart()和setEnd()设置默认值便可。模仿selectNode()和selectNodeContents()并非setStart()和setEnd()的主要用途,它们更胜一筹的地方在于可以选择节点的一部分

  假设只想选择前面HTML示例代码中从“Hello"的"llo"到"world!"的"o"——很容易作到

  第一步是取得全部节点的引用,以下所示:

var p1 = document.getElementById("p1");
var helloNode = p1.firstChild.firstChild;
var worldNode = p1.lastChild;

  实际上,"Hello”文本节点是<p>元素的孙子节点,由于它自己是<b>元素的一个子节点。所以,p1.firstchild取得的是<b>,而p1.firstchild.firstchild取得的才是这个文本节点。"world!"文本节点是<p>元素的第二个子节点(也是最后一个子节点),所以可使用p1.lastChild取得该节点。而后,必须在建立范围时指定相应的起点和终点,以下所示

var range = document.createRange();
range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);

  由于这个范围的选区应该从"Hello"中"e"的后面开始,因此在setStart()中传入helloNode的同时,传入了偏移量2(即"e"的下一个位置;"H"的位置是0)。设置选区的终点时,在setEnd()中传入worldNode的同时传入了偏移量3,表示选区以外的第一个字符的位置,这个字符是”r",它的位置是3(位置0上还有一个空格)。以下所示

  因为helloNode和worldNode都是文本节点,所以它们分别变成了新建范围的startContainer和endContainer。此时startoffset和endOffset分别用以肯定两个节点所包含的文本中的位置,而不是用以肯定子节点的位置(就像传入的参数为元素节点时那样)。此时的commonAncestorContainer是<p>元素,也就是同时包含这两个节点的第一个祖先元素

  固然,仅仅是选择了文档中的某一部分用处并不大。但重要的是,选择以后才能够对选区进行操做

 

操做范围内容

  在建立范围时,内部会为这个范围建立一个文档片断,范围所属的所有节点都被添加到了这个文档片断中。为了建立这个文档片断,范围内容的格式必须正确有效。在前面的例子中,建立的选区分别开始和结束于两个文本节点的内部,所以不能算是格式良好的DOM结构,也就没法经过DOM来表示。可是,范围知道自身缺乏哪些开标签和闭标签,它可以从新构建有效的DOM结构以便对其进行操做

  对于前面的例子而言,范围通过计算知道选区中缺乏一个开始的<b>标签,所以就会在后台动态加入一个该标签,同时还会在前面加入一个表示结束的</b>标签以结束"He"。因而,修改后的DOM就变成了以下所示

<p><b>He</b><b>llo</b> world!</p>

  另外,文本节点"world!"也被拆分为两个文本节点,一个包含"wo",另外一个包含"rid!"。最终的DOM树下图所示,右侧是表示范围的文档片断的内容

  像这样建立了范围以后,就可使用各类方法对范围的内容进行操做了

  [注意]表示范围的内部文档片断中的全部节点,都只是指向文档中相应节点的指针

【deleteContents()】

  操做范围内容的第一个方法是deleteContents(),这个方法可以从文档中删除范围所包含的内容

var p1 = document.getElementById("p1");
var helloNode = p1.firstChild.firstChild;
var worldNode = p1.lastChild;
var range = document.createRange();

range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);
range.deleteContents();

  执行以上代码后,页面中会显示以下HTML代码

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

  因为范围选区在修改底层DOM结构时可以保证格式良好,所以即便内容被删除了,最终的DOM结构依旧是格式良好的

【extractContents()】

  与deleteContents()方法类似,extractContents()方法也会从文档中移除范围选区。但区别在于,extractContents()会返回范围的文档片断。利用这个返回的值,能够将范围的内容插入到文档中的其余地方。以下所示:

var p1 = document.getElementById("p1");
var helloNode = p1.firstChild.firstChild;
var worldNode = p1.lastChild;
var range = document.createRange();

range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);

var fragment = range.extractContents();
p1.parentNode.appendChild(fragment);

  在这个例子中,将提取出来的文档片断添加到了文档<body>元素的末尾

  [注意]在将文档片断传入appendChild()方法中时,添加到文档中的只是片断的子节点,而非片断自己

<p><b>He</b>rld!</p>
<b>llo</b> wo

【cloneContents】

  还有一种作法是使用cloneContents()建立范围对象的一个副本,而后在文档其余地方插入该副本

var p1 = document.getElementById("p1");
var helloNode = p1.firstChild.firstChild;
var worldNode = p1.lastChild;
var range = document.createRange();

range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);

var fragment = range.cloneContents();
p1.parentNode.appendChild(fragment);

  这个方法与extractContents()很是相似,由于它们都返回文档片断。它们的主要区别在于,cloneContents()返回的文档片断包含的是范围中节点的副本,而不是实际的节点。执行上面的操做后,页面中的HTML代码以下所示:

<p><b>Hello</b> world!</p>
<b>llo</b> wo

  [注意]在调用cloneContents()方法以前,拆分的节点并不会产生格式良好的文档片断。换句话说,原始的HTML在DOM被修改以前会始终保持不变

 

插入范围内容

  利用范围,能够删除或复制内容,还能够像前面介绍的那样操做范围中的内容。使用insertNode()方法能够向范围选区的开始处插入一个节点。假设在前面例子中的HTML前面插入如下HTML代码

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

  那么可使用下列代码:

var p1 = document.getElementById("p1");
var helloNode = p1.firstChild.firstChild;
var worldNode = p1.lastChild;
var range = document.createRange();
var span = document.createElement("span");

range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);

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

  运行以上javascript代码,就会获得以下HTML代码

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

  [注意]<span>正好被插入到了"Hello"中的"llo"前面,而该位置就是范围选区的开始位置。使用这种技术能够插入一些帮助提示信息,例如在打开新窗口的连接旁边插入一幅图像

【surroundContents()】

  除了向范围内部插入内容以外,还能够环绕范围插入内容,此时就要使用surroundContents()方法。这个方法接受一个参数,即环绕范围内容的节点。在环绕范围插入内容时,后台会执行下列步骤

  一、提取出范围中的内容(相似执行extractContents())

  二、将给定节点插入到文档中原来范围所在的位置上

  三、将文档片断的内容添加到给定节点中

  可使用这种技术来突出显示网页中的某些词句,例以下列代码

var p1 = document.getElementById("p1");
var helloNode = p1.firstChild.firstChild;
var worldNode = p1.lastChild;
var range = document.createRange();

range.selectNode(helloNode);

var span = document.createElement("span");
span.style.backgroundColor = "yellow";
range.surroundContents(span);

  以上代码会给范围选区加上一个黄色的背景。获得的HTML代码以下所示

<p><b><span style="background-color:yellow">Hello</b> world!</p>

  为了插入<span>,范围必须包含整个DOM选区,而不能仅仅包含选中的DOM节点

 

折叠范围

  所谓折叠范围,就是指范围中未选择文档的任何部分。能够用文本框来描述折叠范围的过程。假设文本框中有一行文本,用鼠标选择了其中一个完整的单词。而后,单击鼠标左键,选区消失,而光标则落在了其中两个字母之间。一样,在折叠范围时,其位置会落在文档中的两个部分之间,多是范围选区的开始位置,也多是结束位置。下图展现了折叠范围时发生的情形 

【collapse()】

  使用collapse()方法来折叠范围,这个方法接受一个参数,该参数是一个布尔值,表示要折叠到范围的哪一端。参数true表示折叠到范围的起点,参数false表示折叠到范围的终点。要肯定范围已经折叠完毕,能够检査collapsed属性,以下所示:

range.collapse(true);//折叠到起点
console.log(range.collapsed);//输出true

  检测某个范围是否处于折叠状态,能够帮咱们肯定范围中的两个节点是否紧密相邻。例如,对于下面的HTML代码

<p id="p1">Paragraph 1</p><p id="p2">Paragraph 2</p>

  若是不知道其实际构成(好比说,这行代码是动态生成的),那么能够像下面这样建立一个范围

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

range.setStartAfter(p1);
range.setStartBefore(p2);
console.log(range.collapsed);//输出true

  在这个例子中,新建立的范围是折叠的,由于p1的后面和p2的前面什么也没有

 

比较范围

【compareBoundaryPoints()】

  在有多个范围的状况下,可使用compareBoundaryPoints()方法来肯定这些范围是否有公共的边界(起点或终点)。这个方法接受两个参数:表示比较方式的常量值和要比较的范围。表示比较方式的常量值以下所示

Range.START_TO_START(0):比较第一个范围和第二个范围的起点
Range.START_TO_END(1):比较第一个范围的起点和第二个范围的终点
Range.END_TO_END⑵:比较第一个范围和第二个范围的终点
Range.END_T0_START(3):比较第一个范围的终点和第一个范围的起点

  compareBoundaryPoints()方法可能的返回值以下:若是第一个范围中的点位于第二个范围中的点以前,返回-1;若是两个点相等,返回0;若是第一个范围中的点位于第二个范围中的点以后,返回1。来看下面的例子

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

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

alert(range1.compareBoundaryPoints(Range.START_TO_START, range2));  //outputs 0
alert(range1.compareBoundaryPoints(Range.END_TO_END, range2));      //outputs 1

  在这个例子中,两个范围的起点其实是相同的,由于它们的起点都是由selectNodeContents()方法设置的默认值来指定的。所以,第一次比较返回0。可是,range2的终点因为调用setEndBefore()已经改变了,结果是range1的终点位于range2的终点后面,所以第二次比较返回1

 

复制范围

【cloneRange】

  可使用cloneRange()方法复制范围。这个方法会建立调用它的范围的一个副本

var newRange = range.cloneRange();

  新建立的范围与原来的范围包含相同的属性,而修改它的端点不会影响原来的范围

 

清理范围

【detach()】

  在使用完范围以后,最好是调用detach()方法,以便从建立范围的文档中分离出该范围。调用detach()以后,就能够放心地解除对范围的引用,从而让垃圾回收机制回收其内存了。来看下面的例子

range.detach();//从文档中分离
range = null;//解除引用

  在使用范围的最后再执行这两个步骤是推荐的方式。一且分离范围,就不能再恢复使用了

 

IE兼容

  IE8-浏览器不支持DOM范围。但支持一种相似的概念,即文本范围(textrange)。文本范围是IE专有的特性,其余浏览器都不支持。顾名思义,文本范围处理的主要是文本(不必定是DOM节点)。经过<body>、<button>、<input>和<textarea>等这几个元素,能够调用createTextRange()方法来建立文本范围。如下是一个例子

var range = document.body.createTextRange();

  像这样经过document建立的范围能够在页面中的任何地方使用(经过其余元素建立的范围则只能在相应的元素中使用)。与DOM范围相似,使用IE文本范围的方式也有不少种

1、简单选择

【findText()】

  选择页面中某一区域的最简单方式,就是使用范围的findText()方法。这个方法会找到第一次出现的给定文本,并将范围移过来以环绕该文本。若是没有找到文本,这个方法返回false;不然返回true。一样,仍然如下面的HTML代码为例

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

  要选择"Hello",可使用下列代码

var range = document.body.createTextRange();
var found = range.findText("Hello");

  执行完第二行代码后,文本"Hello"就被包围在范围以内了。为此,能够检査范围的text属性来确认(这个属性返回范围中包含的文本),或者也能够检查findText()的返回值——在找到了文本的状况下返回值为true。例如:

alert(found); //true
alert(range.text);    //"Hello"

  还能够为findText()传入另外一个参数,即一个表示向哪一个方向继续搜索的数值。负值表示应该从当前位置向后搜索,而正值表示应该从当前位置向前搜索。所以,要査找文档中前两个"Hello"的实例,应该使用下列代码

var found = range.findText("Hello");
var foundAgain = range.findText("Hello", 1);

【moveToElementText()】

  IE中与DOM中的selectNode()方法最接近的方法是moveToElementText(),这个方法接受一个DOM元素,并选择该元素的全部文本,包括HTML标签。下面是一个例子

var range = document.body.createTextRange();
var p1 = document.getElementById("p1")
range.moveToElementText(p1);

  在文本范围中包含HTML的状况下,可使用htmlText属性取得范围的所有内容,包括HTML和文本,以下所示

alert(range.htmlText);

  IE的范围没有任何属性能够随着范围选区的变化而动态更新。不过,parentElement()方法却是与DOM的 commonAncestorContainer属性相似

var ancestor = range.parentElement();

  这样获得的父元素始终均可以反映文本选区的父节点

2、复杂选择

  在IE中建立复杂范围的方法,就是以特定的增量向四周移动范围。为此,IE提供了4个方法:move()、moveStart()、moveEnd()和expand()。这些方法都接受两个参数:移动单位和移动单位的数量。其中,移动单位是下列一种字符串值

"character":逐个字符地移动
"word":逐个单词(一系列非空格字符)地移动
"sentence":逐个句子(一系列以句号、问号或叹号结尾的字符)地移动
"textedit":移动到当前范围选区的开始或结束位置

  经过moveStart()方法能够移动范围的起点,经过moveEnd()方法能够移动范围的终点,移动的幅度由单位数量指定,以下所示

range.moveStart("word", 2);//起点移动2个单词
range.moveEnd("character", 1);//终点移动1个字符

  使用expand()方法能够将范围规范化。换句话说,expand()方法的做用是将任何部分选择的文本所有选中。例如,当前选择的是一个单词中间的两个字符,调用expand("word")能够将整个单词都包含在范围以内

  而move()方法则首先会折叠当前范围(让起点和终点相等),而后再将范围移动指定的单位数量,以下所示

range.move("character", 5);//移动5个字符

  调用move()以后,范围的起点和终点相同,所以必须再使用moveStart()或moveEnd()建立新的选区

3、操做范围内容

  在IE中操做范围中的内容可使用text属性或pasteHTML()方法。如前所述,经过text属性能够取得范围中的内容文本;可是,也能够经过这个属性设置范围中的内容文本。来看一个例子

var range = document.body.createTextRange();
range.findText("Hello");
range.text = "Howdy";

  若是仍之前面的Hello World代码为例,执行以上代码后的HTML代码以下

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

  [注意]在设置text属性的状况下,HTML标签保持不变

【pasteHTML()】

  要向范围中插入HTML代码,就得使用pasteHTML()方法,以下所示

var range = document.body.createTextRange();
range.findText("Hello");
range.pasteHTML("<em>Howdy</em>");

  执行这些代码后,会获得以下HTML

<p id="p1"><b><em>Howdy</em></b> world!</p>

  不过,在范围中包含HTML代码时,不该该使用pasteHTML(),由于这样很容易致使不可预料的结果——极可能是格式不正确的HTML

4、折叠IE范围

【collapse()】

  IE为范围提供的collapse()方法与相应的DOM方法用法同样:传入true把范围折叠到起点,传入false把范围折叠到终点。例如:

range.collapse(true);    //折叠到起点

  惋惜的是,没有对应的collapsed属性让咱们知道范围是否已经折叠完毕。为此,必须使用boundingWidth属性,该属性返回范围的宽度(以像素为单位)。若是boundingwidth属性等于0,就说明范围已经折叠了

var isCollapsed = (range.boundingWidth == 0);

  此外,还有boundingHeight、boundingLeft和boundingTop等属性,虽然它们都不像boundingWidth那么有用,但也能够提供一些有关范围位置的信息

5、比较IE范围

【compareEndPoints()】

  IE中的compareEndPoints()方法与DOM范围的compareBoundaryPoints()方法相似。这个方法接受两个参数:比较的类型和要比较的范围。比较类型的取值范围是下列几个字符串值:"StartToStart"、"StartToEnd"、"EndToEnd"和"EndToStart"。这几种比较类型与比较DOM范围时使用的几个值是相同的

  一样与DOM相似的是,compareEndPoints()方法也会按照相同的规则返回值,即若是第一个范围的边界位于第二个范围的边界前面,返回-1;若是两者边界相同,返回0;若是第一个范围的边界位于第二个范围的边界后面,返回1。仍之前面的Hello World代码为例,下列代码将建立两个范围,一个选择"Hello world!"(包括<b>标签),另外一个选择"Hello"

var range1 = document.body.createTextRange();
var range2 = document.body.createTextRange();

range1.findText("Hello world!");
range2.findText("Hello");

alert(range1.compareEndPoints("StartToStart", range2)); //outputs 0
alert(range1.compareEndPoints("EndToEnd", range2)); //outputs 1;

  因为这两个范围共享同一个起点,因此使用compareEndPoints()比较起点返回0。而range1的终点在range2的终点后面,因此compareEndPoints()返回1

  IE中还有两个方法,也是用于比较范围的:isEqual()用于肯定两个范围是否相等,inRange()用于肯定一个范围是否包含另外一个范围。下面是相应的示例

var range1 = document.body.createTextRange();
var range2 = document.body.createTextRange();
range1.findText("Hello world!");
range2.findText("Hello");
alert("range1.isEqual(range2): " + range1.isEqual(range2));//false
alert("range1.inRange(range2): " + range1.inRange(range2));//true

  这个例子使用了与前面相同的范围来示范这两个方法。因为这两个范围的终点不一样,因此它们不相等,调用isEqual返回false。因为range2实际位于rangel内部,它的终点位于后者的终点以前、起点以后,因此range2被包含在rangel内部,调用inRange()返回true

6、复制IE范围

  在IE中使用duplicate()方法能够复制文本范围,结果会建立原范围的一个副本,以下所示

var newRange = range.duplicate();

  新建立的范围会带有与原范围彻底相同的属性

相关文章
相关标签/搜索