@blocknote/core 0.29.1 → 0.30.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/README.md +125 -0
- package/dist/blocknote.cjs +9 -9
- package/dist/blocknote.cjs.map +1 -1
- package/dist/blocknote.js +1479 -1339
- package/dist/blocknote.js.map +1 -1
- package/dist/locales.cjs +1 -1
- package/dist/locales.cjs.map +1 -1
- package/dist/locales.js +751 -9
- package/dist/locales.js.map +1 -1
- package/dist/style.css +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/webpack-stats.json +1 -1
- package/package.json +3 -6
- package/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap +0 -7
- package/src/api/blockManipulation/commands/mergeBlocks/__snapshots__/mergeBlocks.test.ts.snap +0 -5
- package/src/api/blockManipulation/commands/moveBlocks/__snapshots__/moveBlocks.test.ts.snap +0 -20
- package/src/api/blockManipulation/commands/replaceBlocks/__snapshots__/replaceBlocks.test.ts.snap +0 -12
- package/src/api/blockManipulation/commands/splitBlock/__snapshots__/splitBlock.test.ts.snap +0 -6
- package/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap +0 -17
- package/src/api/clipboard/fromClipboard/pasteExtension.ts +19 -1
- package/src/blocks/AudioBlockContent/AudioBlockContent.ts +5 -0
- package/src/blocks/CodeBlockContent/CodeBlockContent.ts +32 -18
- package/src/blocks/FileBlockContent/FileBlockContent.ts +5 -0
- package/src/blocks/FileBlockContent/helpers/render/createResizableFileBlockWrapper.ts +9 -2
- package/src/blocks/HeadingBlockContent/HeadingBlockContent.ts +3 -0
- package/src/blocks/ImageBlockContent/ImageBlockContent.ts +10 -2
- package/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +9 -25
- package/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts +14 -3
- package/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +9 -26
- package/src/blocks/ListItemBlockContent/getListItemContent.ts +115 -0
- package/src/blocks/ParagraphBlockContent/ParagraphBlockContent.ts +6 -2
- package/src/blocks/QuoteBlockContent/QuoteBlockContent.ts +6 -1
- package/src/blocks/TableBlockContent/TableBlockContent.ts +71 -14
- package/src/blocks/VideoBlockContent/VideoBlockContent.ts +10 -2
- package/src/blocks/defaultBlockHelpers.ts +16 -0
- package/src/editor/Block.css +2 -1
- package/src/editor/BlockNoteEditor.ts +103 -60
- package/src/editor/BlockNoteExtensions.ts +14 -5
- package/src/extensions/Collaboration/CursorPlugin.ts +152 -0
- package/src/extensions/Collaboration/SyncPlugin.ts +15 -0
- package/src/extensions/Collaboration/UndoPlugin.ts +14 -0
- package/src/extensions/FilePanel/FilePanelPlugin.ts +31 -22
- package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +2 -4
- package/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts +3 -0
- package/src/i18n/locales/index.ts +2 -0
- package/src/i18n/locales/ru.ts +2 -2
- package/src/i18n/locales/sk.ts +355 -0
- package/src/i18n/locales/zh-tw.ts +390 -0
- package/src/pm-nodes/BlockContainer.ts +7 -6
- package/src/schema/blocks/createSpec.ts +1 -1
- package/src/schema/blocks/internal.ts +0 -1
- package/src/schema/blocks/types.ts +2 -1
- package/types/src/api/blockManipulation/setupTestEnv.d.ts +8 -4
- package/types/src/blocks/ImageBlockContent/ImageBlockContent.d.ts +8 -4
- package/types/src/blocks/ListItemBlockContent/getListItemContent.d.ts +28 -0
- package/types/src/blocks/VideoBlockContent/VideoBlockContent.d.ts +8 -4
- package/types/src/blocks/defaultBlockHelpers.d.ts +1 -0
- package/types/src/blocks/defaultBlocks.d.ts +16 -8
- package/types/src/editor/BlockNoteEditor.d.ts +18 -1
- package/types/src/extensions/Collaboration/CursorPlugin.d.ts +31 -0
- package/types/src/extensions/Collaboration/SyncPlugin.d.ts +7 -0
- package/types/src/extensions/Collaboration/UndoPlugin.d.ts +6 -0
- package/types/src/extensions/FilePanel/FilePanelPlugin.d.ts +1 -1
- package/types/src/i18n/locales/index.d.ts +2 -0
- package/types/src/i18n/locales/sk.d.ts +313 -0
- package/types/src/i18n/locales/zh-tw.d.ts +2 -0
- package/types/src/schema/blocks/types.d.ts +2 -1
- package/src/extensions/Collaboration/createCollaborationExtensions.ts +0 -147
- package/types/src/extensions/Collaboration/createCollaborationExtensions.d.ts +0 -17
package/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap
CHANGED
|
@@ -302,7 +302,6 @@ exports[`Test updateBlock > Revert all props 2`] = `
|
|
|
302
302
|
"backgroundColor": "default",
|
|
303
303
|
"caption": "",
|
|
304
304
|
"name": "",
|
|
305
|
-
"previewWidth": 512,
|
|
306
305
|
"showPreview": true,
|
|
307
306
|
"textAlignment": "left",
|
|
308
307
|
"url": "https://via.placeholder.com/150",
|
|
@@ -940,7 +939,6 @@ exports[`Test updateBlock > Revert single prop 2`] = `
|
|
|
940
939
|
"backgroundColor": "default",
|
|
941
940
|
"caption": "",
|
|
942
941
|
"name": "",
|
|
943
|
-
"previewWidth": 512,
|
|
944
942
|
"showPreview": true,
|
|
945
943
|
"textAlignment": "left",
|
|
946
944
|
"url": "https://via.placeholder.com/150",
|
|
@@ -1578,7 +1576,6 @@ exports[`Test updateBlock > Update all props 2`] = `
|
|
|
1578
1576
|
"backgroundColor": "default",
|
|
1579
1577
|
"caption": "",
|
|
1580
1578
|
"name": "",
|
|
1581
|
-
"previewWidth": 512,
|
|
1582
1579
|
"showPreview": true,
|
|
1583
1580
|
"textAlignment": "left",
|
|
1584
1581
|
"url": "https://via.placeholder.com/150",
|
|
@@ -2216,7 +2213,6 @@ exports[`Test updateBlock > Update children 2`] = `
|
|
|
2216
2213
|
"backgroundColor": "default",
|
|
2217
2214
|
"caption": "",
|
|
2218
2215
|
"name": "",
|
|
2219
|
-
"previewWidth": 512,
|
|
2220
2216
|
"showPreview": true,
|
|
2221
2217
|
"textAlignment": "left",
|
|
2222
2218
|
"url": "https://via.placeholder.com/150",
|
|
@@ -2561,7 +2557,6 @@ exports[`Test updateBlock > Update inline content to no content 1`] = `
|
|
|
2561
2557
|
"backgroundColor": "default",
|
|
2562
2558
|
"caption": "",
|
|
2563
2559
|
"name": "",
|
|
2564
|
-
"previewWidth": 512,
|
|
2565
2560
|
"showPreview": true,
|
|
2566
2561
|
"textAlignment": "left",
|
|
2567
2562
|
"url": "",
|
|
@@ -2580,7 +2575,6 @@ exports[`Test updateBlock > Update inline content to no content 2`] = `
|
|
|
2580
2575
|
"backgroundColor": "default",
|
|
2581
2576
|
"caption": "",
|
|
2582
2577
|
"name": "",
|
|
2583
|
-
"previewWidth": 512,
|
|
2584
2578
|
"showPreview": true,
|
|
2585
2579
|
"textAlignment": "left",
|
|
2586
2580
|
"url": "",
|
|
@@ -2799,7 +2793,6 @@ exports[`Test updateBlock > Update inline content to no content 2`] = `
|
|
|
2799
2793
|
"backgroundColor": "default",
|
|
2800
2794
|
"caption": "",
|
|
2801
2795
|
"name": "",
|
|
2802
|
-
"previewWidth": 512,
|
|
2803
2796
|
"showPreview": true,
|
|
2804
2797
|
"textAlignment": "left",
|
|
2805
2798
|
"url": "https://via.placeholder.com/150",
|
|
@@ -3722,7 +3715,6 @@ exports[`Test updateBlock > Update inline content to table content 2`] = `
|
|
|
3722
3715
|
"backgroundColor": "default",
|
|
3723
3716
|
"caption": "",
|
|
3724
3717
|
"name": "",
|
|
3725
|
-
"previewWidth": 512,
|
|
3726
3718
|
"showPreview": true,
|
|
3727
3719
|
"textAlignment": "left",
|
|
3728
3720
|
"url": "https://via.placeholder.com/150",
|
|
@@ -7032,7 +7024,6 @@ exports[`Test updateBlock > Update single prop 2`] = `
|
|
|
7032
7024
|
"backgroundColor": "default",
|
|
7033
7025
|
"caption": "",
|
|
7034
7026
|
"name": "",
|
|
7035
|
-
"previewWidth": 512,
|
|
7036
7027
|
"showPreview": true,
|
|
7037
7028
|
"textAlignment": "left",
|
|
7038
7029
|
"url": "https://via.placeholder.com/150",
|
|
@@ -7613,7 +7604,6 @@ exports[`Test updateBlock > Update table content to empty inline content 2`] = `
|
|
|
7613
7604
|
"backgroundColor": "default",
|
|
7614
7605
|
"caption": "",
|
|
7615
7606
|
"name": "",
|
|
7616
|
-
"previewWidth": 512,
|
|
7617
7607
|
"showPreview": true,
|
|
7618
7608
|
"textAlignment": "left",
|
|
7619
7609
|
"url": "https://via.placeholder.com/150",
|
|
@@ -8026,7 +8016,6 @@ exports[`Test updateBlock > Update table content to inline content 2`] = `
|
|
|
8026
8016
|
"backgroundColor": "default",
|
|
8027
8017
|
"caption": "",
|
|
8028
8018
|
"name": "",
|
|
8029
|
-
"previewWidth": 512,
|
|
8030
8019
|
"showPreview": true,
|
|
8031
8020
|
"textAlignment": "left",
|
|
8032
8021
|
"url": "https://via.placeholder.com/150",
|
|
@@ -8203,7 +8192,6 @@ exports[`Test updateBlock > Update table content to no content 1`] = `
|
|
|
8203
8192
|
"backgroundColor": "default",
|
|
8204
8193
|
"caption": "",
|
|
8205
8194
|
"name": "",
|
|
8206
|
-
"previewWidth": 512,
|
|
8207
8195
|
"showPreview": true,
|
|
8208
8196
|
"textAlignment": "left",
|
|
8209
8197
|
"url": "",
|
|
@@ -8443,7 +8431,6 @@ exports[`Test updateBlock > Update table content to no content 2`] = `
|
|
|
8443
8431
|
"backgroundColor": "default",
|
|
8444
8432
|
"caption": "",
|
|
8445
8433
|
"name": "",
|
|
8446
|
-
"previewWidth": 512,
|
|
8447
8434
|
"showPreview": true,
|
|
8448
8435
|
"textAlignment": "left",
|
|
8449
8436
|
"url": "https://via.placeholder.com/150",
|
|
@@ -8475,7 +8462,6 @@ exports[`Test updateBlock > Update table content to no content 2`] = `
|
|
|
8475
8462
|
"backgroundColor": "default",
|
|
8476
8463
|
"caption": "",
|
|
8477
8464
|
"name": "",
|
|
8478
|
-
"previewWidth": 512,
|
|
8479
8465
|
"showPreview": true,
|
|
8480
8466
|
"textAlignment": "left",
|
|
8481
8467
|
"url": "",
|
|
@@ -8910,7 +8896,6 @@ exports[`Test updateBlock > Update type 2`] = `
|
|
|
8910
8896
|
"backgroundColor": "default",
|
|
8911
8897
|
"caption": "",
|
|
8912
8898
|
"name": "",
|
|
8913
|
-
"previewWidth": 512,
|
|
8914
8899
|
"showPreview": true,
|
|
8915
8900
|
"textAlignment": "left",
|
|
8916
8901
|
"url": "https://via.placeholder.com/150",
|
|
@@ -9533,7 +9518,6 @@ exports[`Test updateBlock > Update with plain content 2`] = `
|
|
|
9533
9518
|
"backgroundColor": "default",
|
|
9534
9519
|
"caption": "",
|
|
9535
9520
|
"name": "",
|
|
9536
|
-
"previewWidth": 512,
|
|
9537
9521
|
"showPreview": true,
|
|
9538
9522
|
"textAlignment": "left",
|
|
9539
9523
|
"url": "https://via.placeholder.com/150",
|
|
@@ -10157,7 +10141,6 @@ exports[`Test updateBlock > Update with styled content 2`] = `
|
|
|
10157
10141
|
"backgroundColor": "default",
|
|
10158
10142
|
"caption": "",
|
|
10159
10143
|
"name": "",
|
|
10160
|
-
"previewWidth": 512,
|
|
10161
10144
|
"showPreview": true,
|
|
10162
10145
|
"textAlignment": "left",
|
|
10163
10146
|
"url": "https://via.placeholder.com/150",
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
BlockNoteEditor,
|
|
6
6
|
BlockNoteEditorOptions,
|
|
7
7
|
} from "../../../editor/BlockNoteEditor";
|
|
8
|
+
import { isMarkdown } from "../../parsers/markdown/detectMarkdown.js";
|
|
8
9
|
import {
|
|
9
10
|
BlockSchema,
|
|
10
11
|
InlineContentSchema,
|
|
@@ -13,7 +14,6 @@ import {
|
|
|
13
14
|
import { acceptedMIMETypes } from "./acceptedMIMETypes.js";
|
|
14
15
|
import { handleFileInsertion } from "./handleFileInsertion.js";
|
|
15
16
|
import { handleVSCodePaste } from "./handleVSCodePaste.js";
|
|
16
|
-
import { isMarkdown } from "../../parsers/markdown/detectMarkdown.js";
|
|
17
17
|
|
|
18
18
|
function defaultPasteHandler({
|
|
19
19
|
event,
|
|
@@ -26,6 +26,24 @@ function defaultPasteHandler({
|
|
|
26
26
|
prioritizeMarkdownOverHTML: boolean;
|
|
27
27
|
plainTextAsMarkdown: boolean;
|
|
28
28
|
}) {
|
|
29
|
+
// Special case for code blocks, as they do not support any rich text
|
|
30
|
+
// formatting, so we force pasting plain text.
|
|
31
|
+
const isInCodeBlock = editor.transact(
|
|
32
|
+
(tr) =>
|
|
33
|
+
tr.selection.$from.parent.type.spec.code &&
|
|
34
|
+
tr.selection.$to.parent.type.spec.code
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (isInCodeBlock) {
|
|
38
|
+
const data = event.clipboardData?.getData("text/plain");
|
|
39
|
+
|
|
40
|
+
if (data) {
|
|
41
|
+
editor.pasteText(data);
|
|
42
|
+
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
29
47
|
let format: (typeof acceptedMIMETypes)[number] | undefined;
|
|
30
48
|
for (const mimeType of acceptedMIMETypes) {
|
|
31
49
|
if (event.clipboardData!.types.includes(mimeType)) {
|
|
@@ -78,6 +78,11 @@ export const audioParse = (
|
|
|
78
78
|
element: HTMLElement
|
|
79
79
|
): Partial<Props<typeof audioBlockConfig.propSchema>> | undefined => {
|
|
80
80
|
if (element.tagName === "AUDIO") {
|
|
81
|
+
// Ignore if parent figure has already been parsed.
|
|
82
|
+
if (element.closest("figure")) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
81
86
|
return parseAudioElement(element as HTMLAudioElement);
|
|
82
87
|
}
|
|
83
88
|
|
|
@@ -126,7 +126,10 @@ const CodeBlockContent = createStronglyTypedTiptapNode({
|
|
|
126
126
|
return null;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
return
|
|
129
|
+
return (
|
|
130
|
+
getLanguageId(options.editor.settings.codeBlock, language) ??
|
|
131
|
+
language
|
|
132
|
+
);
|
|
130
133
|
},
|
|
131
134
|
renderHTML: (attributes) => {
|
|
132
135
|
return attributes.language
|
|
@@ -141,10 +144,12 @@ const CodeBlockContent = createStronglyTypedTiptapNode({
|
|
|
141
144
|
},
|
|
142
145
|
parseHTML() {
|
|
143
146
|
return [
|
|
147
|
+
// Parse from internal HTML.
|
|
144
148
|
{
|
|
145
149
|
tag: "div[data-content-type=" + this.name + "]",
|
|
146
|
-
contentElement: "
|
|
150
|
+
contentElement: ".bn-inline-content",
|
|
147
151
|
},
|
|
152
|
+
// Parse from external HTML.
|
|
148
153
|
{
|
|
149
154
|
tag: "pre",
|
|
150
155
|
contentElement: "code",
|
|
@@ -251,7 +256,7 @@ const CodeBlockContent = createStronglyTypedTiptapNode({
|
|
|
251
256
|
if (process.env.NODE_ENV === "development" && !hasWarned) {
|
|
252
257
|
// eslint-disable-next-line no-console
|
|
253
258
|
console.log(
|
|
254
|
-
"For syntax highlighting of code blocks, you must provide a
|
|
259
|
+
"For syntax highlighting of code blocks, you must provide a `codeBlock.createHighlighter` function"
|
|
255
260
|
);
|
|
256
261
|
hasWarned = true;
|
|
257
262
|
}
|
|
@@ -268,15 +273,22 @@ const CodeBlockContent = createStronglyTypedTiptapNode({
|
|
|
268
273
|
}
|
|
269
274
|
);
|
|
270
275
|
}
|
|
271
|
-
|
|
272
|
-
|
|
276
|
+
const language = getLanguageId(
|
|
277
|
+
options.editor.settings.codeBlock,
|
|
278
|
+
parserOptions.language!
|
|
279
|
+
);
|
|
273
280
|
|
|
274
281
|
if (
|
|
275
|
-
language
|
|
276
|
-
language
|
|
277
|
-
|
|
278
|
-
language
|
|
282
|
+
!language ||
|
|
283
|
+
language === "text" ||
|
|
284
|
+
language === "none" ||
|
|
285
|
+
language === "plaintext" ||
|
|
286
|
+
language === "txt"
|
|
279
287
|
) {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!highlighter.getLoadedLanguages().includes(language)) {
|
|
280
292
|
return highlighter.loadLanguage(language);
|
|
281
293
|
}
|
|
282
294
|
|
|
@@ -308,10 +320,9 @@ const CodeBlockContent = createStronglyTypedTiptapNode({
|
|
|
308
320
|
const $start = state.doc.resolve(range.from);
|
|
309
321
|
const languageName = match[1].trim();
|
|
310
322
|
const attributes = {
|
|
311
|
-
language:
|
|
312
|
-
options.editor.settings.codeBlock,
|
|
313
|
-
languageName
|
|
314
|
-
),
|
|
323
|
+
language:
|
|
324
|
+
getLanguageId(options.editor.settings.codeBlock, languageName) ??
|
|
325
|
+
languageName,
|
|
315
326
|
};
|
|
316
327
|
|
|
317
328
|
if (
|
|
@@ -422,10 +433,13 @@ export const CodeBlock = createBlockSpecFromStronglyTypedTiptapNode(
|
|
|
422
433
|
defaultCodeBlockPropSchema
|
|
423
434
|
);
|
|
424
435
|
|
|
425
|
-
function getLanguageId(
|
|
426
|
-
|
|
427
|
-
|
|
436
|
+
function getLanguageId(
|
|
437
|
+
options: CodeBlockOptions,
|
|
438
|
+
languageName: string
|
|
439
|
+
): string | undefined {
|
|
440
|
+
return Object.entries(options.supportedLanguages).find(
|
|
441
|
+
([id, { aliases }]) => {
|
|
428
442
|
return aliases?.includes(languageName) || id === languageName;
|
|
429
|
-
}
|
|
430
|
-
);
|
|
443
|
+
}
|
|
444
|
+
)?.[0];
|
|
431
445
|
}
|
|
@@ -43,6 +43,11 @@ export const fileRender = (
|
|
|
43
43
|
|
|
44
44
|
export const fileParse = (element: HTMLElement) => {
|
|
45
45
|
if (element.tagName === "EMBED") {
|
|
46
|
+
// Ignore if parent figure has already been parsed.
|
|
47
|
+
if (element.closest("figure")) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
46
51
|
return parseEmbedElement(element as HTMLEmbedElement);
|
|
47
52
|
}
|
|
48
53
|
|
|
@@ -19,7 +19,11 @@ export const createResizableFileBlockWrapper = (
|
|
|
19
19
|
);
|
|
20
20
|
const wrapper = dom;
|
|
21
21
|
if (block.props.url && block.props.showPreview) {
|
|
22
|
-
|
|
22
|
+
if (block.props.previewWidth) {
|
|
23
|
+
wrapper.style.width = `${block.props.previewWidth}px`;
|
|
24
|
+
} else {
|
|
25
|
+
wrapper.style.width = "fit-content";
|
|
26
|
+
}
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
const leftResizeHandle = document.createElement("div");
|
|
@@ -87,7 +91,10 @@ export const createResizableFileBlockWrapper = (
|
|
|
87
91
|
|
|
88
92
|
// Ensures the element is not wider than the editor and not narrower than a
|
|
89
93
|
// predetermined minimum width.
|
|
90
|
-
width = Math.
|
|
94
|
+
width = Math.min(
|
|
95
|
+
Math.max(newWidth, minWidth),
|
|
96
|
+
editor.domElement?.firstElementChild?.clientWidth || Number.MAX_VALUE
|
|
97
|
+
);
|
|
91
98
|
wrapper.style.width = `${width}px`;
|
|
92
99
|
};
|
|
93
100
|
// Stops mouse movements from resizing the element and updates the block's
|
|
@@ -118,9 +118,12 @@ const HeadingBlockContent = createStronglyTypedTiptapNode({
|
|
|
118
118
|
},
|
|
119
119
|
parseHTML() {
|
|
120
120
|
return [
|
|
121
|
+
// Parse from internal HTML.
|
|
121
122
|
{
|
|
122
123
|
tag: "div[data-content-type=" + this.name + "]",
|
|
124
|
+
contentElement: ".bn-inline-content",
|
|
123
125
|
},
|
|
126
|
+
// Parse from external HTML.
|
|
124
127
|
{
|
|
125
128
|
tag: "h1",
|
|
126
129
|
attrs: { level: 1 },
|
|
@@ -37,7 +37,8 @@ export const imagePropSchema = {
|
|
|
37
37
|
},
|
|
38
38
|
// File preview width in px.
|
|
39
39
|
previewWidth: {
|
|
40
|
-
default:
|
|
40
|
+
default: undefined,
|
|
41
|
+
type: "number",
|
|
41
42
|
},
|
|
42
43
|
} satisfies PropSchema;
|
|
43
44
|
|
|
@@ -88,6 +89,11 @@ export const imageParse = (
|
|
|
88
89
|
element: HTMLElement
|
|
89
90
|
): Partial<Props<typeof imageBlockConfig.propSchema>> | undefined => {
|
|
90
91
|
if (element.tagName === "IMG") {
|
|
92
|
+
// Ignore if parent figure has already been parsed.
|
|
93
|
+
if (element.closest("figure")) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
91
97
|
return parseImageElement(element as HTMLImageElement);
|
|
92
98
|
}
|
|
93
99
|
|
|
@@ -125,7 +131,9 @@ export const imageToExternalHTML = (
|
|
|
125
131
|
image = document.createElement("img");
|
|
126
132
|
image.src = block.props.url;
|
|
127
133
|
image.alt = block.props.name || block.props.caption || "BlockNote image";
|
|
128
|
-
|
|
134
|
+
if (block.props.previewWidth) {
|
|
135
|
+
image.width = block.props.previewWidth;
|
|
136
|
+
}
|
|
129
137
|
} else {
|
|
130
138
|
image = document.createElement("a");
|
|
131
139
|
image.href = block.props.url;
|
package/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "../../../schema/index.js";
|
|
9
9
|
import { createDefaultBlockDOMOutputSpec } from "../../defaultBlockHelpers.js";
|
|
10
10
|
import { defaultProps } from "../../defaultProps.js";
|
|
11
|
+
import { getListItemContent } from "../getListItemContent.js";
|
|
11
12
|
import { handleEnter } from "../ListItemKeyboardShortcuts.js";
|
|
12
13
|
|
|
13
14
|
export const bulletListItemPropSchema = {
|
|
@@ -73,10 +74,12 @@ const BulletListItemBlockContent = createStronglyTypedTiptapNode({
|
|
|
73
74
|
|
|
74
75
|
parseHTML() {
|
|
75
76
|
return [
|
|
76
|
-
//
|
|
77
|
+
// Parse from internal HTML.
|
|
77
78
|
{
|
|
78
79
|
tag: "div[data-content-type=" + this.name + "]",
|
|
80
|
+
contentElement: ".bn-inline-content",
|
|
79
81
|
},
|
|
82
|
+
// Parse from external HTML.
|
|
80
83
|
{
|
|
81
84
|
tag: "li",
|
|
82
85
|
getAttrs: (element) => {
|
|
@@ -92,36 +95,17 @@ const BulletListItemBlockContent = createStronglyTypedTiptapNode({
|
|
|
92
95
|
|
|
93
96
|
if (
|
|
94
97
|
parent.tagName === "UL" ||
|
|
95
|
-
(parent.tagName === "DIV" && parent.parentElement
|
|
98
|
+
(parent.tagName === "DIV" && parent.parentElement?.tagName === "UL")
|
|
96
99
|
) {
|
|
97
100
|
return {};
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
return false;
|
|
101
104
|
},
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
tag: "p",
|
|
107
|
-
getAttrs: (element) => {
|
|
108
|
-
if (typeof element === "string") {
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const parent = element.parentElement;
|
|
113
|
-
|
|
114
|
-
if (parent === null) {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (parent.getAttribute("data-content-type") === "bulletListItem") {
|
|
119
|
-
return {};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return false;
|
|
123
|
-
},
|
|
124
|
-
priority: 300,
|
|
105
|
+
// As `li` elements can contain multiple paragraphs, we need to merge their contents
|
|
106
|
+
// into a single one so that ProseMirror can parse everything correctly.
|
|
107
|
+
getContent: (node, schema) =>
|
|
108
|
+
getListItemContent(node, schema, this.name),
|
|
125
109
|
node: "bulletListItem",
|
|
126
110
|
},
|
|
127
111
|
];
|
package/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "../../../schema/index.js";
|
|
13
13
|
import { createDefaultBlockDOMOutputSpec } from "../../defaultBlockHelpers.js";
|
|
14
14
|
import { defaultProps } from "../../defaultProps.js";
|
|
15
|
+
import { getListItemContent } from "../getListItemContent.js";
|
|
15
16
|
import { handleEnter } from "../ListItemKeyboardShortcuts.js";
|
|
16
17
|
|
|
17
18
|
export const checkListItemPropSchema = {
|
|
@@ -109,10 +110,12 @@ const checkListItemBlockContent = createStronglyTypedTiptapNode({
|
|
|
109
110
|
|
|
110
111
|
parseHTML() {
|
|
111
112
|
return [
|
|
113
|
+
// Parse from internal HTML.
|
|
112
114
|
{
|
|
113
115
|
tag: "div[data-content-type=" + this.name + "]",
|
|
116
|
+
contentElement: ".bn-inline-content",
|
|
114
117
|
},
|
|
115
|
-
//
|
|
118
|
+
// Parse from external HTML.
|
|
116
119
|
{
|
|
117
120
|
tag: "input",
|
|
118
121
|
getAttrs: (element) => {
|
|
@@ -120,6 +123,11 @@ const checkListItemBlockContent = createStronglyTypedTiptapNode({
|
|
|
120
123
|
return false;
|
|
121
124
|
}
|
|
122
125
|
|
|
126
|
+
// Ignore if we already parsed an ancestor list item to avoid double-parsing.
|
|
127
|
+
if (element.closest("[data-content-type]") || element.closest("li")) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
123
131
|
if ((element as HTMLInputElement).type === "checkbox") {
|
|
124
132
|
return { checked: (element as HTMLInputElement).checked };
|
|
125
133
|
}
|
|
@@ -128,7 +136,6 @@ const checkListItemBlockContent = createStronglyTypedTiptapNode({
|
|
|
128
136
|
},
|
|
129
137
|
node: "checkListItem",
|
|
130
138
|
},
|
|
131
|
-
// Container element for checkbox + label.
|
|
132
139
|
{
|
|
133
140
|
tag: "li",
|
|
134
141
|
getAttrs: (element) => {
|
|
@@ -144,7 +151,7 @@ const checkListItemBlockContent = createStronglyTypedTiptapNode({
|
|
|
144
151
|
|
|
145
152
|
if (
|
|
146
153
|
parent.tagName === "UL" ||
|
|
147
|
-
(parent.tagName === "DIV" && parent.parentElement
|
|
154
|
+
(parent.tagName === "DIV" && parent.parentElement?.tagName === "UL")
|
|
148
155
|
) {
|
|
149
156
|
const checkbox =
|
|
150
157
|
(element.querySelector(
|
|
@@ -160,6 +167,10 @@ const checkListItemBlockContent = createStronglyTypedTiptapNode({
|
|
|
160
167
|
|
|
161
168
|
return false;
|
|
162
169
|
},
|
|
170
|
+
// As `li` elements can contain multiple paragraphs, we need to merge their contents
|
|
171
|
+
// into a single one so that ProseMirror can parse everything correctly.
|
|
172
|
+
getContent: (node, schema) =>
|
|
173
|
+
getListItemContent(node, schema, this.name),
|
|
163
174
|
node: "checkListItem",
|
|
164
175
|
},
|
|
165
176
|
];
|
package/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "../../../schema/index.js";
|
|
10
10
|
import { createDefaultBlockDOMOutputSpec } from "../../defaultBlockHelpers.js";
|
|
11
11
|
import { defaultProps } from "../../defaultProps.js";
|
|
12
|
+
import { getListItemContent } from "../getListItemContent.js";
|
|
12
13
|
import { handleEnter } from "../ListItemKeyboardShortcuts.js";
|
|
13
14
|
import { NumberedListIndexingPlugin } from "./NumberedListIndexingPlugin.js";
|
|
14
15
|
|
|
@@ -101,11 +102,12 @@ const NumberedListItemBlockContent = createStronglyTypedTiptapNode({
|
|
|
101
102
|
|
|
102
103
|
parseHTML() {
|
|
103
104
|
return [
|
|
105
|
+
// Parse from internal HTML.
|
|
104
106
|
{
|
|
105
107
|
tag: "div[data-content-type=" + this.name + "]",
|
|
108
|
+
contentElement: ".bn-inline-content",
|
|
106
109
|
},
|
|
107
|
-
//
|
|
108
|
-
// (e.g.: when pasting from other apps)
|
|
110
|
+
// Parse from external HTML.
|
|
109
111
|
{
|
|
110
112
|
tag: "li",
|
|
111
113
|
getAttrs: (element) => {
|
|
@@ -121,7 +123,7 @@ const NumberedListItemBlockContent = createStronglyTypedTiptapNode({
|
|
|
121
123
|
|
|
122
124
|
if (
|
|
123
125
|
parent.tagName === "OL" ||
|
|
124
|
-
(parent.tagName === "DIV" && parent.parentElement
|
|
126
|
+
(parent.tagName === "DIV" && parent.parentElement?.tagName === "OL")
|
|
125
127
|
) {
|
|
126
128
|
const startIndex =
|
|
127
129
|
parseInt(parent.getAttribute("start") || "1") || 1;
|
|
@@ -137,29 +139,10 @@ const NumberedListItemBlockContent = createStronglyTypedTiptapNode({
|
|
|
137
139
|
|
|
138
140
|
return false;
|
|
139
141
|
},
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
{
|
|
145
|
-
tag: "p",
|
|
146
|
-
getAttrs: (element) => {
|
|
147
|
-
if (typeof element === "string") {
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const parent = element.parentElement;
|
|
152
|
-
|
|
153
|
-
if (parent === null) {
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (parent.getAttribute("data-content-type") === "numberedListItem") {
|
|
158
|
-
return {};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return false;
|
|
162
|
-
},
|
|
142
|
+
// As `li` elements can contain multiple paragraphs, we need to merge their contents
|
|
143
|
+
// into a single one so that ProseMirror can parse everything correctly.
|
|
144
|
+
getContent: (node, schema) =>
|
|
145
|
+
getListItemContent(node, schema, this.name),
|
|
163
146
|
priority: 300,
|
|
164
147
|
node: "numberedListItem",
|
|
165
148
|
},
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { DOMParser, Fragment, Schema } from "prosemirror-model";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This function is used to parse the content of a list item external HTML node.
|
|
5
|
+
*
|
|
6
|
+
* Due to a change in how prosemirror-model handles parsing elements, we have additional flexibility in how we can "fit" content into a list item.
|
|
7
|
+
*
|
|
8
|
+
* We've decided to take an approach that is similar to Notion. The core rules of the algorithm are:
|
|
9
|
+
*
|
|
10
|
+
* - If the first child of an `li` has ONLY text content, take the text content, and flatten it into the list item. Subsequent siblings are carried over as is, as children of the list item.
|
|
11
|
+
* - e.g. `<li><h1>Hello</h1><p>World</p></li> -> <li>Hello<blockGroup><blockContainer><p>World</p></blockContainer></blockGroup></li>`
|
|
12
|
+
* - Else, take the content and insert it as children instead.
|
|
13
|
+
* - e.g. `<li><img src="url" /></li> -> <li><p></p><blockGroup><blockContainer><img src="url" /></blockContainer></blockGroup></li>`
|
|
14
|
+
*
|
|
15
|
+
* This ensures that a list item's content is always valid ProseMirror content. Smoothing over differences between how external HTML may be rendered, and how ProseMirror expects content to be structured.
|
|
16
|
+
*/
|
|
17
|
+
export function getListItemContent(
|
|
18
|
+
/**
|
|
19
|
+
* The `li` element to parse.
|
|
20
|
+
*/
|
|
21
|
+
_node: Node,
|
|
22
|
+
/**
|
|
23
|
+
* The schema to use for parsing.
|
|
24
|
+
*/
|
|
25
|
+
schema: Schema,
|
|
26
|
+
/**
|
|
27
|
+
* The name of the list item node.
|
|
28
|
+
*/
|
|
29
|
+
name: string
|
|
30
|
+
): Fragment {
|
|
31
|
+
/**
|
|
32
|
+
* To actually implement this algorithm, we need to leverage ProseMirror's "fitting" algorithm.
|
|
33
|
+
* Where, if content is parsed which doesn't fit into the current node, it will be moved into the parent node.
|
|
34
|
+
*
|
|
35
|
+
* This allows us to parse multiple pieces of content from within the list item (even though it normally would not match the list item's schema) and "throw" the excess content into the list item's children.
|
|
36
|
+
*
|
|
37
|
+
* The expected return value is a `Fragment` which contains the list item's content as the first element, and the children wrapped in a blockGroup node. Like so:
|
|
38
|
+
* ```
|
|
39
|
+
* Fragment<[Node<Text>, Node<BlockGroup<Node<BlockContainer<any>>>>]>
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
const parser = DOMParser.fromSchema(schema);
|
|
43
|
+
|
|
44
|
+
// TODO: This will be unnecessary in the future: https://github.com/ProseMirror/prosemirror-model/commit/166188d4f9db96eb86fb7de62e72049c86c9dd79
|
|
45
|
+
const node = _node as HTMLElement;
|
|
46
|
+
|
|
47
|
+
// Move the `li` element's content into a new `div` element
|
|
48
|
+
// This is a hacky workaround to not re-trigger list item parsing,
|
|
49
|
+
// when we are looking to understand what the list item's content actually is, in terms of the schema.
|
|
50
|
+
const clonedNodeDiv = document.createElement("div");
|
|
51
|
+
// Mark the `div` element as a `blockGroup` to make the parsing easier.
|
|
52
|
+
clonedNodeDiv.setAttribute("data-node-type", "blockGroup");
|
|
53
|
+
// Clone all children of the `li` element into the new `div` element
|
|
54
|
+
for (const child of Array.from(node.childNodes)) {
|
|
55
|
+
clonedNodeDiv.appendChild(child.cloneNode(true));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Parses children of the `li` element into a `blockGroup` with `blockContainer` node children
|
|
59
|
+
// This is the structure of list item children, so parsing into this structure allows for
|
|
60
|
+
// easy separation of list item content from child list item content.
|
|
61
|
+
let blockGroupNode = parser.parse(clonedNodeDiv, {
|
|
62
|
+
topNode: schema.nodes.blockGroup.create(),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// There is an edge case where a list item's content may contain a `<input>` element.
|
|
66
|
+
// Causing it to be recognized as a `checkListItem`.
|
|
67
|
+
// We want to skip this, and just parse the list item's content as is.
|
|
68
|
+
if (blockGroupNode.firstChild?.firstChild?.type.name === "checkListItem") {
|
|
69
|
+
// We skip the first child, by cutting it out of the `blockGroup` node.
|
|
70
|
+
// and continuing with the rest of the algorithm.
|
|
71
|
+
blockGroupNode = blockGroupNode.copy(
|
|
72
|
+
blockGroupNode.content.cut(
|
|
73
|
+
blockGroupNode.firstChild.firstChild.nodeSize + 2
|
|
74
|
+
)
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Structure above is `blockGroup<blockContainer<any>[]>`
|
|
79
|
+
// We want to extract the first `blockContainer` node's content, and see if it is a text block.
|
|
80
|
+
const listItemsFirstChild = blockGroupNode.firstChild?.firstChild;
|
|
81
|
+
|
|
82
|
+
// If the first node is not a text block, then it's first child is not compatible with the list item node.
|
|
83
|
+
if (!listItemsFirstChild?.isTextblock) {
|
|
84
|
+
// So, we do not try inserting anything into the list item, and instead return anything we found as children for the list item.
|
|
85
|
+
return Fragment.from(blockGroupNode);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// If it is a text block, then we know it only contains text content.
|
|
89
|
+
// So, we extract it, and insert its content into the `listItemNode`.
|
|
90
|
+
// The remaining nodes in the `blockGroup` stay in-place.
|
|
91
|
+
const listItemNode = schema.nodes[name].create(
|
|
92
|
+
{},
|
|
93
|
+
listItemsFirstChild.content
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// We have `blockGroup<listItemsFirstChild, ...blockContainer<any>[]>`
|
|
97
|
+
// We want to extract out the rest of the nodes as `<...blockContainer<any>[]>`
|
|
98
|
+
const remainingListItemChildren = blockGroupNode.content.cut(
|
|
99
|
+
// +2 for the `blockGroup` node's start and end markers
|
|
100
|
+
listItemsFirstChild.nodeSize + 2
|
|
101
|
+
);
|
|
102
|
+
const hasRemainingListItemChildren = remainingListItemChildren.size > 0;
|
|
103
|
+
|
|
104
|
+
if (hasRemainingListItemChildren) {
|
|
105
|
+
// Copy the remaining list item children back into the `blockGroup` node.
|
|
106
|
+
// This will make it back into: `blockGroup<...blockContainer<any>[]>`
|
|
107
|
+
const listItemsChildren = blockGroupNode.copy(remainingListItemChildren);
|
|
108
|
+
|
|
109
|
+
// Return the `listItem` node's content, then add the parsed children after to be lifted out by ProseMirror "fitting" algorithm.
|
|
110
|
+
return listItemNode.content.addToEnd(listItemsChildren);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Otherwise, just return the `listItem` node's content.
|
|
114
|
+
return listItemNode.content;
|
|
115
|
+
}
|