AbstractRange

Baseline 已广泛支持

此特性已得到良好支持,可在多种设备和浏览器版本上使用。自 2021 年 4 月起,所有浏览器均已支持此特性。

AbstractRange 抽象接口是定义所有 DOM 范围类型的基础类。范围(range)是一个对象,用于指示文档中内容部分的起始点和结束点。

注意: 作为一个抽象接口,你不能直接实例化 AbstractRange 类型的对象。你应该使用 RangeStaticRange 接口。要了解这两个接口的区别,以及如何根据你的需求选择合适的接口,请查阅每个接口的文档。

实例属性

collapsed 只读

一个布尔值,如果范围是折叠的(collapsed),则为 true。折叠的范围是指其开始位置和结束位置相同的范围,从而得到一个零字符长度的范围。

endContainer 只读

endOffset 属性指定的范围结束点所在的 Node 对象。

endOffset 只读

一个整数值,表示范围对象所表示的范围的最后一个字符,相对于节点内容的开始位置的偏移量(以字符为单位)。此值必须小于 endContainer 节点 的长度。

startContainer 只读

startOffset 属性指定的范围开始点所在的 DOM Node

startOffset 只读

一个整数值,表示范围对象所引用的内容的第一个字符,相对于节点内容的开始位置的偏移量(以字符为单位)。此值必须小于 startContainer 中指示的节点 的长度。

实例方法

AbstractRange 接口不提供任何方法。

用法说明

范围类型

document 中所有内容范围都使用基于 AbstractRange 的接口实例来描述。有以下两种接口:

Range

Range 接口存在已久,最近才被重新定义为基于 AbstractRange,以满足定义其他形式范围数据的需求。Range 提供了允许你修改范围端点的方法,以及用于比较范围、检测范围交叉等的方法。

StaticRange

StaticRange 是一个基础范围,一旦创建就无法更改。具体来说,随着节点树的变异和改变,该范围保持不变。这在你只需要一次性使用某个范围时非常有用,因为它避免了更复杂的 Range 接口带来的性能和资源影响。

元素的內容

在尝试访问元素的内容时,请记住,元素本身是一个节点,它内部的任何文本也是一个节点。为了在元素的文本内设置范围端点,请确保找到元素内部的文本节点。

js
const startElem = document.querySelector("p");
const endElem = startElem.querySelector("span");
const range = document.createRange();

range.setStart(startElem, 0);
range.setEnd(endElem, endElem.childNodes[0].length / 2);
const contents = range.cloneContents();

document.body.appendChild(contents);

此示例创建一个新的范围 range,并将其起始点设置为第一个元素的第三个子节点。结束点被设置为 span 的第一个子节点的中间位置,然后使用该范围复制范围的内容。

范围与 DOM 的层级结构

为了能够跨越零个或多个节点边界来定义文档中的字符范围,并且尽可能地对 DOM 更改具有弹性,你不能在 HTML 中直接指定第一个和最后一个字符的偏移量。这有几个很好的原因。

首先,在页面加载完成后,浏览器不再以 HTML 的形式思考。一旦加载完成,页面就是一个 DOM Node 对象树,因此你需要使用节点和节点内的位置来指定范围的开始和结束位置。

其次,为了尽可能地支持 DOM 树的可变性,你需要一种方式来表示相对于树中节点的位置,而不是相对于整个文档的全局位置。通过将文档中的点定义为给定节点内的偏移量,即使节点被添加到、从 DOM 树中移除或在 DOM 树中移动(在合理范围内),这些位置也能与内容保持一致。当然存在一些明显的限制(例如,如果一个节点被移动到范围的结束点之后,或者节点的内容被严重修改),但这比什么都没有要好得多。

第三,使用节点相对位置来定义开始和结束位置通常更容易获得高性能。用户代理(浏览器)不需要遍历 DOM 来确定全局偏移量指的是什么,而是可以直接定位到起始位置指示的节点,然后从那里开始,向前移动直到到达结束节点中给定的偏移量。

为了说明这一点,请考虑下面的 HTML

