Slate Range API 完全指南:理解选区核心数据结构与全套静态方法 📅 发布时间:2026/9/19 12:41:47 👁 浏览次数: Slate Range API 完全指南理解选区核心数据结构与全套静态方法【免费下载链接】slateA completely customizable framework for building rich text editors. (Currently in beta.)项目地址: https://gitcode.com/gh_mirrors/sl/slateRange是 Slate 中用来描述文档任意跨度内容的核心数据结构一个Range由anchor与focus两个 Point 组成编辑器当前的选区editor.selection本质上就是一个Range。本文将以 docs/api/locations/range.md 为骨架结合 packages/slate/src/interfaces/range.ts 的实现与 packages/slate/test/interfaces/Range 下的测试用例系统讲解Range的类型定义、检索、检查与变换三大类静态方法帮助你在开发自定义富文本编辑器时熟练地读取、判断与变换选区。一、认识 Range选区的数据模型1.1 类型定义Range是一组指向 Slate 文档特定跨度的点的集合。它可以描述单个节点内部的跨度也可以横跨多个节点编辑器顶层的selection用户当前选中内容正是以Range形式存储的。其接口定义如下interface Range { anchor: Point focus: Point }在源码 packages/slate/src/interfaces/range.ts 中BaseRange被定义为只有这两个字段的接口并通过ExtendedTypeRange, BaseRange支持开发者利用 TypeScript 模块声明扩展自定义属性这与 Point 的扩展机制一致。1.2 anchor 与 focus方向语义anchor与focus两个术语借用自 DOM 的Selection概念。anchor是用户开始选择的位置如按下鼠标处focus是用户结束选择的位置如松开鼠标处。anchor 不一定在文档顺序上位于 focus 之前——用户既可以正向forward选择也可以反向backward选择这与 DOM 中的选区行为一致。一个关键区别是Range 的 anchor 与 focus 永远指向叶子级leaf文本节点而不会指向元素节点。这与 DOM 不同但能显著减少需要处理的边界情况。关于 Range 在选区中的应用可进一步参考 docs/concepts/03-locations.md 的 “Range” 与 “Selection” 章节。二、静态方法总览Range命名空间下的全部静态方法在 packages/slate/src/interfaces/range.ts 的RangeInterface中声明并实现按功能分为三类类别方法检索方法RetrievalRange.edges、Range.end、Range.intersection、Range.points、Range.start检查方法CheckRange.equals、Range.includes、Range.surrounds、Range.isBackward、Range.isCollapsed、Range.isExpanded、Range.isForward、Range.isRange变换方法TransformRange.transform三、检索方法Retrieval Methods3.1Range.edges(range, options?) [Point, Point]按在文档中出现的顺序返回 range 的起点与终点。源码实现range.ts非常直观edges(range: Range, options: RangeEdgesOptions {}): [Point, Point] { const { reverse false } options const { anchor, focus } range return Range.isBackward(range) reverse ? [anchor, focus] : [focus, anchor] }当 range 为正向anchor 在 focus 前时返回[anchor, focus]当 range 为反向时返回[focus, anchor]即自动按文档顺序归一化可选参数options.reverse默认为false。若传入{ reverse: true }则会返回与文档顺序相反的点对。3.2Range.start(range) Point与Range.end(range) PointRange.start返回按文档顺序排列的起点实现为Range.edges(range)的第一个元素Range.end返回按文档顺序排列的终点实现为Range.edges(range)的第二个元素。这两个方法可以视为edges的便捷单值版本当你只关心选区的文档开头/结尾而无需关心 anchor/focus 方向时使用。3.3Range.points(range) GeneratorPointEntry迭代 range 中的两个点条目PointEntry。这是一个生成器先产出代表anchor的PointEntry再产出代表focus的PointEntry。PointEntry是[Point, anchor | focus]形式的元组在 point-entry.md 中有详细说明。源码实现*points(range: Range): GeneratorPointEntry, void, undefined { yield [range.anchor, anchor] yield [range.focus, focus] }它保持了 anchor/focus 的原始方向信息不按文档顺序重排适合需要区分起点与终点身份的场景例如Transforms.move中针对 anchor 与 focus 分别处理移动方向。3.4Range.intersection(range, another) Range | null计算一个 range 与另一个 range 的交集。若两者没有重叠返回null。源码实现range.tsintersection(range: Range, another: Range): Range | null { const { anchor, focus, ...rest } range const [s1, e1] Range.edges(range) const [s2, e2] Range.edges(another) const start Point.isBefore(s1, s2) ? s2 : s1 const end Point.isBefore(e1, e2) ? e1 : e2 if (Point.isBefore(end, start)) { return null } else { return { anchor: start, focus: end, ...rest } } }实现要点先通过edges把两个 range 归一化为文档顺序的[start, end]取两者中较晚的起点与较早的终点若终点在起点之前说明无交集返回null。返回的交集保留了原range上额外的自定义属性通过...rest展开。注意返回值永远是按文档顺序归一化后的前向 range。四、检查方法Check Methods检查方法用于判断 Range 的某种属性始终返回布尔值。4.1Range.equals(range, another) boolean判断两个 range 是否完全相等。实现为 anchor 与 focus 分别调用 Point.equalsequals(range: Range, another: Range): boolean { return ( Point.equals(range.anchor, another.anchor) Point.equals(range.focus, another.focus) ) }注意equals比较的是anchor/focus 的精确匹配因此一个正向 range 与它的反向版本anchor 与 focus 互换会被判定为不相等。4.2Range.includes(range, target) boolean判断 range 是否包含一个路径Path、一个点Point或另一个 range 的一部分。官方文档明确指出includes是部分包含语义——换句话说如果两个 range 相交则互为部分包含。完整的实现逻辑range.ts如下includes(range: Range, target: Location): boolean { if (Location.isRange(target)) { if ( Range.includes(range, target.anchor) || Range.includes(range, target.focus) ) { return true } const [rs, re] Range.edges(range) const [ts, te] Range.edges(target) return Point.isBefore(rs, ts) Point.isAfter(re, te) } const [start, end] Range.edges(range) let isAfterStart false let isBeforeEnd false if (Location.isPoint(target)) { isAfterStart Point.compare(target, start) 0 isBeforeEnd Point.compare(target, end) 0 } else { isAfterStart Path.compare(target, start.path) 0 isBeforeEnd Path.compare(target, end.path) 0 } return isAfterStart isBeforeEnd }对 target 为不同形态时的行为target 是 Point当 target 位于 range 的[start, end]之间含边界时为真target 是 Path当 target 位于 range 起点与终点的 path 之间时为真target 是 Range只要 target 的 anchor 或 focus 之一落在 range 内或者 target 完全被 range 包含range 的起点早于 target 起点且终点晚于 target 终点即为真。测试用例 includes/path-inside.tsx 验证了range 覆盖[1]到[3]时Range.includes(range, [2])返回true。其余测试分别覆盖了 path 在 range 之前/之后/边界处以及 point 在不同 path 与 offset 上的各种组合见 includes 目录。4.3Range.surrounds(range, target) boolean判断 range 是否完全包含另一个 range区别于includes的部分包含语义。实现基于交集surrounds(range: Range, target: Range): boolean { const intersectionRange Range.intersection(range, target) if (!intersectionRange) { return false } return Range.equals(intersectionRange, target) }即先计算两个 range 的交集若交集恰好等于target说明 target 完全落在 range 内部返回true。若 target 只有一部分被包含交集将小于 target返回false。4.4Range.isBackward(range) boolean判断 range 是否为反向即 anchor 在文档顺序上位于 focus 之后isBackward(range: Range): boolean { const { anchor, focus } range return Point.isAfter(anchor, focus) }4.5Range.isForward(range) boolean判断 range 是否为正向即 anchor 在文档顺序上位于 focus 之前或两者相等。实现为!Range.isBackward(range)文档中特别注明这是isBackward的取反仅为提高代码可读性而提供。4.6Range.isCollapsed(range) boolean判断 range 是否折叠collapsed即 anchor 与 focus 指向文档中的完全相同的位置——这正是光标caret状态的典型特征isCollapsed(range: Range): boolean { const { anchor, focus } range return Point.equals(anchor, focus) }4.7Range.isExpanded(range) boolean判断 range 是否展开expanded实现为!Range.isCollapsed(range)同样是仅用于提高可读性的取反封装。4.8Range.isRange(value) value is Range类型守卫判断任意值是否实现了Range接口isRange(value: any): value is Range { return ( isObject(value) Point.isPoint(value.anchor) Point.isPoint(value.focus) ) }它要求value是普通对象且其anchor与focus都是合法的 Point即拥有合法path与数字offset。结合 Location.isRange仅检查anchor in atRange.isRange是更严格的校验可用于运行时判别并收窄类型。五、变换方法Range.transform(range, op, options) Range | nullRange.transform是 Range API 中最核心也最复杂的方法它根据一次 Operation 变换一个 range返回变换后的新 range当操作导致 range 无效如 anchor 或 focus 所指向的节点被删除时返回null。5.1 默认值与亲和性affinitytransform( range: Range | null, op: Operation, options: RangeTransformOptions {} ): Range | null { if (range null) { return null } const { affinity inward } options ... }选项options.affinity的取值与语义类型定义见 range.ts实际值域定义在 packages/slate/src/types/types.ts 的RangeDirection取值语义forwardanchor 与 focus 都采用前向亲和当操作恰好落在边界点时点趋向于向前留在操作位置之后/随着内容前进backward两个点都采用后向亲和outward两个点向外扩散前向 range 的 anchor 用后向、focus 用前向使 range 趋向扩大inward两个点向内收缩前向 range 的 anchor 用前向、focus 用后向使 range 趋向收缩默认值null显式不指定亲和性直接传给Point.transform的默认行为源码中的计算逻辑range.ts值得细读if (affinity inward) { // 如果 range 是折叠的两个点必须使用相同的亲和性 // 以避免两个点互相越过对方、导致选区向相反方向扩张 const isCollapsed Range.isCollapsed(range) if (Range.isForward(range)) { affinityAnchor forward affinityFocus isCollapsed ? affinityAnchor : backward } else { affinityAnchor backward affinityFocus isCollapsed ? affinityAnchor : forward } } else if (affinity outward) { if (Range.isForward(range)) { affinityAnchor backward affinityFocus forward } else { affinityAnchor forward affinityFocus backward } } else { affinityAnchor affinity affinityFocus affinity }关键设计点折叠 range 的 inward 特判当 range 折叠光标状态且采用inward时anchor 与 focus 使用相同的亲和性防止两个相同位置的点在操作后分裂开导致选区意外展开成相反方向outward 与 inward 的方向性前向 range 在outward下 anchor 取backward、focus 取forward向两端扩张在inward下则相反向中间收缩。反向 range 的处理正好对调。5.2 变换的执行随后分别对 anchor 与 focus 调用Point.transformpoint.ts 起const anchor Point.transform(range.anchor, op, { affinity: affinityAnchor, }) const focus Point.transform(range.focus, op, { affinity: affinityFocus }) if (!anchor || !focus) { return null } return { anchor, focus }Point.transform会针对insert_node、move_node、insert_text、remove_text、merge_node、split_node、remove_node等各类操作分别处理 path 与 offset例如insert_text在 offset 小于等于插入位置时推进 offsetsplit_node在分裂点处的行为受 affinity 控制。任一端的变换结果为null例如所指向的文本节点被整体删除时整个Range.transform返回null表示该 range 已失效。5.3 测试验证transform/inward-collapsed.tsx 演示了一个典型场景折叠在[0, 0]偏移 1 处的光标对split_node操作在[0,0]位置 1 处分裂应用{ affinity: inward }变换结果为[0, 1]偏移 0 —— 光标被推向分裂后新文本节点的开头向内吸附。outward-collapsed.tsx则展示了outward亲和下同样的 split 操作会将折叠点推回[0, 0]末尾两个点分处分裂点两侧。这两组用例精确印证了上文关于折叠 range 亲和性行为的描述。六、实战将 Range 方法与编辑器 API 结合Range的静态方法几乎从不单独使用而是与 Editor、Transforms等 API 配合。以下场景是常见组合1. 判断当前选区是否折叠判断是否为纯光标import { Range } from slate if (editor.selection Range.isCollapsed(editor.selection)) { // 没有选中内容处于光标插入状态 }2. 获取选区在文档顺序下的起止点const [start, end] Range.edges(editor.selection) // 或单独获取 const startPoint Range.start(editor.selection) const endPoint Range.end(editor.selection)3. 判断点击位置是否位于选区范围内const target: Point { path: [0, 3], offset: 2 } if (Range.includes(editor.selection, target)) { // 该点位于当前选区之内 }4. 找出包含整个选区的最小公共块来自 docs/concepts/03-locations.mdfunction getCommonBlock(editor) { const range Editor.unhangRange(editor, editor.selection, { voids: true }) let [common, path] SlateNode.common( editor, range.anchor.path, range.focus.path ) if (Editor.isBlock(editor, common) || Editor.isEditor(common)) { return [common, path] } else { return Editor.above(editor, { at: path, match: n Editor.isBlock(editor, n) || Editor.isEditor(n), }) } }这里先通过Editor.unhangRange把可能悬挂在 void 节点上的选区修正为合法 Range再基于range.anchor.path与range.focus.path求公共祖先——体现了 Range 在查询类逻辑中的核心地位。七、关联阅读Range属于 Slate 位置体系Location的一部分与下列接口紧密相关LocationPath | Point | Range的联合类型多数方法直接接受Location以减少类型转换PointRange 的两个组成元素Point.transform是Range.transform的底层支撑PathPoint 的path字段类型PointEntryRange.points的产出类型RangeRef对 Range 的可跟踪引用在变换过程中自动更新OperationRange.transform接受的变换操作类型概念篇 docs/concepts/03-locations.mdRange 与 Selection 的完整背景说明实现源码packages/slate/src/interfaces/range.ts 与测试目录 packages/slate/test/interfaces/Range。【免费下载链接】slateA completely customizable framework for building rich text editors. (Currently in beta.)项目地址: https://gitcode.com/gh_mirrors/sl/slate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考