React富文本编辑器开发(十二)插件

时间:2024-03-12 14:36:45

插件

您已经看到了如何覆盖 Slate 编辑器的行为。这些覆盖也可以打包成 “插件”,以便重用、测试和共享。这是 Slate 架构中最强大的方面之一。

插件简单地是一个接受 Editor 对象并在某种方式上增强它后返回它的函数。

例如,一个将图像节点标记为 “void” 的插件:

const withImages = editor => {
  const { isVoid } = editor

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

  return editor
}

然后要使用插件,简单地:

import { createEditor } from 'slate'

const editor = withImages(createEditor())

这种插件组合模型使得 Slate 极易扩展!

辅助函数

除了插件函数之外,您可能还想公开与您的插件一起使用的辅助函数。例如:

import { Editor, Element } from 'slate'

const MyEditor = {
  ...Editor,
  insertImage(editor, url) {
    const element = { type: 'image', url, children: [{ text: '' }] }
    Transforms.insertNodes(editor, element)
  },
}

const MyElement = {
  ...Element,
  isImageElement(value) {
    return Element.isElement(element) && element.type === 'image'
  },
}

然后您可以在任何地方使用 MyEditor 和 MyElement,并在一个地方访问到所有的辅助函数。

渲染

Slate 最棒的部分之一是它构建在 React 上,因此它可以完美地适应您现有的应用程序。它不会重新发明自己的视图层,您不必学习新的东西。它尽可能地保持与 React 的一致性。

为此,Slate 允许您控制自定义节点和属性在您的富文本领域中的渲染行为。

您可以通过向* <Editable> 组件传递 "render props" 来定义这些行为。

例如,如果您想要渲染自定义元素组件,您可以传递 renderElement prop

import { createEditor } from 'slate'
import { Slate, Editable, withReact } from 'slate-react'

const MyEditor = () => {
  const [editor] = useState(() => withReact(createEditor()))
  const renderElement = useCallback(({ attributes, children, element }) => {
    switch (element.type) {
      case 'quote':
        return <blockquote {...attributes}>{children}</blockquote>
      case 'link':
        return (
          <a {...attributes} href={element.url}>
            {children}
          </a>
        )
      default:
        return <p {...attributes}>{children}</p>
    }
  }, [])

  return (
    <Slate editor={editor}>
      <Editable renderElement={renderElement} />
    </Slate>
  )
}

请确保在自定义组件中混合使用 props.attributesrender props.children!这些 attributes 必须添加到组件内部的* DOM 元素中,因为它们是 SlateDOM 帮助函数所必需的。而 children 则是文本内容和内联元素所持有的 "leaves"

您不必使用简单的 HTML 元素,您也可以使用自己的自定义 React 组件:

const renderElement = useCallback(props => {
  switch (props.element.type) {
    case 'quote':
      return <QuoteElement {...props} />
    case 'link':
      return <LinkElement {...props} />
    default:
      return <DefaultElement {...props} />
  }
}, [])

叶子

当渲染文本级别的格式时,字符被分组为每个具有相同格式(标记)的文本 "leaves"

要自定义每个叶子的渲染,您可以使用自定义 renderLeaf prop

const renderLeaf = useCallback(({ attributes, children, leaf }) => {
  return (
    <span
      {...attributes}
      style={{
        fontWeight: leaf.bold ? 'bold' : 'normal',
        fontStyle: leaf.italic ? 'italic' : 'normal',
      }}
    >
      {children}
    </span>
  )
}, [])

请注意,我们处理它的方式与 renderElement 稍有不同。由于文本格式化通常相对简单,我们选择放弃 switch 语句,而只是切换一些样式开关。(但是,如果您愿意,您也可以使用自定义组件!)

与 Element 渲染器一样,确保在叶子渲染器中混合使用 props.attributesrender props.children!这些 attributes 必须添加到组件内部的* DOM 元素中,因为它们是 SlateDOM 帮助函数所必需的。而 children 则是 Slate 为您自动管理的文档的实际文本内容。

文本级别的格式化的一个缺点是您不能保证任何给定格式是 “连续的” —— 也就是说它会作为一个单独的叶子保留。这与叶子相关的限制类似于 DOM,其中这是无效的:

<em>t<strong>e</em>x</strong>t

上面示例中的元素未正确关闭,因此无效。相反,您应该按以下方式编写上面的 HTML:

<em>t</em><strong><em>e</em>x</strong>t

如果您还添加了另一个重叠的 <strike> 部分到该文本中,您可能不得不再次调整闭合标签。在 Slate 中渲染叶子是类似的——您不能保证即使一个单词具有一种格式,该叶子也是连续的,因为它取决于它与其他格式的重叠方式。

当然,这个叶子的东西听起来很复杂。但是,只要您将文本级别的格式化和元素级别的格式化用于其预期目的,就不必过多考虑它:

  • 文本属性用于非连续的、字符级别的格式化。
  • 元素属性用于文档中连续的、语义化的元素。

装饰

装饰是另一种文本级别的格式化。它们与普通的自定义属性类似,只是每个装饰应用于文档的一个 Range 范围,而不是与给定文本节点关联。

然而,装饰是在渲染时基于内容本身计算的。这对于动态格式化(如语法高亮或搜索关键字)非常有帮助,因为内容的更改(或一些外部数据)可能会改变格式化。

装饰与 Marks 不同之处在于它们不存储在编辑器状态中。

工具栏、菜单、覆盖等等!

除了控制 Slate 内部节点的渲染之外,您还可以使用 useSlate hook 从其他组件中检索当前编辑器上下文。

这样,其他组件就可以执行命令、查询编辑器状态或执行其他任何操作。

一个常见的用例是渲染一个工具栏,其中的格式按钮基于当前选择而高亮:

const MyEditor = () => {
  const [editor] = useState(() => withReact(createEditor()))
  return (
    <Slate editor={editor}>
      <Toolbar />
      <Editable />
    </Slate>
  )
}

const Toolbar = () => {
  const editor = useSlate()
  return (
    <div>
      <Button active={isBoldActive(editor)}>B</Button>
      <Button active={isItalicActive(editor)}>I</Button>
    </div>
  )
}

因为 <Toolbar> 使用 useSlate hook 检索上下文,所以当编辑器更改时它会重新渲染,这样按钮的活动状态就保持同步。

编辑器样式

可以通过在 <Editable> 组件上使用 style prop 来为编辑器自定义样式。

const MyEditor = () => {
  const [editor] = useState(() => withReact(createEditor()))
  return (
    <Slate editor={editor}>
      <Editable style={{ minHeight: '200px', backgroundColor: 'lime' }} />
    </Slate>
  )
}

也可以使用样式表和 className 来应用自定义样式。但是,Slate 使用内联样式为编辑器提供了一些默认样式。由于内联样式优先于样式表,您使用样式表提供的样式将不会覆盖默认样式。如果您尝试使用样式表,但规则没有生效,请执行以下操作之一:

使用 style prop 而不是样式表来提供您的样式,这样会覆盖默认的内联样式。
disableDefaultStyles prop 传递给 <Editable> 组件。
在样式表声明中使用 !important,使其覆盖内联样式。