slate源码解读

535 阅读12分钟

slate官网

视图层: 分析 wangeditorslate-react源码我们可以看出两者功能类似,都是将 slate->createEditor()生成的editor对象转化为vnode,然后挂载在带有 contenteditable 属性的节点上。

model层:slate管理了一个 immer不可变类型的 nodes元素、selection选区 slate editor。它提供的其他API基本都是对 nodes元素、selection选区的操作。

为什么用 slate

我们前端的demo基于execCommand + contenteditable实现了基本的功能,但是有以下痛点:

execCommand API 除了跨浏览器的差异之外,对于越复杂的功能例如表格、多级列表等,该 API 已经无法满足了,而且很容易出现我们无法控制的怪异问题。

这也是近几年像 Slate 这样优秀的框架抛弃了 execCommand API的原因之一,把所有 DOM 插入通过高可用的 API 抽象出来。提供一个 editor对象,返回html对应的Node数据、选区Location,然后提供相应api操作 Node和 Location 数据。

我们可以灵活定义html和Node的转换规则,灵活的使用editor api,这也是基于Slate的其他框架,如wangeditorslate-react实现的部分,最后它们将editor数据渲染成真实的视图。

immer

在解读源码前,先了解一下这个东东

immer概念:

immer 可以在需要使用不可变数据结构的任何上下文中使用。不可变的数据结构允许(高效)的变化检测:如果对对象的引用没有改变,那么对象本身也没有改变。此外,它使克隆对象相对便宜:数据树的未更改部分不需要复制,并且在内存中与相同状态的旧版本共享。

对 JSON 补丁的一流支持

immer 基本思想是,使用 Immer,您会将所有更改应用到临时 draft,它是 currentState 的代理。一旦你完成了所有的 mutations,Immer 将根据对 draft state 的 mutations 生成 nextState


image.png

immer基础使用:

import {produce} from "immer"
const todosObj = {
    id1: {done: false, body: "Take out the trash"},
    id2: {done: false, body: "Check Email"}
}

// 添加
const addedTodosObj = produce(todosObj, draft => {
    draft["id3"] = {done: false, body: "Buy bananas"}
})

// 删除
const deletedTodosObj = produce(todosObj, draft => {
    delete draft["id1"]
})

// 更新
const updatedTodosObj = produce(todosObj, draft => {
    draft["id1"].done = true
})

slate源码结构

slate采用lerna多项目管理

  • slate 实现editor对象,提供相应api
  • slate-history 踪随着时间推移对 Slate 值状态的更改,并启用撤消和重做功能
  • slate-hyperscript 将jsx语法转化为 slate 对象,
  • slate-react Slate的文档数据是一颗类似 DOM 的节点树结构,slate-react 通过递归这颗树生成 children 数组,最终 react 将 children 数组中的组件渲染到页面上。这部分我们在 富文本editor- 3.4slate-react介绍

image.png

读源码前需要了解的概念

Nodes

editor.children的格式就是 Node 形式,类似一颗dom josn树结构,这使得我们很轻松 将nodes、vnode、html相互转化

type Node = Editor | Element | Text

Locations

指定在文档的哪个位置插入、删除等操作

type Location = Path | Point | Range //(Location可以是Path,Point,Range中的任何一个)
  • Path:就是一个索引数组,将dom树按层次顺序存入数组,数字代表父节点第几个节点
  • Point:就是焦点位置,由Path和offset组成,因为要确定一个位置不仅需要知道在哪个节点内,还需要知道在这个节点内的偏移位置
  • Range:就是一段范围,两个点就可以组成一段范围,也就是两个Point,一个起始点,一个终止点

Operations

Slate的数据不能直接更改,Operations属于低级操作

export type BaseOperation = NodeOperation | SelectionOperation | TextOperation
  • Node 节点、的操作
  • Selection 选区的操作
  • Node 下Text文本的操作

Transform

Slate的数据不能直接更改,可以通过Transforms操作对其更改。主要也是对 Node 节点、Selection 选区、 Node 下Text文本的操作,它是高级的封装,封装多个 Operations 操作,具体可看官网 Transform api

  • Node Transform
  • Selection Transform
  • Text Transform
  • editor-transforms

slate源码解读

