自定义功能

在开始为 Lexical 构建自定义功能之前,务必先熟悉 Lexical 文档,特别是"概念"部分。这些基础知识对于理解 Lexical 的核心原理(如节点、编辑器状态和命令)至关重要。

Lexical 功能采用模块化设计,意味着每个功能模块都封装在两个特定接口中:一个用于服务端代码,另一个用于客户端代码。

按照约定,这些文件分别命名为 feature.server.ts(服务端功能)和 feature.client.ts(客户端功能)。主要功能逻辑位于 feature.server.ts 中,用户将在项目中导入该文件。虽然客户端功能单独定义,但会通过服务端功能进行服务端集成和渲染。

这种方式既保持了服务端与客户端代码的清晰界限,又能将功能所需的所有代码集中在一个地方。这种设计有利于管理构成功能的各个部分(如工具栏条目、按钮或新节点),使每个功能都能独立封装和管理。

重要提示: 不要直接从核心 lexical 包导入 - 这可能导致 Payload 小版本升级时出现问题。

请改用 @payloadcms/richtext-lexical 中重新导出的版本。例如,将 import { $insertNodeToNearestRoot } from '@lexical/utils' 改为 import { $insertNodeToNearestRoot } from '@payloadcms/richtext-lexical/lexical/utils'

我需要自定义功能吗?

在开始构建自定义功能之前,请考虑是否可以使用现有的 BlocksFeature 实现所需功能。BlocksFeature 是一个强大的功能,允许你创建具有多种选项的自定义块,包括自定义 React 组件、markdown 转换器等。如果使用 BlocksFeature 就能实现所需功能,建议优先使用它而不是构建自定义功能。

通过 BlocksFeature,你可以向编辑器添加内联块(= 可以插入到段落中,在文本之间)和块级块(= 占据整行)。如果你只是想将自定义 react 组件引入编辑器,这是推荐的方式。

示例:带语言选择器的代码字段块

这个示例展示了如何使用 BlocksFeature 创建一个带语言选择器的自定义代码字段块。首先,请确保在你的项目中显式安装了 @payloadcms/ui

字段配置:

import {
  BlocksFeature,
  lexicalEditor,
} from '@payloadcms/richtext-lexical'

export const languages = {
  ts: 'TypeScript',
  plaintext: 'Plain Text',
  tsx: 'TSX',
  js: 'JavaScript',
  jsx: 'JSX',
}

// ...
{
  name: 'richText',
  type: 'richText',
  editor: lexicalEditor({
    features: ({ defaultFeatures }) => [
      ...defaultFeatures,
      BlocksFeature({
        blocks: [
          {
            slug: 'Code',
            fields: [
              {
                type: 'select',
                name: 'language',
                options: Object.entries(languages).map(([key, value]) => ({
                  label: value,
                  value: key,
                })),
                defaultValue: 'ts',
              },
              {
                admin: {
                  components: {
                    Field: './path/to/CodeComponent#Code',
                  },
                },
                name: 'code',
                type: 'code',
              },
            ],
          }
        ],
        inlineBlocks: [],
      }),
    ],
  }),
},

CodeComponent.tsx:

'use client'
import type { CodeFieldClient, CodeFieldClientProps } from 'payload'

import { CodeField, useFormFields } from '@payloadcms/ui'
import React, { useMemo } from 'react'

import { languages } from './yourFieldConfig'

const languageKeyToMonacoLanguageMap = {
  plaintext: 'plaintext',
  ts: 'typescript',
  tsx: 'typescript',
}

type Language = keyof typeof languageKeyToMonacoLanguageMap

export const Code: React.FC<CodeFieldClientProps> = ({
  autoComplete,
  field,
  forceRender,
  path,
  permissions,
  readOnly,
  renderedBlocks,
  schemaPath,
  validate,
}) => {
  const languageField = useFormFields(([fields]) => fields['language'])

  const language: Language =
    (languageField?.value as Language) ||
    (languageField?.initialValue as Language) ||
    'ts'

  const label = languages[language]

  const props: CodeFieldClient = useMemo<CodeFieldClient>(
    () => ({
      ...field,
      type: 'code',
      admin: {
        ...field.admin,
        editorOptions: undefined,
        language: languageKeyToMonacoLanguageMap[language] || language,
      },
      label,
    }),
    [field, language, label],
  )

  const key = `${field.name}-${language}-${label}`

  return (
    <CodeField
      autoComplete={autoComplete}
      field={props}
      forceRender={forceRender}
      key={key}
      path={path}
      permissions={permissions}
      readOnly={readOnly}
      renderedBlocks={renderedBlocks}
      schemaPath={schemaPath}
      validate={validate}
    />
  )
}

