Skip to content

Commit 0097fd9

Browse files
docs: Examples tab (#322)
* Added pages for examples in docs * Added alert block example * Added keyboard shortcuts example and moved save/load example * Added block manipulation example * Added block from tiptap node example * Updated examples frontmatter * Updated saving & loading text * Fixed alert block example dropdown z-index * Updated Alert block example * Updated Alert block example * Added custom UI docs * Small fix * Fixes for blocks without inline content * Block type dropdown fix * Block type dropdown fix * Block from tiptap node example fix * Block type dropdown fix * Added docs to block type dropdown * Updated alert block example * Implemented PR feedback * Reverted `BNUpdateBlock` changes * Removed block from TipTap node example
1 parent 1b91856 commit 0097fd9

File tree

25 files changed

+1466
-91
lines changed

25 files changed

+1466
-91
lines changed

packages/core/src/extensions/Blocks/nodes/BlockContainer.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -192,18 +192,42 @@ export const BlockContainer = Node.create<{
192192
);
193193
}
194194

195-
// Changes the blockContent node type and adds the provided props as attributes. Also preserves all existing
196-
// attributes that are compatible with the new type.
197-
state.tr.setNodeMarkup(
198-
startPos,
199-
block.type === undefined
200-
? undefined
201-
: state.schema.nodes[block.type],
202-
{
203-
...contentNode.attrs,
204-
...block.props,
205-
}
206-
);
195+
// Since some block types contain inline content and others don't,
196+
// we either need to call setNodeMarkup to just update type &
197+
// attributes, or replaceWith to replace the whole blockContent.
198+
const oldType = contentNode.type.name;
199+
const newType = block.type || oldType;
200+
201+
const oldContentType = state.schema.nodes[oldType].spec.content;
202+
const newContentType = state.schema.nodes[newType].spec.content;
203+
204+
if (oldContentType === "inline*" && newContentType === "") {
205+
// Replaces the blockContent node with one of the new type and
206+
// adds the provided props as attributes. Also preserves all
207+
// existing attributes that are compatible with the new type.
208+
state.tr.replaceWith(
209+
startPos,
210+
endPos,
211+
state.schema.nodes[newType].create({
212+
...contentNode.attrs,
213+
...block.props,
214+
})
215+
);
216+
} else {
217+
// Changes the blockContent node type and adds the provided props
218+
// as attributes. Also preserves all existing attributes that are
219+
// compatible with the new type.
220+
state.tr.setNodeMarkup(
221+
startPos,
222+
block.type === undefined
223+
? undefined
224+
: state.schema.nodes[block.type],
225+
{
226+
...contentNode.attrs,
227+
...block.props,
228+
}
229+
);
230+
}
207231

208232
// Adds all provided props as attributes to the parent blockContainer node too, and also preserves existing
209233
// attributes.