我们阅读的版本是:"0.102.0",入口文件便是index.ts,它export下面所有模块,我们先大概介绍一下每个模块的内容

  • create-editor 导出createEditor方法,创建editor对象,前面也多次用到过过该方法
  • interfaces Node/Selection基本定义以及9种基础transform
  • core apply综合执行4种transform命令、也提供其他基础transform操作
  • editor 主要用于获取nodes/location数据
  • transforms 封装node、selection、text操作
    • transforms-node
    • transforms-selection
    • transforms-text

image.png

createEditor

我们可以看到它返回一个editor对象,该对象有很多方法,都是在其他目录实现的

  • children属性包含构成编辑器内容的节点的文档树。
  • selection属性包含用户的当前选择(如果有的话)。不要直接设置;使用transforms选择
  • operations属性包含自上次刷新“更改”以来应用的所有操作。(由于Slate将操作批处理到事件循环的刻度中。)
  • marks属性存储编辑器插入文本时要应用的格式。如果标记为null,则格式将取自当前所选内容。不要直接设置;使用Editor.addMark和Editor.removeMark。
  • onChange 编辑器数据更改后执行该回调,暴露给需要监听编辑器更改的上层应用
export const createEditor = (): Editor => {
  const editor: Editor = {
    children: [],
    operations: [],
    selection: null,
    marks: null,
    onChange: () => {},

    // Core
    apply: (...args) => apply(editor, ...args),
    getDirtyPaths: (...args) => getDirtyPaths(editor, ...args),
    getFragment: (...args) => getFragment(editor, ...args),
    
    // Editor
    deleteBackward: (...args) => deleteBackward(editor, ...args),
    insertBreak: (...args) => insertBreak(editor, ...args),
    insertNode: (...args) => insertNode(editor, ...args),

    // interface
    node: (...args) => node(editor, ...args),
    after: (...args) => after(editor, ...args),
    collapse: (...args) => collapse(editor, ...args),

    // transforms-text
    delete: (...args) => deleteText(editor, ...args),
    
    // transforms-selection
    select: (...args) => select(editor, ...args),
    setPoint: (...args) => setPoint(editor, ...args),
    setSelection: (...args) => setSelection(editor, ...args),
    
    // transforms-node
    insertNodes: (...args) => insertNodes(editor, ...args),
    liftNodes: (...args) => liftNodes(editor, ...args),
    mergeNodes: (...args) => mergeNodes(editor, ...args),
    ...
  }

  return editor
}

interfaces

Nodes 与 Location 定义

目录:packages/slate/src/interfaces/

  • Node 类的定义 packages/slate/src/interfaces/node.ts
  • Point 类的定义 packages/slate/src/interfaces/point.ts
  • Path 类的定义 packages/slate/src/interfaces/path.ts
  • Range 类的定义 packages/slate/src/interfaces/range.ts

Operations 定义

根据isOperation 我们可以看出有下面9种基本类型,对应下面GeneralTransforms

  • insert_node
  • insert_text
  • merge_node
  • move_node
  • remove_node
  • remove_text
  • set_node
  • set_selection
  • split_node

Transforms定义

目录:packages/slate/src/interfaces/transforms/

  • NodeTransforms node基本操作,具体实现在 目录 transforms-node
  • SelectionTransforms selection 基本操作 ,具体实现在 目录 transforms-selection
  • TextTransforms text 基本操作,具体实现在 目录 transforms-text
  • GeneralTransforms editor-transforms基本操作,具体如下:

packages/slate/src/interfaces/transforms/general.ts,主要看applyToDraft,9种基本Operations,可以看出都是对 Node 和 selection的操作

