Slate 中的选区(Selection)是一个非常关键的概念,它决定了用户在编辑器中当前所操纵的文本或元素的范围。
一个选区由两个部分组成:
- 锚点(Anchor):选区的起始位置。
- 焦点(Focus):选区的结束位置。
这两个位置都由一个路径(Path)和一个偏移量(Offset)表示。路径是一个数字数组,表示从根节点到所在节点的路线,偏移量表示在该节点中的位置。
当锚点和焦点重合时,选区就表示一个光标。当锚点和焦点不重合时,选区就表示一个选中的文本范围。
在第一章 《slate源码解析(一):总体概览》中,我们介绍了Selection、Path、Point三个概念,本文就重点带大家看一下在slate里面是怎么处理选区。
视图层的事件处理
我们继续去看在 Editable.tsx 组件中是如何处理选区事件的:
useIsomorphicLayoutEffect(() => {
const window = ReactEditor.getWindow(editor)
window.document.addEventListener(
'selectionchange',
scheduleOnDOMSelectionChange
)
return () => {
window.document.removeEventListener(
'selectionchange',
scheduleOnDOMSelectionChange
)
}
}, [scheduleOnDOMSelectionChange])
在组件的 useIsomorphicLayoutEffect钩子中(实际就是对使用 useEffect还是 useLayoutEffect做了一层判断),监听了 selectionchange事件然后绑定了 scheduleOnDOMSelectionChange 函数:
const scheduleOnDOMSelectionChange = useMemo(
() => debounce(onDOMSelectionChange, 0),
[onDOMSelectionChange]
)
scheduleOnDOMSelectionChange 函数主要就是对 onDOMSelectionChange 做了一层防抖的处理,所以我们主要来看 函数内部的代码:
const onDOMSelectionChange = useMemo(
() =>
throttle(() => {
const domSelection = root.getSelection()
if (!domSelection) {
return Transforms.deselect(editor)
}
const range = ReactEditor.toSlateRange(editor, domSelection, {
exactMatch: false,
suppressThrow: true,
})
if (range) {
Transforms.select(editor, range)
}
}, 100),
[editor, readOnly, state]
)
通过 getSelection() 来获取选中的 DOM 选区,从选区中获取起点和终点的 DOM 节点以及偏移量信息,通过 ReactEditor.toSlateRange 来将 DOM 选区转换为slate内部选区对象。获取到的range对象如下表示:
{
"anchor": {
"path": [
1,
2
],
"offset": 50
},
"focus": {
"path": [
1,
2
],
"offset": 49
}
}
逻辑层修改模型处理
通过前面的方法获取到对应range之后,将需要修改的slate range对象传入到setSelection方法中:
export const setSelection: SelectionTransforms['setSelection'] = (
editor,
props
) => {
const { selection } = editor
const oldProps: Partial<Range> | null = {}
const newProps: Partial<Range> = {}
if (!selection) {
return
}
for (const k in props) {
if (
(k === 'anchor' &&
props.anchor != null &&
!Point.equals(props.anchor, selection.anchor)) ||
(k === 'focus' &&
props.focus != null &&
!Point.equals(props.focus, selection.focus)) ||
(k !== 'anchor' && k !== 'focus' && props[k] !== selection[k])
) {
oldProps[k] = selection[k]
newProps[k] = props[k]
}
}
if (Object.keys(oldProps).length > 0) {
editor.apply({
type: 'set_selection',
properties: oldProps,
newProperties: newProps,
})
}
}
setSelection 函数接受两个参数:editor 和 props。函数首先获取当前编辑器的选择对象 selection。 然后,创建两个空对象 oldProps 和 newProps,用于存储要应用的变更操作的旧属性和新属性。
对于 props 对象中的每个属性 k,进行以下判断:
- 如果属性为
anchor并且props.anchor不为null,并且props.anchor与当前选择的anchor不相等,则将该属性添加到oldProps和newProps对象中。 - 如果属性为
focus并且props.focus不为null,并且props.focus与当前选择的focus不相等,则将该属性添加到oldProps和newProps对象中。 - 对于其他非
anchor和focus的属性,如果props[k]与当前选择的属性selection[k]不相等,则将该属性添加到oldProps和newProps对象中。
如果 oldProps 对象中存在属性,则调用 editor.apply 方法应用变更操作。editor.apply 接受一个对象作为参数,描述了要应用的选择变更操作的类型、旧值和新值。
然后通过 editor.apply 函数,走到 applyToDraft 的 set_selection 阶段:
// packages/slate/src/interfaces/transforms/general.ts
case 'set_selection': {
const { newProperties } = op
if (newProperties == null) {
selection = newProperties
} else {
if (selection == null) {
if (!Range.isRange(newProperties)) {
throw new Error(
`Cannot apply an incomplete "set_selection" operation properties ${Scrubber.stringify(
newProperties
)} when there is no current selection.`
)
}
selection = { ...newProperties }
}
for (const key in newProperties) {
const value = newProperties[key]
if (value == null) {
if (key === 'anchor' || key === 'focus') {
throw new Error(`Cannot remove the "${key}" selection property`)
}
delete selection[key]
} else {
selection[key] = value
}
}
}
break
}
总结来说,上述代码处理了 set_selection 操作的逻辑。根据传入的 newProperties 对象,更新编辑器的选择范围。如果 newProperties 为 null,则将选择范围设置为 null。如果选择范围为空,且 newProperties 不是有效的范围对象,则抛出错误。如果选择范围为空,将 newProperties 复制给选择对象。遍历 newProperties 中的属性,根据属性值的情况进行相应的处理,包括删除属性或更新属性值。