盒子
盒子

Range对象详解

Range对象详解

  1. 什么是Range对象?

Range表示HTML文档的一部分内容,它可以在任何点开始和结束,最常见的Range就是用户选择的一段文本。通过Range对象,你可以找到开始点和结束点,你可以复制或者删除它,或者替换成另一段文本,甚至是一段HTML代码。

获取Range对象有三种方法:

  • document.createRange 方法

    1
    var range = document.createRange();
  • Selection对象的getRangeAt方法

    1
    2
    var selection = window.getSelection() ; // 获取用户选中的内容Selection对象
    var range = selection.getRangeAt( 0 ) ; // 从Selection对象获取Range对象
  • Range的构造函数来创建一个Range对象

    1
    var range = new Range() ;

下面介绍一下Range对象(实例)的属性:

  1. Range.collapsed 只读,返回一个判断当前Range对象的起始位置和终止位置是否相同的布尔值。

    例1-1:

    1
    2
    3
    var selection = window.getSelection() ; // Selection对象
    var range = selection.getRangeAt( 0 ) ; // W3C Range对象
    console.log( range.collapsed ) ;

    range-collapsed

    当选中’年底’二字的时候,起始位置和终止位置不同,返回false;当光标在’年底’之后,起始位置和终止位置相同,collapsed返回true。

  2. commonAncestorContainer只读,返回包含startContainerendContainer的最深的节点

    一般为文本节点,比如选中’年底’,返回的是包含’年底’的那个文本节点。

    注意⚠️:返回的文本节点,是包含年底二字的文本节点,’每到年底…’整段都是那个文本节点。如果Range对象跨标签,<p>每到年底,很多新闻</p><span>比如洞察宇宙的</span>当选中很多新闻</p><span>比如这样子的文本,返回的最深节点为包含p标签和span标签的共同父标签。

  3. startContainer/endContainer只读,返回Range对象开始和终止的节点对象。

    一般选中文字,返回的都是文本节点

  4. startOffset/endOffset只读,表示Range起始位置和终止位置的数字

    例1-2:

    1
    2
    3
    var selection = window.getSelection() ; // Selection对象
    var range = selection.getRangeAt( 0 ) ; // W3C Range对象
    console.log( range.startOffset , range.endOffset )

    range-offsetStart

    ​ 当Range.collapsed为true是,startOffset和endOffset值相等。

下面介绍一下Range对象拥有的方法:

1、Range.setStart()/Range.setEnd()设置Range的起点和终点,这两个方法都接受两个参数Node和offset;如果起始节点是Text,Comment, or CDATASection之一,那么offset指的是从起始节点算起的字符的偏移量,对于其他Node类型节点,offset是指从起始节点开始算起子节点的偏移量[^2]。

例1-3:

1
2
3
4
5
6
7
8
9
10
var selection = window.getSelection() ;
var range = document.createRange() ;
var startNode = document.getElementsByTagName( 'p' )[0].firstChild ; // 获取文本节点
var startOffset = 1 ;
range.setStart(startNode, startOffset) ;
range.setEnd(startNode, startOffset + 4 ) ;
selection.removeAllRanges() ;
selection.addRange( range ) ;

range-setStart

设置startOffset为1,从’每’第一个字符后开始,设置endOffset为,起始之后的4个字符,逗号之后。最后通过Selection对象的removeAllRanges()addRange()方法,建立拖蓝^1的文本选区。Selection对象的API可以看MDN的文档,见下面的参考资料超链接。

2、Range.setStartBefore()/Range.setStartAfter()/Range.setEndBefore()/Range.setEndAfter()以其他节点之前,之后为基准,设置Range对象的起止点。这四个方法共同接受一个参数,referenceNode。

比如,你想要选中<p>每到年底,很多新闻机构都会评选年度新闻。小编注意到,习主席今年贺词提到的具体成就格外关注科技和创新。</p>整个p标签的文本,可以:

例1-4

1
2
3
4
5
6
7
8
9
var selection = window.getSelection() ;
var range = document.createRange() ;
var pTextNode = document.getElementsByTagName( 'p' )[0].firstChild ;
// 设置setStartBefore和setEndAfter为共同的一个文本节点即可选中这个标签的所有文本
range.setStartBefore( pTextNode ) ;
range.setEndAfter( pTextNode ) ;
selection.removeAllRanges() ;
selection.addRange( range ) ;

range-setStartBefore

3、Range.selectNode()设置Range对象包含指定的Node节点和它的内容,参数为需要选中的Node,referenceNode;执行之后,Range对象的startContainer和endContainer为传入的referenceNode的父元素。