html
<div class="container">
  <div class="header">
    <img src="" class="sitelogo" />
    <h1>The Ultimate Website</h1>
  </div>
  <article>
    <section class="entry" id="entry1">
      <h2>Section 1: An interesting thing…</h2>
      <p>A <em>very</em> interesting thing happened on the way to the forum…</p>
      <aside class="callout">
        <h2>Aside</h2>
        <p>An interesting aside to share with you…</p>
      </aside>
    </section>
  </article>
  <pre id="log"></pre>
</div>

加载 HTML 并构建文档的 DOM 表示后,生成的 DOM 树如下所示:

Diagram of the DOM for a simple web page

在此图中,代表 HTML 元素的节点显示为绿色。它们下方的每一行显示了 DOM 树中下一层级的深度。蓝色节点是文本节点,包含屏幕上显示的文本。每个元素的子节点都链接在其下方,可能通过元素包含其他元素和文本节点,从而在下方形成一系列分支。

如果你想创建一个包含 <p> 元素(其内容是 "A <em>very</em> interesting thing happened on the way to the forum…")的范围,可以这样做:

js
const pRange = document.createRange();
pRange.selectNodeContents(document.querySelector("#entry1 p"));

由于我们希望选择 <p> 元素及其子元素的全部内容,所以这样做是完美的。

如果我们希望复制 <section> 的标题(一个 h2 元素)中的文本“An interesting thing…”一直到下面段落中 <em> 中的字母“ve”结束,则以下代码会起作用:

js
const range = document.createRange();
const startNode = document.querySelector("section h2").childNodes[0];
range.setStart(startNode, 11);

const endNode = document.querySelector("#entry1 p em").childNodes[0];
range.setEnd(endNode, 2);

const fragment = range.cloneContents();

这里出现了一个有趣的问题——我们正在捕获来自 DOM 层级中不同位置的多个节点的内容,并且只捕获其中一部分。结果应该是什么样的?

事实证明,DOM 规范恰好解决了这个问题。例如,在这种情况下,我们正在调用范围的 cloneContents() 方法来创建一个新的 DocumentFragment 对象,该对象提供了一个 DOM 子树,复制了指定范围的内容。为了做到这一点,cloneContents() 会构建所有必要节点来保留指定范围的结构,但不会多余。

在此示例中,指定范围的开始点位于 section 标题下方的文本节点中,这意味着新的 DocumentFragment 需要包含一个 h2,并在其下方包含一个文本节点。

范围的结束点位于 <p> 元素下方,因此它需要包含在新片段中。包含“A”的文本节点也需要,因为它包含在范围内。最后,一个 <em> 元素及其下方的文本节点也将被添加到 <p> 元素下方。

文本节点的具体内容由调用 setStart()setEnd() 时给定的文本节点内的偏移量决定。由于标题文本的偏移量为 11,因此该节点将包含“An interesting thing…”。同样,根据对结束节点前两个字符的要求,最后一个文本节点将包含“ve”。

生成的文档片段如下所示:

A DocumentFragment representing the cloned content

特别要注意的是,此片段的内容都位于其内顶层节点共享公共父节点的下方。父节点 <section> 不需要复制克隆的内容,因此未包含在内。

示例

考虑以下简单的 HTML 片段:

html
<p><strong>This</strong> is a paragraph.</p>

假设使用 Range 来提取其中的单词“paragraph”。实现此目的的代码如下所示:

js
const paraNode = document.querySelector("p");
const paraTextNode = paraNode.childNodes[1];

const range = document.createRange();
range.setStart(paraTextNode, 6);
range.setEnd(paraTextNode, paraTextNode.length - 1);

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

首先,我们获取对段落节点本身以及段落内第二个子节点的引用。第一个子节点是 <strong> 元素。第二个子节点是文本节点“ is a paragraph.”。

有了文本节点的引用后,我们通过在 Document 本身上调用 createRange() 来创建一个新的 Range 对象。我们将范围的起始位置设置为文本节点字符串的第六个字符,结束位置设置为文本节点字符串的长度减一。这会将范围设置为包含单词“paragraph”。

然后,我们通过调用 Range 上的 cloneContents() 来完成操作,创建一个新的 DocumentFragment 对象,该对象包含范围所覆盖的文档部分。之后,我们使用 appendChild() 将该片段添加到文档的 body 末尾,该 body 通过 document.body 获取。

结果如下所示

规范

规范
DOM
# interface-abstractrange

浏览器兼容性