文档
Custom Blocks

自定义区块类型

除了 BlockNote 提供的默认区块类型之外,您还可以使用 React 组件创建自己的自定义区块。请看下面的演示,我们在 BlockNote 编辑器中添加了一个自定义的警告区块,以及一个用于插入该区块的自定义 斜杠菜单项

import { defaultProps } from "@blocknote/core";
import { createReactBlockSpec } from "@blocknote/react";
import { Menu } from "@mantine/core";
import { MdCancel, MdCheckCircle, MdError, MdInfo } from "react-icons/md";
 
import "./styles.css";
 
// The types of alerts that users can choose from.
export const alertTypes = [
  {
    title: "Warning",
    value: "warning",
    icon: MdError,
    color: "#e69819",
    backgroundColor: {
      light: "#fff6e6",
      dark: "#805d20",
    },
  },
  {
    title: "Error",
    value: "error",
    icon: MdCancel,
    color: "#d80d0d",
    backgroundColor: {
      light: "#ffe6e6",
      dark: "#802020",
    },
  },
  {
    title: "Info",
    value: "info",
    icon: MdInfo,
    color: "#507aff",
    backgroundColor: {
      light: "#e6ebff",
      dark: "#203380",
    },
  },
  {
    title: "Success",
    value: "success",
    icon: MdCheckCircle,
    color: "#0bc10b",
    backgroundColor: {
      light: "#e6ffe6",
      dark: "#208020",
    },
  },
] as const;
 
// The Alert block.
export const Alert = createReactBlockSpec(
  {
    type: "alert",
    propSchema: {
      textAlignment: defaultProps.textAlignment,
      textColor: defaultProps.textColor,
      type: {
        default: "warning",
        values: ["warning", "error", "info", "success"],
      },
    },
    content: "inline",
  },
  {
    render: (props) => {
      const alertType = alertTypes.find(
        (a) => a.value === props.block.props.type,
      )!;
      const Icon = alertType.icon;
      return (
        <div className={"alert"} data-alert-type={props.block.props.type}>
          {/*Icon which opens a menu to choose the Alert type*/}
          <Menu withinPortal={false}>
            <Menu.Target>
              <div className={"alert-icon-wrapper"} contentEditable={false}>
                <Icon
                  className={"alert-icon"}
                  data-alert-icon-type={props.block.props.type}
                  size={32}
                />
              </div>
            </Menu.Target>
            {/*Dropdown to change the Alert type*/}
            <Menu.Dropdown>
              <Menu.Label>Alert Type</Menu.Label>
              <Menu.Divider />
              {alertTypes.map((type) => {
                const ItemIcon = type.icon;
 
                return (
                  <Menu.Item
                    key={type.value}
                    leftSection={
                      <ItemIcon
                        className={"alert-icon"}
                        data-alert-icon-type={type.value}
                      />
                    }
                    onClick={() =>
                      props.editor.updateBlock(props.block, {
                        type: "alert",
                        props: { type: type.value },
                      })
                    }
                  >
                    {type.title}
                  </Menu.Item>
                );
              })}
            </Menu.Dropdown>
          </Menu>
          {/*Rich text field for user to type in*/}
          <div className={"inline-content"} ref={props.contentRef} />
        </div>
      );
    },
  },
);
 

创建自定义区块类型

使用 createReactBlockSpec 函数来创建自定义区块类型。该函数接受两个参数:

function createReactBlockSpec(
  blockConfig: CustomBlockConfig,
  blockImplementation: ReactCustomBlockImplementation,
);

让我们来看演示中的自定义警告区块,并逐一说明各字段的作用:

const Alert = createReactBlockSpec(
  {
    type: "alert",
    propSchema: {
      textAlignment: defaultProps.textAlignment,
      textColor: defaultProps.textColor,
      type: {
        default: "warning",
        values: ["warning", "error", "info", "success"],
      },
    },
    content: "inline",
  },
  {
    render: (props) => {
      ...
    },
  }
);

