版权声明 © 2025 万维网联盟 (World Wide Web Consortium)。适用 W3C® 责任限制、商标及宽松文档许可规则。
本文档是 Selection API 及与选区相关功能的规范初步草案。它取代了 HTML 规范中几个旧章节,以及旧版 DOM Range 规范中的选区部分。
本文档定义了用于选区的 API,允许用户和作者选择文档的一部分,或指定用于复制、粘贴及其他编辑操作的关注点。
本节描述本文档在发布时的状态。当前的 W3C 出版物列表以及本技术报告的最新修订版可在 https://w3org.cn/TR/ 的 W3C 技术报告索引 中找到。
此工作正在进行中。
本文档由 Web 编辑工作组 (Web Editing Working Group) 作为工作草案发布,采用 推荐标准轨道 (Recommendation track)。
发布为工作草案并不意味着 W3C 及其成员的认可。
这是一份草案文档,可能会随时被其他文档更新、替换或废弃。将其作为非进行中工作引用是不恰当的。
本文档由在 W3C 专利政策下运作的小组编制。W3C 维护了一份与该小组可交付成果相关的专利披露公开列表;该页面还包含披露专利的说明。任何个人如果确实知悉某项其认为包含必要权利要求 (Essential Claim(s)) 的专利,必须按照 W3C 专利政策第 6 节披露相关信息。
本文档受 2023 年 11 月 3 日 W3C 流程文档 管辖。
本节是非规范性的。
IE9 和 Firefox 6.0a2 允许选区中包含任意范围,这遵循了本规范最初的描述。然而,这导致了一些作者、实现者和规范编写者都必须处理的麻烦的边界情况,且它们并无实际意义。Chrome 14 开发版和 Opera 11.11 对选区进行了积极的规范化,例如不允许选区位于空元素内等,但这也被认为是个坏主意,因为它剥夺了作者的灵活性。
因此,我将规范修改为一个折衷方案,它允许一定的简化,但不对作者施加太多限制。详见 讨论。基本上,它会在某些地方抛出异常,试图阻止选区包含一个非 Element 或 Text 节点的边界点的范围,或者一个不从属于 Document 的边界点。
但这导致 getRangeAt() 必须开始返回副本而非引用。此外,它容易在边界情况下出现奇怪的失败。或许最重要的是,当发生 DOM 变更时,可能会出现各种问题,例如如果边界节点的父节点被移除,且变更规则会将新的边界点置于非 Text/Element 节点内。最后,之前指定的行为具有符合两大主流实现的优势,而新行为则无人遵循。因此,我将其改了回来。
详见 bug 15470。IE9、Firefox 12.0a1、Chrome 17 开发版和 Opera Next 12.00 alpha 全部将范围初始化为 null。
每个具有 浏览上下文 的 文档 都关联着一个唯一的 选区 (selection)。
这是 HTML 规范的要求。IE9 和 Opera Next 12.00 alpha 似乎遵循了这一点,而 Firefox 12.0a1 和 Chrome 17 开发版似乎没有。参见 Mozilla bug, WebKit bug。
此单一的 选区 必须被 文档 的所有内容共享(嵌套的 文档 除外),包括 文档 中的任何 编辑宿主 (editing hosts)。
每个 选区 可关联一个单一的 范围 (range)。当没有 范围 关联到 选区 时,该选区是 空的 (empty)。选区必须初始为 空的。
文档 的 选区 是与该 文档 关联的单例对象,因此当调用 Document.open() 时,它会被替换为一个新对象。详见 bug 15470。IE9 和 Opera Next 12.00 alpha 允许用户通过在某处点击来将范围重置为 null;Firefox 12.0a1 和 Chrome 17 开发版则不允许。我们遵循 Gecko/WebKit 的做法,因为它降低了 getRangeAt(0) 抛出异常的几率。
一旦 选区 与给定的 范围 关联,除非本规范另有要求,否则它必须继续与该相同的 范围 保持关联。
例如,如果 DOM 以某种改变范围边界点的方式发生变化,或者脚本修改了范围的边界点,则同一个范围对象必须继续与选区关联。然而,如果用户更改了选区或脚本调用了 addRange(),则根据本规范其他部分的要求,选区必须与一个新的范围对象关联。
如果 选区 的 范围 不为 null 且已 折叠 (collapsed),则插入符位置必须位于该 范围 的 边界点。当 选区 未 折叠 时,本规范不定义插入符位置;用户代理应遵循平台惯例,决定插入符位于 选区 的开头、结尾还是其他位置。
每个 选区 都有一个 方向 (direction):向前 (forwards)、向后 (backwards) 或 无方向 (directionless)。如果用户通过首先指示 范围 的一个 边界点,然后再指示另一个(例如点击一点并拖动到另一点)来创建 选区,且第一个指示的 边界点 在第二个 之后,则相应的 选区 最初必须是 向后 的。如果第一个指示的 边界点 在第二个 之前,则相应的 选区 最初必须是 向前 的。否则,它必须是 无方向 的。
当 选区 的 范围 被脚本变更时(例如通过 selectNode(node)),必须保留 选区 的 方向。
每个 选区 都有一个 锚点 (anchor) 和一个 焦点 (focus)。如果 选区 的 范围 为 null,则其 锚点 和 焦点 均为 null。如果 选区 的 范围 不为 null 且其 方向 为 向前,则其 锚点 为 范围 的 开始,其 焦点 为 结束。否则,其 焦点 为 开始,其 锚点 为 结束。
每个 文档、input 元素和 textarea 元素都有一个布尔值 has scheduled selectionchange event (已调度 selectionchange 事件),初始为 false。
Selection 接口提供了与每个文档关联的 选区 进行交互的方式。
WebIDL[Exposed=Window]
interface Selection {
readonly attribute Node? anchorNode;
readonly attribute unsigned long anchorOffset;
readonly attribute Node? focusNode;
readonly attribute unsigned long focusOffset;
readonly attribute boolean isCollapsed;
readonly attribute unsigned long rangeCount;
readonly attribute DOMString type;
readonly attribute DOMString direction;
Range getRangeAt(unsigned long index);
undefined addRange(Range range);
undefined removeRange(Range range);
undefined removeAllRanges();
undefined empty();
sequence<StaticRange> getComposedRanges(optional GetComposedRangesOptions options = {});
undefined collapse(Node? node, optional unsigned long offset = 0);
undefined setPosition(Node? node, optional unsigned long offset = 0);
undefined collapseToStart();
undefined collapseToEnd();
undefined extend(Node node, optional unsigned long offset = 0);
undefined setBaseAndExtent(Node anchorNode, unsigned long anchorOffset, Node focusNode, unsigned long focusOffset);
undefined selectAllChildren(Node node);
undefined modify(optional DOMString alter, optional DOMString direction, optional DOMString granularity);
[CEReactions] undefined deleteFromDocument();
boolean containsNode(Node node, optional boolean allowPartialContainment = false);
stringifier;
};
dictionary GetComposedRangesOptions {
sequence<ShadowRoot> shadowRoots = [];
};
anchorNode
anchorOffset
focusNode
focusOffset
isCollapsed
当且仅当 锚点 和 焦点 相同(包括两者均为 null 的情况)时,该属性必须返回 true。否则必须返回 false。
rangeCount
type
如果 此对象 为 空,或者 焦点 或 锚点 不在 文档树 中,该属性必须返回 "None";如果 此对象 的 范围 已 折叠,则返回 "Caret";否则返回 "Range"。
direction
如果 此对象 为 空 或此选区为 无方向,该属性必须返回 "none";如果此选区的方向为 向前,则返回 "forward";如果方向为 向后,则返回 "backward"。
getRangeAt() 方法如果 index 不为 0,或者 此对象 为 空,或者 焦点 或 锚点 不在 文档树 中,该方法必须抛出 IndexSizeError 异常。否则,它必须返回对 此对象 的 范围 的引用(而非副本)。
addRange() 方法The method must follow these steps
removeRange() 方法如果 此对象 的 范围 是 range,该方法必须通过解除其 范围 的关联来使 此对象 变为 空。否则,它必须抛出 NotFoundError。
removeAllRanges() 方法empty() 方法该方法必须是 removeAllRanges() 的别名,并表现一致。
getComposedRanges() 方法shadowRoots"] 中任一节点的 包含阴影的包含祖先 时,重复以下步骤:
shadowRoots"] 中任一节点的 包含阴影的包含祖先 时,重复以下步骤:
StaticRange 的数组,其 开始节点 为 startNode,开始偏移量 为 startOffset,结束节点 为 endNode,结束偏移量 为 endOffset。collapse() 方法The method must follow these steps
removeAllRanges() 一致并中止这些步骤。DocumentType,抛出 InvalidNodeTypeError 异常并中止这些步骤。IndexSizeError 异常并中止这些步骤。setPosition() 方法该方法必须是 collapse() 的别名,并表现一致。
collapseToStart() 方法如果 此对象 为 空,该方法必须抛出 InvalidStateError 异常。否则,它必须创建一个新的 范围,设置 其 开始 和 结束 均为 此对象 的 范围 的 开始,然后将 此对象 的 范围 设置为该新创建的 范围。
对于 collapseToStart/End,IE9 会变更现有范围,而 Firefox 9.0a2 和 Chrome 15 开发版则将其替换为一个新范围。本规范遵循多数派,将其替换为新范围,使旧 Range 对象保持不变。
collapseToEnd() 方法如果 此对象 为 空,该方法必须抛出 InvalidStateError 异常。否则,它必须创建一个新的 范围,设置 其 开始 和 结束 均为 此对象 的 范围 的 结束,然后将 此对象 的 范围 设置为该新创建的 范围。
extend() 方法The method must follow these steps
InvalidStateError 异常并中止这些步骤。约 2011 年 1 月逆向工程的结果。IE 不支持该方法,因此我依赖于 Firefox(2000 年前就实现了 extend())和 WebKit(2007 年实现)。我基本忽略了 Opera,因为 gsnedders 告诉我它的实现不兼容。Firefox 12.0a1 似乎会变更现有范围。IE9 不支持 extend(),且无法判断 Chrome 17 开发版或 Opera Next 12.00 alpha 是变更还是替换范围,因为 getRangeAt() 本身返回的就是副本。尽管如此,为了与 collapse() 保持一致,我在此违背了 Gecko 的做法。
setBaseAndExtent() 方法The method must follow these steps
IndexSizeError 异常并中止这些步骤。selectAllChildren() 方法The method must follow these steps
DocumentType,抛出 InvalidNodeTypeError 异常并中止这些步骤。0)。主要基于 Firefox 9.0a2。它有一个我没有重现的 bug,即如果将 Document 作为参数传递,结束偏移量会变成 1 而不是其子节点数量。它还会抛出 RangeException 而不是 DOMException,因为其实现早于两者的合并。
IE9 的表现类似,但有缺陷。如果节点是分离的或 display:none,它会抛出“未指定的错误”,显然在其他一些随机情况下也会这样。对于分离的注释(仅限注释!),它会抛出“无效参数”。最后,如果你传递给它一个注释,它似乎会选择整个注释,而不像对待文本节点那样。
Chrome 16 开发版的表现符合其 Selection 实现的预期。它拒绝选择任何不可见的内容,所以它几乎总是错误的。Opera 11.50 在我的所有测试中都什么也不做,一如既往。
新范围替换任何现有范围,而不变更它。这与 IE9 和 Firefox 12.0a1 一致。(Chrome 17 开发版和 Opera Next 12.00 alpha 无法测试,因为 getRangeAt() 返回的是副本。)
modify() 方法The method must follow these steps
我们需要更精确地定义按每种颗粒度扩展或移动选区的含义。
deleteFromDocument() 方法如果 此对象 不为 空,且 焦点 和 锚点 均在 文档树 中,该方法必须在 此对象 的 范围 上调用 deleteContents()。否则该方法必须不执行任何操作。
这是唯一一个实际变更范围而不是替换它的方法。这与 IE9 和 Firefox 12.0a1 一致。(Chrome 17 开发版和 Opera Next 12.00 alpha 无法测试,因为 getRangeAt() 返回的是副本。)
containsNode() 方法如果 此对象 为 空,或者 node 的 根节点 不是与 此对象 关联的文档,该方法必须返回 false。
否则,如果 allowPartialContainment 为 false,当且仅当其 范围 的 开始 在 node 内的第一个 边界点 之前或在视觉上等效,且其 范围 的 结束 在 node 内的最后一个 边界点 之后或在视觉上等效时,该方法必须返回 true。
如果 allowPartialContainment 为 true,当且仅当其 范围 的 开始 在 node 内的最后一个 边界点 之前或在视觉上等效,且其 范围 的 结束 在 node 内的第一个 边界点 之后或在视觉上等效时,该方法必须返回 true。
stringifier
另请参见 Gecko 的 nsISelection.idl。本规范尚未包含其中的所有内容,特别是 selectionLanguageChange() 和 containsNode() 缺失。它们缺失是因为我无法找到如何用范围 (Ranges) 来定义它们。
最初,Selection 接口是 Netscape 的一项功能。最初的实现被带到了 Gecko (Firefox) 中,该功能后来由其他浏览器引擎独立实现。Netscape 实现总是允许在单个选区中有多个范围,例如这样用户可以选择表格的一列。然而,多范围选区被证明是一个 Web 开发人员不了解的棘手边界情况,甚至 Gecko 开发人员也很少能正确处理。其他浏览器引擎从未实现该功能,而是以各种不兼容的方式将选区限制为单一范围。
本规范遵循非 Gecko 引擎,将选区限制为最多一个范围,但 API 最初仍是为具有任意数量范围的选区设计的。这解释了诸如 removeRange() 和 removeAllRanges() 共存,以及一个接收必须始终为零的整数参数的 getRangeAt() 方法等奇怪现象。
Selection 接口的所有成员都是根据对对象表示的 范围 对象(如果有)的操作来定义的。这些操作可能会像 Range 接口定义的那样抛出异常;因此,这可能导致 Selection 接口的成员除了上面明确指出的异常外,也会抛出异常。
本规范扩展了几个接口,为本规范定义的接口提供了入口点。
WebIDLpartial interface Document {
Selection? getSelection();
};
WebIDLpartial interface Window {
Selection? getSelection();
};
getSelection() 方法该方法必须在 此对象 的 Window.document 属性上调用 getSelection() 并返回结果。
GlobalEventHandlers 接口定义于 [HTML]。
WebIDLpartial interface mixin GlobalEventHandlers {
attribute EventHandler onselectstart;
attribute EventHandler onselectionchange;
};
onselectstart
该属性必须是所有 HTML 元素、Document 对象和 Window 对象支持的 selectstart 事件的 事件处理程序 IDL 属性。
onselectionchange
该属性必须是所有 HTML 元素、Document 对象和 Window 对象支持的 selectionchange 事件的 事件处理程序 IDL 属性。
当用户代理要对 CharacterData 替换数据 或 截取子字符串 时,用户代理必须就像其为 动态范围 (live range) 一样,更新与该 CharacterData 的 节点文档 的 选区 关联的 范围。
当用户代理要拆分 Text 节点 时,用户代理必须就像其为 动态范围 一样,更新与该 Text 的 节点文档 的 选区 关联的 范围。
当用户代理要运行 normalize() 方法的步骤时,用户代理必须就像其为 动态范围 一样,更新与 此对象 的 节点文档 的 选区 关联的 范围。
当用户代理要 移除 或 插入 节点 时,用户代理必须就像其为 动态范围 一样,更新与该 节点 的 节点文档 的 选区 关联的 范围。
用户代理应允许用户更改与 活动文档 关联的 选区。如果用户对 选区 进行任何修改,用户代理必须创建一个具有合适 开始 和 结束 的新 范围,并使 选区 与此新 范围 关联(而非修改现有 范围),且如果 开始 在 结束 之前或相等,则将 选区 的 方向 更新为 向前;如果 结束 在 开始 之前,则更新为 向后;或者如果由于平台惯例无法对 开始 和 结束 进行排序,则更新为 无方向。
用户代理在响应任何用户操作(例如点击非可编辑区域)时,不得使之前非 空 的 选区 变 空。
详见 bug 15470。IE9 和 Opera Next 12.00 alpha 允许用户通过在某处点击来将范围重置为 null;Firefox 12.0a1 和 Chrome 17 开发版则不允许。我遵循 Gecko/WebKit 的做法,因为它降低了 getRangeAt(0) 抛出异常的几率。
当用户代理准备响应用户发起的动作而将新范围 newRange 与 选区 关联时,如果 选区 之前是 空的 或之前关联的范围已 折叠,则用户代理必须在更改选区之前,在与 newRange 的 开始 的 边界点 关联的 节点 上 触发一个名为 selectstart 的冒泡且可取消的事件。
如果事件被取消,用户代理不得更改 选区。
当 选区 与其 范围 解除关联、与新 范围 关联,或关联的 范围 的 边界点 被用户或内容脚本修改时,用户代理必须在 文档 上 调度 selectionchange 事件。
当 input 或 textarea 元素提供文本选区且其选区发生变化(范围或 方向 变化)时,用户代理必须在该元素上 调度 selectionchange 事件。
要在节点 target 上 调度 selectionchange 事件 (schedule a selectionchange event),请运行以下步骤:
要在节点 target 上 触发 selectionchange 事件 (fire a selectionchange event),请运行以下步骤:
selectionchange 的冒泡且不可取消的事件。selectionchange 的非冒泡且不可取消的事件。除了标记为非规范性的章节外,本规范中的所有创作指南、图表、示例和注释均为非规范性内容。本规范中的其他所有内容均为规范性内容。
本规范定义了适用于单一产品的符合性准则:实现本规范所含接口的 用户代理 (user agent)。
目前没有针对本标准的已知安全考量。
为了减轻暴露用户使用辅助技术带来的潜在隐私风险,例如,当用户选择修改文档的 选区 时,用户代理 可以选择模拟通常与 selectstart 或 selectionchange 事件相关的鼠标和键盘事件。
非常感谢:
引用自
引用自
引用自