const applyToDraft = (editor: Editor, selection: Selection, op: Operation) => {
  switch (op.type) {
    case 'insert_node': {
      const { path, node } = op
      const parent = Node.parent(editor, path)
      const index = path[path.length - 1]
      parent.children.splice(index, 0, node)
      if (selection) {
        for (const [point, key] of Range.points(selection)) {
          selection[key] = Point.transform(point, op)!
        }
      }

      break
    }

    case 'insert_text': {
      const { path, offset, text } = op
      if (text.length === 0) break
      const node = Node.leaf(editor, path)
      const before = node.text.slice(0, offset)
      const after = node.text.slice(offset)
      node.text = before + text + after

      if (selection) {
        for (const [point, key] of Range.points(selection)) {
          selection[key] = Point.transform(point, op)!
        }
      }

      break
    }

    case 'merge_node': {
      const { path } = op
      const node = Node.get(editor, path)
      const prevPath = Path.previous(path)
      const prev = Node.get(editor, prevPath)
      const parent = Node.parent(editor, path)
      const index = path[path.length - 1]

      if (Text.isText(node) && Text.isText(prev)) {
        prev.text += node.text
      } else if (!Text.isText(node) && !Text.isText(prev)) {
        prev.children.push(...node.children)
      } 

      parent.children.splice(index, 1)

      if (selection) {
        for (const [point, key] of Range.points(selection)) {
          selection[key] = Point.transform(point, op)!
        }
      }

      break
    }

    case 'move_node': {
      const { path, newPath } = op

      const node = Node.get(editor, path)
      const parent = Node.parent(editor, path)
      const index = path[path.length - 1]

      parent.children.splice(index, 1)
      const truePath = Path.transform(path, op)!
      const newParent = Node.get(editor, Path.parent(truePath)) as Ancestor
      const newIndex = truePath[truePath.length - 1]

      newParent.children.splice(newIndex, 0, node)

      if (selection) {
        for (const [point, key] of Range.points(selection)) {
          selection[key] = Point.transform(point, op)!
        }
      }

      break
    }

    case 'remove_node': {
      const { path } = op
      const index = path[path.length - 1]
      const parent = Node.parent(editor, path)
      parent.children.splice(index, 1)

      if (selection) {
        for (const [point, key] of Range.points(selection)) {
          const result = Point.transform(point, op)

          if (selection != null && result != null) {
            selection[key] = result
          } else {
            let prev: NodeEntry<Text> | undefined
            let next: NodeEntry<Text> | undefined

            for (const [n, p] of Node.texts(editor)) {
              if (Path.compare(p, path) === -1) {
                prev = [n, p]
              } else {
                next = [n, p]
                break
              }
            }

            let preferNext = false
            if (prev && next) {
              if (Path.equals(next[1], path)) {
                preferNext = !Path.hasPrevious(next[1])
              } else {
                preferNext =
                  Path.common(prev[1], path).length <
                  Path.common(next[1], path).length
              }
            }

            if (prev && !preferNext) {
              point.path = prev[1]
              point.offset = prev[0].text.length
            } else if (next) {
              point.path = next[1]
              point.offset = 0
            } else {
              selection = null
            }
          }
        }
      }

      break
    }

    case 'remove_text': {
      const { path, offset, text } = op
      if (text.length === 0) break
      const node = Node.leaf(editor, path)
      const before = node.text.slice(0, offset)
      const after = node.text.slice(offset + text.length)
      node.text = before + after

      if (selection) {
        for (const [point, key] of Range.points(selection)) {
          selection[key] = Point.transform(point, op)!
        }
      }

      break
    }

    case 'set_node': {
      const { path, properties, newProperties } = op

      if (path.length === 0) {
        throw new Error(`Cannot set properties on the root node!`)
      }

      const node = Node.get(editor, path)

      for (const key in newProperties) {
        if (key === 'children' || key === 'text') {
          throw new Error(`Cannot set the "${key}" property of nodes!`)
        }

        const value = newProperties[<keyof Node>key]

        if (value == null) {
          delete node[<keyof Node>key]
        } else {
          node[<keyof Node>key] = value
        }
      }

      // properties that were previously defined, but are now missing, must be deleted
      for (const key in properties) {
        if (!newProperties.hasOwnProperty(key)) {
          delete node[<keyof Node>key]
        }
      }

      break
    }

    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[<keyof Range>key]

          if (value == null) {
            if (key === 'anchor' || key === 'focus') {
              throw new Error(`Cannot remove the "${key}" selection property`)
            }

            delete selection[<keyof Range>key]
          } else {
            selection[<keyof Range>key] = value
          }
        }
      }

      break
    }

    case 'split_node': {
      const { path, position, properties } = op

      if (path.length === 0) {
        throw new Error(
          `Cannot apply a "split_node" operation at path [${path}] because the root node cannot be split.`
        )
      }

      const node = Node.get(editor, path)
      const parent = Node.parent(editor, path)
      const index = path[path.length - 1]
      let newNode: Descendant

      if (Text.isText(node)) {
        const before = node.text.slice(0, position)
        const after = node.text.slice(position)
        node.text = before
        newNode = {
          ...(properties as Partial<Text>),
          text: after,
        }
      } else {
        const before = node.children.slice(0, position)
        const after = node.children.slice(position)
        node.children = before

        newNode = {
          ...(properties as Partial<Element>),
          children: after,
        }
      }

      parent.children.splice(index + 1, 0, newNode)

      if (selection) {
        for (const [point, key] of Range.points(selection)) {
          selection[key] = Point.transform(point, op)!
        }
      }

      break
    }
  }
  return selection
}