服务器功能

自定义块(Custom Blocks)还不够用?要开始构建自定义功能,你应该从服务器功能(server feature)入手,这是功能的入口点。

示例 myFeature/feature.server.ts:

import { createServerFeature } from '@payloadcms/richtext-lexical'

export const MyFeature = createServerFeature({
  feature: {},
  key: 'myFeature',
})

createServerFeature 是一个辅助函数,可以让你无需编写样板代码就能创建新功能。

现在,这个功能已经可以在编辑器中使用:

import { MyFeature } from './myFeature/feature.server';
import { lexicalEditor } from '@payloadcms/richtext-lexical';

//...
 {
    name: 'richText',
    type: 'richText',
    editor: lexicalEditor({
      features: [
        MyFeature(),
      ],
    }),
 },

默认情况下,这个服务器功能什么都不做 - 你还没有添加任何功能。根据你想要实现的功能,ServerFeature 类型提供了各种属性,你可以通过设置这些属性来向 lexical 编辑器注入自定义功能。

国际化(i18n)

每个功能都可以注册自己的翻译文本,这些翻译会自动限定在功能键(feature key)的作用域内:

import { createServerFeature } from '@payloadcms/richtext-lexical'

export const MyFeature = createServerFeature({
  feature: {
    i18n: {
      en: {
        label: 'My Feature',
      },
      de: {
        label: 'Mein Feature',
      },
    },
  },
  key: 'myFeature',
})

这允许你添加限定在功能作用域内的国际化翻译。这个特定的翻译示例将在 lexical:myFeature:label 下可用 - 其中 myFeature 就是你的功能键。

Markdown 转换器

与 Client Feature 类似,Server Feature 也允许你添加 markdown 转换器。服务器端的 markdown 转换器用于将编辑器内容与 markdown 相互转换

import { createServerFeature } from '@payloadcms/richtext-lexical'
import type { ElementTransformer } from '@payloadcms/richtext-lexical/lexical/markdown'
import { $createMyNode, $isMyNode, MyNode } from './nodes/MyNode'

const MyMarkdownTransformer: ElementTransformer = {
  type: 'element',
  dependencies: [MyNode],
  export: (node, exportChildren) => {
    if (!$isMyNode(node)) {
      return null
    }
    return '+++'
  },
  // 匹配 ---
  regExp: /^+++\s*$/,
  replace: (parentNode) => {
    const node = $createMyNode()
    if (node) {
      parentNode.replace(node)
    }
  },
}

export const MyFeature = createServerFeature({
  feature: {
    markdownTransformers: [MyMarkdownTransformer],
  },
  key: 'myFeature',
})

在这个示例中,节点在 Markdown 中会被输出为 +++,而 markdown 中的 +++ 会被转换为编辑器中的 MyNode 节点。

节点

虽然添加到 server feature 的节点不控制节点在编辑器中的渲染方式,但它们控制着节点的其他方面:

  • HTML 转换
  • 节点钩子
  • 子字段
  • 在无头编辑器中的行为

推荐使用 createNode 辅助函数来创建具有正确类型定义的节点。

import { createServerFeature, createNode } from '@payloadcms/richtext-lexical'
import { MyNode } from './nodes/MyNode'

export const MyFeature = createServerFeature({
  feature: {
    nodes: [
      // 使用 createNode 辅助函数可以更轻松地创建具有正确类型定义的节点
      createNode({
        converters: {
          html: {
            converter: () => {
              return `<hr/>`
            },
            nodeTypes: [MyNode.getType()],
          },
        },
        // 在这里添加实际的节点。在服务端,它们将被用于
        // 初始化一个无头编辑器,可用于执行编辑器操作,
        // 如 markdown/html 转换。
        node: MyNode,
      }),
    ],
  },
  key: 'myFeature',
})

