Skip to content

Commit be86b22

Browse files
committed
special node approach
1 parent b3fad88 commit be86b22

8 files changed

Lines changed: 869 additions & 5 deletions

File tree

examples/01-basic/01-minimal/src/App.tsx

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,62 @@ import "@blocknote/core/fonts/inter.css";
22
import { BlockNoteView } from "@blocknote/mantine";
33
import "@blocknote/mantine/style.css";
44
import { useCreateBlockNote } from "@blocknote/react";
5+
import { useEffect } from "react";
56

67
export default function App() {
7-
// Creates a new editor instance.
8+
// Creates a new editor instance with a default empty paragraph.
89
const editor = useCreateBlockNote();
910

11+
// After the editor is created, replace its document with a ProseMirror
12+
// structure that includes a specialNode before the blockContainer's paragraph.
13+
useEffect(() => {
14+
// Use editor.transact to dispatch a ProseMirror transaction that replaces
15+
// the entire document content.
16+
editor.transact((tr) => {
17+
const { nodes } = editor.pmSchema;
18+
19+
// Build the specialNode containing a paragraph
20+
const specialParagraph = nodes.paragraph.create(
21+
{
22+
backgroundColor: "default",
23+
textAlignment: "left",
24+
textColor: "default",
25+
},
26+
[editor.pmSchema.text("Hello from specialNode!")],
27+
);
28+
const specialNode = nodes.specialNode.create(null, [specialParagraph]);
29+
30+
// Build the main blockContent paragraph
31+
const mainParagraph = nodes.paragraph.create(
32+
{
33+
backgroundColor: "default",
34+
textAlignment: "left",
35+
textColor: "default",
36+
},
37+
[editor.pmSchema.text("Hello from blockContainer!")],
38+
);
39+
40+
// Build the blockContainer with specialNode before blockContent
41+
//
42+
// Target structure:
43+
// doc
44+
// └─ blockGroup
45+
// └─ blockContainer
46+
// ├─ specialNode
47+
// │ └─ paragraph("Hello from specialNode!")
48+
// └─ paragraph("Hello from blockContainer!")
49+
const blockContainer = nodes.blockContainer.create(
50+
{ id: "block-1" },
51+
[specialNode, mainParagraph],
52+
);
53+
54+
const blockGroup = nodes.blockGroup.create(null, [blockContainer]);
55+
const newDoc = nodes.doc.create(null, [blockGroup]);
56+
57+
tr.replaceWith(0, tr.doc.content.size, newDoc.content);
58+
});
59+
}, [editor]);
60+
1061
// Renders the editor instance using a React component.
1162
return <BlockNoteView editor={editor} />;
1263
}

packages/core/src/editor/managers/ExtensionManager/extensions.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@ import {
3838
TextColorExtension,
3939
UniqueID,
4040
} from "../../../extensions/tiptap-extensions/index.js";
41-
import { BlockContainer, BlockGroup, Doc } from "../../../pm-nodes/index.js";
41+
import {
42+
BlockContainer,
43+
BlockGroup,
44+
Doc,
45+
SpecialNode,
46+
} from "../../../pm-nodes/index.js";
4247
import type {
4348
BlockNoteEditor,
4449
BlockNoteEditorOptions,
@@ -114,6 +119,10 @@ export function getDefaultTiptapExtensions(
114119
BlockGroup.configure({
115120
domAttributes: options.domAttributes,
116121
}),
122+
SpecialNode.configure({
123+
editor: editor,
124+
domAttributes: options.domAttributes,
125+
}),
117126
...Object.values(editor.schema.inlineContentSpecs)
118127
.filter((a) => a.config !== "link" && a.config !== "text")
119128
.map((inlineContentSpec) => {

packages/core/src/pm-nodes/BlockContainer.ts

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,88 @@ const BlockAttributes: Record<string, string> = {
1313
depthChange: "data-depth-change",
1414
};
1515

16+
/**
17+
* The main "Block node" documents consist of
18+
*/
19+
export const SpecialNode = Node.create<{
20+
domAttributes?: BlockNoteDOMAttributes;
21+
editor: BlockNoteEditor<any, any, any>;
22+
}>({
23+
name: "specialNode",
24+
group: "blockGroupChild bnBlock bnSpecialNode",
25+
// A block always contains content, and optionally a blockGroup which contains nested blocks
26+
content: "blockContent blockGroup?",
27+
// Ensures content-specific keyboard handlers trigger first.
28+
priority: 50,
29+
defining: true,
30+
marks: "y-attributed-insert y-attributed-format y-attributed-delete",
31+
addAttributes() {
32+
return {
33+
XYZ: {
34+
isRequired: true,
35+
type: "string",
36+
},
37+
};
38+
},
39+
parseHTML() {
40+
return [
41+
// TODO not sure whether this rule is needed or not
42+
// {
43+
// tag: "div[data-node-type=" + this.name + "]",
44+
// getAttrs: (element) => {
45+
// if (typeof element === "string") {
46+
// return false;
47+
// }
48+
49+
// const attrs: Record<string, string> = {};
50+
// for (const [nodeAttr, HTMLAttr] of Object.entries(BlockAttributes)) {
51+
// if (element.getAttribute(HTMLAttr)) {
52+
// attrs[nodeAttr] = element.getAttribute(HTMLAttr)!;
53+
// }
54+
// }
55+
56+
// return attrs;
57+
// },
58+
// },
59+
// Ignore `blockOuter` divs, but parse the `blockContainer` divs inside them.
60+
{
61+
tag: `div[data-node-type="specialNodeOuter"]`,
62+
skip: true,
63+
},
64+
];
65+
},
66+
67+
renderHTML({ HTMLAttributes }) {
68+
const specialNodeOuter = document.createElement("div");
69+
specialNodeOuter.className = "bn-block-outer";
70+
specialNodeOuter.setAttribute("data-node-type", "specialNodeOuter");
71+
for (const [attribute, value] of Object.entries(HTMLAttributes)) {
72+
if (attribute !== "class") {
73+
specialNodeOuter.setAttribute(attribute, value);
74+
}
75+
}
76+
77+
const blockHTMLAttributes = {
78+
...(this.options.domAttributes?.block || {}),
79+
...HTMLAttributes,
80+
};
81+
const block = document.createElement("div");
82+
block.className = mergeCSSClasses("bn-block", blockHTMLAttributes.class);
83+
block.setAttribute("data-node-type", this.name);
84+
for (const [attribute, value] of Object.entries(blockHTMLAttributes)) {
85+
if (attribute !== "class") {
86+
block.setAttribute(attribute, value);
87+
}
88+
}
89+
90+
specialNodeOuter.appendChild(block);
91+
92+
return {
93+
dom: specialNodeOuter,
94+
contentDOM: block,
95+
};
96+
},
97+
});
1698
/**
1799
* The main "Block node" documents consist of
18100
*/
@@ -23,7 +105,7 @@ export const BlockContainer = Node.create<{
23105
name: "blockContainer",
24106
group: "blockGroupChild bnBlock",
25107
// A block always contains content, and optionally a blockGroup which contains nested blocks
26-
content: "blockContent blockGroup?",
108+
content: "bnSpecialNode? blockContent bnSpecialNode? blockGroup?",
27109
// Ensures content-specific keyboard handlers trigger first.
28110
priority: 50,
29111
defining: true,

0 commit comments

Comments
 (0)