Core -> apply

  • 可以看出它是对 4中 transform 类型的综合更改。
  • 更改完后触发 editor.onChange 回调

代码位置 packages/slate/src/core/apply.ts

export const apply: WithEditorFirstArg<Editor['apply']> = (editor, op) => {
  for (const ref of Editor.pathRefs(editor)) {
    PathRef.transform(ref, op)
  }

  for (const ref of Editor.pointRefs(editor)) {
    PointRef.transform(ref, op)
  }

  for (const ref of Editor.rangeRefs(editor)) {
    RangeRef.transform(ref, op)
  }

  // update dirty paths
  if (!isBatchingDirtyPaths(editor)) {
    const transform = Path.operationCanTransformPath(op)
      ? (p: Path) => Path.transform(p, op)
      : undefined
    updateDirtyPaths(editor, editor.getDirtyPaths(op), transform)
  }

  Transforms.transform(editor, op)
  editor.operations.push(op)
  Editor.normalize(editor, {
    operation: op,
  })

  // Clear any formats applied to the cursor if the selection changes.
  if (op.type === 'set_selection') {
    editor.marks = null
  }

  if (!FLUSHING.get(editor)) {
    FLUSHING.set(editor, true)

    Promise.resolve().then(() => {
      FLUSHING.set(editor, false)
      editor.onChange({ operation: op })
      editor.operations = []
    })
  }
}

normalizeNode

“Normalize”是指如何确保编辑器的内容始终具有特定的结构。它类似于“验证”,只是它的工作不是仅仅确定内容是有效还是无效,而是修复内容以使其再次有效。

代码位置 packages/slate/src/core/normalize-node.ts

export const normalizeNode: WithEditorFirstArg<Editor['normalizeNode']> = (
  editor,
  entry
) => {
  const [node, path] = entry

  // There are no core normalizations for text nodes.
  if (Text.isText(node)) {
    return
  }

  // Ensure that block and inline nodes have at least one text child.
  if (Element.isElement(node) && node.children.length === 0) {
    const child = { text: '' }
    Transforms.insertNodes(editor, child, {
      at: path.concat(0),
      voids: true,
    })
    return
  }

  // Determine whether the node should have block or inline children.
  const shouldHaveInlines = Editor.isEditor(node)
    ? false
    : Element.isElement(node) &&
      (editor.isInline(node) ||
        node.children.length === 0 ||
        Text.isText(node.children[0]) ||
        editor.isInline(node.children[0]))

  // Since we'll be applying operations while iterating, keep track of an
  // index that accounts for any added/removed nodes.
  let n = 0

  for (let i = 0; i < node.children.length; i++, n++) {
    const currentNode = Node.get(editor, path)
    if (Text.isText(currentNode)) continue
    const child = currentNode.children[n] as Descendant
    const prev = currentNode.children[n - 1] as Descendant
    const isLast = i === node.children.length - 1
    const isInlineOrText =
      Text.isText(child) || (Element.isElement(child) && editor.isInline(child))

    // Only allow block nodes in the top-level children and parent blocks
    // that only contain block nodes. Similarly, only allow inline nodes in
    // other inline nodes, or parent blocks that only contain inlines and
    // text.
    if (isInlineOrText !== shouldHaveInlines) {
      Transforms.removeNodes(editor, { at: path.concat(n), voids: true })
      n--
    } else if (Element.isElement(child)) {
      // Ensure that inline nodes are surrounded by text nodes.
      if (editor.isInline(child)) {
        if (prev == null || !Text.isText(prev)) {
          const newChild = { text: '' }
          Transforms.insertNodes(editor, newChild, {
            at: path.concat(n),
            voids: true,
          })
          n++
        } else if (isLast) {
          const newChild = { text: '' }
          Transforms.insertNodes(editor, newChild, {
            at: path.concat(n + 1),
            voids: true,
          })
          n++
        }
      }
    } else {
      // Merge adjacent text nodes that are empty or match.
      if (prev != null && Text.isText(prev)) {
        if (Text.equals(child, prev, { loose: true })) {
          Transforms.mergeNodes(editor, { at: path.concat(n), voids: true })
          n--
        } else if (prev.text === '') {
          Transforms.removeNodes(editor, {
            at: path.concat(n - 1),
            voids: true,
          })
          n--
        } else if (child.text === '') {
          Transforms.removeNodes(editor, {
            at: path.concat(n),
            voids: true,
          })
          n--
        }
      }
    }
  }
}