与 client feature 中节点直接添加到 nodes 数组不同,server feature 中的节点可以附带以下同级选项一起添加:

OptionDescription
getSubFields如果节点包含子字段(例如 block 和 link 节点),在此处传递子字段 schema 将使 Payload 自动填充并为其运行钩子。
getSubFieldsData如果节点包含子字段,除了 getSubFields 返回其 schema 外,还需要在此处返回子字段数据。
graphQLPopulationPromises允许在从 GraphQL 请求节点数据时运行填充逻辑。虽然 getSubFieldsgetSubFieldsData 会自动处理子字段填充(因为它们在这些字段上运行钩子),但这些填充仅在 Rest API 中生效。这是因为 Rest API 钩子无法访问 GraphQL 提供的 'depth' 属性。为了在 GraphQL 中正确填充,需要在此处提供填充逻辑。
node需要在此处提供实际的 lexical 节点。这也支持 lexical 节点替换
validations允许提供节点验证逻辑,这些验证会在文档验证时与其他 Payload 字段一起运行。如果节点数据不正确,可以使用它来抛出特定节点的验证错误。
converters允许定义如何将节点序列化为不同格式。目前仅支持 HTML。Markdown 转换器在 markdownTransformers 中定义,而非此处。
hooks与 Payload 字段类似,可以为特定节点提供钩子。这些被称为节点钩子。

功能加载顺序

服务器功能也可以接受一个函数作为 feature 属性(这在清理 props 时很有用,如下所述)。这个函数会在 Payload 清理过程中加载该功能时被调用:

import { createServerFeature } from '@payloadcms/richtext-lexical'

createServerFeature({
  //...
  feature: async ({
    config,
    isRoot,
    props,
    resolvedFeatures,
    unSanitizedEditorConfig,
    featureProviderMap,
  }) => {
    return {
      //实际的服务器功能在这里...
    }
  },
})

这里的"加载"指的是调用这个 feature 函数的过程。默认情况下,功能会按照它们被添加到编辑器中的顺序依次调用。 但有时你可能希望在一个功能加载之后再加载另一个功能,或者要求必须先加载另一个功能,否则会抛出错误。

在 lexical 中,列表功能就是一个例子。UnorderedListFeatureOrderedListFeature 都注册了相同的 ListItem 节点。在 UnorderedListFeature 中我们正常注册它,但在 OrderedListFeature 中,我们希望只有在 UnorderedListFeature 不存在时才注册 ListItem 节点 - 否则,我们会有两个功能注册同一个节点。

以下是实现方式:

import { createServerFeature, createNode } from '@payloadcms/richtext-lexical'

export const OrderedListFeature = createServerFeature({
  feature: ({ featureProviderMap }) => {
    return {
      // ...
      nodes: featureProviderMap.has('unorderedList')
        ? []
        : [
            createNode({
              // ...
            }),
          ],
    }
  },
  key: 'orderedList',
})

featureProviderMap 始终可用并包含所有功能,包括尚未加载的功能,因此我们可以通过检查其 key 是否存在于映射中来检查某个功能是否已加载。

如果你想确保某个功能在另一个功能之前加载,可以使用 dependenciesPriority 属性:

import { createServerFeature } from '@payloadcms/richtext-lexical'

export const MyFeature = createServerFeature({
  feature: ({ featureProviderMap }) => {
    return {
      // ...
    }
  },
  key: 'myFeature',
  dependenciesPriority: ['otherFeature'],
})
选项描述
dependenciesSoft该功能所需的软依赖项键名。这些是可选的。Payload 会尝试在加载此功能之前加载它们,但如果无法加载也不会抛出错误。
dependencies该功能所需的依赖项键名。这些依赖项不需要先加载,但它们必须存在,否则会抛出错误。
dependenciesPriority该功能所需的优先依赖项键名。这些依赖项必须先加载且必须存在,否则会抛出错误。它们将在 feature 属性中可用。

客户端功能

用户实际看到和交互的大部分功能,如工具栏项和节点的 React 组件,都位于客户端。

