JavaScript Selection API详解
JavaScript Selection API 是用于操作和获取用户在网页中选中的文本内容的接口。它提供了一系列方法和属性,使开发者能够获取、修改和监控用户在页面上的文本选择,可以用于实现富文本编辑器、文本高亮、自定义选择逻辑等功能。以下是 Selection API 的详细解析:
1. 核心概念
Selection API 主要涉及以下几个核心对象和概念:
Selection对象:表示用户在页面中选中的文本范围。可以通过window.getSelection()获取当前的 Selection 对象。Range对象:表示文档中的一个连续范围,可以包含部分或全部节点。Selection 对象可以包含一个或多个 Range 对象。- 锚点(
Anchor)和焦点(Focus):Selection 对象的起始点称为锚点,结束点称为焦点。如果用户从左向右选择文本,锚点在左,焦点在右;反之亦然。
2. Selection 对象的属性
| 属性 | 描述 |
|---|---|
anchorNode | 返回 Selection 的锚点所在的节点。 |
anchorOffset | 返回 Selection 的锚点在 anchorNode 中的偏移量。 |
focusNode | 返回 Selection 的焦点所在的节点。 |
focusOffset | 返回 Selection 的焦点在 focusNode 中的偏移量。 |
isCollapsed | 如果 Selection 的起始点和结束点相同,返回 true,表示没有选中任何文本。 |
rangeCount | 返回 Selection 中包含的 Range 对象的数量。 |
type | 返回 Selection 的类型,如 "None"、"Caret" 或 "Range"。 |
3. Selection 对象的方法
| 方法 | 描述 |
|---|---|
getRangeAt(index) | 返回 Selection 中指定索引的 Range 对象。 |
addRange(range) | 向 Selection 中添加一个 Range 对象。 |
removeRange(range) | 从 Selection 中移除一个 Range 对象。 |
removeAllRanges() | 移除 Selection 中的所有 Range 对象。 |
collapse(node, offset) | 将 Selection 的起始点和结束点移动到指定的节点和偏移量。 |
extend(node, offset) | 将 Selection 的焦点移动到指定的节点和偏移量。 |
selectAllChildren(node) | 将 Selection 的范围扩展到包含指定节点的所有子节点。 |
deleteFromDocument() | 从文档中删除 Selection 的内容。 |
toString() | 返回 Selection 的文本内容。 |
4. Selection 对象使用示例
4.1. 示例:获取选中的文本
const selection = window.getSelection();
if (!selection.isCollapsed) {const selectedText = selection.toString();console.log("选中的文本:", selectedText);
}
4.2. 示例:修改选中的文本
const selection = window.getSelection();
if (!selection.isCollapsed) {const range = selection.getRangeAt(0);const newTextNode = document.createTextNode("新文本");range.deleteContents(); // 删除选中的内容range.insertNode(newTextNode); // 插入新文本
}
4.3. 监控 Selection 的变化
可以通过监听 selectionchange 事件来监控 Selection 的变化:
document.addEventListener("selectionchange", () => {const selection = window.getSelection();console.log("Selection 变化:", selection.toString());
});
5. Range 对象的属性
| 属性 | 描述 |
|---|---|
startContainer | 返回 Range 的起始节点。 |
startOffset | 返回 Range 的起始节点中的偏移量。 |
endContainer | 返回 Range 的结束节点。 |
endOffset | 返回 Range 的结束节点中的偏移量。 |
collapsed | 如果 Range 的起始点和结束点相同,返回 true。 |
commonAncestorContainer | 返回包含 startContainer 和 endContainer 的最深层的共同祖先节点。 |
6. Range 对象的方法
6.1 设置 Range 的范围
| 方法 | 描述 |
|---|---|
setStart(node, offset) | 设置 Range 的起始点为指定节点和偏移量。 |
setEnd(node, offset) | 设置 Range 的结束点为指定节点和偏移量。 |
setStartBefore(node) | 将 Range 的起始点设置为指定节点之前。 |
setStartAfter(node) | 将 Range 的起始点设置为指定节点之后。 |
setEndBefore(node) | 将 Range 的结束点设置为指定节点之前。 |
setEndAfter(node) | 将 Range 的结束点设置为指定节点之后。 |
示例:
const range = new Range();
const element = document.getElementById("example");
range.setStart(element, 0); // 将起始点设置为 element 的第一个子节点之前
range.setEnd(element, 1); // 将结束点设置为 element 的第一个子节点之后
6.2 选择节点
| 方法 | 描述 |
|---|---|
selectNode(node) | 将 Range 设置为包含指定节点及其所有子节点。 |
selectNodeContents(node) | 将 Range 设置为包含指定节点的所有子节点。 |
示例:
const range = new Range();
const element = document.getElementById("example");
range.selectNode(element); // 选择整个 element 节点
// 或
range.selectNodeContents(element); // 选择 element 的所有子节点
6.3 操作 Range 的内容
| 方法 | 描述 |
|---|---|
deleteContents() | 删除 Range 所包含的内容。 |
extractContents() | 从文档中提取 Range 的内容,并返回一个 DocumentFragment。 |
cloneContents() | 克隆 Range 的内容,并返回一个 DocumentFragment。 |
insertNode(node) | 在 Range 的起始点插入一个节点。 |
surroundContents(newParent) | 将 Range 的内容包裹在一个新的父节点中。 |
示例:
const range = new Range();
const element = document.getElementById("example");
range.selectNodeContents(element);
range.deleteContents(); // 删除 element 的所有子节点
// 或
const fragment = range.extractContents(); // 提取内容
// 或
const newNode = document.createElement("span");
range.insertNode(newNode); // 在 Range 的起始点插入新节点
6.4 比较 Range 的位置
| 方法 | 描述 |
|---|---|
compareBoundaryPoints(type, otherRange) | 比较两个 Range 的边界点。type 可以是:Range.START_TO_START、Range.END_TO_END、Range.START_TO_END、Range.END_TO_START。 |
isPointInRange(node, offset) | 检查指定的节点和偏移量是否在 Range 内。 |
intersectsNode(node) | 检查 Range 是否与指定节点相交。 |
示例:
const range1 = new Range();
const range2 = new Range();
const result = range1.compareBoundaryPoints(Range.START_TO_START, range2);
console.log(result); // -1, 0, 或 1
6.5 克隆和拓展 Range
| 方法 | 描述 |
|---|---|
cloneRange() | 克隆一个 Range 对象。 |
collapse(toStart) | 将 Range 的结束点移动到起始点。toStart 为 true 时,Range 将折叠到起始点;为 false 时,折叠到结束点。 |
detach() | 释放 Range 对象,使其不再引用文档。 |
示例:
const range = new Range();
range.setStart(element, 0);
range.setEnd(element, 1);
const clonedRange = range.cloneRange(); // 克隆 Range
range.collapse(true); // 折叠到起始点
7. 综合示例:使用JavaScript Selection API修改文档内容
以下示例展示两行文本和一个加粗按钮,任意选择部分文本,按下加粗按钮,选中的文本将被一个class属性为new-strong的strong结点包围,且原有的html标签结构不会被破坏。
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>加粗选中文本</title><style>button {margin: 10px;padding: 5px 10px;}.new-strong {color: green;}</style>
</head>
<body><div>第一块<span>234<span>567</span></span></div><div>第二块</div><button id="boldButton">加粗</button><script>document.getElementById("boldButton").addEventListener("click", function() {const selection = window.getSelection();if (selection.rangeCount > 0) {const range = selection.getRangeAt(0);if (!range.collapsed) {// 处理选中的文本surroundSelectedTextWithNewNode(range, "strong", "new-strong");// 清除选中状态selection.removeAllRanges();}}});function surroundSelectedTextWithNewNode(range, tagName, className) {// 获取选中的文本节点const textNodes = getTextNodesInRange(range);// 对每个文本节点进行处理textNodes.forEach(node => {const parent = node.parentNode;const text = node.nodeValue;const startOffset = node === range.startContainer ? range.startOffset : 0;const endOffset = node === range.endContainer ? range.endOffset : text.length; const newNode = document.createElement(tagName);newNode.classList.add(className);if (startOffset > 0 || endOffset < text.length) {// 截取被选中的部分const beforeText = text.substring(0, startOffset);const selectedText = text.substring(startOffset, endOffset);const afterText = text.substring(endOffset);// 创建新的文本节点const beforeNode = document.createTextNode(beforeText);const afterNode = document.createTextNode(afterText);newNode.appendChild(document.createTextNode(selectedText));// 替换原文本节点parent.insertBefore(beforeNode, node);parent.insertBefore(newNode, node);parent.insertBefore(afterNode, node);parent.removeChild(node);} else {// 整个文本节点被选中newNode.appendChild(node.cloneNode(true));parent.insertBefore(newNode, node);parent.removeChild(node);}// 控制台输出当前结点修改后的html以观察效果if(parent !== document.body){console.log(parent.outerHTML);}});}function getTextNodesInRange(range) {const textNodes = [];const startNode = range.startContainer;const endNode = range.endContainer;const startOffset = range.startOffset;const endOffset = range.endOffset;// 从 startNode 开始遍历,直到 endNodelet currentNode = startNode;while (currentNode && currentNode !== endNode) {if (currentNode.nodeType === Node.TEXT_NODE) {textNodes.push(currentNode);}currentNode = getNextNode(currentNode);}// 添加 endNode(如果是文本节点)if (endNode.nodeType === Node.TEXT_NODE) {textNodes.push(endNode);}return textNodes;}function getNextNode(node) {if (node.firstChild) {return node.firstChild;}while (node) {if (node.nextSibling) {return node.nextSibling;}node = node.parentNode;}return null;}</script>
</body>
</html>
在浏览器中运行上面的网页,选择绿色部分文字(原本是无格式的),然后点击“加粗”按钮,无格式文本就变成了加粗绿色格式了:

原始的html结构:
<div>第一块<span>234<span>567</span></span></div>
<div>第二块</div>
在控制台中的输出如下:

可以看到没有破坏原始的html结构,文本格式化标签均为纯文本结点的直接父结点。
8. 注意事项
- 浏览器兼容性:Selection API 在现代浏览器中支持良好,但在旧版本的浏览器中可能存在兼容性问题。