transforms-node -> insertNodes

代码位置 packages/slate/src/transforms-node/insert-nodes.ts

export const insertNodes: NodeTransforms['insertNodes'] = (
  editor,
  nodes,
  options = {}
) => {
  Editor.withoutNormalizing(editor, () => {
    const {
      hanging = false,
      voids = false,
      mode = 'lowest',
      batchDirty = true,
    } = options
    let { at, match, select } = options

    if (Node.isNode(nodes)) {
      nodes = [nodes]
    }

    if (nodes.length === 0) {
      return
    }

    const [node] = nodes

    if (!at) {
      at = getDefaultInsertLocation(editor)
      if (select !== false) {
        select = true
      }
    }

    if (select == null) {
      select = false
    }

    if (Range.isRange(at)) {
      if (!hanging) {
        at = Editor.unhangRange(editor, at, { voids })
      }

      if (Range.isCollapsed(at)) {
        at = at.anchor
      } else {
        const [, end] = Range.edges(at)
        const pointRef = Editor.pointRef(editor, end)
        Transforms.delete(editor, { at })
        at = pointRef.unref()!
      }
    }

    if (Point.isPoint(at)) {
      if (match == null) {
        if (Text.isText(node)) {
          match = n => Text.isText(n)
        } else if (editor.isInline(node)) {
          match = n => Text.isText(n) || Editor.isInline(editor, n)
        } else {
          match = n => Element.isElement(n) && Editor.isBlock(editor, n)
        }
      }

      const [entry] = Editor.nodes(editor, {
        at: at.path,
        match,
        mode,
        voids,
      })

      if (entry) {
        const [, matchPath] = entry
        const pathRef = Editor.pathRef(editor, matchPath)
        const isAtEnd = Editor.isEnd(editor, at, matchPath)
        Transforms.splitNodes(editor, { at, match, mode, voids })
        const path = pathRef.unref()!
        at = isAtEnd ? Path.next(path) : path
      } else {
        return
      }
    }

    const parentPath = Path.parent(at)
    let index = at[at.length - 1]

    if (!voids && Editor.void(editor, { at: parentPath })) {
      return
    }

    if (batchDirty) {
      // PERF: batch update dirty paths
      // batched ops used to transform existing dirty paths
      const batchedOps: BaseInsertNodeOperation[] = []
      const newDirtyPaths: Path[] = Path.levels(parentPath)
      batchDirtyPaths(
        editor,
        () => {
          for (const node of nodes as Node[]) {
            const path = parentPath.concat(index)
            index++

            const op: BaseInsertNodeOperation = {
              type: 'insert_node',
              path,
              node,
            }
            editor.apply(op)
            at = Path.next(at as Path)

            batchedOps.push(op)
            if (!Text.isText) {
              newDirtyPaths.push(path)
            } else {
              newDirtyPaths.push(
                ...Array.from(Node.nodes(node), ([, p]) => path.concat(p))
              )
            }
          }
        },
        () => {
          updateDirtyPaths(editor, newDirtyPaths, p => {
            let newPath: Path | null = p
            for (const op of batchedOps) {
              if (Path.operationCanTransformPath(op)) {
                newPath = Path.transform(newPath, op)
                if (!newPath) {
                  return null
                }
              }
            }
            return newPath
          })
        }
      )
    } else {
      for (const node of nodes as Node[]) {
        const path = parentPath.concat(index)
        index++

        editor.apply({ type: 'insert_node', path, node })
        at = Path.next(at as Path)
      }
    }

    at = Path.previous(at)

    if (select) {
      const point = Editor.end(editor, at)

      if (point) {
        Transforms.select(editor, point)
      }
    }
  })
}

transforms-selection -> move移动选区

代码位置 packages/slate/src/transforms-node/insert-nodes.ts

