From 6bb3c97622d616c6b12ccb9d3d5200aec2ee7b8a Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 17 Sep 2025 18:09:13 +0200 Subject: [PATCH 1/3] Updated docs --- .../features/custom-schemas/custom-blocks.mdx | 140 +++++++++++++++--- .../custom-schemas/custom-inline-content.mdx | 25 +++- .../features/custom-schemas/custom-styles.mdx | 11 ++ 3 files changed, 157 insertions(+), 19 deletions(-) diff --git a/docs/content/docs/features/custom-schemas/custom-blocks.mdx b/docs/content/docs/features/custom-schemas/custom-blocks.mdx index 56545c26ae..9fbb9bb795 100644 --- a/docs/content/docs/features/custom-schemas/custom-blocks.mdx +++ b/docs/content/docs/features/custom-schemas/custom-blocks.mdx @@ -12,12 +12,13 @@ In addition to the default block types that BlockNote offers, you can also make ## Creating a Custom Block Type -Use the `createReactBlockSpec` function to create a custom block type. This function takes two arguments: +Use the `createReactBlockSpec` function to create a custom block type. This function takes three arguments: ```typescript function createReactBlockSpec( blockConfig: CustomBlockConfig, blockImplementation: ReactCustomBlockImplementation, + extensions?: BlockNoteExtension[], ); ``` @@ -54,8 +55,6 @@ type BlockConfig = { type: string; content: "inline" | "none"; readonly propSchema: PropSchema; - isSelectable?: boolean; - hardBreakShortcut?: "shift+enter" | "enter" | "none"; }; ``` @@ -64,8 +63,7 @@ type BlockConfig = { `content:` `inline` if your custom block should support rich text content, `none` if not. - _In the alert demo, we want the user to be able to type text in our alert, so - we set `content` to `"inline"`._ + _In the alert demo, we want the user to be able to type text in our alert, so we set `content` to `"inline"`._ `propSchema:` The `PropSchema` specifies the props that the block supports. Block props (properties) are data stored with your Block in the document, and can be used to customize its appearance or behavior. @@ -100,18 +98,9 @@ If you do not want the prop to have a default value, you can define it as an obj - `values?:` Specifies an array of values that the prop can take, for example, to limit the value to a list of pre-defined strings. If `values` is not defined, BlockNote assumes the prop can be any value of `PrimitiveType`. - _In the alert demo, we add a `type` prop for the type of alert that we want - (warning / error / info / success). We also want basic styling options, so we - add text alignment and text color from the [Default Block - Properties](/docs/features/blocks#default-block-properties)._ + _In the alert demo, we add a `type` prop for the type of alert that we want (warning / error / info / success). We also want basic styling options, so we add text alignment and text color from the [Default Block Properties](/docs/features/blocks#default-block-properties)._ -`isSelectable?:` Can be set to false in order to make the block non-selectable, both using the mouse and keyboard. This also helps with being able to select non-editable content within the block. Should only be set to false when `content` is `none` and defaults to true. - -`hardBreakShortcut?:` Defines which keyboard shortcut should be used to insert a hard break into the block's inline content. Defaults to `"shift+enter"`. - -#### File Block Config - ### Block Implementation (`ReactCustomBlockImplementation`) The Block Implementation defines how the block should be rendered in the editor, and how it should be parsed from and converted to HTML. @@ -129,6 +118,16 @@ type ReactCustomBlockImplementation = { contentRef?: (node: HTMLElement | null) => void; }>; parse?: (element: HTMLElement) => PartialBlock["props"] | undefined; + parseContent?: (options: { el: HTMLElement; schema: Schema }) => Fragment; + runsBefore?: string[]; + meta?: { + hardBreakShortcut?: "shift+enter" | "enter" | "none"; + selectable?: boolean; + fileBlockAccept?: string[]; + code?: boolean; + defining?: boolean; + isolating?: boolean; + }; }; ``` @@ -143,15 +142,120 @@ type ReactCustomBlockImplementation = { `toExternalHTML?:` This component is used whenever the block is being exported to HTML for use outside BlockNote, for example when copying it to the clipboard. If it's not defined, BlockNote will just use `render` for the HTML conversion. Takes the same props as `render`. - _Note that your component passed to `toExternalHTML` is rendered and - serialized in a separate React root, which means you can't use hooks that rely - on React Contexts._ + _Note that your component passed to `toExternalHTML` is rendered and serialized in a separate React root, which means you can't use hooks that rely on React Contexts._ `parse?:` The `parse` function defines how to parse HTML content into your block, for example when pasting contents from the clipboard. If the element should be parsed into your custom block, you return the props that the block should be given. Otherwise, return `undefined`. Takes a single argument: - `element`: The HTML element that's being parsed. +`parseContent?:` While `parse` specifies which HTML elements to parse the block from, `parseContent` specifies how to find the block's content from those elements. This is only needed for advanced use cases where certain text elements should be ignored, combined, or moved. By default, BlockNote automatically parses the content automatically. Takes a single argument: + +- `options:` An object containing the HTML element to parse content from, and the schema of the underlying ProseMirror editor used by BlockNote. The schema is there for use in a [`DOMParser`](https://prosemirror.net/docs/ref/#model.DOMParser). + +`runsBefore?:` If this block has parsing or extensions that need to be given priority over any other blocks, you can pass their `type`s in an array here. + +`meta?:` An object for setting various generic properties of the block. + +- `hardBreakShortcut?:` Defines which keyboard shortcut should be used to insert a hard break into the block's inline content. Defaults to `"shift+enter"`. + +- `selectable?:` Can be set to false in order to make the block non-selectable, both using the mouse and keyboard. This also helps with being able to select non-editable content within the block. Should only be set to false when `content` is `none` and defaults to true. + +- `fileBlockAccept?:` For custom file blocks, this specifies which MIME types are accepted when uploading a file. All file blocks should specify this property, and should use a [`FileBlockWrapper`](https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/blocks/File/helpers/render/FileBlockWrapper.tsx)/[`ResizableFileBlockWrapper`](https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/blocks/File/helpers/render/ResizableFileBlockWrapper.tsx) component in their `render` functions (see next subsection). + +- `code?:` Whether this block contains [code](https://prosemirror.net/docs/ref/#model.NodeSpec.code). + +- `defining?:` Whether this block is [defining](https://prosemirror.net/docs/ref/#model.NodeSpec.defining). + +- `isolating?:` Whether this block is [isolating](https://prosemirror.net/docs/ref/#model.NodeSpec.isolating). + +To see an example of this, check out the [built-in heading block](https://github.com/TypeCellOS/BlockNote/blob/main/packages/core/src/blocks/Heading/block.ts). + +### Block Extensions + +While not shown in the demo, the `createBlockSpec` function also takes a third argument, `extensions`. It takes an array of `BlockNoteExtension` objects, which are most easily created using the `createBlockNoteExtension` function: + +```typescript +type BlockNoteExtensionOptions = { + key: string; + keyboardShortcuts?: Record< + string, + (ctx: { editor: BlockNoteEditor; }) => boolean + >; + inputRules?: { + find: RegExp; + replace: (props: { + match: RegExpMatchArray; + range: { from: number; to: number }; + editor: BlockNoteEditor; + }) => PartialBlock | undefined; + }[]; + plugins?: Plugin[]; + tiptapExtensions?: AnyExtension[]; +} + +const customBlockExtensionOptions: BlockNoteExtensionOptions = { + key: "customBlockExtension", + keyboardShortcuts: ..., + inputRules: ..., + plugins: ..., + tiptapExtensions: ..., +} + +const CustomBlock = createReactBlockSpec( + { + type: ..., + propSchema: ..., + content: ..., + }, + { + render: ..., + ... + }, + [createBlockNoteExtension(customBlockExtensionOptions)] +) +``` + +Let's go over the options that can be passed into `createBlockNoteExtension`: + +`key:` The name of the extension. + +`keyboardShortcuts?:` Keyboard shortcuts can be used to run code when a key combination is pressed in the editor. The key names are the same as those used in the [`KeyboardEvent.key` property](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values). Takes an object which maps key name combinations (e.g. `Meta+Shift+ArrowDown`) to functions, which return `true` when the key press event is handled, or `false` otherwise. The functions have a single argument: + +- `ctx:` An object containing the BlockNote editor instance. + +`inputRules?:` Input rules update blocks when given regular expressions are found in them. Takes an array of objects. Each object has a `find` field for the regular expression to find, and a `replace` field, for a function that should run on a match. The function should return a [`PartialBlock`](docs/reference/editor/manipulating-content#partial-blocks) which specifies how the block should be updated, or avoid updating it. It also has a single argument: + +- `props:` An object containing the result of the regular expression match, a range for the [Prosemirror position indices](https://prosemirror.net/docs/guide/#doc.indexing) spanned by the match, and the BlockNote editor instance. + +`plugins?:` An array of [ProseMirror plugins](https://prosemirror.net/docs/ref/#state.Plugin_System). + +`tiptapExtensions?:` An array of [TipTap extensions](https://tiptap.dev/docs/editor/extensions/custom-extensions/create-new). + +### Block Config Options + +In some cases, you may want to have a customizable block config. For example, you may want to be able to have a code block with syntax highlighting for either web or embedded code, or a heading block with a flexible number of heading levels. You can use the same API for this use case, with some minor changes: + +```typescript +// Arbitrary options that your block can take, e.g. number of heading levels or +// available code syntax highlight languages. +type CustomBlockConfigOptions = { + ... +} + +const CustomBlock = createReactBlockSpec( + createBlockConfig((options: CustomBlockConfigOptions) => ({ + type: ..., + propSchema: ..., + content: ..., + })), + (options: CustomBlockConfigOptions) => ({ + render: ..., + ... + }) +) +``` + ## Adding Custom Blocks to the Editor Finally, create a BlockNoteSchema using the definition of your custom blocks: diff --git a/docs/content/docs/features/custom-schemas/custom-inline-content.mdx b/docs/content/docs/features/custom-schemas/custom-inline-content.mdx index 2774d864df..b754fc4f47 100644 --- a/docs/content/docs/features/custom-schemas/custom-inline-content.mdx +++ b/docs/content/docs/features/custom-schemas/custom-inline-content.mdx @@ -110,9 +110,18 @@ The Inline Content Implementation defines how the inline content should be rende type ReactCustomInlineContentImplementation = { render: React.FC<{ inlineContent: InlineContent; + editor: BlockNoteEditor; + contentRef?: (node: HTMLElement | null) => void; + }>; + toExternalHTML?: React.FC<{ + inlineContent: InlineContent; + editor: BlockNoteEditor; contentRef?: (node: HTMLElement | null) => void; - draggable?: boolean; }>; + parse?: (element: HTMLElement) => PartialInlineContent["props"] | undefined; + meta?: { + draggable?: boolean; + }; }; ``` @@ -124,6 +133,20 @@ type ReactCustomInlineContentImplementation = { - `draggable:` Specifies whether the inline content can be dragged within the editor. If set to `true`, the inline content will be draggable. Defaults to `false` if not specified. If this is true, you should add `data-drag-handle` to the DOM element that should function as the drag handle. +`toExternalHTML?:` This component is used whenever the inline content is being exported to HTML for use outside BlockNote, for example when copying it to the clipboard. If it's not defined, BlockNote will just use `render` for the HTML conversion. Takes the same props as `render`. + + + _Note that your component passed to `toExternalHTML` is rendered and + serialized in a separate React root, which means you can't use hooks that rely + on React Contexts._ + + +`parse?:` The `parse` function defines how to parse HTML content into your inline content, for example when pasting contents from the clipboard. If the element should be parsed into your custom inline content, you return the props that the block should be given. Otherwise, return `undefined`. Takes a single argument: + +- `element`: The HTML element that's being parsed. + +`meta?.draggable?:` Whether the inline content should be draggable. + _Note that since inline content is, by definition, inline, your component should also return an HTML inline element._ diff --git a/docs/content/docs/features/custom-schemas/custom-styles.mdx b/docs/content/docs/features/custom-schemas/custom-styles.mdx index 1e33a9e75a..c0d983e9f7 100644 --- a/docs/content/docs/features/custom-schemas/custom-styles.mdx +++ b/docs/content/docs/features/custom-schemas/custom-styles.mdx @@ -67,6 +67,11 @@ type ReactCustomStyleImplementation = { value?: string; contentRef: (node: HTMLElement | null) => void; }>; + toExternalHTML?: React.FC<{ + value?: string; + contentRef: (node: HTMLElement | null) => void; + }>; + parse?: (element: HTMLElement) => string | true | undefined; }; ``` @@ -82,6 +87,12 @@ type ReactCustomStyleImplementation = { should be plain React functions analogous to the example._ +`toExternalHTML?:` This component is used whenever the style is being exported to HTML for use outside BlockNote, for example when copying it to the clipboard. If it's not defined, BlockNote will just use `render` for the HTML conversion. Takes the same props as `render`. + +`parse?:` The `parse` function defines how to parse HTML content into your style, for example when pasting contents from the clipboard. If the element should be parsed into your custom style, you return a `string` or `true`. If the `propSchema` is `"string"`, you should likewise return a string value, or `true` otherwise. Returning `undefined` will not parse the style from the HTML element. Takes a single argument: + +- `element`: The HTML element that's being parsed. + ## Adding Custom Style to the Editor Finally, create a BlockNoteSchema using the definition of your custom style: From ea2482eb1dc087919fad3635a6ce44ed4bfe82e3 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 17 Sep 2025 18:16:28 +0200 Subject: [PATCH 2/3] Small fix --- .../docs/features/custom-schemas/custom-styles.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/content/docs/features/custom-schemas/custom-styles.mdx b/docs/content/docs/features/custom-schemas/custom-styles.mdx index c0d983e9f7..de9443d3bf 100644 --- a/docs/content/docs/features/custom-schemas/custom-styles.mdx +++ b/docs/content/docs/features/custom-schemas/custom-styles.mdx @@ -81,14 +81,14 @@ type ReactCustomStyleImplementation = { - `contentRef:` A React `ref` to mark the editable element. +`toExternalHTML?:` This component is used whenever the style is being exported to HTML for use outside BlockNote, for example when copying it to the clipboard. If it's not defined, BlockNote will just use `render` for the HTML conversion. Takes the same props as `render`. + - _Note that in contrast to Custom Blocks and Inline Content, the `render` - function of Custom Styles cannot access React Context or other state. They - should be plain React functions analogous to the example._ + _Note that your component passed to `toExternalHTML` is rendered and + serialized in a separate React root, which means you can't use hooks that rely + on React Contexts._ -`toExternalHTML?:` This component is used whenever the style is being exported to HTML for use outside BlockNote, for example when copying it to the clipboard. If it's not defined, BlockNote will just use `render` for the HTML conversion. Takes the same props as `render`. - `parse?:` The `parse` function defines how to parse HTML content into your style, for example when pasting contents from the clipboard. If the element should be parsed into your custom style, you return a `string` or `true`. If the `propSchema` is `"string"`, you should likewise return a string value, or `true` otherwise. Returning `undefined` will not parse the style from the HTML element. Takes a single argument: - `element`: The HTML element that's being parsed. From 2e9f88128631776d76db0871ceb931d7c2974a54 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Thu, 18 Sep 2025 18:36:46 +0200 Subject: [PATCH 3/3] Implemented PR feedback --- .../features/custom-schemas/custom-blocks.mdx | 109 ++++++------------ .../custom-schemas/custom-inline-content.mdx | 19 ++- .../features/custom-schemas/custom-styles.mdx | 6 +- .../docs/features/custom-schemas/index.mdx | 59 ++++++++-- docs/content/docs/features/extensions.mdx | 101 ++++++++++++++++ docs/content/docs/features/meta.json | 1 + .../01-alert-block/src/Alert.tsx | 2 +- .../01-alert-block/src/App.tsx | 10 +- .../05-alert-block-full-ux/src/Alert.tsx | 2 +- .../05-alert-block-full-ux/src/App.tsx | 10 +- packages/core/src/editor/BlockNoteEditor.ts | 4 +- 11 files changed, 211 insertions(+), 112 deletions(-) create mode 100644 docs/content/docs/features/extensions.mdx diff --git a/docs/content/docs/features/custom-schemas/custom-blocks.mdx b/docs/content/docs/features/custom-schemas/custom-blocks.mdx index 9fbb9bb795..6bee335c2e 100644 --- a/docs/content/docs/features/custom-schemas/custom-blocks.mdx +++ b/docs/content/docs/features/custom-schemas/custom-blocks.mdx @@ -19,13 +19,15 @@ function createReactBlockSpec( blockConfig: CustomBlockConfig, blockImplementation: ReactCustomBlockImplementation, extensions?: BlockNoteExtension[], -); +): (options? BlockOptions) => BlockSpec; ``` -Let's look at our custom alert block from the demo, and go over each field to explain how it works: +It returns a function that you can call to create an instance of your custom block, or a `BlockSpec`. This `BlockSpec` then gets passed into your [BlockNote schema](/docs/features/custom-schemas#creating-your-own-schema) to add the block to the editor. This function may also take arbitrary options, which you can find out more about [below](/docs/features/custom-schemas/custom-blocks#block-config-options). + +Let's look at our custom alert block from the demo, and go over everything we pass to `createReactBlockSpec`: ```typescript -const Alert = createReactBlockSpec( +const createAlert = createReactBlockSpec( { type: "alert", propSchema: { @@ -63,7 +65,8 @@ type BlockConfig = { `content:` `inline` if your custom block should support rich text content, `none` if not. - _In the alert demo, we want the user to be able to type text in our alert, so we set `content` to `"inline"`._ + _In the alert demo, we want the user to be able to type text in our alert, so + we set `content` to `"inline"`._ `propSchema:` The `PropSchema` specifies the props that the block supports. Block props (properties) are data stored with your Block in the document, and can be used to customize its appearance or behavior. @@ -98,7 +101,10 @@ If you do not want the prop to have a default value, you can define it as an obj - `values?:` Specifies an array of values that the prop can take, for example, to limit the value to a list of pre-defined strings. If `values` is not defined, BlockNote assumes the prop can be any value of `PrimitiveType`. - _In the alert demo, we add a `type` prop for the type of alert that we want (warning / error / info / success). We also want basic styling options, so we add text alignment and text color from the [Default Block Properties](/docs/features/blocks#default-block-properties)._ + _In the alert demo, we add a `type` prop for the type of alert that we want + (warning / error / info / success). We also want basic styling options, so we + add text alignment and text color from the [Default Block + Properties](/docs/features/blocks#default-block-properties)._ ### Block Implementation (`ReactCustomBlockImplementation`) @@ -118,7 +124,6 @@ type ReactCustomBlockImplementation = { contentRef?: (node: HTMLElement | null) => void; }>; parse?: (element: HTMLElement) => PartialBlock["props"] | undefined; - parseContent?: (options: { el: HTMLElement; schema: Schema }) => Fragment; runsBefore?: string[]; meta?: { hardBreakShortcut?: "shift+enter" | "enter" | "none"; @@ -142,17 +147,15 @@ type ReactCustomBlockImplementation = { `toExternalHTML?:` This component is used whenever the block is being exported to HTML for use outside BlockNote, for example when copying it to the clipboard. If it's not defined, BlockNote will just use `render` for the HTML conversion. Takes the same props as `render`. - _Note that your component passed to `toExternalHTML` is rendered and serialized in a separate React root, which means you can't use hooks that rely on React Contexts._ + _Note that your component passed to `toExternalHTML` is rendered and + serialized in a separate React root, which means you can't use hooks that rely + on React Contexts._ `parse?:` The `parse` function defines how to parse HTML content into your block, for example when pasting contents from the clipboard. If the element should be parsed into your custom block, you return the props that the block should be given. Otherwise, return `undefined`. Takes a single argument: - `element`: The HTML element that's being parsed. -`parseContent?:` While `parse` specifies which HTML elements to parse the block from, `parseContent` specifies how to find the block's content from those elements. This is only needed for advanced use cases where certain text elements should be ignored, combined, or moved. By default, BlockNote automatically parses the content automatically. Takes a single argument: - -- `options:` An object containing the HTML element to parse content from, and the schema of the underlying ProseMirror editor used by BlockNote. The schema is there for use in a [`DOMParser`](https://prosemirror.net/docs/ref/#model.DOMParser). - `runsBefore?:` If this block has parsing or extensions that need to be given priority over any other blocks, you can pass their `type`s in an array here. `meta?:` An object for setting various generic properties of the block. @@ -169,68 +172,11 @@ type ReactCustomBlockImplementation = { - `isolating?:` Whether this block is [isolating](https://prosemirror.net/docs/ref/#model.NodeSpec.isolating). -To see an example of this, check out the [built-in heading block](https://github.com/TypeCellOS/BlockNote/blob/main/packages/core/src/blocks/Heading/block.ts). - ### Block Extensions -While not shown in the demo, the `createBlockSpec` function also takes a third argument, `extensions`. It takes an array of `BlockNoteExtension` objects, which are most easily created using the `createBlockNoteExtension` function: - -```typescript -type BlockNoteExtensionOptions = { - key: string; - keyboardShortcuts?: Record< - string, - (ctx: { editor: BlockNoteEditor; }) => boolean - >; - inputRules?: { - find: RegExp; - replace: (props: { - match: RegExpMatchArray; - range: { from: number; to: number }; - editor: BlockNoteEditor; - }) => PartialBlock | undefined; - }[]; - plugins?: Plugin[]; - tiptapExtensions?: AnyExtension[]; -} - -const customBlockExtensionOptions: BlockNoteExtensionOptions = { - key: "customBlockExtension", - keyboardShortcuts: ..., - inputRules: ..., - plugins: ..., - tiptapExtensions: ..., -} - -const CustomBlock = createReactBlockSpec( - { - type: ..., - propSchema: ..., - content: ..., - }, - { - render: ..., - ... - }, - [createBlockNoteExtension(customBlockExtensionOptions)] -) -``` - -Let's go over the options that can be passed into `createBlockNoteExtension`: - -`key:` The name of the extension. - -`keyboardShortcuts?:` Keyboard shortcuts can be used to run code when a key combination is pressed in the editor. The key names are the same as those used in the [`KeyboardEvent.key` property](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values). Takes an object which maps key name combinations (e.g. `Meta+Shift+ArrowDown`) to functions, which return `true` when the key press event is handled, or `false` otherwise. The functions have a single argument: +While the example on this page doesn't use it, `createReactBlockSpec` takes a third, optional argument `extensions`. This is for adding editor `extensions` that are specific to the block, which you can find out more about [here](/docs/features/extensions). -- `ctx:` An object containing the BlockNote editor instance. - -`inputRules?:` Input rules update blocks when given regular expressions are found in them. Takes an array of objects. Each object has a `find` field for the regular expression to find, and a `replace` field, for a function that should run on a match. The function should return a [`PartialBlock`](docs/reference/editor/manipulating-content#partial-blocks) which specifies how the block should be updated, or avoid updating it. It also has a single argument: - -- `props:` An object containing the result of the regular expression match, a range for the [Prosemirror position indices](https://prosemirror.net/docs/guide/#doc.indexing) spanned by the match, and the BlockNote editor instance. - -`plugins?:` An array of [ProseMirror plugins](https://prosemirror.net/docs/ref/#state.Plugin_System). - -`tiptapExtensions?:` An array of [TipTap extensions](https://tiptap.dev/docs/editor/extensions/custom-extensions/create-new). +Block extensions are typically things like e.g. adding keyboard shortcuts to change the current block type to a custom block. For a table of contents block, an extension could also add a ProseMirror plugin to scan for headings to put in the ToC. ### Block Config Options @@ -243,9 +189,9 @@ type CustomBlockConfigOptions = { ... } -const CustomBlock = createReactBlockSpec( +const createCustomBlock = createReactBlockSpec( createBlockConfig((options: CustomBlockConfigOptions) => ({ - type: ..., + type: "customBlock" propSchema: ..., content: ..., })), @@ -254,8 +200,25 @@ const CustomBlock = createReactBlockSpec( ... }) ) + +const options: CustomBlockConfigOptions = { + ... +}; + +const schema = BlockNoteSchema.create().extend({ + blockSpecs: { + // Creates an instance of the custom block and adds it to the schema. + customBlock: createCustomBlock(options), + }, +}); ``` +You can see that instead of passing plain objects for the config and implementation, we instead pass functions. These take the block options as an argument, and return the config and implementation objects respectively. Additionally, the function for creating the config is wrapped in a `createBlockConfig` function. + +Also notice that for the example on this page, we create a new Alert block instance by simply calling `createAlert()` with no arguments. When a custom block takes options though, you can pass them in when creating an instance, as shown above. + +To see a full example of block options being used, check out the [built-in heading block](https://github.com/TypeCellOS/BlockNote/blob/main/packages/core/src/blocks/Heading/block.ts). + ## Adding Custom Blocks to the Editor Finally, create a BlockNoteSchema using the definition of your custom blocks: @@ -267,7 +230,7 @@ const schema = BlockNoteSchema.create({ ...defaultBlockSpecs, // Add your own custom blocks: - alert: Alert, + alert: createAlert(), }, }); ``` diff --git a/docs/content/docs/features/custom-schemas/custom-inline-content.mdx b/docs/content/docs/features/custom-schemas/custom-inline-content.mdx index 1b14c8884a..73bc739006 100644 --- a/docs/content/docs/features/custom-schemas/custom-inline-content.mdx +++ b/docs/content/docs/features/custom-schemas/custom-inline-content.mdx @@ -18,10 +18,12 @@ Use the `createReactInlineContentSpec` function to create a custom inline conten function createReactInlineContentSpec( blockConfig: CustomInlineContentConfig, blockImplementation: ReactInlineContentImplementation, -); +): InlineContentSpec; ``` -Let's look at our custom mentions tag from the demo, and go over each field to explain how it works: +It returns an instance of your custom inline content, or an `InlineContentSpec`. This `InlineContentSpec` then gets passed into your [BlockNote schema](/docs/features/custom-schemas#creating-your-own-schema) to add the inline content to the editor. + +Let's look at our custom mentions tag from the demo, and go over everything we pass to `createReactInlineContentSpec`: ```typescript const Mention = createReactInlineContentSpec( @@ -59,8 +61,7 @@ type CustomInlineContentConfig = { `content:` `styled` if your custom inline content should contain [`StyledText`](/docs/foundations/document-structure#inline-content-objects), `none` if not. - _In the mentions demo, we want each mention to be a single, non-editable - element, so we set `content` to `"none"`._ + _In the mentions demo, we want each mention to be a single, non-editable element, so we set `content` to `"none"`._ `propSchema:` The `PropSchema` specifies the props that the inline content supports. Inline content props (properties) are data stored with your inline content in the document, and can be used to customize its appearance or behavior. @@ -95,8 +96,7 @@ If you do not want the prop to have a default value, you can define it as an obj - `values?:` Specifies an array of values that the prop can take, for example, to limit the value to a list of pre-defined strings. If `values` is not defined, BlockNote assumes the prop can be any value of `PrimitiveType`. - _In the mentions demo, we add a `user` prop for the user that's being - mentioned._ + _In the mentions demo, we add a `user` prop for the user that's being mentioned._ ### Inline Content Implementation (`ReactCustomInlineContentImplementation`) @@ -136,9 +136,7 @@ type ReactCustomInlineContentImplementation = { `toExternalHTML?:` This component is used whenever the inline content is being exported to HTML for use outside BlockNote, for example when copying it to the clipboard. If it's not defined, BlockNote will just use `render` for the HTML conversion. Takes the same props as `render`. - _Note that your component passed to `toExternalHTML` is rendered and - serialized in a separate React root, which means you can't use hooks that rely - on React Contexts._ + _Note that your component passed to `toExternalHTML` is rendered and serialized in a separate React root, which means you can't use hooks that rely on React Contexts._ `parse?:` The `parse` function defines how to parse HTML content into your inline content, for example when pasting contents from the clipboard. If the element should be parsed into your custom inline content, you return the props that the block should be given. Otherwise, return `undefined`. Takes a single argument: @@ -148,8 +146,7 @@ type ReactCustomInlineContentImplementation = { `meta?.draggable?:` Whether the inline content should be draggable. - _Note that since inline content is, by definition, inline, your component - should also return an HTML inline element._ + _Note that since inline content is, by definition, inline, your component should also return an HTML inline element._ ## Adding Custom Inline Content to the Editor diff --git a/docs/content/docs/features/custom-schemas/custom-styles.mdx b/docs/content/docs/features/custom-schemas/custom-styles.mdx index de9443d3bf..2ae91be8d0 100644 --- a/docs/content/docs/features/custom-schemas/custom-styles.mdx +++ b/docs/content/docs/features/custom-schemas/custom-styles.mdx @@ -18,10 +18,12 @@ Use the `createReactStyleSpec` function to create a custom style type. This func function createReactStyleSpec( styleConfig: CustomStyleConfig, styleImplementation: ReactStyleImplementation, -); +): StyleSpec; ``` -Let's look at our custom font style from the demo, and go over each field to explain how it works: +It returns an instance of your custom inline content, or a `StyleSpec`. This `StyleSpec` then gets passed into your [BlockNote schema](/docs/features/custom-schemas#creating-your-own-schema) to add the style to the editor. + +Let's look at our custom font style from the demo, and go over everything we pass to `createReactStyleSpec`: ```typescript export const Font = createReactStyleSpec( diff --git a/docs/content/docs/features/custom-schemas/index.mdx b/docs/content/docs/features/custom-schemas/index.mdx index 4f57a3b8e9..c8298fe080 100644 --- a/docs/content/docs/features/custom-schemas/index.mdx +++ b/docs/content/docs/features/custom-schemas/index.mdx @@ -28,35 +28,72 @@ Text Styles are properties that can be applied to a piece of text, such as bold, ## Creating your own schema -Once you have defined your custom blocks (see the links above), inline content or styles, you can create a schema and pass this to the initialization of the editor. +Once you have defined your custom blocks (see the links above), inline content or styles, you can create a schema and pass this to the initialization of the editor. There are two ways to create a new schema. + +### Extending an existing schema + +You can call `BlockNoteSchema.extend` to add custom blocks, inline content, or styles to an existing schema. While this works for any existing schema, it's most common to use this to extend the default schema. + +```typescript +// Creates an instance of the default schema when nothing is passed to +// `BlockNoteSchema.create`. +const schema = BlockNoteSchema.create() + // Adds custom blocks, inline content, or styles to the default schema. + .extend({ + blockSpecs: { + // Add your own custom blocks: + customBlock: CustomBlock, + ... + }, + inlineContentSpecs: { + // Add your own custom inline content: + customInlineContent: CustomInlineContent, + ... + }, + styleSpecs: { + // Add your own custom styles: + customStyle: CustomStyle, + ... + }, + }); +``` + +### Creating a schema from scratch + +Passing custom blocks, inline content, or styles directly into `BlockNoteSchema.create` will produce a new schema with only the things you pass. This can be useful if you only need a few basic things from the default schema, and intend to implement everything else yourself. ```typescript const schema = BlockNoteSchema.create({ blockSpecs: { - // enable the default blocks if desired - ...defaultBlockSpecs, + // Add only the default paragraph block: + paragraph: defaultBlockSpecs.paragraph, // Add your own custom blocks: - // customBlock: CustomBlock, + customBlock: CustomBlock, + ... }, inlineContentSpecs: { - // enable the default inline content if desired - ...defaultInlineContentSpecs, + // Add only the default text inline content: + text: defaultInlineContentSpecs.text, // Add your own custom inline content: - // customInlineContent: CustomInlineContent, + customInlineContent: CustomInlineContent, + ... }, styleSpecs: { - // enable the default styles if desired - ...defaultStyleSpecs, + // Add only the default bold style: + bold: defaultStyleSpecs.bold, // Add your own custom styles: - // customStyle: CustomStyle + customStyle: CustomStyle, + ... }, }); ``` -You can then pass this to the instantiation of your BlockNoteEditor (`BlockNoteEditor.create` or `useCreateBlockNote`): +## Using your own schema + +Once you've created an instance of your schema using `BlockNoteSchema.create` or `BlockNoteSchema.extend`, you can pass it to the `schema` option of your BlockNoteEditor (`BlockNoteEditor.create` or `useCreateBlockNote`): ```typescript const editor = useCreateBlockNote({ diff --git a/docs/content/docs/features/extensions.mdx b/docs/content/docs/features/extensions.mdx new file mode 100644 index 0000000000..e2fa8e904e --- /dev/null +++ b/docs/content/docs/features/extensions.mdx @@ -0,0 +1,101 @@ +--- +title: Extensions +description: Add extensions to the editor to add keyboard shortcuts, input rules, and more. +--- + +# Extensions + +BlockNote includes an extensions system which lets you expand the editor's behaviour. Extensions can include any of the following features: + +- Keyboard shortcuts +- Input rules +- [ProseMirror plugins](https://prosemirror.net/docs/ref/#state.Plugin_System) +- [TipTap extensions](https://tiptap.dev/docs/editor/extensions/custom-extensions/create-new) + +## Creating an extension + +An extension is an instance of the [`BlockNoteExtension`](https://github.com/TypeCellOS/BlockNote/blob/10cdbfb5f77ef82f3617c0fa1191e0bf5b7358c5/packages/core/src/editor/BlockNoteExtension.ts#L13) class. However, it's recommended for most use cases to create extensions using the `createBlockNoteExtension` function, rather than instanciating the class directly: + +```typescript +type BlockNoteExtensionOptions = { + key: string; + keyboardShortcuts?: Record< + string, + (ctx: { editor: BlockNoteEditor; }) => boolean + >; + inputRules?: { + find: RegExp; + replace: (props: { + match: RegExpMatchArray; + range: { from: number; to: number }; + editor: BlockNoteEditor; + }) => PartialBlock | undefined; + }[]; + plugins?: Plugin[]; + tiptapExtensions?: AnyExtension[]; +} + +const customBlockExtensionOptions: BlockNoteExtensionOptions = { + key: "customBlockExtension", + keyboardShortcuts: ..., + inputRules: ..., + plugins: ..., + tiptapExtensions: ..., +} + +const CustomExtension = createBlockNoteExtension(customBlockExtensionOptions); +``` + +Let's go over the options that can be passed into `createBlockNoteExtension`: + +`key:` The name of the extension. + +`keyboardShortcuts?:` Keyboard shortcuts can be used to run code when a key combination is pressed in the editor. The key names are the same as those used in the [`KeyboardEvent.key` property](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values). Takes an object which maps key name combinations (e.g. `Meta+Shift+ArrowDown`) to functions, which return `true` when the key press event is handled, or `false` otherwise. The functions have a single argument: + +- `ctx:` An object containing the BlockNote editor instance. + +`inputRules?:` Input rules update blocks when given regular expressions are found in them. Takes an array of objects. Each object has a `find` field for the regular expression to find, and a `replace` field, for a function that should run on a match. The function should return a [`PartialBlock`](docs/reference/editor/manipulating-content#partial-blocks) which specifies how the block should be updated, or avoid updating it. It also has a single argument: + +- `props:` An object containing the result of the regular expression match, a range for the [Prosemirror position indices](https://prosemirror.net/docs/guide/#doc.indexing) spanned by the match, and the BlockNote editor instance. + +`plugins?:` An array of [ProseMirror plugins](https://prosemirror.net/docs/ref/#state.Plugin_System). + +`tiptapExtensions?:` An array of [TipTap extensions](https://tiptap.dev/docs/editor/extensions/custom-extensions/create-new). + +## Adding extensions to the editor + +Extensions can be added to the editor on their own via the [editor options](/docs/reference/editor/overview#options) or as part of [custom blocks](/docs/features/custom-schemas/custom-blocks). + +### Adding directly to the editor + +The `extensions` [editor option](/docs/reference/editor/overview#options) takes an array of `BlockNoteExtension`s to be added to the editor: + +```typescript +const editor = useCreateBlockNote({ + extensions: [ + // Add extensions here: + createBlockNoteExtension({ ... }) + ], +}); +``` + +### Adding to custom blocks + +When creating a [custom block](/docs/features/custom-schemas/custom-blocks#creating-a-custom-block-type) using `createReactBlockSpec`, you can pass an array of `BlockNoteExtension`s to the third parameter: + +```typescript +const createCustomBlock = createReactBlockSpec( + { + // Block config + ... + }, + { + // Block implementation + ... + } + [ + // Add extensions here: + createBlockNoteExtension({ ... }) + ], +}); +``` diff --git a/docs/content/docs/features/meta.json b/docs/content/docs/features/meta.json index e6d3535cbe..97ed9ffce7 100644 --- a/docs/content/docs/features/meta.json +++ b/docs/content/docs/features/meta.json @@ -8,6 +8,7 @@ "import", "server-processing", "localization", + "extensions", "..." ] } diff --git a/examples/06-custom-schema/01-alert-block/src/Alert.tsx b/examples/06-custom-schema/01-alert-block/src/Alert.tsx index 4b2af03fbe..079cb8bebc 100644 --- a/examples/06-custom-schema/01-alert-block/src/Alert.tsx +++ b/examples/06-custom-schema/01-alert-block/src/Alert.tsx @@ -50,7 +50,7 @@ export const alertTypes = [ ] as const; // The Alert block. -export const Alert = createReactBlockSpec( +export const createAlert = createReactBlockSpec( { type: "alert", propSchema: { diff --git a/examples/06-custom-schema/01-alert-block/src/App.tsx b/examples/06-custom-schema/01-alert-block/src/App.tsx index 416e3e5686..929f5b8459 100644 --- a/examples/06-custom-schema/01-alert-block/src/App.tsx +++ b/examples/06-custom-schema/01-alert-block/src/App.tsx @@ -4,16 +4,14 @@ import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; import { useCreateBlockNote } from "@blocknote/react"; -import { Alert } from "./Alert"; +import { createAlert } from "./Alert"; // Our schema with block specs, which contain the configs and implementations for // blocks that we want our editor to use. -const schema = BlockNoteSchema.create({ +const schema = BlockNoteSchema.create().extend({ blockSpecs: { - // Adds all default blocks. - ...defaultBlockSpecs, - // Adds the Alert block. - alert: Alert(), + // Creates an instance of the Alert block and adds it to the schema. + alert: createAlert(), }, }); diff --git a/examples/06-custom-schema/05-alert-block-full-ux/src/Alert.tsx b/examples/06-custom-schema/05-alert-block-full-ux/src/Alert.tsx index 4b2af03fbe..079cb8bebc 100644 --- a/examples/06-custom-schema/05-alert-block-full-ux/src/Alert.tsx +++ b/examples/06-custom-schema/05-alert-block-full-ux/src/Alert.tsx @@ -50,7 +50,7 @@ export const alertTypes = [ ] as const; // The Alert block. -export const Alert = createReactBlockSpec( +export const createAlert = createReactBlockSpec( { type: "alert", propSchema: { diff --git a/examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx b/examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx index ce21795c7a..495356ff98 100644 --- a/examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx +++ b/examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx @@ -18,16 +18,14 @@ import { } from "@blocknote/react"; import { RiAlertFill } from "react-icons/ri"; -import { Alert } from "./Alert"; +import { createAlert } from "./Alert"; // Our schema with block specs, which contain the configs and implementations for // blocks that we want our editor to use. -const schema = BlockNoteSchema.create({ +const schema = BlockNoteSchema.create().extend({ blockSpecs: { - // Adds all default blocks. - ...defaultBlockSpecs, - // Adds the Alert block. - alert: Alert(), + // Creates an instance of the Alert block and adds it to the schema. + alert: createAlert(), }, }); diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 93b7c11193..dc3e5df85a 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -433,7 +433,9 @@ export type BlockNoteEditorOptions< /** * Register extensions to the editor. * - * @internal + * See [Extensions](/docs/features/extensions) for more info. + * + * @remarks `BlockNoteExtension[]` */ extensions?: Array; };