自定义区块类型
除了 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
以使区块不可被鼠标或键盘选中。这对于能够选择区块内非编辑内容很有帮助。仅当 content
为 none
时才应设置为 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>
);
},
},
);