export const move: SelectionTransforms['move'] = (editor, options = {}) => {
  const { selection } = editor
  const { distance = 1, unit = 'character', reverse = false } = options
  let { edge = null } = options

  if (!selection) {
    return
  }

  if (edge === 'start') {
    edge = Range.isBackward(selection) ? 'focus' : 'anchor'
  }

  if (edge === 'end') {
    edge = Range.isBackward(selection) ? 'anchor' : 'focus'
  }

  const { anchor, focus } = selection
  const opts = { distance, unit, ignoreNonSelectable: true }
  const props: Partial<Range> = {}

  if (edge == null || edge === 'anchor') {
    const point = reverse
      ? Editor.before(editor, anchor, opts)
      : Editor.after(editor, anchor, opts)

    if (point) {
      props.anchor = point
    }
  }

  if (edge == null || edge === 'focus') {
    const point = reverse
      ? Editor.before(editor, focus, opts)
      : Editor.after(editor, focus, opts)

    if (point) {
      props.focus = point
    }
  }

  Transforms.setSelection(editor, props)
}

transforms-text -> insertFragment

代码位置 packages/slate/src/transforms-text/insert-fragment.ts

export const insertFragment: TextTransforms['insertFragment'] = (
  editor,
  fragment,
  options = {}
) => {
  Editor.withoutNormalizing(editor, () => {
    const { hanging = false, voids = false } = options
    let { at = getDefaultInsertLocation(editor), batchDirty = true } = options

    if (!fragment.length) {
      return
    }

    if (Range.isRange(at)) {
      if (!hanging) {
        at = Editor.unhangRange(editor, at, { voids })
      }

      if (Range.isCollapsed(at)) {
        at = at.anchor
      } else {
        const [, end] = Range.edges(at)

        if (!voids && Editor.void(editor, { at: end })) {
          return
        }

        const pointRef = Editor.pointRef(editor, end)
        Transforms.delete(editor, { at })
        at = pointRef.unref()!
      }
    } else if (Path.isPath(at)) {
      at = Editor.start(editor, at)
    }

    if (!voids && Editor.void(editor, { at })) {
      return
    }

    // If the insert point is at the edge of an inline node, move it outside
    // instead since it will need to be split otherwise.
    const inlineElementMatch = Editor.above(editor, {
      at,
      match: n => Element.isElement(n) && Editor.isInline(editor, n),
      mode: 'highest',
      voids,
    })

    if (inlineElementMatch) {
      const [, inlinePath] = inlineElementMatch

      if (Editor.isEnd(editor, at, inlinePath)) {
        const after = Editor.after(editor, inlinePath)!
        at = after
      } else if (Editor.isStart(editor, at, inlinePath)) {
        const before = Editor.before(editor, inlinePath)!
        at = before
      }
    }

    const blockMatch = Editor.above(editor, {
      match: n => Element.isElement(n) && Editor.isBlock(editor, n),
      at,
      voids,
    })!
    const [, blockPath] = blockMatch
    const isBlockStart = Editor.isStart(editor, at, blockPath)
    const isBlockEnd = Editor.isEnd(editor, at, blockPath)
    const isBlockEmpty = isBlockStart && isBlockEnd
    const mergeStart = !isBlockStart || (isBlockStart && isBlockEnd)
    const mergeEnd = !isBlockEnd
    const [, firstPath] = Node.first({ children: fragment }, [])
    const [, lastPath] = Node.last({ children: fragment }, [])

    const matches: NodeEntry[] = []
    const matcher = ([n, p]: NodeEntry) => {
      const isRoot = p.length === 0
      if (isRoot) {
        return false
      }

      if (isBlockEmpty) {
        return true
      }

      if (
        mergeStart &&
        Path.isAncestor(p, firstPath) &&
        Element.isElement(n) &&
        !editor.isVoid(n) &&
        !editor.isInline(n)
      ) {
        return false
      }

      if (
        mergeEnd &&
        Path.isAncestor(p, lastPath) &&
        Element.isElement(n) &&
        !editor.isVoid(n) &&
        !editor.isInline(n)
      ) {
        return false
      }

      return true
    }

    for (const entry of Node.nodes({ children: fragment }, { pass: matcher })) {
      if (matcher(entry)) {
        matches.push(entry)
      }
    }

    const starts = []
    const middles = []
    const ends = []
    let starting = true
    let hasBlocks = false

    for (const [node] of matches) {
      if (Element.isElement(node) && !editor.isInline(node)) {
        starting = false
        hasBlocks = true
        middles.push(node)
      } else if (starting) {
        starts.push(node)
      } else {
        ends.push(node)
      }
    }

    const [inlineMatch] = Editor.nodes(editor, {
      at,
      match: n => Text.isText(n) || Editor.isInline(editor, n),
      mode: 'highest',
      voids,
    })!

    const [, inlinePath] = inlineMatch
    const isInlineStart = Editor.isStart(editor, at, inlinePath)
    const isInlineEnd = Editor.isEnd(editor, at, inlinePath)

    const middleRef = Editor.pathRef(
      editor,
      isBlockEnd && !ends.length ? Path.next(blockPath) : blockPath
    )

    const endRef = Editor.pathRef(
      editor,
      isInlineEnd ? Path.next(inlinePath) : inlinePath
    )

    Transforms.splitNodes(editor, {
      at,
      match: n =>
        hasBlocks
          ? Element.isElement(n) && Editor.isBlock(editor, n)
          : Text.isText(n) || Editor.isInline(editor, n),
      mode: hasBlocks ? 'lowest' : 'highest',
      always:
        hasBlocks &&
        (!isBlockStart || starts.length > 0) &&
        (!isBlockEnd || ends.length > 0),
      voids,
    })

    const startRef = Editor.pathRef(
      editor,
      !isInlineStart || (isInlineStart && isInlineEnd)
        ? Path.next(inlinePath)
        : inlinePath
    )

    Transforms.insertNodes(editor, starts, {
      at: startRef.current!,
      match: n => Text.isText(n) || Editor.isInline(editor, n),
      mode: 'highest',
      voids,
      batchDirty,
    })

    if (isBlockEmpty && !starts.length && middles.length && !ends.length) {
      Transforms.delete(editor, { at: blockPath, voids })
    }

    Transforms.insertNodes(editor, middles, {
      at: middleRef.current!,
      match: n => Element.isElement(n) && Editor.isBlock(editor, n),
      mode: 'lowest',
      voids,
      batchDirty,
    })

    Transforms.insertNodes(editor, ends, {
      at: endRef.current!,
      match: n => Text.isText(n) || Editor.isInline(editor, n),
      mode: 'highest',
      voids,
      batchDirty,
    })

    if (!options.at) {
      let path

      if (ends.length > 0 && endRef.current) {
        path = Path.previous(endRef.current)
      } else if (middles.length > 0 && middleRef.current) {
        path = Path.previous(middleRef.current)
      } else if (startRef.current) {
        path = Path.previous(startRef.current)
      }

      if (path) {
        const end = Editor.end(editor, path)
        Transforms.select(editor, end)
      }
    }

    startRef.unref()
    middleRef.unref()
    endRef.unref()
  })
}