区块配置(CustomBlockConfig

区块配置描述了自定义区块的结构。用它来指定区块的类型、属性(props)以及内容支持:

type BlockConfig = {
  type: string;
  content: "inline" | "none";
  readonly propSchema: PropSchema;
  isSelectable?: boolean;
  hardBreakShortcut?: "shift+enter" | "enter" | "none";
};

type

定义自定义区块的标识符。

content

如果您的自定义区块支持富文本内容,则设为 inline,否则设为 none

在警告示例中,我们希望用户能够在警告区块中输入文本,因此设为 "inline"

propSchema

PropSchema 指定区块支持的属性。区块属性是存储在文档中的数据,用于定制区块的外观或行为。

type PropSchema<
  PrimitiveType extends "boolean" | "number" | "string"
> = Record<
  string,
  {
    default: PrimitiveType;
    values?: PrimitiveType[];
  }
>

[key: string] 是属性的名称,值是一个包含两个字段的对象:

  • default:指定该属性的默认值

  • values(可选):指定该属性允许的值的数组,例如限制为预定义字符串列表。如果未定义 values,BlockNote 会假定该属性可以是任何 PrimitiveType 类型的值。

在警告示例中,我们为警告类型新增了一个 type 属性(可选值为 warning / error / info / success)。同时我们还使用了 默认区块属性 中的文本对齐和文本颜色来实现基本样式选项。

isSelectable

可以设置为 false 以使区块不可被鼠标或键盘选中。这对于能够选择区块内非编辑内容很有帮助。仅当 contentnone 时才应设置为 false,默认值为 true

hardBreakShortcut

定义在区块的内联内容中插入换行符的键盘快捷键。默认值为 "shift+enter"

文件区块配置

区块实现(ReactCustomBlockImplementation

区块实现定义了区块在编辑器中的渲染方式,以及如何从 HTML 解析和转换为 HTML。

type ReactCustomBlockImplementation = {
  render: React.FC<{
    block: Block;
    editor: BlockNoteEditor;
    contentRef?: (node: HTMLElement | null) => void;
  }>;
  toExternalHTML?: React.FC<{
    block: Block;
    editor: BlockNoteEditor;
    contentRef?: (node: HTMLElement | null) => void;
  }>;
  parse?: (element: HTMLElement) => PartialBlock["props"] | undefined;
};

render

这是您的 React 组件,用于定义如何在编辑器中渲染自定义区块,接收三个 React 属性:

block: 要渲染的区块,其类型和 props 将与区块配置中的类型和 PropSchema 匹配。

editor: 区块所在的 BlockNote 编辑器实例。

contentRef: 一个 React ref,用于标记区块中哪些元素是可编辑的,仅当区块配置中的 content"inline" 时可用。

toExternalHTML(可选)

该组件用于将区块导出为 HTML(如复制到剪贴板后)以供 BlockNote 之外使用。如果未定义,则 BlockNote 会使用 render 进行 HTML 转换。

注意传给 toExternalHTML 的组件会在单独的 React 根中渲染和序列化,因此无法使用依赖 React Context 的 hook。

parse(可选)

parse 函数定义如何将 HTML 内容解析为您的区块,例如从剪贴板粘贴内容时。如果元素应被解析为您定义的自定义区块,则返回区块应使用的 props;否则返回 undefined

element:正在解析的 HTML 元素。

向编辑器添加自定义区块

最后,使用您定义的自定义区块创建一个 BlockNoteSchema:

const schema = BlockNoteSchema.create({
  blockSpecs: {
    // 如需启用默认区块,请使用展开语法
    ...defaultBlockSpecs,
 
    // 添加您自己的自定义区块:
    alert: Alert,
  },
});

然后,您可以使用该自定义 schema 实例化编辑器,具体说明见 自定义 Schema 页面。

改善用户体验

现在您已经知道如何创建自定义区块并将其添加到编辑器,但用户尚无方式往文档中创建该区块实例。

为解决此问题,建议实现一个用于在斜杠菜单中插入自定义区块的 命令、以及在区块类型选择器中添加对应的 选项。下面的演示基于之前的警告区块示例,新增了这两项内容。

import { defaultProps } from "@blocknote/core";
import { createReactBlockSpec } from "@blocknote/react";
import { Menu } from "@mantine/core";
import { MdCancel, MdCheckCircle, MdError, MdInfo } from "react-icons/md";
 
import "./styles.css";
 
// The types of alerts that users can choose from.
export const alertTypes = [
  {
    title: "Warning",
    value: "warning",
    icon: MdError,
    color: "#e69819",
    backgroundColor: {
      light: "#fff6e6",
      dark: "#805d20",
    },
  },
  {
    title: "Error",
    value: "error",
    icon: MdCancel,
    color: "#d80d0d",
    backgroundColor: {
      light: "#ffe6e6",
      dark: "#802020",
    },
  },
  {
    title: "Info",
    value: "info",
    icon: MdInfo,
    color: "#507aff",
    backgroundColor: {
      light: "#e6ebff",
      dark: "#203380",
    },
  },
  {
    title: "Success",
    value: "success",
    icon: MdCheckCircle,
    color: "#0bc10b",
    backgroundColor: {
      light: "#e6ffe6",
      dark: "#208020",
    },
  },
] as const;
 
// The Alert block.
export const Alert = createReactBlockSpec(
  {
    type: "alert",
    propSchema: {
      textAlignment: defaultProps.textAlignment,
      textColor: defaultProps.textColor,
      type: {
        default: "warning",
        values: ["warning", "error", "info", "success"],
      },
    },
    content: "inline",
  },
  {
    render: (props) => {
      const alertType = alertTypes.find(
        (a) => a.value === props.block.props.type,
      )!;
      const Icon = alertType.icon;
      return (
        <div className={"alert"} data-alert-type={props.block.props.type}>
          {/*Icon which opens a menu to choose the Alert type*/}
          <Menu withinPortal={false}>
            <Menu.Target>
              <div className={"alert-icon-wrapper"} contentEditable={false}>
                <Icon
                  className={"alert-icon"}
                  data-alert-icon-type={props.block.props.type}
                  size={32}
                />
              </div>
            </Menu.Target>
            {/*Dropdown to change the Alert type*/}
            <Menu.Dropdown>
              <Menu.Label>Alert Type</Menu.Label>
              <Menu.Divider />
              {alertTypes.map((type) => {
                const ItemIcon = type.icon;
 
                return (
                  <Menu.Item
                    key={type.value}
                    leftSection={
                      <ItemIcon
                        className={"alert-icon"}
                        data-alert-icon-type={type.value}
                      />
                    }
                    onClick={() =>
                      props.editor.updateBlock(props.block, {
                        type: "alert",
                        props: { type: type.value },
                      })
                    }
                  >
                    {type.title}
                  </Menu.Item>
                );
              })}
            </Menu.Dropdown>
          </Menu>
          {/*Rich text field for user to type in*/}
          <div className={"inline-content"} ref={props.contentRef} />
        </div>
      );
    },
  },
);