Skip to content

Commit 8047945

Browse files
Updated docs for custom blocks and added tables (#442)
1 parent 6e7d43a commit 8047945

File tree

6 files changed

+198
-111
lines changed

6 files changed

+198
-111
lines changed

packages/react/src/TableHandles/components/TableHandleMenu/DefaultButtons/AddButton.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const AddRowButton = <
2424
props.editor.updateBlock(props.block, {
2525
type: "table",
2626
content: {
27+
type: "tableContent",
2728
rows,
2829
},
2930
} as PartialBlock<BSchema, any, any>);

packages/website/docs/docs/block-types.md

Lines changed: 177 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,24 @@ type ImageBlock = {
124124

125125
`width:` The image width in pixels.
126126

127+
### Table
128+
129+
**Appearance**
130+
131+
<img :src="isDark ? '/img/screenshots/table_type_dark.png' : '/img/screenshots/table_type.png'" alt="image" style="width: 70%">
132+
133+
**Type & Props**
134+
135+
```typescript
136+
type TableBlock = {
137+
id: string;
138+
type: "table";
139+
props: DefaultProps;
140+
content: TableContent[];
141+
children: Block[];
142+
};
143+
```
144+
127145
## Default Block Properties
128146

129147
While each type of block can have its own set of properties, there are some properties that all built-in block types have by default, which you can find in the definition for `DefaultProps`:
@@ -144,98 +162,112 @@ type DefaultProps = {
144162

145163
## Custom Block Types
146164

147-
In addition to the default block types that BlockNote offers, you can also make your own custom blocks. Take a look at the demo below, in which we add a custom block containing an image and caption to a BlockNote editor, as well as a custom [Slash Menu Item](/docs/slash-menu#custom-items) to insert it.
165+
In addition to the default block types that BlockNote offers, you can also make your own custom blocks. Take a look at the demo below, in which we add a custom block containing a paragraph with a different font to a BlockNote editor, as well as a custom [Slash Menu Item](/docs/slash-menu#custom-items) to insert it.
148166

149167
::: sandbox {template=react-ts}
150168

151169
```typescript-vue /App.tsx
152170
import {
153-
BlockNoteEditor,
154-
BlockSchema,
155-
DefaultBlockSchema,
156171
defaultBlockSchema,
172+
defaultBlockSpecs,
157173
defaultProps,
158174
} from "@blocknote/core";
159175
import {
160176
BlockNoteView,
161177
useBlockNote,
162178
createReactBlockSpec,
163-
InlineContent,
164179
ReactSlashMenuItem,
165180
getDefaultReactSlashMenuItems,
166181
} from "@blocknote/react";
167182
import "@blocknote/core/style.css";
168-
import { RiImage2Fill } from "react-icons/ri";
183+
import { RiText } from "react-icons/ri";
169184

170185
export default function App() {
171-
// Creates a custom image block.
172-
const ImageBlock = createReactBlockSpec({
173-
type: "image",
174-
propSchema: {
175-
...defaultProps,
176-
src: {
177-
default: "https://via.placeholder.com/1000",
178-
},
179-
alt: {
180-
default: "image",
186+
// Creates a paragraph block with custom font.
187+
const FontParagraphBlock = createReactBlockSpec(
188+
{
189+
type: "fontParagraph",
190+
propSchema: {
191+
...defaultProps,
192+
font: {
193+
default: "Comic Sans MS",
194+
},
181195
},
196+
content: "inline",
182197
},
183-
content: "inline",
184-
render: ({ block }) => (
185-
<div id="image-wrapper">
186-
<img
187-
src={block.props.src}
188-
alt={block.props.alt}
189-
contentEditable={false}
190-
/>
191-
<InlineContent />
192-
</div>
193-
),
194-
});
198+
{
199+
render: ({ block, contentRef }) => {
200+
const style = {
201+
fontFamily: block.props.font
202+
};
203+
204+
return (
205+
<p ref={contentRef} style={style} />
206+
);
207+
},
208+
toExternalHTML: ({ contentRef }) => <p ref={contentRef} />,
209+
parse: (element) => {
210+
const font = element.style.fontFamily;
211+
212+
if (font === "") {
213+
return;
214+
}
215+
216+
return {
217+
font: font || undefined,
218+
};
219+
},
220+
}
221+
);
195222

196-
// The custom schema, which includes the default blocks and the custom image
197-
// block.
198-
const customSchema = {
223+
// Our block schema, which contains the configs for blocks that we want our
224+
// editor to use.
225+
const blockSchema = {
199226
// Adds all default blocks.
200227
...defaultBlockSchema,
201-
// Adds the custom image block.
202-
image: ImageBlock,
203-
} satisfies BlockSchema;
228+
// Adds the font paragraph.
229+
fontParagraph: FontParagraphBlock.config,
230+
};
231+
// Our block specs, which contain the configs and implementations for blocks
232+
// that we want our editor to use.
233+
const blockSpecs = {
234+
// Adds all default blocks.
235+
...defaultBlockSpecs,
236+
// Adds the font paragraph.
237+
fontParagraph: FontParagraphBlock,
238+
};
204239

205-
// Creates a slash menu item for inserting an image block.
206-
const insertImage: ReactSlashMenuItem<typeof customSchema> = {
207-
name: "Insert Image",
240+
// Creates a slash menu item for inserting a font paragraph block.
241+
const insertFontParagraph: ReactSlashMenuItem<typeof blockSchema> = {
242+
name: "Insert Font Paragraph",
208243
execute: (editor) => {
209-
const src: string | null = prompt("Enter image URL");
210-
const alt: string | null = prompt("Enter image alt text");
244+
const font = prompt("Enter font name");
211245

212246
editor.insertBlocks(
213247
[
214248
{
215-
type: "image",
249+
type: "fontParagraph",
216250
props: {
217-
src: src || "https://via.placeholder.com/1000",
218-
alt: alt || "image",
251+
font: font || undefined,
219252
},
220253
},
221254
],
222255
editor.getTextCursorPosition().block,
223256
"after"
224257
);
225258
},
226-
aliases: ["image", "img", "picture", "media"],
227-
group: "Media",
228-
icon: <RiImage2Fill />,
229-
hint: "Insert an image",
259+
aliases: ["p", "paragraph", "font"],
260+
group: "Other",
261+
icon: <RiText />,
230262
};
231263

232264
// Creates a new editor instance.
233265
const editor = useBlockNote({
234266
// Tells BlockNote which blocks to use.
235-
blockSchema: customSchema,
267+
blockSpecs: blockSpecs,
236268
slashMenuItems: [
237-
...getDefaultReactSlashMenuItems(customSchema),
238-
insertImage,
269+
...getDefaultReactSlashMenuItems(blockSchema),
270+
insertFontParagraph,
239271
],
240272
});
241273

@@ -270,105 +302,143 @@ We'd love to hear your feedback on GitHub or in our Discord community!
270302
To define a custom block type, we use the `createReactBlockSpec` function, for which you can see the definition below:
271303

272304
```typescript
273-
type PropSchema = Record<
305+
type PropSchema<
306+
PrimitiveType extends "boolean" | "number" | "string"
307+
> = Record<
274308
string,
275309
{
276-
default: string;
277-
values?: string[];
310+
default: PrimitiveType;
311+
values?: PrimitiveType[];
278312
};
279313
>
280314

281-
function createReactBlockSpec(config: {
282-
type: string;
283-
propSchema: PropSchema;
284-
containsInlineContent: boolean;
285-
render: (props: {
286-
block: Block,
287-
editor: BlockNoteEditor
288-
}) => JSX.Element;
289-
}): BlockType;
315+
function createReactBlockSpec(
316+
blockConfig: {
317+
type: string;
318+
propSchema: PropSchema<"boolean" | "number" | "string">;
319+
content: "inline" | "none"
320+
},
321+
blockImplementation: {
322+
render: React.FC<{
323+
block: Block;
324+
editor: BlockNoteEditor;
325+
contentRef: (node: HTMLElement | null) => void;
326+
}>,
327+
toExternalHTML?: React.FC<{
328+
block: Block;
329+
editor: BlockNoteEditor;
330+
contentRef: (node: HTMLElement | null) => void;
331+
}>,
332+
parse?: (
333+
element: HTMLElement
334+
) => PartialBlock["props"] | undefined;
335+
}
336+
): BlockType;
290337
```
291338

292339
Let's look at our custom image block from the demo, and go over each field in-depth to explain how it works:
293340

294341
```typescript jsx
295-
const ImageBlock = createReactBlockSpec({
296-
type: "image",
297-
propSchema: {
298-
src: {
299-
default: "https://via.placeholder.com/1000",
342+
const FontparagraphBlock = createReactBlockSpec(
343+
{
344+
type: "fontParagraph",
345+
propSchema: {
346+
...defaultProps,
347+
font: {
348+
default: "Comic Sans MS",
349+
},
300350
},
351+
content: "inline",
301352
},
302-
content: "inline",
303-
render: ({ block }) => (
304-
<div
305-
style={{
306-
display: "flex",
307-
flexDirection: "column",
308-
}}>
309-
<img
310-
style={{
311-
width: "100%",
312-
}}
313-
src={block.props.src}
314-
alt={"Image"}
315-
contentEditable={false}
316-
/>
317-
<InlineContent />
318-
</div>
319-
),
320-
});
353+
{
354+
render: ({ block, contentRef }) => {
355+
const style = {
356+
fontFamily: block.props.font
357+
};
358+
359+
return (
360+
<p ref={contentRef} style={style} />
361+
);
362+
},
363+
parse: (element) => {
364+
const font = element.style.fontFamily;
365+
return {
366+
font: font || undefined,
367+
};
368+
},
369+
}
370+
);
321371
```
322372

323-
#### `type`
373+
You can see that `createReactBlockSpec` takes two object arguments:
374+
375+
#### `blockConfig`
376+
377+
This defines the block's type, properties, and content type. It allows BlockNote to know how to handle manipulating the block internally and provide typing.
378+
379+
**`type`**
324380

325381
Defines the name of the block, in this case, `image`.
326382

327-
#### `propSchema`
383+
**`content`**
384+
385+
As we saw in [Block Objects](/docs/blocks#block-objects), blocks can contain editable rich text which is represented as [Inline Content](/docs/inline-content). The `content` field allows your custom block to contain an editable rich-text field. Since we want to be able to type in our paragraph, we set it to `"inline"`.
328386

329-
This is an object which defines the props that the block should have. In this case, we want the block to have a `src` prop for the URL of the image, so we add a `src` key. We also want basic styling options for the image block, so we also add the [Default Block Properties](/docs/block-types#default-block-properties) using `defaultProps`. The value of each key is an object with a mandatory `default` field and an optional `values` field:
387+
**`propSchema`**
330388

331-
`default:` Stores the prop's default value, so we use a placeholder image URL for `src` if no URL is provided.
389+
This is an object which defines the props that the block should have. In this case, we want the block to have a `font` prop for the font that we want the paragraph to use, so we add a `font` key. We also want basic styling options, so we add the [Default Block Properties](/docs/block-types#default-block-properties) using `defaultProps`. The value of each key is an object with a mandatory `default` field and an optional `values` field:
332390

333-
`values:` Stores an array of strings that the prop can take. If `values` is not defined, BlockNote assumes the prop can be any string, which makes sense for `src`, since it can be any image URL.
391+
`default:` Stores the prop's default value, in this case the Comic Sans MS font.
334392

335-
#### `containsInlineContent`
393+
`values:` Stores an array of strings that the prop can take. If `values` is not defined, BlockNote assumes the prop can be any string, which makes sense for `font`, since we don't want to list every possible font name.
336394

337-
As we saw in [Block Objects](/docs/blocks#block-objects), blocks can contain editable rich text which is represented as [Inline Content](/docs/inline-content). The `containsInlineContent` field allows your custom block to contain an editable rich-text field. For the custom image block, we use an inline content field to create our caption, so it's set to `true`.
395+
#### `blockImplementation`
338396

339-
#### `render`
397+
This defines how the block should be rendered in the editor, and how it should be parsed from and converted to HTML.
340398

341-
This is a React component which defines how your custom block should be rendered in the editor, and takes two props:
399+
**`render`**
342400

343-
`block:` The block that should be rendered.
401+
This is a React component which defines how your custom block should be rendered in the editor, and takes three props:
402+
403+
`block:` The block that should be rendered. This will always have the same type, props, and content as defined in the block's config.
344404

345405
`editor:` The BlockNote editor instance that the block is in.
346406

347-
For our custom image block, we use a parent `div` which contains the image and caption. Since `block` will always be an `image` block, we also know it contains a `src` prop, and can pass it to the child `img` element.
407+
`contentRef:` A React `ref` that marks which element in your block is editable, This is only useful if your block config contains `content: "inline"`.
408+
409+
**`toExternalHTML`**
410+
411+
This is identical in definition as `render`, but is used whenever the block is being exported to HTML for use outside BlockNote, namely when copying it to the clipboard. If it's not defined, BlockNote will just use `render` for the HTML conversion.
348412

349-
But what's this `InlineContent` component? Since we set `containsInlineContent` to `true`, it means we want to include an editable rich-text field somewhere in the image block. You should use the `InlineContent` component to represent this field in your `render` component. Since we're using it to create our caption, we add it below the `img` element.
413+
**`parse`**
350414

351-
In the DOM, the `InlineContent` component is rendered as a `div` by default, but you can make it use a different element by passing `as={"elementTag"}` as a prop. For example, `as={"p"}` will make the `InlineContent` component get rendered to a paragraph.
415+
This is a function that allows you to define which HTML elements should be parsed into your block when importing HTML from outside BlockNote, namely when pasting it from the clipboard. If the element should be parsed into your custom block, you should return the props that the block should be given. Otherwise, return `undefined`.
352416

353-
While the `InlineContent` component can be put anywhere inside the component you pass to `render`, you should make sure to only have one. If `containsInlineContent` is set to false, `render` shouldn't contain any.
417+
`element`: The HTML element that's being parsed.
354418

355419
### Adding Custom Blocks to the Editor
356420

357-
Now, we need to tell BlockNote to use our custom image block by passing it to the editor in the `blockSchema` option. Let's again look at the image block from the demo as an example:
421+
Now, we need to tell BlockNote to use our font paragraph block by passing it to the editor in the `blockSpecs` option. Let's again look at the image block from the demo as an example:
358422

359423
```typescript jsx
424+
// Our block specs, which contain the configs and implementations for blocks
425+
// that we want our editor to use.
426+
const blockSpecs = {
427+
// Adds all default blocks.
428+
...defaultBlockSpecs,
429+
// Adds the font paragraph.
430+
fontParagraph: FontParagraphBlock,
431+
};
432+
433+
...
434+
360435
// Creates a new editor instance.
361436
const editor = useBlockNote({
362437
// Tells BlockNote which blocks to use.
363-
blockSchema: {
364-
// Adds all default blocks.
365-
...defaultBlockSchema,
366-
// Adds the custom image block.
367-
image: ImageBlock,
368-
},
438+
blockSpecs: blockSpecs,
369439
});
370440
```
371441

372-
Since we still want the editor to use the [Built-In Block Types](/docs/block-types#built-in-block-types), we add `defaultBlockSchema` to our custom block schema. The key which we use for the custom image block is the same string we use for its type. Make sure that this is always the case for your own custom blocks.
442+
Since we still want the editor to use the [Built-In Block Types](/docs/block-types#built-in-block-types), we add `defaultBlockSpecs` too. The key which we use for the font paragraph block should also be the same string we use for its type. Make sure that this is always the case for your own custom blocks.
373443

374444
And we're done! You now know how to create custom blocks and add them to the editor. Head to [Manipulating Blocks](/docs/manipulating-blocks) to see what you can do with them in the editor.

0 commit comments

Comments
 (0)