要设置你的客户端功能,请按照以下三个步骤操作:

  1. 创建单独文件:首先为你的客户端功能创建一个新文件,例如 myFeature/feature.client.ts。保持客户端和服务器功能在单独文件中非常重要,这样可以维护服务器和客户端代码之间的清晰边界。
  2. 'use client':在文件顶部添加 'use client' 指令标记该文件
  3. 注册客户端功能:通过将客户端功能传递给 ClientFeature 属性,在你的服务器功能中注册它。这是必要的,因为服务器功能是你功能的唯一入口点。这也意味着你不能在没有服务器功能的情况下创建客户端功能,否则你将无法注册它。

示例 myFeature/feature.client.ts:

'use client'

import { createClientFeature } from '@payloadcms/richtext-lexical/client'

export const MyClientFeature = createClientFeature({})

通过 ClientFeature 提供的 API 来添加你需要的特定功能。请记住,在客户端工作时不要直接从 '@payloadcms/richtext-lexical' 导入,这会导致 webpack 或 turbopack 错误。相反,所有客户端导入都应使用 '@payloadcms/richtext-lexical/client'。类型导入不受此规则限制,可以随时导入。

为服务器功能添加客户端功能

在你的服务器功能中,可以像这样提供一个导入路径指向客户端功能:

import { createServerFeature } from '@payloadcms/richtext-lexical'

export const MyFeature = createServerFeature({
  feature: {
    ClientFeature: './path/to/feature.client#MyClientFeature',
  },
  key: 'myFeature',
  dependenciesPriority: ['otherFeature'],
})

节点

客户端和服务器功能中都向 nodes 数组添加节点。在服务器端,节点用于后端操作,如无头编辑器中的HTML转换。在客户端,这些节点对于编辑器中内容的显示和管理至关重要,影响它们的渲染方式、行为方式以及如何保存到数据库中。

示例:

myFeature/feature.client.ts:

'use client'

import { createClientFeature } from '@payloadcms/richtext-lexical/client'
import { MyNode } from './nodes/MyNode'

export const MyClientFeature = createClientFeature({
  nodes: [MyNode],
})

这也支持lexical节点替换

myFeature/nodes/MyNode.tsx:

以下是一个基本的 DecoratorNode 示例:

import type {
  DOMConversionMap,
  DOMConversionOutput,
  DOMExportOutput,
  EditorConfig,
  LexicalNode,
  SerializedLexicalNode,
} from '@payloadcms/richtext-lexical/lexical'

import { $applyNodeReplacement, DecoratorNode } from '@payloadcms/richtext-lexical/lexical'

// SerializedLexicalNode 是默认的 lexical 节点。
// 通过将你的 SerializedMyNode 类型设置为 SerializedLexicalNode,
// 你基本上是在说这个节点不保存任何额外数据。
// 如果你想保存数据,可以自由扩展它
export type SerializedMyNode = SerializedLexicalNode

// 在这里懒加载你的节点 React 组件
const MyNodeComponent = React.lazy(() =>
  import('../component/index.js').then((module) => ({
    default: module.MyNodeComponent,
  })),
)

/**
 * 这个节点是一个 DecoratorNode。DecoratorNode 允许你在编辑器中渲染 React 组件。
 *
 * 它们需要同时具备 createDom 和 decorate 函数。createDom => 在 html 外部。decorate => 在 html 内部的 React 组件。
 *
 * 如果我们使用 DecoratorBlockNode 替代,则只需要一个 decorate 方法
 */
export class MyNode extends DecoratorNode<React.ReactElement> {
  static clone(node: MyNode): MyNode {
    return new MyNode(node.__key)
  }

  static getType(): string {
    return 'myNode'
  }

  /**
   * 定义当你从其他页面复制一个 div 元素并粘贴到 lexical 编辑器时会发生什么
   *
   * 这也决定了 lexical 内部 HTML -> Lexical 转换器的行为
   */
  static importDOM(): DOMConversionMap | null {
    return {
      div: () => ({
        conversion: $yourConversionMethod,
        priority: 0,
      }),
    }
  }

  /**
   * 这个节点的数据以 JSON 格式序列化存储。这是该节点的"加载函数":它接收保存的数据并将其转换为节点。
   */
  static importJSON(serializedNode: SerializedMyNode): MyNode {
    return $createMyNode()
  }