packages/core/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContent.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export const HeadingBlockContent = createTipTapBlock<"heading">({
7373
return [
7474
"div",
7575
mergeAttributes(HTMLAttributes, {
76+
...blockContentDOMAttributes,
7677
class: mergeCSSClasses(
7778
styles.blockContent,
7879
blockContentDOMAttributes.class
@@ -82,6 +83,7 @@ export const HeadingBlockContent = createTipTapBlock<"heading">({
8283
[
8384
"h" + node.attrs.level,
8485
{
86+
...inlineContentDOMAttributes,
8587
class: mergeCSSClasses(
8688
styles.inlineContent,
8789
inlineContentDOMAttributes.class

packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export const BulletListItemBlockContent = createTipTapBlock<"bulletListItem">({
9191
return [
9292
"div",
9393
mergeAttributes(HTMLAttributes, {
94+
...blockContentDOMAttributes,
9495
class: mergeCSSClasses(
9596
styles.blockContent,
9697
blockContentDOMAttributes.class
@@ -100,6 +101,7 @@ export const BulletListItemBlockContent = createTipTapBlock<"bulletListItem">({
100101
[
101102
"p",
102103
{
104+
...inlineContentDOMAttributes,
103105
class: mergeCSSClasses(
104106
styles.inlineContent,
105107
inlineContentDOMAttributes.class

packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export const NumberedListItemBlockContent =
115115
return [
116116
"div",
117117
mergeAttributes(HTMLAttributes, {
118+
...blockContentDOMAttributes,
118119
class: mergeCSSClasses(
119120
styles.blockContent,
120121
blockContentDOMAttributes.class
@@ -126,6 +127,7 @@ export const NumberedListItemBlockContent =
126127
[
127128
"p",
128129
{
130+
...inlineContentDOMAttributes,
129131
class: mergeCSSClasses(
130132
styles.inlineContent,
131133
inlineContentDOMAttributes.class

packages/core/src/extensions/TrailingNode/TrailingNodeExtension.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,19 @@ export const TrailingNode = Extension.create<TrailingNodeOptions>({
6464
if (!lastNode || lastNode.type.name !== "blockContainer") {
6565
throw new Error("Expected blockContainer");
6666
}
67-
return lastNode.nodeSize > 4; // empty <block><content/></block> is length 4
67+
68+
const lastContentNode = lastNode.firstChild;
69+
70+
if (!lastContentNode) {
71+
throw new Error("Expected blockContent");
72+
}
73+
74+
// If last node is not empty (size > 4) or it doesn't contain
75+
// inline content, we need to add a trailing node.
76+
return (
77+
lastNode.nodeSize > 4 ||
78+
lastContentNode.type.spec.content !== "inline*"
79+
);
6880
},
6981
},
7082
}),

packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { useMemo, useState } from "react";
2-
import { BlockNoteEditor, BlockSchema } from "@blocknote/core";
2+
import {
3+
Block,
4+
BlockNoteEditor,
5+
BlockSchema,
6+
PartialBlock,
7+
} from "@blocknote/core";
38
import { IconType } from "react-icons";
49
import {
510
RiH1,
@@ -20,41 +25,57 @@ export type BlockTypeDropdownItem = {
2025
type: string;
2126
props?: Record<string, string>;
2227
icon: IconType;
28+
isSelected: (block: Block<BlockSchema>) => boolean;
2329
};
2430

2531
export const defaultBlockTypeDropdownItems: BlockTypeDropdownItem[] = [
2632
{
2733
name: "Paragraph",
2834
type: "paragraph",
2935
icon: RiText,
36+
isSelected: (block) => block.type === "paragraph",
3037
},
3138
{
3239
name: "Heading 1",
3340
type: "heading",
3441
props: { level: "1" },
3542
icon: RiH1,
43+
isSelected: (block) =>
44+
block.type === "heading" &&
45+
"level" in block.props &&
46+
block.props.level === "1",
3647
},
3748
{
3849
name: "Heading 2",
3950
type: "heading",
4051
props: { level: "2" },
4152
icon: RiH2,
53+
isSelected: (block) =>
54+
block.type === "heading" &&
55+
"level" in block.props &&
56+
block.props.level === "2",
4257
},
4358
{
4459
name: "Heading 3",
4560
type: "heading",
4661
props: { level: "3" },
4762
icon: RiH3,
63+
isSelected: (block) =>
64+
block.type === "heading" &&
65+
"level" in block.props &&
66+
block.props.level === "3",
4867
},
4968
{
5069
name: "Bullet List",
5170
type: "bulletListItem",
5271
icon: RiListUnordered,
72+
isSelected: (block) => block.type === "bulletListItem",
5373
},
5474
{
5575
name: "Numbered List",
5676
type: "numberedListItem",
5777
icon: RiListOrdered,
78+
isSelected: (block) => block.type === "numberedListItem",
5879
},
5980
];
6081

@@ -100,22 +121,22 @@ export const BlockTypeDropdown = <BSchema extends BlockSchema>(props: {
100121
[block.type, filteredItems]
101122
);
102123

103-
const fullItems: ToolbarDropdownItemProps[] = useMemo(
104-
() =>
105-
filteredItems.map((item) => ({
106-
text: item.name,
107-
icon: item.icon,
108-
onClick: () => {
109-
props.editor.focus();
110-
props.editor.updateBlock(block, {
111-
type: item.type,
112-
props: {},
113-
});
114-
},
115-
isSelected: block.type === item.type,
116-
})),
117-
[block, filteredItems, props.editor]
118-
);
124+
const fullItems: ToolbarDropdownItemProps[] = useMemo(() => {
125+
const onClick = (item: BlockTypeDropdownItem) => {
126+
props.editor.focus();
127+
props.editor.updateBlock(block, {
128+
type: item.type,
129+
props: item.props,
130+
} as PartialBlock<BlockSchema>);
131+
};
132+
133+
return filteredItems.map((item) => ({
134+
text: item.name,
135+
icon: item.icon,
136+
onClick: () => onClick(item),
137+
isSelected: item.isSelected(block as Block<BlockSchema>),
138+
}));
139+
}, [block, filteredItems, props.editor]);
119140

120141
useEditorContentChange(props.editor, () => {
121142
setBlock(props.editor.getTextCursorPosition().block);

packages/website/docs/.vitepress/config.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,36 @@ const SIDEBAR_DEFAULT = [
119119
},
120120
];
121121

122+
const EXAMPLES_SIDEBAR = [
123+
{
124+
text: "Basic Examples",
125+
items: [
126+
{ text: "Block Manipulation", link: "/examples/block-manipulation" },
127+
{
128+
text: "Keyboard Shortcuts",
129+
link: "/examples/keyboard-shortcuts",
130+
},
131+
{
132+
text: "Saving & Loading",
133+
link: "/examples/saving-loading",
134+
},
135+
],
136+
},
137+
{
138+
text: "Advanced Examples",
139+
items: [
140+
{
141+
text: "Making UI Elements From Scratch",
142+
link: "/examples/custom-ui",
143+
},
144+
{
145+
text: "Alert Block",
146+
link: "/examples/alert-block",
147+
},
148+
],
149+
},
150+
];
151+
122152
const METADATA_DEFAULT = {
123153
title: "BlockNote",
124154
description:
@@ -154,9 +184,13 @@ export default defineConfig({
154184
light: "/img/logos/banner.svg",
155185
dark: "/img/logos/banner.dark.svg",
156186
},
157-
nav: [{ text: "Documentation", link: "/docs/introduction" }],
187+
nav: [
188+
{ text: "Documentation", link: "/docs/introduction" },
189+
{ text: "Examples", link: "/examples/block-manipulation" },
190+
],
158191
sidebar: {
159192
"/docs/": SIDEBAR_DEFAULT,
193+
"/examples/": EXAMPLES_SIDEBAR,
160194
// "/tutorial/": SIDEBAR_DEFAULT,
161195
// "/api": SIDEBAR_DEFAULT,
162196
},

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ path: /docs/block-types
77

88
<script setup>
99
import { useData } from 'vitepress';
10-
import { getTheme, getStyles } from "./demoUtils";
10+
import { getTheme, getStyles } from "../demoUtils";
1111

1212
const { isDark } = useData();
1313
</script>

packages/website/docs/docs/blocks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ path: /docs/blocks
77

88
<script setup>
99
import { useData } from 'vitepress';
10-
import { getTheme, getStyles } from "./demoUtils";
10+
import { getTheme, getStyles } from "../demoUtils";
1111

1212
const { isDark } = useData();
1313
</script>

packages/website/docs/docs/converting-blocks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ path: /docs/converting-blocks
77

88
<script setup>
99
import { useData } from 'vitepress';
10-
import { getTheme, getStyles } from "./demoUtils";
10+
import { getTheme, getStyles } from "../demoUtils";
1111

1212
const { isDark } = useData();
1313
</script>

packages/website/docs/docs/cursor-selections.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ path: /docs/cursor-selections
77

88
<script setup>
99
import { useData } from 'vitepress';
10-
import { getTheme, getStyles } from "./demoUtils";
10+
import { getTheme, getStyles } from "../demoUtils";
1111

1212
const { isDark } = useData();
1313
</script>

packages/website/docs/docs/editor.md

Lines changed: 2 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ path: /docs/editor
77

88
<script setup>
99
import { useData } from 'vitepress';
10-
import { getTheme, getStyles } from "./demoUtils";
10+
import { getTheme, getStyles } from "../demoUtils";
1111

1212
const { isDark } = useData();
1313
</script>
@@ -31,8 +31,6 @@ export type BlockNoteEditorOptions = Partial<{
3131
onEditorContentChange: (editor: BlockNoteEditor) => void;
3232
onTextCursorPositionChange: (editor: BlockNoteEditor) => void;
3333
slashMenuItems: ReactSlashMenuItem[];
34-
customElements: CustomElements;
35-
uiFactories: UiFactories;
3634
defaultStyles: boolean;
3735
}>;
3836
```
@@ -41,7 +39,7 @@ export type BlockNoteEditorOptions = Partial<{
4139

4240
`initialContent:` The content that should be in the editor when it's created, represented as an array of [partial block objects](/docs/manipulating-blocks#partial-blocks).
4341

44-
`editorDOMAttributes:` An object containing attributes that should be added to the editor's HTML element. For example, you can pass `{ class: "my-editor-class" }` to set a custom class name.
42+
`domAttributes:` An object containing HTML attributes that should be added to various DOM elements in the editor. See [Adding DOM Attributes](/docs/theming#adding-dom-attributes) for more.
4543

4644
`onEditorReady:` A callback function that runs when the editor is ready to be used.
4745

@@ -51,49 +49,4 @@ export type BlockNoteEditorOptions = Partial<{
5149

5250
`slashMenuItems:` The commands that are listed in the editor's [Slash Menu](/docs/slash-menu). If this option isn't defined, a default list of commands is loaded.
5351

54-
`customElements:` React components for a custom [Formatting Toolbar](/docs/formatting-toolbar#custom-formatting-toolbar) and/or [Drag Handle Menu](/docs/side-menu#custom-drag-handle-menu) to use.
55-
56-
`uiFactories:` UI element factories for creating a custom UI, including custom positioning & rendering. You can find out more about UI factories in [Creating Your Own UI Elements](/docs/vanilla-js#creating-your-own-ui-elements).
57-
5852
`defaultStyles`: Whether to use the default font and reset the styles of `<p>`, `<li>`, `<h1>`, etc. elements that are used in BlockNote. Defaults to true if undefined.
59-
60-
## Demo: Saving & Restoring Editor Contents
61-
62-
By default, BlockNote doesn't preserve the editor contents when your app is reopened or refreshed. However, using the editor options, you can change this by using the editor options.
63-
64-
In the example below, we use the `onEditorContentChange` option to save the editor contents in local storage whenever they change, then pass them to `initialContent` whenever the page is reloaded.
65-
66-
::: sandbox {template=react-ts}
67-
68-
```typescript-vue /App.tsx
69-
import { BlockNoteEditor } from "@blocknote/core";
70-
import { BlockNoteView, useBlockNote } from "@blocknote/react";
71-
import "@blocknote/core/style.css";
72-
73-
// Gets the previously stored editor contents.
74-
const initialContent: string | null = localStorage.getItem("editorContent");
75-
76-
export default function App() {
77-
// Creates a new editor instance.
78-
const editor: BlockNoteEditor | null = useBlockNote({
79-
// If the editor contents were previously saved, restores them.
80-
initialContent: initialContent ? JSON.parse(initialContent) : undefined,
81-
// Serializes and saves the editor contents to local storage.
82-
onEditorContentChange: (editor) => {
83-
localStorage.setItem(
84-
"editorContent",
85-
JSON.stringify(editor.topLevelBlocks)
86-
);
87-
}
88-
});
89-
90-
// Renders the editor instance.
91-
return <BlockNoteView editor={editor} theme={"{{ getTheme(isDark) }}"} />;
92-
}
93-
```
94-
95-
```css-vue /styles.css [hidden]
96-
{{ getStyles(isDark) }}
97-
```
98-
99-
:::

0 commit comments

Comments
 (0)