@blocknote/core 0.15.7 → 0.15.10

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.
Files changed (82) hide show
  1. package/dist/blocknote.js +1304 -1194
  2. package/dist/blocknote.js.map +1 -1
  3. package/dist/blocknote.umd.cjs +5 -5
  4. package/dist/blocknote.umd.cjs.map +1 -1
  5. package/dist/webpack-stats.json +1 -1
  6. package/package.json +23 -23
  7. package/src/api/exporters/copyExtension.ts +48 -33
  8. package/src/api/exporters/html/__snapshots__/complex/misc/external.html +1 -1
  9. package/src/api/exporters/html/__snapshots__/customParagraph/styled/external.html +1 -1
  10. package/src/api/exporters/html/__snapshots__/file/basic/external.html +1 -1
  11. package/src/api/exporters/html/__snapshots__/file/nested/external.html +1 -1
  12. package/src/api/exporters/html/__snapshots__/file/noCaption/external.html +1 -1
  13. package/src/api/exporters/html/__snapshots__/file/noName/external.html +1 -1
  14. package/src/api/exporters/html/__snapshots__/fontSize/basic/external.html +1 -1
  15. package/src/api/exporters/html/__snapshots__/hardbreak/basic/external.html +1 -1
  16. package/src/api/exporters/html/__snapshots__/hardbreak/between-links/external.html +1 -1
  17. package/src/api/exporters/html/__snapshots__/hardbreak/end/external.html +1 -1
  18. package/src/api/exporters/html/__snapshots__/hardbreak/link/external.html +1 -1
  19. package/src/api/exporters/html/__snapshots__/hardbreak/multiple/external.html +1 -1
  20. package/src/api/exporters/html/__snapshots__/hardbreak/only/external.html +1 -1
  21. package/src/api/exporters/html/__snapshots__/hardbreak/start/external.html +1 -1
  22. package/src/api/exporters/html/__snapshots__/hardbreak/styles/external.html +1 -1
  23. package/src/api/exporters/html/__snapshots__/image/basic/external.html +1 -1
  24. package/src/api/exporters/html/__snapshots__/image/nested/external.html +1 -1
  25. package/src/api/exporters/html/__snapshots__/image/noCaption/external.html +1 -1
  26. package/src/api/exporters/html/__snapshots__/image/noName/external.html +1 -1
  27. package/src/api/exporters/html/__snapshots__/image/noPreview/external.html +1 -1
  28. package/src/api/exporters/html/__snapshots__/link/adjacent/external.html +1 -1
  29. package/src/api/exporters/html/__snapshots__/link/basic/external.html +1 -1
  30. package/src/api/exporters/html/__snapshots__/link/styled/external.html +1 -1
  31. package/src/api/exporters/html/__snapshots__/mention/basic/external.html +1 -1
  32. package/src/api/exporters/html/__snapshots__/paragraph/basic/external.html +1 -1
  33. package/src/api/exporters/html/__snapshots__/paragraph/empty/external.html +1 -1
  34. package/src/api/exporters/html/__snapshots__/paragraph/lineBreaks/external.html +1 -1
  35. package/src/api/exporters/html/__snapshots__/paragraph/nested/external.html +1 -1
  36. package/src/api/exporters/html/__snapshots__/paragraph/styled/external.html +1 -1
  37. package/src/api/exporters/html/__snapshots__/simpleCustomParagraph/basic/external.html +1 -1
  38. package/src/api/exporters/html/__snapshots__/simpleCustomParagraph/nested/external.html +1 -1
  39. package/src/api/exporters/html/__snapshots__/simpleCustomParagraph/styled/external.html +1 -1
  40. package/src/api/exporters/html/__snapshots__/simpleImage/basic/external.html +1 -1
  41. package/src/api/exporters/html/__snapshots__/simpleImage/button/external.html +1 -1
  42. package/src/api/exporters/html/__snapshots__/simpleImage/nested/external.html +1 -1
  43. package/src/api/exporters/html/__snapshots__/simpleImage/noCaption/external.html +1 -1
  44. package/src/api/exporters/html/__snapshots__/simpleImage/noName/external.html +1 -1
  45. package/src/api/exporters/html/__snapshots__/simpleImage/noPreview/external.html +1 -1
  46. package/src/api/exporters/html/__snapshots__/small/basic/external.html +1 -1
  47. package/src/api/exporters/html/__snapshots__/tag/basic/external.html +1 -1
  48. package/src/api/exporters/html/__snapshots_fragment_edge_cases__/selectionLeavesBlockChildren.html +1 -1
  49. package/src/api/exporters/html/__snapshots_fragment_edge_cases__/selectionSpansBlocksChildren.html +1 -1
  50. package/src/api/exporters/html/__snapshots_fragment_edge_cases__/selectionWithinBlockChildren.html +1 -1
  51. package/src/api/exporters/html/util/simplifyBlocksRehypePlugin.ts +51 -2
  52. package/src/api/parsers/handleFileInsertion.ts +30 -17
  53. package/src/blocks/AudioBlockContent/AudioBlockContent.ts +23 -47
  54. package/src/blocks/FileBlockContent/FileBlockContent.ts +4 -22
  55. package/src/blocks/FileBlockContent/fileBlockHelpers.ts +72 -1
  56. package/src/blocks/ImageBlockContent/ImageBlockContent.ts +35 -61
  57. package/src/blocks/VideoBlockContent/VideoBlockContent.ts +33 -58
  58. package/src/editor/BlockNoteEditor.test.ts +13 -0
  59. package/src/editor/BlockNoteEditor.ts +71 -6
  60. package/src/editor/BlockNoteExtensions.ts +4 -2
  61. package/src/editor/BlockNoteTipTapEditor.ts +4 -1
  62. package/src/extensions/FilePanel/FilePanelPlugin.ts +10 -6
  63. package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +0 -1
  64. package/src/extensions/SideMenu/SideMenuPlugin.ts +22 -11
  65. package/src/extensions/SuggestionMenu/SuggestionPlugin.ts +6 -3
  66. package/src/extensions/TableHandles/TableHandlesPlugin.ts +5 -1
  67. package/src/extensions/UniqueID/UniqueID.ts +15 -4
  68. package/src/pm-nodes/BlockContainer.ts +1 -2
  69. package/src/schema/blocks/createSpec.ts +31 -2
  70. package/src/schema/blocks/types.ts +2 -0
  71. package/src/schema/inlineContent/createSpec.ts +58 -6
  72. package/types/src/blocks/AudioBlockContent/AudioBlockContent.d.ts +2 -2
  73. package/types/src/blocks/FileBlockContent/FileBlockContent.d.ts +2 -2
  74. package/types/src/blocks/FileBlockContent/fileBlockHelpers.d.ts +11 -1
  75. package/types/src/blocks/ImageBlockContent/ImageBlockContent.d.ts +2 -2
  76. package/types/src/blocks/VideoBlockContent/VideoBlockContent.d.ts +2 -2
  77. package/types/src/editor/BlockNoteEditor.d.ts +23 -4
  78. package/types/src/editor/BlockNoteExtensions.d.ts +1 -0
  79. package/types/src/extensions/FilePanel/FilePanelPlugin.d.ts +1 -1
  80. package/types/src/schema/blocks/createSpec.d.ts +3 -0
  81. package/types/src/schema/blocks/types.d.ts +2 -0
  82. package/types/src/schema/inlineContent/createSpec.d.ts +5 -4