  /**
   * 决定 hr 元素在 lexical 编辑器中的渲染方式。这只是"初始"/"外部"的 HTML 元素。
   */
  createDOM(config: EditorConfig): HTMLElement {
    const element = document.createElement('div')
    return element
  }

  /**
   * 允许你在 createDOM 返回的内容内渲染一个 React 组件。
   */
  decorate(): React.ReactElement {
    return <MyNodeComponent nodeKey={this.__key} />
  }

  /**
   * 与 importDOM 相反,这个函数定义当你从 lexical 编辑器复制一个 div 元素并粘贴到其他页面时会发生什么。
   *
   * 这也决定了 lexical 内部 Lexical -> HTML 转换器的行为
   */
  exportDOM(): DOMExportOutput {
    return { element: document.createElement('div') }
  }
  /**
   * 与 importJSON 相反。这决定了哪些数据会被保存在数据库/lexical 编辑器状态中。
   */
  exportJSON(): SerializedLexicalNode {
    return {
      type: 'myNode',
      version: 1,
    }
  }

  getTextContent(): string {
    return '\n'
  }

  isInline(): false {
    return false
  }

  updateDOM(): boolean {
    return false
  }
}

// 这个方法用于 importDOM 方法。如果你不希望当某些 dom 元素被复制粘贴到编辑器时自动创建你的节点,这个方法完全是可选的。
function $yourConversionMethod(): DOMConversionOutput {
  return { node: $createMyNode() }
}

// 这是一个工具方法,用于创建新的 MyNode。以 $ 为前缀的工具方法明确表示这应该只在 lexical 内部使用
export function $createMyNode(): MyNode {
  return $applyNodeReplacement(new MyNode())
}

// 这只是一个工具方法,用于检查一个节点是否是 MyNode。这也确保了正确的类型检查。
export function $isMyNode(
  node: LexicalNode | null | undefined,
): node is MyNode {
  return node instanceof MyNode
}

请不要在你的节点中添加任何 'use client' 指令,因为节点类可以在服务器端使用。

插件

插件是功能的一个小组成部分。这个名称来源于 lexical playground 插件,只是 lexical 功能的一小部分。 插件本质上是可以添加到编辑器中的 React 组件,它们位于所有 lexical 上下文提供者内部。通过利用 lexical API,插件可以为编辑器添加任何功能。

最常见的用途是注册 lexical 监听器节点转换命令。 例如,你可以为插件添加一个抽屉组件,并注册一个打开它的命令。这个命令可以从 lexical 的任何地方调用,比如从你的自定义 lexical 节点内部。

要添加插件,只需将其添加到客户端功能的 plugins 数组中:

'use client'

import { createClientFeature } from '@payloadcms/richtext-lexical/client'
import { MyPlugin } from './plugin'

export const MyClientFeature = createClientFeature({
  plugins: [MyPlugin],
})

插件示例 plugin.tsx:

'use client'
import type { LexicalCommand } from '@payloadcms/richtext-lexical/lexical'

import {
  createCommand,
  $getSelection,
  $isRangeSelection,
  COMMAND_PRIORITY_EDITOR,
} from '@payloadcms/richtext-lexical/lexical'

import { useLexicalComposerContext } from '@payloadcms/richtext-lexical/lexical/react/LexicalComposerContext'
import { $insertNodeToNearestRoot } from '@payloadcms/richtext-lexical/lexical/utils'
import { useEffect } from 'react'

import type { PluginComponent } from '@payloadcms/richtext-lexical' // 类型导入可以从 @payloadcms/richtext-lexical 导入 - 即使在客户端

import { $createMyNode } from '../nodes/MyNode'
import './index.scss'

export const INSERT_MYNODE_COMMAND: LexicalCommand<void> = createCommand(
  'INSERT_MYNODE_COMMAND',
)

/**
 * 注册 lexical 命令以向编辑器插入新 MyNode 的插件
 */
export const MyNodePlugin: PluginComponent = () => {
  // 使用 useLexicalComposerContext 钩子可以访问 lexical 编辑器实例
  const [editor] = useLexicalComposerContext()

  useEffect(() => {
    return editor.registerCommand(
      INSERT_MYNODE_COMMAND,
      (type) => {
        const selection = $getSelection()

        if (!$isRangeSelection(selection)) {
          return false
        }

        const focusNode = selection.focus.getNode()

        if (focusNode !== null) {
          const newMyNode = $createMyNode()
          $insertNodeToNearestRoot(newMyNode)
        }

        return true
      },
      COMMAND_PRIORITY_EDITOR,
    )
  }, [editor])

  return null
}