写在最后!!!

富文本编辑器 系列文章:

  1. 富文本editor
  2. wangeditor源码分析
  3. slate源码解读

通过对编辑器源码的解读,我学会了很多新思想,下面总结一下

  • model层:slate管理了一个 immer不可变类型的 nodes元素、selection选区 slate editor
  • view层:wangeditorslate-react利用 slate editor的数据来渲染视图
  1. 文本标签 input 和 textarea它们都不能设置丰富的样式,于是我们采用 contenteditable 属性的编辑框,常规做法是结合 document.execCommand 命令对编辑区域的元素进行设置,这就类似于我们初代的前端代码原生js/jquery,更改页面效果通过对真实dom的增删改查;而wangeditorslate-react采用了一个新的思想,这就类似我们用 react/vue等框架开发页面,通过数据驱动的思想更改页面的元素。

  2. MV:分析 wangeditorslate-react源码我们可以看出两者功能类似,都是将 slate editor 转化为vnode,然后将vnode挂载在带有 contenteditable 属性的节点上;slate-react是基于react,wangeditor是通过snabbdom.js,做到了与框架无关

  3. VM:菜单工具的原理无非是渲染各种按钮,给按钮绑定点击事件。事件用于更改编辑器区域slate editor的内容,它主要思路就是 通过 editor的api 对其nodes元素、selection选区更改

参考文档

欢迎关注我的前端自检清单,我和你一起成长