slate源码解析(三):选区处理

629 阅读3分钟

Slate 中的选区(Selection)是一个非常关键的概念,它决定了用户在编辑器中当前所操纵的文本或元素的范围。

一个选区由两个部分组成:

  1. 锚点(Anchor):选区的起始位置。
  2. 焦点(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 不相等,则将该属性添加到 oldPropsnewProps 对象中。
  • 如果属性为 focus 并且 props.focus 不为 null,并且 props.focus 与当前选择的 focus 不相等,则将该属性添加到 oldPropsnewProps 对象中。
  • 对于其他非 anchorfocus 的属性,如果 props[k] 与当前选择的属性 selection[k] 不相等,则将该属性添加到 oldPropsnewProps 对象中。

如果 oldProps 对象中存在属性,则调用 editor.apply 方法应用变更操作。editor.apply 接受一个对象作为参数,描述了要应用的选择变更操作的类型、旧值和新值。

然后通过 editor.apply 函数,走到 applyToDraftset_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 对象,更新编辑器的选择范围。如果 newPropertiesnull,则将选择范围设置为 null。如果选择范围为空,且 newProperties 不是有效的范围对象,则抛出错误。如果选择范围为空,将 newProperties 复制给选择对象。遍历 newProperties 中的属性,根据属性值的情况进行相应的处理,包括删除属性或更新属性值。