4、Range.selectNodeContents()设置Range对象包含指定Node节点的内容,例1-4的代码可改写成如下:

例1-5:

1
2
3
4
5
6
7
8
var selection = window.getSelection() ;
var range = document.createRange() ;
var pTextNode = document.getElementsByTagName( 'p' )[0] ;
// setStartBefore和setEndAfter去掉,换成selectNodeContents
range.selectNodeContents( pTextNode ) ;
selection.removeAllRanges() ;
selection.addRange( range ) ;

细心的你可能发现第三行代码,最后的获取的节点不一样了,有firstChild的获取的是text节点,没有的,获取的是element节点。setStartBefore传入的参数若是一个element节点,则Range对象会包含这个element节点,因为选区的内容是从element节点之前开始的。倘如传入的是element节点下面的text节点,则Range对象不会包含这个element节点,单单只包含text节点的内容。

调用了range.selectNodeContents方法之后:

The parent Node of the start and end of the Range will be the reference node. The startOffset is 0, and the endOffset is the number of child Nodes or number of characters contained in the reference node.

翻译一下,意思大概:

Range对象的开始节点和结束节点的父节点为传入的reference node,起始偏移量为0,结束偏移量为子节点的个数[^2]或者reference node节点包含的文本字符数。

我测试了一下,当执行上面的代码之后:console.log( range.startContainer === pTextNode , range.endContainer === pTextNode )

打印了一下,true true,结果两个都是为true。正确意思应该是startContainer和endContainer都应该是reference node,而不是它们的父节点是reference node。这个和Range.selectNode的方法实现有点不一样,Range.selectNode执行完之后,startContainer和endContainer代表的是reference node的父元素。

4、Range.collpase()移动Range对象的开始或者结束节点,到另外一个边界点。即,折叠开始节点到结束节点,或者折叠结束节点到开始节点,让开始和结束节点重合。这个方法接受一个布尔值的参数,如果为true,则折叠结束节点到开始节点,false,折叠开始节点到结束节点。折叠之后的Range对象是空的,没有内容,在dom树中指定了一个单一的入口节点。判断一个Range对象是否已经被折叠过了,可以查看Range.collapsed属性。

例1-6:

1
2
3
4
5
6
7
8
9
var selection = window.getSelection() ;
var range = document.createRange() ;
var pTextNode = document.getElementsByTagName( 'p' )[0].firstChild ;
range.selectNode( pTextNode ) ;
range.collapse( false ) ;
selection.removeAllRanges() ;
selection.addRange( range ) ;

range-collpased

光标选中了pTextNode的句号之后的最后一部分。

以上为定位方法,下面介绍编辑方法:

5、Range.cloneContents()返回DocumentFragment类型的Range中节点的文档片段。事件监听器不会被复制,html的属性中绑定的事件会被复制,id也会被复制,所以处理的时候要小心。

例1-7:

1
2
3
4
5
6
7
8
var selection = window.getSelection() ;
var range = document.createRange() ;
var pTextNode = document.getElementsByTagName( 'p' )[0] ;
range.selectNode( pTextNode ) ;
var cloned = range.cloneContents() ;
document.body.appendChild( cloned ) ;

range-cloneContents

被复制出来的节点内容是:<p>每到年底,很多新闻机构都会评选年度新闻。小编注意到,习主席今年贺词提到的具体成就格外关注科技和创新。</p>包含标签。

6、Range.deleteContents()从文档中移除Range选中的内容,无返回值。

7、Range.extractContents()Range的内容从文档树中提取到文档片段中。返回的也是DocumentFragment类型。

例1-8:

1
2
3
4
5
6
7
8
var selection = window.getSelection() ;
var range = document.createRange() ;
var pTextNode = document.getElementsByTagName( 'p' )[0] ;
range.selectNode( pTextNode ) ;
var extracted = range.extractContents() ;
document.body.appendChild( extracted ) ;

range-extractContents

注意⚠️:如果Range对象跨标签,extractContents方法会返回补齐后的标签,比如<p>abc</p><span>efg</span>如果选中c</p><span>e的话,返回的的DocumentFragment中内容是:<p>c</p><span>e</span>

8、Range.insertNode()在起点处插入节点。该方法接受一个参数,newNode,要插入的节点。

