@blocknote/core 0.7.1-alpha.0 → 0.8.1
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 +1711 -1469
- package/dist/blocknote.js.map +1 -1
- package/dist/blocknote.umd.cjs +6 -2
- package/dist/blocknote.umd.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +3 -3
- package/src/BlockNoteEditor.ts +104 -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/__snapshots__/nodeConversions.test.ts.snap +292 -0
- package/src/api/nodeConversions/nodeConversions.test.ts +265 -10
- package/src/api/nodeConversions/nodeConversions.ts +199 -47
- package/src/api/nodeConversions/testUtil.ts +8 -4
- package/src/editor.module.css +5 -6
- 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 +28 -16
- 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 +57 -14
- package/src/extensions/FormattingToolbar/FormattingToolbarExtension.ts +21 -16
- package/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts +9 -5
- package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +38 -58
- package/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +19 -0
- 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 +38 -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 +12 -11
- 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/Placeholder/localisation/index.d.ts +2 -0
- package/types/src/extensions/Placeholder/localisation/translation.d.ts +51 -0
- 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
- package/types/src/extensions/Blocks/api/alertBlock.d.ts +0 -13
- package/types/src/extensions/Blocks/api/alertBlock2.d.ts +0 -13
|
@@ -2,13 +2,14 @@ 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,
|
|
11
|
-
Link,
|
|
12
13
|
PartialInlineContent,
|
|
13
14
|
PartialLink,
|
|
14
15
|
StyledText,
|
|
@@ -32,7 +33,7 @@ const colorStyles = new Set<ColorStyle>(["textColor", "backgroundColor"]);
|
|
|
32
33
|
* Convert a StyledText inline element to a
|
|
33
34
|
* prosemirror text node with the appropriate marks
|
|
34
35
|
*/
|
|
35
|
-
function
|
|
36
|
+
function styledTextToNodes(styledText: StyledText, schema: Schema): Node[] {
|
|
36
37
|
const marks: Mark[] = [];
|
|
37
38
|
|
|
38
39
|
for (const [style, value] of Object.entries(styledText.styles)) {
|
|
@@ -43,7 +44,22 @@ function styledTextToNode(styledText: StyledText, schema: Schema): Node {
|
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
return
|
|
47
|
+
return (
|
|
48
|
+
styledText.text
|
|
49
|
+
// Splits text & line breaks.
|
|
50
|
+
.split(/(\n)/g)
|
|
51
|
+
// If the content ends with a line break, an empty string is added to the
|
|
52
|
+
// end, which this removes.
|
|
53
|
+
.filter((text) => text.length > 0)
|
|
54
|
+
// Converts text & line breaks to nodes.
|
|
55
|
+
.map((text) => {
|
|
56
|
+
if (text === "\n") {
|
|
57
|
+
return schema.nodes["hardBreak"].create();
|
|
58
|
+
} else {
|
|
59
|
+
return schema.text(text, marks);
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
);
|
|
47
63
|
}
|
|
48
64
|
|
|
49
65
|
/**
|
|
@@ -56,7 +72,14 @@ function linkToNodes(link: PartialLink, schema: Schema): Node[] {
|
|
|
56
72
|
});
|
|
57
73
|
|
|
58
74
|
return styledTextArrayToNodes(link.content, schema).map((node) => {
|
|
59
|
-
|
|
75
|
+
if (node.type.name === "text") {
|
|
76
|
+
return node.mark([...node.marks, linkMark]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (node.type.name === "hardBreak") {
|
|
80
|
+
return node;
|
|
81
|
+
}
|
|
82
|
+
throw new Error("unexpected node type");
|
|
60
83
|
});
|
|
61
84
|
}
|
|
62
85
|
|
|
@@ -71,12 +94,14 @@ function styledTextArrayToNodes(
|
|
|
71
94
|
let nodes: Node[] = [];
|
|
72
95
|
|
|
73
96
|
if (typeof content === "string") {
|
|
74
|
-
nodes.push(
|
|
97
|
+
nodes.push(
|
|
98
|
+
...styledTextToNodes({ type: "text", text: content, styles: {} }, schema)
|
|
99
|
+
);
|
|
75
100
|
return nodes;
|
|
76
101
|
}
|
|
77
102
|
|
|
78
103
|
for (const styledText of content) {
|
|
79
|
-
nodes.push(
|
|
104
|
+
nodes.push(...styledTextToNodes(styledText, schema));
|
|
80
105
|
}
|
|
81
106
|
return nodes;
|
|
82
107
|
}
|
|
@@ -105,7 +130,10 @@ export function inlineContentToNodes(
|
|
|
105
130
|
/**
|
|
106
131
|
* Converts a BlockNote block to a TipTap node.
|
|
107
132
|
*/
|
|
108
|
-
export function blockToNode
|
|
133
|
+
export function blockToNode<BSchema extends BlockSchema>(
|
|
134
|
+
block: PartialBlock<BSchema>,
|
|
135
|
+
schema: Schema
|
|
136
|
+
) {
|
|
109
137
|
let id = block.id;
|
|
110
138
|
|
|
111
139
|
if (id === undefined) {
|
|
@@ -156,15 +184,39 @@ export function blockToNode(block: PartialBlock, schema: Schema) {
|
|
|
156
184
|
*/
|
|
157
185
|
function contentNodeToInlineContent(contentNode: Node) {
|
|
158
186
|
const content: InlineContent[] = [];
|
|
159
|
-
|
|
160
|
-
let currentLink: Link | undefined = undefined;
|
|
187
|
+
let currentContent: InlineContent | undefined = undefined;
|
|
161
188
|
|
|
162
189
|
// Most of the logic below is for handling links because in ProseMirror links are marks
|
|
163
190
|
// while in BlockNote links are a type of inline content
|
|
164
191
|
contentNode.content.forEach((node) => {
|
|
165
|
-
|
|
192
|
+
// hardBreak nodes do not have an InlineContent equivalent, instead we
|
|
193
|
+
// add a newline to the previous node.
|
|
194
|
+
if (node.type.name === "hardBreak") {
|
|
195
|
+
if (currentContent) {
|
|
196
|
+
// Current content exists.
|
|
197
|
+
if (currentContent.type === "text") {
|
|
198
|
+
// Current content is text.
|
|
199
|
+
currentContent.text += "\n";
|
|
200
|
+
} else if (currentContent.type === "link") {
|
|
201
|
+
// Current content is a link.
|
|
202
|
+
currentContent.content[currentContent.content.length - 1].text +=
|
|
203
|
+
"\n";
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
// Current content does not exist.
|
|
207
|
+
currentContent = {
|
|
208
|
+
type: "text",
|
|
209
|
+
text: "\n",
|
|
210
|
+
styles: {},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
166
216
|
|
|
217
|
+
const styles: Styles = {};
|
|
167
218
|
let linkMark: Mark | undefined;
|
|
219
|
+
|
|
168
220
|
for (const mark of node.marks) {
|
|
169
221
|
if (mark.type.name === "link") {
|
|
170
222
|
linkMark = mark;
|
|
@@ -177,47 +229,132 @@ function contentNodeToInlineContent(contentNode: Node) {
|
|
|
177
229
|
}
|
|
178
230
|
}
|
|
179
231
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
232
|
+
// Parsing links and text.
|
|
233
|
+
// Current content exists.
|
|
234
|
+
if (currentContent) {
|
|
235
|
+
// Current content is text.
|
|
236
|
+
if (currentContent.type === "text") {
|
|
237
|
+
if (!linkMark) {
|
|
238
|
+
// Node is text (same type as current content).
|
|
239
|
+
if (
|
|
240
|
+
JSON.stringify(currentContent.styles) === JSON.stringify(styles)
|
|
241
|
+
) {
|
|
242
|
+
// Styles are the same.
|
|
243
|
+
currentContent.text += node.textContent;
|
|
244
|
+
} else {
|
|
245
|
+
// Styles are different.
|
|
246
|
+
content.push(currentContent);
|
|
247
|
+
currentContent = {
|
|
248
|
+
type: "text",
|
|
249
|
+
text: node.textContent,
|
|
250
|
+
styles,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
// Node is a link (different type to current content).
|
|
255
|
+
content.push(currentContent);
|
|
256
|
+
currentContent = {
|
|
257
|
+
type: "link",
|
|
258
|
+
href: linkMark.attrs.href,
|
|
259
|
+
content: [
|
|
260
|
+
{
|
|
261
|
+
type: "text",
|
|
262
|
+
text: node.textContent,
|
|
263
|
+
styles,
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
} else if (currentContent.type === "link") {
|
|
269
|
+
// Current content is a link.
|
|
270
|
+
if (linkMark) {
|
|
271
|
+
// Node is a link (same type as current content).
|
|
272
|
+
// Link URLs are the same.
|
|
273
|
+
if (currentContent.href === linkMark.attrs.href) {
|
|
274
|
+
// Styles are the same.
|
|
275
|
+
if (
|
|
276
|
+
JSON.stringify(
|
|
277
|
+
currentContent.content[currentContent.content.length - 1].styles
|
|
278
|
+
) === JSON.stringify(styles)
|
|
279
|
+
) {
|
|
280
|
+
currentContent.content[currentContent.content.length - 1].text +=
|
|
281
|
+
node.textContent;
|
|
282
|
+
} else {
|
|
283
|
+
// Styles are different.
|
|
284
|
+
currentContent.content.push({
|
|
285
|
+
type: "text",
|
|
286
|
+
text: node.textContent,
|
|
287
|
+
styles,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
// Link URLs are different.
|
|
292
|
+
content.push(currentContent);
|
|
293
|
+
currentContent = {
|
|
294
|
+
type: "link",
|
|
295
|
+
href: linkMark.attrs.href,
|
|
296
|
+
content: [
|
|
297
|
+
{
|
|
298
|
+
type: "text",
|
|
299
|
+
text: node.textContent,
|
|
300
|
+
styles,
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
// Node is text (different type to current content).
|
|
307
|
+
content.push(currentContent);
|
|
308
|
+
currentContent = {
|
|
194
309
|
type: "text",
|
|
195
310
|
text: node.textContent,
|
|
196
311
|
styles,
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// Current content does not exist.
|
|
317
|
+
else {
|
|
318
|
+
// Node is text.
|
|
319
|
+
if (!linkMark) {
|
|
320
|
+
currentContent = {
|
|
321
|
+
type: "text",
|
|
322
|
+
text: node.textContent,
|
|
323
|
+
styles,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
// Node is a link.
|
|
327
|
+
else {
|
|
328
|
+
currentContent = {
|
|
329
|
+
type: "link",
|
|
330
|
+
href: linkMark.attrs.href,
|
|
331
|
+
content: [
|
|
332
|
+
{
|
|
333
|
+
type: "text",
|
|
334
|
+
text: node.textContent,
|
|
335
|
+
styles,
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
};
|
|
339
|
+
}
|
|
209
340
|
}
|
|
210
341
|
});
|
|
342
|
+
|
|
343
|
+
if (currentContent) {
|
|
344
|
+
content.push(currentContent);
|
|
345
|
+
}
|
|
346
|
+
|
|
211
347
|
return content;
|
|
212
348
|
}
|
|
213
349
|
|
|
214
350
|
/**
|
|
215
351
|
* Convert a TipTap node to a BlockNote block.
|
|
216
352
|
*/
|
|
217
|
-
export function nodeToBlock(
|
|
353
|
+
export function nodeToBlock<BSchema extends BlockSchema>(
|
|
218
354
|
node: Node,
|
|
219
|
-
|
|
220
|
-
|
|
355
|
+
blockSchema: BSchema,
|
|
356
|
+
blockCache?: WeakMap<Node, Block<BSchema>>
|
|
357
|
+
): Block<BSchema> {
|
|
221
358
|
if (node.type.name !== "blockContainer") {
|
|
222
359
|
throw Error(
|
|
223
360
|
"Node must be of type blockContainer, but is of type" +
|
|
@@ -246,29 +383,44 @@ export function nodeToBlock(
|
|
|
246
383
|
...blockInfo.node.attrs,
|
|
247
384
|
...blockInfo.contentNode.attrs,
|
|
248
385
|
})) {
|
|
249
|
-
|
|
386
|
+
const blockSpec = blockSchema[blockInfo.contentType.name];
|
|
387
|
+
if (!blockSpec) {
|
|
250
388
|
throw Error(
|
|
251
389
|
"Block is of an unrecognized type: " + blockInfo.contentType.name
|
|
252
390
|
);
|
|
253
391
|
}
|
|
254
392
|
|
|
255
|
-
const
|
|
393
|
+
const propSchema = blockSpec.propSchema;
|
|
256
394
|
|
|
257
|
-
if (
|
|
395
|
+
if (attr in propSchema) {
|
|
258
396
|
props[attr] = value;
|
|
259
397
|
}
|
|
398
|
+
// Block ids are stored as node attributes the same way props are, so we
|
|
399
|
+
// need to ensure we don't attempt to read block ids as props.
|
|
400
|
+
|
|
401
|
+
// the second check is for the backgroundColor & textColor props.
|
|
402
|
+
// Since we want them to be inherited by child blocks, we can't put them on the blockContent node,
|
|
403
|
+
// and instead have to put them on the blockContainer node.
|
|
404
|
+
// The blockContainer node is the same for all block types, but some custom blocks might not use backgroundColor & textColor,
|
|
405
|
+
// so these 2 props are technically unexpected but we shouldn't log a warning.
|
|
406
|
+
// (this is a bit hacky)
|
|
407
|
+
else if (attr !== "id" && !(attr in defaultProps)) {
|
|
408
|
+
console.warn("Block has an unrecognized attribute: " + attr);
|
|
409
|
+
}
|
|
260
410
|
}
|
|
261
411
|
|
|
262
412
|
const content = contentNodeToInlineContent(blockInfo.contentNode);
|
|
263
413
|
|
|
264
|
-
const children: Block[] = [];
|
|
414
|
+
const children: Block<BSchema>[] = [];
|
|
265
415
|
for (let i = 0; i < blockInfo.numChildBlocks; i++) {
|
|
266
|
-
children.push(
|
|
416
|
+
children.push(
|
|
417
|
+
nodeToBlock(blockInfo.node.lastChild!.child(i), blockSchema, blockCache)
|
|
418
|
+
);
|
|
267
419
|
}
|
|
268
420
|
|
|
269
|
-
const block: Block = {
|
|
421
|
+
const block: Block<BSchema> = {
|
|
270
422
|
id,
|
|
271
|
-
type: blockInfo.contentType.name
|
|
423
|
+
type: blockInfo.contentType.name,
|
|
272
424
|
props,
|
|
273
425
|
content,
|
|
274
426
|
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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
.bnEditor {
|
|
4
4
|
outline: none;
|
|
5
|
-
padding-inline:
|
|
5
|
+
padding-inline: 54px;
|
|
6
6
|
border-radius: 8px;
|
|
7
7
|
|
|
8
8
|
/* Define a set of colors to be used throughout the app for consistency
|
|
@@ -38,7 +38,6 @@ Tippy popups that are appended to document.body directly
|
|
|
38
38
|
.defaultStyles h3,
|
|
39
39
|
.defaultStyles li {
|
|
40
40
|
all: unset !important;
|
|
41
|
-
flex-grow: 1 !important;
|
|
42
41
|
margin: 0;
|
|
43
42
|
padding: 0;
|
|
44
43
|
font-size: inherit;
|
|
@@ -57,13 +56,13 @@ Tippy popups that are appended to document.body directly
|
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
[data-theme="light"] {
|
|
60
|
-
background-color: #
|
|
61
|
-
color: #
|
|
59
|
+
background-color: #FFFFFF;
|
|
60
|
+
color: #3F3F3F;
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
[data-theme="dark"] {
|
|
65
|
-
background-color: #
|
|
66
|
-
color: #
|
|
64
|
+
background-color: #1F1F1F;
|
|
65
|
+
color: #CFCFCF;
|
|
67
66
|
}
|
|
68
67
|
|
|
69
68
|
.dragPreview {
|
|
@@ -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
|
+
}
|