@@ -106,28 +106,22 @@ export async function handleFileInsertion<
106
106
 
107
107
  const file = items[i].getAsFile();
108
108
  if (file) {
109
- const updateData = await editor.uploadFile(file);
109
+ const fileBlock = {
110
+ type: fileBlockType,
111
+ props: {
112
+ name: file.name,
113
+ },
114
+ } as PartialBlock<BSchema, I, S>;
110
115
 
111
- const fileBlock =
112
- typeof updateData === "string"
113
- ? ({
114
- type: fileBlockType,
115
- props: {
116
- name: file.name,
117
- url: updateData,
118
- },
119
- } as PartialBlock<BSchema, I, S>)
120
- : { type: fileBlockType, ...updateData };
116
+ let insertedBlockId: string | undefined = undefined;
121
117
 
122
118
  if (event.type === "paste") {
123
- editor.insertBlocks(
119
+ insertedBlockId = editor.insertBlocks(
124
120
  [fileBlock],
125
121
  editor.getTextCursorPosition().block,
126
122
  "after"
127
- );
128
- }
129
-
130
- if (event.type === "drop") {
123
+ )[0].id;
124
+ } else if (event.type === "drop") {
131
125
  const coords = {
132
126
  left: (event as DragEvent).clientX,
133
127
  top: (event as DragEvent).clientY,
@@ -143,8 +137,27 @@ export async function handleFileInsertion<
143
137
  pos.pos
144
138
  );
145
139
 
146
- editor.insertBlocks([fileBlock], blockInfo.id, "after");
140
+ insertedBlockId = editor.insertBlocks(
141
+ [fileBlock],
142
+ blockInfo.id,
143
+ "after"
144
+ )[0].id;
145
+ } else {
146
+ return;
147
147
  }
148
+
149
+ const updateData = await editor.uploadFile(file, insertedBlockId);
150
+
151
+ const updatedFileBlock =
152
+ typeof updateData === "string"
153
+ ? ({
154
+ props: {
155
+ url: updateData,
156
+ },
157
+ } as PartialBlock<BSchema, I, S>)
158
+ : { ...updateData };
159
+
160
+ editor.updateBlock(insertedBlockId, updatedFileBlock);
148
161
  }