例1-9:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var selection = window.getSelection() ; // Selection对象
var range = document.createRange() ;
var pTextNode = document.getElementsByTagName( 'p' )[0] ;
range.selectNode( pTextNode ) ;
var text = '我是新的内容' ,
newNode = document.createTextNode( text ) ;
range.insertNode( newNode ) ;
range.setStart( newNode , text.length ) ;
range.collapse( true ) ;
selection.removeAllRanges() ;
selection.addRange( range ) ;

range-insertNode

newNode可以是documentFragment类型的节点。

9、Range.surroundContents接收一个Node类型的参数newNode。把Range对象的内容移动到newNode中,并且,将newNode放入当前Range对象的开始位置。包裹之后,Range对象的边界在也会受newNode影响。

例1-10:

1
2
3
4
5
6
var selection = window.getSelection() ; // Selection对象
var range = document.createRange() ;
var pTextNode = document.getElementsByTagName( 'p' )[0] ;
range.selectNode( pTextNode ) ;
range.surroundContents( document.createElement('div') ) ; // 用div包裹p标签

range-surroundContents

An exception will be thrown, however, if the Range splits a non-Text node with only one of its boundary points. That is, unlike the alternative above, if there are partially selected nodes, they will not be cloned and instead the operation will fail.

也有例外情况, 如果选定的Range区域包含仅有一个节点标签的Text. 那标签将不会自动成对生成,操作将失败.

其他一些方法:

10、Range.compareBoundaryPoints()接受两个参数,第一个参数是how,第二个参数是Range类型的sourceRange,指示当前Range的相应边界点是否分别在sourceRange的对应边界点之前,等于或者之后。返回一个数字类型的值,-1,0,1。具体参数配置,请移步MDN Range.compareBoundaryPoints()

例1-11:

1
2
3
4
5
6
7
8
9
10
11
var range = document.createRange() ;
var range2 = document.createRange() ;
var pTextNode = document.getElementsByTagName( 'p' )[0] ;
var span = document.getElementsByTagName( 'span' )[0] ; // span 标签毗邻p标签之后
range.selectNode( pTextNode ) ;
range2.selectNode( span ) ;
var compare = range2.compareBoundaryPoints( Range.END_TO_END , range ) ;
console.log( compare ) // 返回 1 说明range2的end确实在range的end之后。

11、Range.clone()返回拥有和原 Range 相同端点的克隆 Range 对象。

The returned clone is copied by value, not reference, so a change in either Range does not affect the other.

返回的clone对象拷贝了值,不是返回原有range的引用,所以改变其中的一个range不会影响另外一个。

12、Range.detach()从使用状态释放 Range,此方法用于改善性能。

Subsequent attempts to use the detached range will result in a DOMException being thrown with an error code of INVALID_STATE_ERR.

随后尝试使用已经释放的range会抛出一个DOMException的异常,错误状态码:INVALID_STATE_ERR。

13、Range.toString()Range对象的内容作为字符串返回。alert( Range对象 )默认调用的就是toString方法,弹出选中的内容。

还有一些实验性的方法,不推荐在生产环境中使用:

14、Range.getBoundingClientRect()返回一个 ClientRect 对象,该对象限定了选定的文档对象的内容,该方法返回了一个矩形,这个矩形包围了该文档对象中所有元素的边界矩形集合。

This is method is useful for determining the viewport coordinates of the cursor or selection inside a text box. See Element.getBoundingClientRect() for the details on the returned value.

当想获取视图窗口的坐标系时,这个方法还是挺有用的。返回当前Range的top,right,bottom,left,width,height。

15、Range.getClientRects()返回一系列包含ClientReact对象的list。每个ClientRect对象代表在当前Range对象里的元素的ClientRect。

The Range.getClientRects()** **method returns a list of ClientRect objects representing the area of the screen occupied by the range. This is created by aggregating the results of calls to Element.getClientRects() for all the elements in the range.

16、其他方法,比如:Range.compareNode()Range.comparePoint()Range.createContextualFragment()Range.intersectsNode()Range.isPointInRange()都是Gecko 内核的方法,可移步MDN Web Api接口 Range

[^2]: 当range的开始节点或者结束节点不是文本节点的时候,偏移量代表想要选中的子节点个数的数字,不能超过子节点的长度,MDN上解释如下:If the endNode is a Node of type Text, Comment, or CDATASection, then endOffset is the number of characters from the start of endNode. For other Node types, endOffsetis the number of child nodes between the start of the endNode.

参考资料

1、MDN Web Api接口 Range

2、MDN Web Api接口 Selection

2、JavaScript 获取输入时的光标位置及场景问题

支持一下
扫一扫,支持lcoder