@blocknote/core 0.7.1-alpha.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blocknote.js +1428 -1252
- package/dist/blocknote.js.map +1 -1
- package/dist/blocknote.umd.cjs +2 -2
- package/dist/blocknote.umd.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +3 -3
- package/src/BlockNoteEditor.ts +100 -53
- package/src/BlockNoteExtensions.ts +24 -14
- package/src/api/blockManipulation/blockManipulation.test.ts +6 -3
- package/src/api/blockManipulation/blockManipulation.ts +7 -6
- package/src/api/formatConversions/formatConversions.test.ts +13 -8
- package/src/api/formatConversions/formatConversions.ts +15 -12
- package/src/api/nodeConversions/nodeConversions.test.ts +29 -10
- package/src/api/nodeConversions/nodeConversions.ts +33 -12
- package/src/api/nodeConversions/testUtil.ts +8 -4
- package/src/editor.module.css +0 -1
- package/src/extensions/Blocks/api/block.ts +229 -0
- package/src/extensions/Blocks/api/blockTypes.ts +158 -71
- package/src/extensions/Blocks/api/cursorPositionTypes.ts +5 -5
- package/src/extensions/Blocks/api/defaultBlocks.ts +44 -0
- package/src/extensions/Blocks/api/selectionTypes.ts +3 -3
- package/src/extensions/Blocks/api/serialization.ts +29 -0
- package/src/extensions/Blocks/index.ts +0 -8
- package/src/extensions/Blocks/nodes/Block.module.css +24 -12
- package/src/extensions/Blocks/nodes/BlockContainer.ts +8 -4
- package/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContent.ts +4 -4
- package/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +5 -5
- package/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +100 -97
- package/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts +4 -4
- package/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts +11 -9
- package/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts +6 -5
- package/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts +12 -11
- package/src/extensions/FormattingToolbar/FormattingToolbarExtension.ts +21 -16
- package/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts +9 -5
- package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +30 -42
- package/src/extensions/Placeholder/PlaceholderExtension.ts +1 -0
- package/src/extensions/SlashMenu/BaseSlashMenuItem.ts +5 -2
- package/src/extensions/SlashMenu/SlashMenuExtension.ts +37 -33
- package/src/extensions/SlashMenu/defaultSlashMenuItems.tsx +14 -10
- package/src/extensions/SlashMenu/index.ts +2 -2
- package/src/index.ts +4 -0
- package/src/shared/plugins/suggestion/SuggestionPlugin.ts +29 -13
- package/types/src/BlockNoteEditor.d.ts +37 -23
- package/types/src/BlockNoteExtensions.d.ts +15 -8
- package/types/src/api/blockManipulation/blockManipulation.d.ts +4 -4
- package/types/src/api/formatConversions/formatConversions.d.ts +5 -5
- package/types/src/api/nodeConversions/nodeConversions.d.ts +3 -3
- package/types/src/api/nodeConversions/testUtil.d.ts +2 -2
- package/types/src/extensions/Blocks/api/block.d.ts +2 -4
- package/types/src/extensions/Blocks/api/blockTypes.d.ts +77 -33
- package/types/src/extensions/Blocks/api/cursorPositionTypes.d.ts +5 -5
- package/types/src/extensions/Blocks/api/defaultBlocks.d.ts +4 -4
- package/types/src/extensions/Blocks/api/selectionTypes.d.ts +3 -3
- package/types/src/extensions/Blocks/api/serialization.d.ts +2 -0
- package/types/src/extensions/Blocks/nodes/BlockContainer.d.ts +3 -3
- package/types/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContent.d.ts +1 -2
- package/types/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.d.ts +1 -2
- package/types/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.d.ts +1 -2
- package/types/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.d.ts +1 -2
- package/types/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.d.ts +7 -7
- package/types/src/extensions/DraggableBlocks/DraggableBlocksExtension.d.ts +5 -4
- package/types/src/extensions/DraggableBlocks/DraggableBlocksPlugin.d.ts +11 -10
- package/types/src/extensions/FormattingToolbar/FormattingToolbarExtension.d.ts +6 -5
- package/types/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.d.ts +4 -3
- package/types/src/extensions/FormattingToolbar/FormattingToolbarPlugin.d.ts +16 -19
- package/types/src/extensions/SlashMenu/BaseSlashMenuItem.d.ts +4 -3
- package/types/src/extensions/SlashMenu/SlashMenuExtension.d.ts +5 -4
- package/types/src/extensions/SlashMenu/defaultSlashMenuItems.d.ts +66 -1
- package/types/src/extensions/SlashMenu/index.d.ts +2 -2
- package/types/src/index.d.ts +4 -0
- package/types/src/shared/plugins/suggestion/SuggestionPlugin.d.ts +5 -4
|
@@ -7,13 +7,14 @@ import remarkParse from "remark-parse";
|
|
|
7
7
|
import remarkRehype from "remark-rehype";
|
|
8
8
|
import remarkStringify from "remark-stringify";
|
|
9
9
|
import { unified } from "unified";
|
|
10
|
-
import { Block } from "../../extensions/Blocks/api/blockTypes";
|
|
10
|
+
import { Block, BlockSchema } from "../../extensions/Blocks/api/blockTypes";
|
|
11
|
+
|
|
11
12
|
import { blockToNode, nodeToBlock } from "../nodeConversions/nodeConversions";
|
|
12
13
|
import { removeUnderlines } from "./removeUnderlinesRehypePlugin";
|
|
13
14
|
import { simplifyBlocks } from "./simplifyBlocksRehypePlugin";
|
|
14
15
|
|
|
15
|
-
export async function blocksToHTML(
|
|
16
|
-
blocks: Block[],
|
|
16
|
+
export async function blocksToHTML<BSchema extends BlockSchema>(
|
|
17
|
+
blocks: Block<BSchema>[],
|
|
17
18
|
schema: Schema
|
|
18
19
|
): Promise<string> {
|
|
19
20
|
const htmlParentElement = document.createElement("div");
|
|
@@ -37,27 +38,28 @@ export async function blocksToHTML(
|
|
|
37
38
|
return htmlString.value as string;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
export async function HTMLToBlocks(
|
|
41
|
+
export async function HTMLToBlocks<BSchema extends BlockSchema>(
|
|
41
42
|
html: string,
|
|
43
|
+
blockSchema: BSchema,
|
|
42
44
|
schema: Schema
|
|
43
|
-
): Promise<Block[]> {
|
|
45
|
+
): Promise<Block<BSchema>[]> {
|
|
44
46
|
const htmlNode = document.createElement("div");
|
|
45
47
|
htmlNode.innerHTML = html.trim();
|
|
46
48
|
|
|
47
49
|
const parser = DOMParser.fromSchema(schema);
|
|
48
50
|
const parentNode = parser.parse(htmlNode);
|
|
49
51
|
|
|
50
|
-
const blocks: Block[] = [];
|
|
52
|
+
const blocks: Block<BSchema>[] = [];
|
|
51
53
|
|
|
52
54
|
for (let i = 0; i < parentNode.firstChild!.childCount; i++) {
|
|
53
|
-
blocks.push(nodeToBlock(parentNode.firstChild!.child(i)));
|
|
55
|
+
blocks.push(nodeToBlock(parentNode.firstChild!.child(i), blockSchema));
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
return blocks;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
export async function blocksToMarkdown(
|
|
60
|
-
blocks: Block[],
|
|
61
|
+
export async function blocksToMarkdown<BSchema extends BlockSchema>(
|
|
62
|
+
blocks: Block<BSchema>[],
|
|
61
63
|
schema: Schema
|
|
62
64
|
): Promise<string> {
|
|
63
65
|
const markdownString = await unified()
|
|
@@ -71,10 +73,11 @@ export async function blocksToMarkdown(
|
|
|
71
73
|
return markdownString.value as string;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
export async function markdownToBlocks(
|
|
76
|
+
export async function markdownToBlocks<BSchema extends BlockSchema>(
|
|
75
77
|
markdown: string,
|
|
78
|
+
blockSchema: BSchema,
|
|
76
79
|
schema: Schema
|
|
77
|
-
): Promise<Block[]> {
|
|
80
|
+
): Promise<Block<BSchema>[]> {
|
|
78
81
|
const htmlString = await unified()
|
|
79
82
|
.use(remarkParse)
|
|
80
83
|
.use(remarkGfm)
|
|
@@ -82,5 +85,5 @@ export async function markdownToBlocks(
|
|
|
82
85
|
.use(rehypeStringify)
|
|
83
86
|
.process(markdown);
|
|
84
87
|
|
|
85
|
-
return HTMLToBlocks(htmlString.value as string, schema);
|
|
88
|
+
return HTMLToBlocks(htmlString.value as string, blockSchema, schema);
|
|
86
89
|
}
|
|
@@ -5,14 +5,18 @@ import { BlockNoteEditor, PartialBlock } from "../..";
|
|
|
5
5
|
import UniqueID from "../../extensions/UniqueID/UniqueID";
|
|
6
6
|
import { blockToNode, nodeToBlock } from "./nodeConversions";
|
|
7
7
|
import { partialBlockToBlockForTesting } from "./testUtil";
|
|
8
|
+
import {
|
|
9
|
+
defaultBlockSchema,
|
|
10
|
+
DefaultBlockSchema,
|
|
11
|
+
} from "../../extensions/Blocks/api/defaultBlocks";
|
|
8
12
|
|
|
9
13
|
let editor: BlockNoteEditor;
|
|
10
14
|
let tt: Editor;
|
|
11
15
|
|
|
12
|
-
let simpleBlock: PartialBlock
|
|
16
|
+
let simpleBlock: PartialBlock<DefaultBlockSchema>;
|
|
13
17
|
let simpleNode: Node;
|
|
14
18
|
|
|
15
|
-
let complexBlock: PartialBlock
|
|
19
|
+
let complexBlock: PartialBlock<DefaultBlockSchema>;
|
|
16
20
|
let complexNode: Node;
|
|
17
21
|
|
|
18
22
|
beforeEach(() => {
|
|
@@ -119,7 +123,10 @@ describe("Simple ProseMirror Node Conversions", () => {
|
|
|
119
123
|
});
|
|
120
124
|
|
|
121
125
|
it("Convert simple node to block", async () => {
|
|
122
|
-
const firstBlockConversion = nodeToBlock(
|
|
126
|
+
const firstBlockConversion = nodeToBlock<DefaultBlockSchema>(
|
|
127
|
+
simpleNode,
|
|
128
|
+
defaultBlockSchema
|
|
129
|
+
);
|
|
123
130
|
|
|
124
131
|
expect(firstBlockConversion).toMatchSnapshot();
|
|
125
132
|
|
|
@@ -137,7 +144,10 @@ describe("Complex ProseMirror Node Conversions", () => {
|
|
|
137
144
|
});
|
|
138
145
|
|
|
139
146
|
it("Convert complex node to block", async () => {
|
|
140
|
-
const firstBlockConversion = nodeToBlock(
|
|
147
|
+
const firstBlockConversion = nodeToBlock<DefaultBlockSchema>(
|
|
148
|
+
complexNode,
|
|
149
|
+
defaultBlockSchema
|
|
150
|
+
);
|
|
141
151
|
|
|
142
152
|
expect(firstBlockConversion).toMatchSnapshot();
|
|
143
153
|
|
|
@@ -149,7 +159,7 @@ describe("Complex ProseMirror Node Conversions", () => {
|
|
|
149
159
|
|
|
150
160
|
describe("links", () => {
|
|
151
161
|
it("Convert a block with link", async () => {
|
|
152
|
-
const block: PartialBlock = {
|
|
162
|
+
const block: PartialBlock<DefaultBlockSchema> = {
|
|
153
163
|
id: UniqueID.options.generateID(),
|
|
154
164
|
type: "paragraph",
|
|
155
165
|
content: [
|
|
@@ -162,7 +172,10 @@ describe("links", () => {
|
|
|
162
172
|
};
|
|
163
173
|
const node = blockToNode(block, tt.schema);
|
|
164
174
|
expect(node).toMatchSnapshot();
|
|
165
|
-
const outputBlock = nodeToBlock(
|
|
175
|
+
const outputBlock = nodeToBlock<DefaultBlockSchema>(
|
|
176
|
+
node,
|
|
177
|
+
defaultBlockSchema
|
|
178
|
+
);
|
|
166
179
|
|
|
167
180
|
// Temporary fix to set props to {}, because at this point
|
|
168
181
|
// we don't have an easy way to access default props at runtime,
|
|
@@ -174,7 +187,7 @@ describe("links", () => {
|
|
|
174
187
|
});
|
|
175
188
|
|
|
176
189
|
it("Convert link block with marks", async () => {
|
|
177
|
-
const block: PartialBlock = {
|
|
190
|
+
const block: PartialBlock<DefaultBlockSchema> = {
|
|
178
191
|
id: UniqueID.options.generateID(),
|
|
179
192
|
type: "paragraph",
|
|
180
193
|
content: [
|
|
@@ -200,7 +213,10 @@ describe("links", () => {
|
|
|
200
213
|
};
|
|
201
214
|
const node = blockToNode(block, tt.schema);
|
|
202
215
|
// expect(node).toMatchSnapshot();
|
|
203
|
-
const outputBlock = nodeToBlock(
|
|
216
|
+
const outputBlock = nodeToBlock<DefaultBlockSchema>(
|
|
217
|
+
node,
|
|
218
|
+
defaultBlockSchema
|
|
219
|
+
);
|
|
204
220
|
|
|
205
221
|
// Temporary fix to set props to {}, because at this point
|
|
206
222
|
// we don't have an easy way to access default props at runtime,
|
|
@@ -212,7 +228,7 @@ describe("links", () => {
|
|
|
212
228
|
});
|
|
213
229
|
|
|
214
230
|
it("Convert two adjacent links in a block", async () => {
|
|
215
|
-
const block: PartialBlock = {
|
|
231
|
+
const block: PartialBlock<DefaultBlockSchema> = {
|
|
216
232
|
id: UniqueID.options.generateID(),
|
|
217
233
|
type: "paragraph",
|
|
218
234
|
content: [
|
|
@@ -231,7 +247,10 @@ describe("links", () => {
|
|
|
231
247
|
|
|
232
248
|
const node = blockToNode(block, tt.schema);
|
|
233
249
|
expect(node).toMatchSnapshot();
|
|
234
|
-
const outputBlock = nodeToBlock(
|
|
250
|
+
const outputBlock = nodeToBlock<DefaultBlockSchema>(
|
|
251
|
+
node,
|
|
252
|
+
defaultBlockSchema
|
|
253
|
+
);
|
|
235
254
|
|
|
236
255
|
// Temporary fix to set props to {}, because at this point
|
|
237
256
|
// we don't have an easy way to access default props at runtime,
|
|
@@ -2,9 +2,11 @@ import { Mark } from "@tiptap/pm/model";
|
|
|
2
2
|
import { Node, Schema } from "prosemirror-model";
|
|
3
3
|
import {
|
|
4
4
|
Block,
|
|
5
|
-
|
|
5
|
+
BlockSchema,
|
|
6
6
|
PartialBlock,
|
|
7
7
|
} from "../../extensions/Blocks/api/blockTypes";
|
|
8
|
+
|
|
9
|
+
import { defaultProps } from "../../extensions/Blocks/api/defaultBlocks";
|
|
8
10
|
import {
|
|
9
11
|
ColorStyle,
|
|
10
12
|
InlineContent,
|
|
@@ -105,7 +107,10 @@ export function inlineContentToNodes(
|
|
|
105
107
|
/**
|
|
106
108
|
* Converts a BlockNote block to a TipTap node.
|
|
107
109
|
*/
|
|
108
|
-
export function blockToNode
|
|
110
|
+
export function blockToNode<BSchema extends BlockSchema>(
|
|
111
|
+
block: PartialBlock<BSchema>,
|
|
112
|
+
schema: Schema
|
|
113
|
+
) {
|
|
109
114
|
let id = block.id;
|
|
110
115
|
|
|
111
116
|
if (id === undefined) {
|
|
@@ -214,10 +219,11 @@ function contentNodeToInlineContent(contentNode: Node) {
|
|
|
214
219
|
/**
|
|
215
220
|
* Convert a TipTap node to a BlockNote block.
|
|
216
221
|
*/
|
|
217
|
-
export function nodeToBlock(
|
|
222
|
+
export function nodeToBlock<BSchema extends BlockSchema>(
|
|
218
223
|
node: Node,
|
|
219
|
-
|
|
220
|
-
|
|
224
|
+
blockSchema: BSchema,
|
|
225
|
+
blockCache?: WeakMap<Node, Block<BSchema>>
|
|
226
|
+
): Block<BSchema> {
|
|
221
227
|
if (node.type.name !== "blockContainer") {
|
|
222
228
|
throw Error(
|
|
223
229
|
"Node must be of type blockContainer, but is of type" +
|
|
@@ -246,29 +252,44 @@ export function nodeToBlock(
|
|
|
246
252
|
...blockInfo.node.attrs,
|
|
247
253
|
...blockInfo.contentNode.attrs,
|
|
248
254
|
})) {
|
|
249
|
-
|
|
255
|
+
const blockSpec = blockSchema[blockInfo.contentType.name];
|
|
256
|
+
if (!blockSpec) {
|
|
250
257
|
throw Error(
|
|
251
258
|
"Block is of an unrecognized type: " + blockInfo.contentType.name
|
|
252
259
|
);
|
|
253
260
|
}
|
|
254
261
|
|
|
255
|
-
const
|
|
262
|
+
const propSchema = blockSpec.propSchema;
|
|
256
263
|
|
|
257
|
-
if (
|
|
264
|
+
if (attr in propSchema) {
|
|
258
265
|
props[attr] = value;
|
|
259
266
|
}
|
|
267
|
+
// Block ids are stored as node attributes the same way props are, so we
|
|
268
|
+
// need to ensure we don't attempt to read block ids as props.
|
|
269
|
+
|
|
270
|
+
// the second check is for the backgroundColor & textColor props.
|
|
271
|
+
// Since we want them to be inherited by child blocks, we can't put them on the blockContent node,
|
|
272
|
+
// and instead have to put them on the blockContainer node.
|
|
273
|
+
// The blockContainer node is the same for all block types, but some custom blocks might not use backgroundColor & textColor,
|
|
274
|
+
// so these 2 props are technically unexpected but we shouldn't log a warning.
|
|
275
|
+
// (this is a bit hacky)
|
|
276
|
+
else if (attr !== "id" && !(attr in defaultProps)) {
|
|
277
|
+
console.warn("Block has an unrecognized attribute: " + attr);
|
|
278
|
+
}
|
|
260
279
|
}
|
|
261
280
|
|
|
262
281
|
const content = contentNodeToInlineContent(blockInfo.contentNode);
|
|
263
282
|
|
|
264
|
-
const children: Block[] = [];
|
|
283
|
+
const children: Block<BSchema>[] = [];
|
|
265
284
|
for (let i = 0; i < blockInfo.numChildBlocks; i++) {
|
|
266
|
-
children.push(
|
|
285
|
+
children.push(
|
|
286
|
+
nodeToBlock(blockInfo.node.lastChild!.child(i), blockSchema, blockCache)
|
|
287
|
+
);
|
|
267
288
|
}
|
|
268
289
|
|
|
269
|
-
const block: Block = {
|
|
290
|
+
const block: Block<BSchema> = {
|
|
270
291
|
id,
|
|
271
|
-
type: blockInfo.contentType.name
|
|
292
|
+
type: blockInfo.contentType.name,
|
|
272
293
|
props,
|
|
273
294
|
content,
|
|
274
295
|
children,
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Block,
|
|
3
|
+
BlockSchema,
|
|
4
|
+
PartialBlock,
|
|
5
|
+
} from "../../extensions/Blocks/api/blockTypes";
|
|
2
6
|
import {
|
|
3
7
|
InlineContent,
|
|
4
8
|
PartialInlineContent,
|
|
@@ -39,9 +43,9 @@ function partialContentToInlineContent(
|
|
|
39
43
|
});
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
export function partialBlockToBlockForTesting(
|
|
43
|
-
partialBlock: PartialBlock
|
|
44
|
-
): Block {
|
|
46
|
+
export function partialBlockToBlockForTesting<BSchema extends BlockSchema>(
|
|
47
|
+
partialBlock: PartialBlock<BSchema>
|
|
48
|
+
): Block<BSchema> {
|
|
45
49
|
const withDefaults = {
|
|
46
50
|
id: "",
|
|
47
51
|
type: "paragraph" as any,
|
package/src/editor.module.css
CHANGED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { Attribute, Node } from "@tiptap/core";
|
|
2
|
+
import { BlockNoteEditor } from "../../..";
|
|
3
|
+
import styles from "../nodes/Block.module.css";
|
|
4
|
+
import {
|
|
5
|
+
BlockConfig,
|
|
6
|
+
BlockSchema,
|
|
7
|
+
BlockSpec,
|
|
8
|
+
PropSchema,
|
|
9
|
+
TipTapNode,
|
|
10
|
+
TipTapNodeConfig,
|
|
11
|
+
} from "./blockTypes";
|
|
12
|
+
|
|
13
|
+
export function camelToDataKebab(str: string): string {
|
|
14
|
+
return "data-" + str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Function that uses the 'propSchema' of a blockConfig to create a TipTap
|
|
18
|
+
// node's `addAttributes` property.
|
|
19
|
+
export function propsToAttributes<
|
|
20
|
+
BType extends string,
|
|
21
|
+
PSchema extends PropSchema,
|
|
22
|
+
ContainsInlineContent extends boolean,
|
|
23
|
+
BSchema extends BlockSchema
|
|
24
|
+
>(
|
|
25
|
+
blockConfig: Omit<
|
|
26
|
+
BlockConfig<BType, PSchema, ContainsInlineContent, BSchema>,
|
|
27
|
+
"render"
|
|
28
|
+
>
|
|
29
|
+
) {
|
|
30
|
+
const tiptapAttributes: Record<string, Attribute> = {};
|
|
31
|
+
|
|
32
|
+
Object.entries(blockConfig.propSchema).forEach(([name, spec]) => {
|
|
33
|
+
tiptapAttributes[name] = {
|
|
34
|
+
default: spec.default,
|
|
35
|
+
keepOnSplit: true,
|
|
36
|
+
// Props are displayed in kebab-case as HTML attributes. If a prop's
|
|
37
|
+
// value is the same as its default, we don't display an HTML
|
|
38
|
+
// attribute for it.
|
|
39
|
+
parseHTML: (element) => element.getAttribute(camelToDataKebab(name)),
|
|
40
|
+
renderHTML: (attributes) =>
|
|
41
|
+
attributes[name] !== spec.default
|
|
42
|
+
? {
|
|
43
|
+
[camelToDataKebab(name)]: attributes[name],
|
|
44
|
+
}
|
|
45
|
+
: {},
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return tiptapAttributes;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Function that uses the 'parse' function of a blockConfig to create a
|
|
53
|
+
// TipTap node's `parseHTML` property. This is only used for parsing content
|
|
54
|
+
// from the clipboard.
|
|
55
|
+
export function parse<
|
|
56
|
+
BType extends string,
|
|
57
|
+
PSchema extends PropSchema,
|
|
58
|
+
ContainsInlineContent extends boolean,
|
|
59
|
+
BSchema extends BlockSchema
|
|
60
|
+
>(
|
|
61
|
+
blockConfig: Omit<
|
|
62
|
+
BlockConfig<BType, PSchema, ContainsInlineContent, BSchema>,
|
|
63
|
+
"render"
|
|
64
|
+
>
|
|
65
|
+
) {
|
|
66
|
+
return [
|
|
67
|
+
{
|
|
68
|
+
tag: "div[data-content-type=" + blockConfig.type + "]",
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Function that uses the 'render' function of a blockConfig to create a
|
|
74
|
+
// TipTap node's `renderHTML` property. Since custom blocks use node views,
|
|
75
|
+
// this is only used for serializing content to the clipboard.
|
|
76
|
+
export function render<
|
|
77
|
+
BType extends string,
|
|
78
|
+
PSchema extends PropSchema,
|
|
79
|
+
ContainsInlineContent extends boolean,
|
|
80
|
+
BSchema extends BlockSchema
|
|
81
|
+
>(
|
|
82
|
+
blockConfig: Omit<
|
|
83
|
+
BlockConfig<BType, PSchema, ContainsInlineContent, BSchema>,
|
|
84
|
+
"render"
|
|
85
|
+
>,
|
|
86
|
+
HTMLAttributes: Record<string, any>
|
|
87
|
+
) {
|
|
88
|
+
// Create blockContent element
|
|
89
|
+
const blockContent = document.createElement("div");
|
|
90
|
+
// Add blockContent HTML attribute
|
|
91
|
+
blockContent.setAttribute("data-content-type", blockConfig.type);
|
|
92
|
+
// Add props as HTML attributes in kebab-case with "data-" prefix
|
|
93
|
+
for (const [attribute, value] of Object.entries(HTMLAttributes)) {
|
|
94
|
+
blockContent.setAttribute(attribute, value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// TODO: This only works for content copied within BlockNote.
|
|
98
|
+
// Creates contentDOM element to serialize inline content into.
|
|
99
|
+
let contentDOM: HTMLDivElement | undefined;
|
|
100
|
+
if (blockConfig.containsInlineContent) {
|
|
101
|
+
contentDOM = document.createElement("div");
|
|
102
|
+
blockContent.appendChild(contentDOM);
|
|
103
|
+
} else {
|
|
104
|
+
contentDOM = undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return contentDOM !== undefined
|
|
108
|
+
? {
|
|
109
|
+
dom: blockContent,
|
|
110
|
+
contentDOM: contentDOM,
|
|
111
|
+
}
|
|
112
|
+
: {
|
|
113
|
+
dom: blockContent,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// A function to create custom block for API consumers
|
|
118
|
+
// we want to hide the tiptap node from API consumers and provide a simpler API surface instead
|
|
119
|
+
export function createBlockSpec<
|
|
120
|
+
BType extends string,
|
|
121
|
+
PSchema extends PropSchema,
|
|
122
|
+
ContainsInlineContent extends boolean,
|
|
123
|
+
BSchema extends BlockSchema
|
|
124
|
+
>(
|
|
125
|
+
blockConfig: BlockConfig<BType, PSchema, ContainsInlineContent, BSchema>
|
|
126
|
+
): BlockSpec<BType, PSchema> {
|
|
127
|
+
const node = createTipTapBlock<BType>({
|
|
128
|
+
name: blockConfig.type,
|
|
129
|
+
content: blockConfig.containsInlineContent ? "inline*" : "",
|
|
130
|
+
selectable: blockConfig.containsInlineContent,
|
|
131
|
+
|
|
132
|
+
addOptions() {
|
|
133
|
+
return {
|
|
134
|
+
editor: undefined,
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
addAttributes() {
|
|
139
|
+
return propsToAttributes(blockConfig);
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
parseHTML() {
|
|
143
|
+
return parse(blockConfig);
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
renderHTML({ HTMLAttributes }) {
|
|
147
|
+
return render(blockConfig, HTMLAttributes);
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
addNodeView() {
|
|
151
|
+
return ({ HTMLAttributes, getPos }) => {
|
|
152
|
+
// Create blockContent element
|
|
153
|
+
const blockContent = document.createElement("div");
|
|
154
|
+
// Sets blockContent class
|
|
155
|
+
blockContent.className = styles.blockContent;
|
|
156
|
+
// Add blockContent HTML attribute
|
|
157
|
+
blockContent.setAttribute("data-content-type", blockConfig.type);
|
|
158
|
+
// Add props as HTML attributes in kebab-case with "data-" prefix
|
|
159
|
+
for (const [attribute, value] of Object.entries(HTMLAttributes)) {
|
|
160
|
+
blockContent.setAttribute(attribute, value);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Gets BlockNote editor instance
|
|
164
|
+
const editor = this.options.editor! as BlockNoteEditor<
|
|
165
|
+
BSchema & { [k in BType]: BlockSpec<BType, PSchema> }
|
|
166
|
+
>;
|
|
167
|
+
// Gets position of the node
|
|
168
|
+
if (typeof getPos === "boolean") {
|
|
169
|
+
throw new Error(
|
|
170
|
+
"Cannot find node position as getPos is a boolean, not a function."
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
const pos = getPos();
|
|
174
|
+
// Gets TipTap editor instance
|
|
175
|
+
const tipTapEditor = editor._tiptapEditor;
|
|
176
|
+
// Gets parent blockContainer node
|
|
177
|
+
const blockContainer = tipTapEditor.state.doc.resolve(pos!).node();
|
|
178
|
+
// Gets block identifier
|
|
179
|
+
const blockIdentifier = blockContainer.attrs.id;
|
|
180
|
+
|
|
181
|
+
// Get the block
|
|
182
|
+
const block = editor.getBlock(blockIdentifier)!;
|
|
183
|
+
if (block.type !== blockConfig.type) {
|
|
184
|
+
throw new Error("Block type does not match");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Render elements
|
|
188
|
+
const rendered = blockConfig.render(block as any, editor);
|
|
189
|
+
// Add inlineContent class to inline content
|
|
190
|
+
if ("contentDOM" in rendered) {
|
|
191
|
+
rendered.contentDOM.className = `${
|
|
192
|
+
rendered.contentDOM.className
|
|
193
|
+
? rendered.contentDOM.className + " "
|
|
194
|
+
: ""
|
|
195
|
+
}${styles.inlineContent}`;
|
|
196
|
+
}
|
|
197
|
+
// Add elements to blockContent
|
|
198
|
+
blockContent.appendChild(rendered.dom);
|
|
199
|
+
|
|
200
|
+
return "contentDOM" in rendered
|
|
201
|
+
? {
|
|
202
|
+
dom: blockContent,
|
|
203
|
+
contentDOM: rendered.contentDOM,
|
|
204
|
+
}
|
|
205
|
+
: {
|
|
206
|
+
dom: blockContent,
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
node: node,
|
|
214
|
+
propSchema: blockConfig.propSchema,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function createTipTapBlock<Type extends string>(
|
|
219
|
+
config: TipTapNodeConfig<Type>
|
|
220
|
+
): TipTapNode<Type> {
|
|
221
|
+
// Type cast is needed as Node.name is mutable, though there is basically no
|
|
222
|
+
// reason to change it after creation. Alternative is to wrap Node in a new
|
|
223
|
+
// class, which I don't think is worth it since we'd only be changing 1
|
|
224
|
+
// attribute to be read only.
|
|
225
|
+
return Node.create({
|
|
226
|
+
...config,
|
|
227
|
+
group: "blockContent",
|
|
228
|
+
}) as TipTapNode<Type>;
|
|
229
|
+
}
|