149
162
  }
150
163
  }
@@ -9,10 +9,9 @@ import {
9
9
  import { defaultProps } from "../defaultProps";
10
10
 
11
11
  import {
12
- createAddFileButton,
13
- createDefaultFilePreview,
14
12
  createFigureWithCaption,
15
13
  createFileAndCaptionWrapper,
14
+ createFileBlockWrapper,
16
15
  createLinkWithCaption,
17
16
  parseFigureElement,
18
17
  } from "../FileBlockContent/fileBlockHelpers";
@@ -50,51 +49,28 @@ export const audioRender = (
50
49
  block: BlockFromConfig<typeof audioBlockConfig, any, any>,
51
50
  editor: BlockNoteEditor<any, any, any>
52
51
  ) => {
53
- const wrapper = document.createElement("div");
54
- wrapper.className = "bn-file-block-content-wrapper";
55
-
56
- if (block.props.url === "") {
57
- const fileBlockAudioIcon = document.createElement("div");
58
- fileBlockAudioIcon.innerHTML =
59
- '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2 16.0001H5.88889L11.1834 20.3319C11.2727 20.405 11.3846 20.4449 11.5 20.4449C11.7761 20.4449 12 20.2211 12 19.9449V4.05519C12 3.93977 11.9601 3.8279 11.887 3.73857C11.7121 3.52485 11.3971 3.49335 11.1834 3.66821L5.88889 8.00007H2C1.44772 8.00007 1 8.44778 1 9.00007V15.0001C1 15.5524 1.44772 16.0001 2 16.0001ZM23 12C23 15.292 21.5539 18.2463 19.2622 20.2622L17.8445 18.8444C19.7758 17.1937 21 14.7398 21 12C21 9.26016 19.7758 6.80629 17.8445 5.15557L19.2622 3.73779C21.5539 5.75368 23 8.70795 23 12ZM18 12C18 10.0883 17.106 8.38548 15.7133 7.28673L14.2842 8.71584C15.3213 9.43855 16 10.64 16 12C16 13.36 15.3213 14.5614 14.2842 15.2841L15.7133 16.7132C17.106 15.6145 18 13.9116 18 12Z"></path></svg>';
60
- const addAudioButton = createAddFileButton(
61
- block,
62
- editor,
63
- editor.dictionary.file_blocks.audio.add_button_text,
64
- fileBlockAudioIcon.firstElementChild as HTMLElement
65
- );
66
- wrapper.appendChild(addAudioButton.dom);
67
-
68
- return {
69
- dom: wrapper,
70
- destroy: () => {
71
- addAudioButton?.destroy?.();
72
- },
73
- };
74
- } else if (!block.props.showPreview) {
75
- const file = createDefaultFilePreview(block).dom;
76
- const element = createFileAndCaptionWrapper(block, file);
77
-
78
- return {
79
- dom: element.dom,
80
- };
81
- } else {
82
- const audio = document.createElement("audio");
83
- audio.className = "bn-audio";
84
- editor.resolveFileUrl(block.props.url).then((downloadUrl) => {
85
- audio.src = downloadUrl;
86
- });
87
- audio.controls = true;
88
- audio.contentEditable = "false";
89
- audio.draggable = false;
90
-
91
- const element = createFileAndCaptionWrapper(block, audio);
92
- wrapper.appendChild(element.dom);
93
-
94
- return {
95
- dom: wrapper,
96
- };
97
- }
52
+ const icon = document.createElement("div");
53
+ icon.innerHTML =
54
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2 16.0001H5.88889L11.1834 20.3319C11.2727 20.405 11.3846 20.4449 11.5 20.4449C11.7761 20.4449 12 20.2211 12 19.9449V4.05519C12 3.93977 11.9601 3.8279 11.887 3.73857C11.7121 3.52485 11.3971 3.49335 11.1834 3.66821L5.88889 8.00007H2C1.44772 8.00007 1 8.44778 1 9.00007V15.0001C1 15.5524 1.44772 16.0001 2 16.0001ZM23 12C23 15.292 21.5539 18.2463 19.2622 20.2622L17.8445 18.8444C19.7758 17.1937 21 14.7398 21 12C21 9.26016 19.7758 6.80629 17.8445 5.15557L19.2622 3.73779C21.5539 5.75368 23 8.70795 23 12ZM18 12C18 10.0883 17.106 8.38548 15.7133 7.28673L14.2842 8.71584C15.3213 9.43855 16 10.64 16 12C16 13.36 15.3213 14.5614 14.2842 15.2841L15.7133 16.7132C17.106 15.6145 18 13.9116 18 12Z"></path></svg>';
55
+
56
+ const audio = document.createElement("audio");
57
+ audio.className = "bn-audio";
58
+ editor.resolveFileUrl(block.props.url).then((downloadUrl) => {
59
+ audio.src = downloadUrl;
60
+ });
61
+ audio.controls = true;
62
+ audio.contentEditable = "false";
63
+ audio.draggable = false;
64
+
65
+ const element = createFileAndCaptionWrapper(block, audio);
66
+
67
+ return createFileBlockWrapper(
68
+ block,
69
+ editor,
70
+ element,
71
+ editor.dictionary.file_blocks.audio.add_button_text,
72
+ icon.firstElementChild as HTMLElement
73
+ );
98
74
  };
99
75
 
100
76
  export const audioParse = (
@@ -7,9 +7,9 @@ import {
7
7
  } from "../../schema";
8
8
  import { defaultProps } from "../defaultProps";
9
9
  import {
10
- createAddFileButton,
11
10
  createDefaultFilePreview,
12
11
  createFileAndCaptionWrapper,
12
+ createFileBlockWrapper,
13
13
  createLinkWithCaption,
14
14
  parseEmbedElement,
15
15
  parseFigureElement,
@@ -42,28 +42,10 @@ export const fileRender = (
42
42
  block: BlockFromConfig<typeof fileBlockConfig, any, any>,
43
43
  editor: BlockNoteEditor<any, any, any>
44
44
  ) => {
45
- // Wrapper element to set the file alignment, contains both file/file
46
- // upload dashboard and caption.
47
- const wrapper = document.createElement("div");
48
- wrapper.className = "bn-file-block-content-wrapper";
45
+ const file = createDefaultFilePreview(block).dom;
46
+ const element = createFileAndCaptionWrapper(block, file);
49
47
 
50
- if (block.props.url === "") {
51
- const addFileButton = createAddFileButton(block, editor);
52
- wrapper.appendChild(addFileButton.dom);
53
-
54
- return {
55
- dom: wrapper,
56
- destroy: addFileButton.destroy,
57
- };
58
- } else {
59
- const file = createDefaultFilePreview(block).dom;
60
- const element = createFileAndCaptionWrapper(block, file);
61
- wrapper.appendChild(element.dom);
62
-
63
- return {
64
- dom: wrapper,
65
- };
66
- }
48
+ return createFileBlockWrapper(block, editor, element);
67
49
  };
68
50
 
69
51
  export const fileParse = (element: HTMLElement) => {
@@ -1,5 +1,76 @@
1
1
  import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
2
- import { BlockFromConfig, FileBlockConfig } from "../../schema";
2
+ import {
3
+ BlockFromConfig,
4
+ BlockSchemaWithBlock,
5
+ FileBlockConfig,
6
+ } from "../../schema";
7
+
8
+ export const createFileBlockWrapper = (
9
+ block: BlockFromConfig<FileBlockConfig, any, any>,
10
+ editor: BlockNoteEditor<
11
+ BlockSchemaWithBlock<FileBlockConfig["type"], FileBlockConfig>,
12
+ any,
13
+ any
14
+ >,
15
+ // TODO: Maybe make optional for default preview
16
+ element: { dom: HTMLElement; destroy?: () => void },
17
+ buttonText?: string,
18
+ buttonIcon?: HTMLElement
19
+ ) => {
20
+ const wrapper = document.createElement("div");
21
+ wrapper.className = "bn-file-block-content-wrapper";
22
+
23
+ if (block.props.url === "") {
24
+ const addFileButton = createAddFileButton(
25
+ block,
26
+ editor,
27
+ buttonText,
28
+ buttonIcon
29
+ );
30
+ wrapper.appendChild(addFileButton.dom);
31
+
32
+ const loading = document.createElement("div");
33
+ loading.className = "bn-file-loading-preview";
34
+ loading.textContent = "Loading...";
35
+
36
+ const destroyUploadStartHandler = editor.onUploadStart((blockId) => {
37
+ if (blockId === block.id) {
38
+ wrapper.removeChild(addFileButton.dom);
39
+ wrapper.appendChild(loading);
40
+ }
41
+ });
42
+ const destroyUploadEndHandler = editor.onUploadEnd((blockId) => {
43
+ if (blockId === block.id) {
44
+ wrapper.removeChild(loading);
45
+ wrapper.appendChild(addFileButton.dom);
46
+ }
47
+ });
48
+
49
+ return {
50
+ dom: wrapper,
51
+ destroy: () => {
52
+ addFileButton.destroy?.();
53
+ destroyUploadStartHandler();
54
+ destroyUploadEndHandler();
55
+ },
56
+ };
57
+ } else if (block.props.showPreview === false) {
58
+ // TODO: Not using the wrapper element here?
59
+ const file = createDefaultFilePreview(block).dom;
60
+ const element = createFileAndCaptionWrapper(block, file);
61
+
62
+ return {
63
+ dom: element.dom,
64
+ };
65
+ } else {
66
+ wrapper.appendChild(element.dom);
67
+
68
+ return {
69
+ dom: wrapper,
70
+ destroy: element.destroy,
71
+ };
72
+ }
73
+ };
3
74
 
4
75
  // Default file preview, displaying a file icon and file name.
5
76
  export const createDefaultFilePreview = (
@@ -7,12 +7,10 @@ import {
7
7
  PropSchema,
8
8
  } from "../../schema";
9
9
  import { defaultProps } from "../defaultProps";
10
-
11
10
  import {
12
- createAddFileButton,
13
- createDefaultFilePreview,
14
11
  createFigureWithCaption,
15
12
  createFileAndCaptionWrapper,
13
+ createFileBlockWrapper,
16
14
  createLinkWithCaption,
17
15
  createResizeHandlesWrapper,
18
16
  parseFigureElement,
@@ -56,64 +54,40 @@ export const imageRender = (
56
54
  block: BlockFromConfig<typeof imageBlockConfig, any, any>,
57
55
  editor: BlockNoteEditor<any, any, any>
58
56
  ) => {
59
- const wrapper = document.createElement("div");
60
- wrapper.className = "bn-file-block-content-wrapper";
61
-
62
- if (block.props.url === "") {
63
- const fileBlockImageIcon = document.createElement("div");
64
- fileBlockImageIcon.innerHTML =
65
- '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5 11.1005L7 9.1005L12.5 14.6005L16 11.1005L19 14.1005V5H5V11.1005ZM4 3H20C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3ZM15.5 10C14.6716 10 14 9.32843 14 8.5C14 7.67157 14.6716 7 15.5 7C16.3284 7 17 7.67157 17 8.5C17 9.32843 16.3284 10 15.5 10Z"></path></svg>';
66
- const addImageButton = createAddFileButton(
67
- block,
68
- editor,
69
- editor.dictionary.file_blocks.image.add_button_text,
70
- fileBlockImageIcon.firstElementChild as HTMLElement
71
- );
72
- wrapper.appendChild(addImageButton.dom);
73
-
74
- return {
75
- dom: wrapper,
76
- destroy: () => {
77
- addImageButton?.destroy?.();
78
- },
79
- };
80
- } else if (!block.props.showPreview) {
81
- const file = createDefaultFilePreview(block).dom;
82
- const element = createFileAndCaptionWrapper(block, file);
83
-
84
- return {
85
- dom: element.dom,
86
- };
87
- } else {
88
- const image = document.createElement("img");
89
- image.className = "bn-visual-media";
90
- editor.resolveFileUrl(block.props.url).then((downloadUrl) => {
91
- image.src = downloadUrl;
92
- });
93
- image.alt = block.props.name || block.props.caption || "BlockNote image";
94
- image.contentEditable = "false";
95
- image.draggable = false;
96
- image.width = Math.min(
97
- block.props.previewWidth,
98
- editor.domElement.firstElementChild!.clientWidth
99
- );
100
-
101
- const file = createResizeHandlesWrapper(
102
- block,
103
- editor,
104
- image,
105
- () => image.width,
106
- (width) => (image.width = width)
107
- );
108
-
109
- const element = createFileAndCaptionWrapper(block, file.dom);
110
- wrapper.appendChild(element.dom);
111
-
112
- return {
113
- dom: wrapper,
114
- destroy: file.destroy,
115
- };
116
- }
57
+ const icon = document.createElement("div");
58
+ icon.innerHTML =
59
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5 11.1005L7 9.1005L12.5 14.6005L16 11.1005L19 14.1005V5H5V11.1005ZM4 3H20C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3ZM15.5 10C14.6716 10 14 9.32843 14 8.5C14 7.67157 14.6716 7 15.5 7C16.3284 7 17 7.67157 17 8.5C17 9.32843 16.3284 10 15.5 10Z"></path></svg>';
60
+
61
+ const image = document.createElement("img");
62
+ image.className = "bn-visual-media";
63
+ editor.resolveFileUrl(block.props.url).then((downloadUrl) => {
64
+ image.src = downloadUrl;
65
+ });
66
+ image.alt = block.props.name || block.props.caption || "BlockNote image";
67
+ image.contentEditable = "false";
68
+ image.draggable = false;
69
+ image.width = Math.min(
70
+ block.props.previewWidth,
71
+ editor.domElement.firstElementChild!.clientWidth
72
+ );
73
+
74
+ const file = createResizeHandlesWrapper(
75
+ block,
76
+ editor,
77
+ image,
78
+ () => image.width,
79
+ (width) => (image.width = width)
80
+ );
81
+
82
+ const element = createFileAndCaptionWrapper(block, file.dom);
83
+
84
+ return createFileBlockWrapper(
85
+ block,
86
+ editor,
87
+ element,
88
+ editor.dictionary.file_blocks.image.add_button_text,
89
+ icon.firstElementChild as HTMLElement
90
+ );
117
91
  };
118
92
 
119
93
  export const imageParse = (
@@ -9,10 +9,9 @@ import {
9
9
  import { defaultProps } from "../defaultProps";
10
10
 
11
11
  import {
12
- createAddFileButton,
13
- createDefaultFilePreview,
14
12
  createFigureWithCaption,
15
13
  createFileAndCaptionWrapper,
14
+ createFileBlockWrapper,
16
15
  createLinkWithCaption,
17
16
  createResizeHandlesWrapper,
18
17
  parseFigureElement,
@@ -56,62 +55,38 @@ export const videoRender = (
56
55
  block: BlockFromConfig<typeof videoBlockConfig, any, any>,
57
56
  editor: BlockNoteEditor<any, any, any>
58
57
  ) => {
59
- const wrapper = document.createElement("div");
60
- wrapper.className = "bn-file-block-content-wrapper";
61
-
62
- if (block.props.url === "") {
63
- const fileBlockVideoIcon = document.createElement("div");
64
- fileBlockVideoIcon.innerHTML =
65
- '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2 3.9934C2 3.44476 2.45531 3 2.9918 3H21.0082C21.556 3 22 3.44495 22 3.9934V20.0066C22 20.5552 21.5447 21 21.0082 21H2.9918C2.44405 21 2 20.5551 2 20.0066V3.9934ZM8 5V19H16V5H8ZM4 5V7H6V5H4ZM18 5V7H20V5H18ZM4 9V11H6V9H4ZM18 9V11H20V9H18ZM4 13V15H6V13H4ZM18 13V15H20V13H18ZM4 17V19H6V17H4ZM18 17V19H20V17H18Z"></path></svg>';
66
- const addVideoButton = createAddFileButton(
67
- block,
68
- editor,
69
- editor.dictionary.file_blocks.video.add_button_text,
70
- fileBlockVideoIcon.firstElementChild as HTMLElement
71
- );
72
- wrapper.appendChild(addVideoButton.dom);
73
-
74
- return {
75
- dom: wrapper,
76
- destroy: () => {
77
- addVideoButton?.destroy?.();
78
- },
79
- };
80
- } else if (!block.props.showPreview) {
81
- const file = createDefaultFilePreview(block).dom;
82
- const element = createFileAndCaptionWrapper(block, file);
83
-
84
- return {
85
- dom: element.dom,
86
- };
87
- } else {
88
- const video = document.createElement("video");
89
- video.className = "bn-visual-media";
90
- video.src = block.props.url;
91
- video.controls = true;
92
- video.contentEditable = "false";
93
- video.draggable = false;
94
- video.width = Math.min(
95
- block.props.previewWidth,
96
- editor.domElement.firstElementChild!.clientWidth
97
- );
98
-
99
- const file = createResizeHandlesWrapper(
100
- block,
101
- editor,
102
- video,
103
- () => video.width,
104
- (width) => (video.width = width)
105
- );
106
-
107
- const element = createFileAndCaptionWrapper(block, file.dom);
108
- wrapper.appendChild(element.dom);
109
-
110
- return {
111
- dom: wrapper,
112
- destroy: file.destroy,
113
- };
114
- }
58
+ const icon = document.createElement("div");
59
+ icon.innerHTML =
60
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2 3.9934C2 3.44476 2.45531 3 2.9918 3H21.0082C21.556 3 22 3.44495 22 3.9934V20.0066C22 20.5552 21.5447 21 21.0082 21H2.9918C2.44405 21 2 20.5551 2 20.0066V3.9934ZM8 5V19H16V5H8ZM4 5V7H6V5H4ZM18 5V7H20V5H18ZM4 9V11H6V9H4ZM18 9V11H20V9H18ZM4 13V15H6V13H4ZM18 13V15H20V13H18ZM4 17V19H6V17H4ZM18 17V19H20V17H18Z"></path></svg>';
61
+
62
+ const video = document.createElement("video");
63
+ video.className = "bn-visual-media";
64
+ video.src = block.props.url;
65
+ video.controls = true;
66
+ video.contentEditable = "false";
67
+ video.draggable = false;
68
+ video.width = Math.min(
69
+ block.props.previewWidth,
70
+ editor.domElement.firstElementChild!.clientWidth
71
+ );
72
+
73
+ const file = createResizeHandlesWrapper(
74
+ block,
75
+ editor,
76
+ video,
77
+ () => video.width,
78
+ (width) => (video.width = width)
79
+ );
80
+
81
+ const element = createFileAndCaptionWrapper(block, file.dom);
82
+
83
+ return createFileBlockWrapper(
84
+ block,
85
+ editor,
86
+ element,
87
+ editor.dictionary.file_blocks.video.add_button_text,
88
+ icon.firstElementChild as HTMLElement
89
+ );
115
90
  };
116
91
 
117
92
  export const videoParse = (
@@ -57,3 +57,16 @@ it("immediately replaces doc", async () => {
57
57
  ]
58
58
  `);
59
59
  });
60
+
61
+ it("adds id attribute when requested", async () => {
62
+ const editor = BlockNoteEditor.create({
63
+ setIdAttribute: true,
64
+ });
65
+ const blocks = await editor.tryParseMarkdownToBlocks(
66
+ "This is a normal text\n\n# And this is a large heading"
67
+ );
68
+ editor.replaceBlocks(editor.document, blocks);
69
+ expect(
70
+ await editor.blocksToFullHTML(editor.document)
71
+ ).toMatchInlineSnapshot(`"<div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="1" id="1"><div class="bn-block" data-node-type="blockContainer" data-id="1" id="1"><div class="bn-block-content" data-content-type="paragraph"><p class="bn-inline-content">This is a normal text</p></div></div></div><div class="bn-block-outer" data-node-type="blockOuter" data-id="2" id="2"><div class="bn-block" data-node-type="blockContainer" data-id="2" id="2"><div class="bn-block-content" data-content-type="heading" data-level="1"><h1 class="bn-inline-content">And this is a large heading</h1></div></div></div></div>"`);
72
+ });
@@ -41,9 +41,9 @@ import {
41
41
  InlineContentSchema,
42
42
  InlineContentSpecs,
43
43
  PartialInlineContent,
44
+ Styles,
44
45
  StyleSchema,
45
46
  StyleSpecs,
46
- Styles,
47
47
  } from "../schema";
48
48
  import { mergeCSSClasses } from "../util/browser";
49
49
  import { NoInfer, UnreachableCaseError } from "../util/typescript";
@@ -67,6 +67,7 @@ import { en } from "../i18n/locales";
67
67
 
68
68
  import { Transaction } from "@tiptap/pm/state";
69
69
  import { createInternalHTMLSerializer } from "../api/exporters/html/internalHTMLSerializer";
70
+ import { PreviousBlockTypePlugin } from "../extensions/PreviousBlockType/PreviousBlockTypePlugin";
70
71
  import "../style.css";
71
72
  import { initializeESMDependencies } from "../util/esmDependencies";
72
73
 
@@ -75,6 +76,13 @@ export type BlockNoteEditorOptions<
75
76
  ISchema extends InlineContentSchema,
76
77
  SSchema extends StyleSchema
77
78
  > = {
79
+ /**
80
+ * Whether changes to blocks (like indentation, creating lists, changing headings) should be animated or not. Defaults to `true`.
81
+ *
82
+ * @default true
83
+ */
84
+ animations?: boolean;
85
+
78
86
  disableExtensions: string[];
79
87
  /**
80
88
  * A dictionary object containing translations for the editor.
@@ -119,7 +127,10 @@ export type BlockNoteEditorOptions<
119
127
  * @param file The file that should be uploaded.
120
128
  * @returns The URL of the uploaded file OR an object containing props that should be set on the file block (such as an id)
121
129
  */
122
- uploadFile: (file: File) => Promise<string | Record<string, any>>;
130
+ uploadFile: (
131
+ file: File,
132
+ blockId?: string
133
+ ) => Promise<string | Record<string, any>>;
123
134
 
124
135
  /**
125
136
  * Resolve a URL of a file block to one that can be displayed or downloaded. This can be used for creating authenticated URL or
@@ -166,6 +177,16 @@ export type BlockNoteEditorOptions<
166
177
  * You probably don't need to set this manually, but use the `server-util` package instead that uses this option internally
167
178
  */
168
179
  _headless: boolean;
180
+
181
+ /**
182
+ * A flag indicating whether to set an HTML ID for every block
183
+ *
184
+ * When set to `true`, on each block an id attribute will be set with the block id
185
+ * Otherwise, the HTML ID attribute will not be set.
186
+ *
187
+ * (note that the id is always set on the `data-id` attribute)
188
+ */
189
+ setIdAttribute?: boolean;
169
190
  };
170
191
 
171
192
  const blockNoteTipTapOptions = {
@@ -255,9 +276,12 @@ export class BlockNoteEditor<
255
276
  * @returns The URL of the uploaded file OR an object containing props that should be set on the file block (such as an id)
256
277
  */
257
278
  public readonly uploadFile:
258
- | ((file: File) => Promise<string | Record<string, any>>)
279
+ | ((file: File, blockId?: string) => Promise<string | Record<string, any>>)
259
280
  | undefined;
260
281
 
282
+ private onUploadStartCallbacks: ((blockId?: string) => void)[] = [];
283
+ private onUploadEndCallbacks: ((blockId?: string) => void)[] = [];
284
+
261
285
  public readonly resolveFileUrl: (url: string) => Promise<string>;
262
286
 
263
287
  public get pmSchema() {
@@ -273,7 +297,7 @@ export class BlockNoteEditor<
273
297
  }
274
298
 
275
299
  protected constructor(
276
- private readonly options: Partial<BlockNoteEditorOptions<any, any, any>>
300
+ protected readonly options: Partial<BlockNoteEditorOptions<any, any, any>>
277
301
  ) {
278
302
  const anyOpts = options as any;
279
303
  if (anyOpts.onEditorContentChange) {
@@ -339,6 +363,7 @@ export class BlockNoteEditor<
339
363
  collaboration: newOptions.collaboration,
340
364
  trailingBlock: newOptions.trailingBlock,
341
365
  disableExtensions: newOptions.disableExtensions,
366
+ setIdAttribute: newOptions.setIdAttribute,
342
367
  });
343
368
 
344
369
  const blockNoteUIExtension = Extension.create({
@@ -353,12 +378,30 @@ export class BlockNoteEditor<
353
378
  ...(this.filePanel ? [this.filePanel.plugin] : []),
354
379
  ...(this.tableHandles ? [this.tableHandles.plugin] : []),
355
380
  PlaceholderPlugin(this, newOptions.placeholders),
381
+ ...(this.options.animations ?? true
382
+ ? [PreviousBlockTypePlugin()]
383
+ : []),
356
384
  ];
357
385
  },
358
386
  });
359
387
  extensions.push(blockNoteUIExtension);
360
388
 
361
- this.uploadFile = newOptions.uploadFile;
389
+ if (newOptions.uploadFile) {
390
+ const uploadFile = newOptions.uploadFile;
391
+ this.uploadFile = async (file, block) => {
392
+ this.onUploadStartCallbacks.forEach((callback) =>
393
+ callback.apply(this, [block])
394
+ );
395
+ try {
396
+ return await uploadFile(file, block);
397
+ } finally {
398
+ this.onUploadEndCallbacks.forEach((callback) =>
399
+ callback.apply(this, [block])
400
+ );
401
+ }
402
+ };
403
+ }
404
+
362
405
  this.resolveFileUrl = newOptions.resolveFileUrl || (async (url) => url);
363
406
  this.headless = newOptions._headless;
364
407
 
@@ -463,6 +506,28 @@ export class BlockNoteEditor<
463
506
  this._tiptapEditor.view.focus();
464
507
  }
465
508
 
509
+ public onUploadStart(callback: (blockId?: string) => void) {
510
+ this.onUploadStartCallbacks.push(callback);
511
+
512
+ return () => {
513
+ const index = this.onUploadStartCallbacks.indexOf(callback);
514
+ if (index > -1) {
515
+ this.onUploadStartCallbacks.splice(index, 1);
516
+ }
517
+ };
518
+ }
519
+
520
+ public onUploadEnd(callback: (blockId?: string) => void) {
521
+ this.onUploadEndCallbacks.push(callback);
522
+
523
+ return () => {
524
+ const index = this.onUploadEndCallbacks.indexOf(callback);
525
+ if (index > -1) {
526
+ this.onUploadEndCallbacks.splice(index, 1);
527
+ }
528
+ };
529
+ }
530
+
466
531
  /**
467
532
  * @deprecated, use `editor.document` instead
468
533
  */
@@ -550,7 +615,7 @@ export class BlockNoteEditor<
550
615
  blockArray: Block<BSchema, ISchema, SSchema>[]
551
616
  ): boolean {
552
617
  for (const block of blockArray) {
553
- if (!callback(block)) {
618
+ if (callback(block) === false) {
554
619
  return false;
555
620
  }
556
621