在这个例子中,我们注册了一个 lexical 命令,它简单地向编辑器插入一个新的 MyNode。这个命令可以从 lexical 的任何地方调用,比如从自定义节点内部。

工具栏组

工具栏组是容纳工具栏项目的可视化容器。不同的工具栏组类型决定了工具栏项目的_显示方式_:dropdown(下拉式)和 buttons(按钮式)。

所有默认的工具栏组都从 @payloadcms/richtext-lexical/client 导出。你可以使用它们将自己的工具栏项目添加到编辑器中:

  • 下拉式:toolbarAddDropdownGroupWithItems
  • 下拉式:toolbarTextDropdownGroupWithItems
  • 按钮式:toolbarFormatGroupWithItems
  • 按钮式:toolbarFeatureButtonsGroupWithItems

在下拉式组中,项目在展开时垂直排列,并包含图标和标签。在按钮式组中,项目水平排列且仅显示图标。如果使用相同键声明了两次工具栏组,其所有项目将被合并到一个组中。

自定义按钮工具栏组

选项描述
items属于该工具栏组的所有工具栏项都需要在此添加。
key每个工具栏组都需要有一个唯一键。具有相同键的组将会合并它们的项。
order决定工具栏组的位置。
type控制工具栏组类型。设置为 buttons 可创建一个按钮工具栏组,该组会水平显示工具栏项且仅显示图标。

示例:

import type {
  ToolbarGroup,
  ToolbarGroupItem,
} from '@payloadcms/richtext-lexical'

export const toolbarFormatGroupWithItems = (
  items: ToolbarGroupItem[],
): ToolbarGroup => {
  return {
    type: 'buttons',
    items,
    key: 'myButtonsToolbar',
    order: 10,
  }
}

自定义下拉工具栏组

OptionDescription
items该工具栏组中的所有工具栏项都需要在此处添加。
key每个工具栏组都需要有一个唯一键。具有相同键的组将会合并它们的项。
order决定工具栏组的位置。
type控制工具栏组类型。设置为 dropdown 可创建一个按钮工具栏组,当下拉菜单打开时,会垂直显示工具栏项的图标和标签。
ChildComponent下拉工具栏的 ChildComponent 允许传入一个 React 组件,该组件将显示在下拉按钮内。

示例:

import type {
  ToolbarGroup,
  ToolbarGroupItem,
} from '@payloadcms/richtext-lexical'

import { MyIcon } from './icons/MyIcon'

export const toolbarAddDropdownGroupWithItems = (
  items: ToolbarGroupItem[],
): ToolbarGroup => {
  return {
    type: 'dropdown',
    ChildComponent: MyIcon,
    items,
    key: 'myDropdownToolbar',
    order: 10,
  }
}

工具栏项目

如果自定义节点和功能无法添加到编辑器中,它们就毫无意义。你需要接入我们的接口之一,让用户能够与编辑器交互:

  • 固定工具栏:始终固定在编辑器顶部
  • 内联浮动工具栏:选择文本时出现
  • 斜杠菜单:在编辑器中输入 / 时出现
  • Markdown 转换器:输入特定文本模式时触发
  • 或通过自定义插件添加的任何其他接口。我们的工具栏就是典型例子——它们本身就是插件。

要将工具栏项目添加到固定或内联工具栏,你可以向客户端功能的 toolbarFixedtoolbarInline 属性添加一个带有 ToolbarItem 的 ToolbarGroup:

'use client'

import {
  createClientFeature,
  toolbarAddDropdownGroupWithItems,
} from '@payloadcms/richtext-lexical/client'
import { IconComponent } from './icon'
import { $isHorizontalRuleNode } from './nodes/MyNode'
import { INSERT_MYNODE_COMMAND } from './plugin'
import { $isNodeSelection } from '@payloadcms/richtext-lexical/lexical'

