React富文本编辑器开发(十一)命令与编辑器

命令

在编辑富文本内容时,您的用户将执行诸如插入文本、删除文本、拆分段落、添加格式等操作。在底层,这些编辑操作使用转换和操作来表达。但在高级别上,我们将它们称为 "命令"。

命令是表示用户特定意图的高级别操作。它们表示为编辑器接口上的辅助函数。核心中包含了一些常见富文本行为的帮助程序,但鼓励您编写自己的命令来模拟您特定领域的行为。

例如,以下是一些内置命令:

javascript 复制代码
Editor.insertText(editor, '要插入的新文本字符串。')

Editor.deleteBackward(editor, { unit: 'word' })

Editor.insertBreak(editor)

但您可以(而且应该!)定义自己的自定义命令,以模拟您的领域。例如,您可能想要定义一个 formatQuote 命令,或者一个 insertImage 命令,或者一个 toggleBold 命令,具体取决于您允许的内容类型。

命令总是描述要执行的操作,就好像用户正在执行操作一样。因此,它们永远不需要定义执行命令的位置,因为它们始终作用于用户当前的选择。

命令的概念基于DOM的内置 execCommand API。但是,Slate定义了自己更简单(且可扩展!)的API版本,因为DOM的版本过于主观和不一致。

在底层,Slate负责将每个命令转换为一组低级别的 "操作",这些操作被应用以产生新值。这就是协同编辑实现成为可能的原因。但是您不必担心这一点,因为它会自动发生。

自定义命令

在定义自定义命令时,您可以创建自己的命名空间:

javascript 复制代码
const MyEditor = {
  ...Editor,

  insertParagraph(editor) {
    // ...
  },
}

在编写自己的命令时,您经常会使用Slate提供的Transforms帮助程序。

转换

转换是一组特定的帮助程序,允许您对文档执行各种特定的更改,例如:

javascript 复制代码
// 在范围内的所有文本节点上设置 "bold" 格式。
// 通常,您会使用Editor.addMark()命令应用类似粗体的样式。
// addMark()命令执行类似的setNodes转换,但它使用了更复杂的匹配函数,以便在markableVoid元素中应用标记。
Transforms.setNodes(
  editor,
  { bold: true },
  {
    at: range,
    match: node => Text.isText(node),
    split: true,
  }
)

// 在文档中某一点的最低块外面包装引用块。
Transforms.wrapNodes(
  editor,
  { type: 'quote', children: [] },
  {
    at: point,
    match: node => Editor.isBlock(editor, node),
    mode: 'lowest',
  }
)

// 插入新文本以替换特定路径处节点中的文本。
Transforms.insertText(editor, '要插入的新文本字符串。', { at: path })

// ... 还有许多其他转换!

转换帮助程序设计成可以一起组合使用。因此,您可能会为每个命令使用一些帮助程序。

编辑器 Editor

Slate编辑器的所有行为、内容和状态都被整合到一个单独的顶级 Editor 对象中。它的接口如下:

javascript 复制代码
interface Editor {
  // 当前编辑器状态
  children: Node[]
  selection: Range | null
  operations: Operation[]
  marks: Omit<Text, 'text'> | null
  // 模式特定的节点行为。
  isInline: (element: Element) => boolean
  isVoid: (element: Element) => boolean
  markableVoid: (element: Element) => boolean
  normalizeNode: (entry: NodeEntry) => void
  onChange: (options?: { operation?: Operation }) => void
  // 可重写的核心操作。
  addMark: (key: string, value: any) => void
  apply: (operation: Operation) => void
  deleteBackward: (unit: 'character' | 'word' | 'line' | 'block') => void
  deleteForward: (unit: 'character' | 'word' | 'line' | 'block') => void
  deleteFragment: () => void
  insertBreak: () => void
  insertSoftBreak: () => void
  insertFragment: (fragment: Node[]) => void
  insertNode: (node: Node) => void
  insertText: (text: string) => void
  removeMark: (key: string) => void
}

它比其他对象稍微复杂一些,因为它包含了定义您自定义的、特定领域行为的所有顶级函数。

  • children 属性包含组成编辑器内容的节点文档树。

  • selection 属性包含用户当前的选择,如果有的话。不要直接设置它;使用 Transforms.select

  • operations 属性包含自上次 "change" 被刷新以来应用的所有操作。(因为 Slate 将操作批处理成事件循环的时刻。)

  • marks 属性存储在编辑器插入文本时要应用的格式。如果 marksnull,则格式将从当前选择中获取。不要直接设置它;使用 Editor.addMark 和 Editor.removeMark。

覆盖行为

在之前的指南中我们已经提到过,但是您可以通过覆盖其函数属性来覆盖编辑器的任何行为。

例如,如果您想要定义内联节点的链接元素:

javascript 复制代码
const { isInline } = editor

editor.isInline = element => {
  return element.type === 'link' ? true : isInline(element)
}

或者也许您想要覆盖 insertText 行为以将 URL "链接化":

javascript 复制代码
const { insertText } = editor

editor.insertText = text => {
  if (isUrl(text)) {
    // ...
    return
  }

  insertText(text)
}

如果您有可以接受粗体或斜体等标记的空 "mention" 元素:

javascript 复制代码
const { isVoid, markableVoid } = editor

editor.isVoid = element => {
  return element.type === 'mention' ? true : isVoid(element)
}

editor.markableVoid = element => {
  return element.type === 'mention' || markableVoid(element)
}

或者您甚至可以定义自定义的 "normalizations" 来确保链接遵循某些约束:

javascript 复制代码
const { normalizeNode } = editor

editor.normalizeNode = entry => {
  const [node, path] = entry

  if (Element.isElement(node) && node.type === 'link') {
    // ...
    return
  }

  normalizeNode(entry)
}

每当您覆盖行为时,请确保调用现有的函数作为默认行为的后备机制。除非您确实想要完全删除默认行为(这很少是一个好主意)。

辅助函数

Editor 接口像所有 Slate 接口一样,公开了在实现某些行为时有用的辅助函数。有许多,许多与编辑器相关的辅助函数。例如:

javascript 复制代码
// 获取指定路径节点的起始点。
const point = Editor.start(editor, [0, 0])

// 在范围内获取片段(文档的一部分)。
const fragment = Editor.fragment(editor, range)

还有许多基于迭代器的辅助函数,例如:

javascript 复制代码
// 遍历范围内的每个节点。
for (const [node, path] of Editor.nodes(editor, { at: range })) {
  // ...
}

// 遍历当前选择中每个文本节点中的每个点。
for (const point of Editor.positions(editor)) {
  // ...
}
相关推荐
WeiXiao_Hyy38 分钟前
成为 Top 1% 的工程师
java·开发语言·javascript·经验分享·后端
吃杠碰小鸡1 小时前
高中数学-数列-导数证明
前端·数学·算法
kingwebo'sZone1 小时前
C#使用Aspose.Words把 word转成图片
前端·c#·word
xjt_09011 小时前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农2 小时前
Vue 2.3
前端·javascript·vue.js
夜郎king2 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳2 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵3 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星3 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_3 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js