export const MyClientFeature = createClientFeature({
  toolbarFixed: {
    groups: [
      toolbarAddDropdownGroupWithItems([
        {
          ChildComponent: IconComponent,
          isActive: ({ selection }) => {
            if (!$isNodeSelection(selection) || !selection.getNodes().length) {
              return false
            }

            const firstNode = selection.getNodes()[0]
            return $isHorizontalRuleNode(firstNode)
          },
          key: 'myNode',
          label: ({ i18n }) => {
            return i18n.t('lexical:myFeature:label')
          },
          onSelect: ({ editor }) => {
            editor.dispatchCommand(INSERT_MYNODE_COMMAND, undefined)
          },
        },
      ]),
    ],
  },
})

你需要先提供一个工具栏组,然后为该工具栏组添加项目(上文已详述)。

ToolbarItem 有多种属性可用于自定义其行为:

选项描述
ChildComponent在工具栏项目默认按钮组件内渲染的 React 组件。通常这是一个图标。
Component完全替换工具栏项目默认按钮组件的 React 组件。此时会忽略 ChildComponentonSelect 属性。
label当工具栏项目位于下拉组中时显示的标签。为支持国际化,可以是一个函数。
key每个工具栏项目需要有一个唯一键。
onSelect点击工具栏项目时调用的函数。
isEnabled可选属性,控制工具栏项目是否可点击。如果返回 false,项目会变灰且不可点击。
isActive可选属性,控制工具栏项目是否高亮显示

添加到浮动内联工具栏 (toolbarInline) 的 API 完全相同。如果你想同时在固定和内联工具栏添加项目,可以将其提取为独立变量(类型为 ToolbarGroup[]),然后同时添加到 toolbarFixedtoolbarInline 属性中。

斜杠菜单组

我们从 @payloadcms/richtext-lexical/client 导出了 slashMenuBasicGroupWithItems,你可以用它来向标记为"Basic"的斜杠菜单添加项目。如果你想创建自己的斜杠菜单组,这里有一个示例:

import type {
  SlashMenuGroup,
  SlashMenuItem,
} from '@payloadcms/richtext-lexical'

export function mwnSlashMenuGroupWithItems(
  items: SlashMenuItem[],
): SlashMenuGroup {
  return {
    items,
    key: 'myGroup',
    label: 'My Group', // <= 这里可以是一个函数以便使用 i18n
  }
}

通过创建这样的辅助函数,你可以轻松地重复使用它并添加项目。所有具有相同 key 的斜杠菜单组将会合并它们的项目。

选项描述
items一个 SlashMenuItem 数组,将会显示在斜杠菜单中。
label该标签会显示在你的斜杠菜单组之前。为了使用 i18n,这里可以是一个函数。
key用于类名,如果未提供 label 也会用于显示。具有相同 key 的斜杠菜单将会合并它们的项目。

斜杠菜单项

添加项目到斜杠菜单的 API 类似。斜杠菜单包含多个组,每个组又包含多个项目。以下是一个示例:

'use client'

import {
  createClientFeature,
  slashMenuBasicGroupWithItems,
} from '@payloadcms/richtext-lexical/client'
import { INSERT_MYNODE_COMMAND } from './plugin'
import { IconComponent } from './icon'

export const MyClientFeature = createClientFeature({
  slashMenu: {
    groups: [
      slashMenuBasicGroupWithItems([
        {
          Icon: IconComponent,
          key: 'myNode',
          keywords: ['myNode', 'myFeature', 'someOtherKeyword'],
          label: ({ i18n }) => {
            return i18n.t('lexical:myFeature:label')
          },
          onSelect: ({ editor }) => {
            editor.dispatchCommand(INSERT_MYNODE_COMMAND, undefined)
          },
        },
      ]),
    ],
  },
})
选项描述
Icon在斜杠菜单项中显示的图标。
label在斜杠菜单项中显示的标签。为了支持国际化(i18n),可以设置为一个函数。
key每个斜杠菜单项需要一个唯一的键值。该键值用于匹配输入内容,在没有设置 label 属性时显示,并用于生成类名。
onSelect当斜杠菜单项被选中时调用的函数。
keywords关键词用于匹配用户在斜杠后输入的不同文本。例如,你可能希望当用户输入 /hr、/separator 或 /horizontal 时都显示水平线项目。除了关键词外,标签和键值也会用于查找正确的斜杠菜单项。

Markdown 转换器

与 Server Feature 类似,Client Feature 也允许你添加 markdown 转换器。客户端的 markdown 转换器用于在编辑器中输入特定 markdown 模式时创建新节点。

import { createClientFeature } from '@payloadcms/richtext-lexical/client'
import type { ElementTransformer } from '@payloadcms/richtext-lexical/lexical/markdown'
import { $createMyNode, $isMyNode, MyNode } from './nodes/MyNode'

const MyMarkdownTransformer: ElementTransformer = {
  type: 'element',
  dependencies: [MyNode],
  export: (node, exportChildren) => {
    if (!$isMyNode(node)) {
      return null
    }
    return '+++'
  },
  // 匹配 ---
  regExp: /^+++\s*$/,
  replace: (parentNode) => {
    const node = $createMyNode()
    if (node) {
      parentNode.replace(node)
    }
  },
}

export const MyFeature = createClientFeature({
  markdownTransformers: [MyMarkdownTransformer],
})

在这个示例中,当输入 +++ 时,一个新的 MyNode 将被插入到编辑器中。

提供者 (Providers)

你可以向 client feature 添加 providers,这些 providers 将被嵌套在 EditorConfigProvider 下方。如果你想为节点或 feature 的其他部分提供一些上下文,这会非常有用。

'use client'

import { createClientFeature } from '@payloadcms/richtext-lexical/client'
import { TableContext } from './context'

export const MyClientFeature = createClientFeature({
  providers: [TableContext],
})

属性

要在你的功能中接收属性(Props),请将它们作为泛型类型进行定义。

服务器端功能(Server Feature):

createServerFeature<UnSanitizedProps, SanitizedProps, UnSanitizedClientProps>({
  //...
})

客户端功能(Client Feature):

createClientFeature<UnSanitizedClientProps, SanitizedClientProps>({
  //...
})

未清理(unSanitized)的属性是用户在调用提供者函数并将其添加到编辑器配置时传递给功能的属性。然后你可以选择对这些属性进行清理(sanitize)。

要在服务器端功能中清理这些属性,你可以传递一个函数给feature而不是对象:

createServerFeature<UnSanitizedProps, SanitizedProps, UnSanitizedClientProps>({
  //...
  feature: async ({
    config,
    isRoot,
    props,
    resolvedFeatures,
    unSanitizedEditorConfig,
    featureProviderMap,
  }) => {
    const sanitizedProps = doSomethingWithProps(props)

    return {
      sanitizedServerFeatureProps: sanitizedProps,
      //实际的服务器端功能在这里...
    }
  },
})

请注意,任何清理后的属性都必须在sanitizedServerFeatureProps属性中返回。

在客户端功能中,工作方式类似:

createClientFeature<UnSanitizedClientProps, SanitizedClientProps>(
  ({
    clientFunctions,
    featureProviderMap,
    props,
    resolvedFeatures,
    unSanitizedEditorConfig,
  }) => {
    const sanitizedProps = doSomethingWithProps(props)
    return {
      sanitizedClientFeatureProps: sanitizedProps,
      //实际的客户端功能在这里...
    }
  },
)

将属性从服务器传递到客户端

默认情况下,客户端功能不会从服务器功能接收任何属性。为了将属性从服务器传递到客户端,你需要在服务器功能中返回这些属性:

type UnSanitizedClientProps = {
  test: string
}

createServerFeature<UnSanitizedProps, SanitizedProps, UnSanitizedClientProps>({
  //...
  feature: {
    clientFeatureProps: {
      test: 'myValue',
    },
  },
})

客户端功能默认不拥有与服务器相同的属性,是因为所有客户端属性都必须是可序列化的。你完全可以在服务器功能中接收函数或 Map 等类型的属性,但无法将它们发送到客户端。最终这些属性需要通过网络从服务器传输到客户端,因此它们必须是可序列化的。

更多信息

可以参考我们已经构建的功能 - 理解它们的工作原理将帮助你创建自己的功能。默认包含的功能与你自己创建的功能之间没有区别 - 因为这些功能都与"核心"隔离,无论功能是否是 Payload 的一部分,你都能访问相同的 API!