@blocknote/core 0.13.2 → 0.13.3

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 (137) hide show
  1. package/dist/blocknote.js +5282 -2785
  2. package/dist/blocknote.js.map +1 -1
  3. package/dist/blocknote.umd.cjs +7 -7
  4. package/dist/blocknote.umd.cjs.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/dist/webpack-stats.json +1 -1
  7. package/package.json +2 -2
  8. package/src/api/exporters/html/__snapshots__/file/basic/external.html +1 -0
  9. package/src/api/exporters/html/__snapshots__/file/basic/internal.html +1 -0
  10. package/src/api/exporters/html/__snapshots__/file/button/external.html +1 -0
  11. package/src/api/exporters/html/__snapshots__/file/button/internal.html +1 -0
  12. package/src/api/exporters/html/__snapshots__/file/nested/external.html +1 -0
  13. package/src/api/exporters/html/__snapshots__/file/nested/internal.html +1 -0
  14. package/src/api/exporters/html/__snapshots__/file/noCaption/external.html +1 -0
  15. package/src/api/exporters/html/__snapshots__/file/noCaption/internal.html +1 -0
  16. package/src/api/exporters/html/__snapshots__/file/noName/external.html +1 -0
  17. package/src/api/exporters/html/__snapshots__/file/noName/internal.html +1 -0
  18. package/src/api/exporters/html/__snapshots__/image/basic/external.html +1 -1
  19. package/src/api/exporters/html/__snapshots__/image/basic/internal.html +1 -1
  20. package/src/api/exporters/html/__snapshots__/image/button/external.html +1 -1
  21. package/src/api/exporters/html/__snapshots__/image/button/internal.html +1 -1
  22. package/src/api/exporters/html/__snapshots__/image/nested/external.html +1 -1
  23. package/src/api/exporters/html/__snapshots__/image/nested/internal.html +1 -1
  24. package/src/api/exporters/html/__snapshots__/image/noCaption/external.html +1 -0
  25. package/src/api/exporters/html/__snapshots__/image/noCaption/internal.html +1 -0
  26. package/src/api/exporters/html/__snapshots__/image/noName/external.html +1 -0
  27. package/src/api/exporters/html/__snapshots__/image/noName/internal.html +1 -0
  28. package/src/api/exporters/html/__snapshots__/image/noPreview/external.html +1 -0
  29. package/src/api/exporters/html/__snapshots__/image/noPreview/internal.html +1 -0
  30. package/src/api/exporters/html/__snapshots__/simpleFile/basic/external.html +1 -0
  31. package/src/api/exporters/html/__snapshots__/simpleFile/basic/internal.html +1 -0
  32. package/src/api/exporters/html/__snapshots__/simpleFile/button/external.html +1 -0
  33. package/src/api/exporters/html/__snapshots__/simpleFile/button/internal.html +1 -0
  34. package/src/api/exporters/html/__snapshots__/simpleFile/nested/external.html +1 -0
  35. package/src/api/exporters/html/__snapshots__/simpleFile/nested/internal.html +1 -0
  36. package/src/api/exporters/html/__snapshots__/simpleImage/basic/external.html +1 -1
  37. package/src/api/exporters/html/__snapshots__/simpleImage/basic/internal.html +1 -1
  38. package/src/api/exporters/html/__snapshots__/simpleImage/button/external.html +1 -1
  39. package/src/api/exporters/html/__snapshots__/simpleImage/button/internal.html +1 -1
  40. package/src/api/exporters/html/__snapshots__/simpleImage/nested/external.html +1 -1
  41. package/src/api/exporters/html/__snapshots__/simpleImage/nested/internal.html +1 -1
  42. package/src/api/exporters/html/__snapshots__/simpleImage/noCaption/external.html +1 -0
  43. package/src/api/exporters/html/__snapshots__/simpleImage/noCaption/internal.html +1 -0
  44. package/src/api/exporters/html/__snapshots__/simpleImage/noName/external.html +1 -0
  45. package/src/api/exporters/html/__snapshots__/simpleImage/noName/internal.html +1 -0
  46. package/src/api/exporters/html/__snapshots__/simpleImage/noPreview/external.html +1 -0
  47. package/src/api/exporters/html/__snapshots__/simpleImage/noPreview/internal.html +1 -0
  48. package/src/api/exporters/markdown/__snapshots__/file/basic/markdown.md +3 -0
  49. package/src/api/exporters/markdown/__snapshots__/file/button/markdown.md +1 -0
  50. package/src/api/exporters/markdown/__snapshots__/file/nested/markdown.md +7 -0
  51. package/src/api/exporters/markdown/__snapshots__/file/noCaption/markdown.md +1 -0
  52. package/src/api/exporters/markdown/__snapshots__/file/noName/markdown.md +3 -0
  53. package/src/api/exporters/markdown/__snapshots__/image/basic/markdown.md +1 -1
  54. package/src/api/exporters/markdown/__snapshots__/image/button/markdown.md +1 -1
  55. package/src/api/exporters/markdown/__snapshots__/image/nested/markdown.md +2 -2
  56. package/src/api/exporters/markdown/__snapshots__/image/noCaption/markdown.md +1 -0
  57. package/src/api/exporters/markdown/__snapshots__/image/noName/markdown.md +3 -0
  58. package/src/api/exporters/markdown/__snapshots__/image/noPreview/markdown.md +3 -0
  59. package/src/api/exporters/markdown/__snapshots__/simpleFile/basic/markdown.md +3 -0
  60. package/src/api/exporters/markdown/__snapshots__/simpleFile/button/markdown.md +1 -0
  61. package/src/api/exporters/markdown/__snapshots__/simpleFile/nested/markdown.md +7 -0
  62. package/src/api/exporters/markdown/__snapshots__/simpleImage/basic/markdown.md +3 -1
  63. package/src/api/exporters/markdown/__snapshots__/simpleImage/button/markdown.md +1 -0
  64. package/src/api/exporters/markdown/__snapshots__/simpleImage/nested/markdown.md +6 -2
  65. package/src/api/exporters/markdown/__snapshots__/simpleImage/noCaption/markdown.md +1 -0
  66. package/src/api/exporters/markdown/__snapshots__/simpleImage/noName/markdown.md +3 -0
  67. package/src/api/exporters/markdown/__snapshots__/simpleImage/noPreview/markdown.md +3 -0
  68. package/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +212 -4
  69. package/src/api/parsers/html/__snapshots__/paste/parse-basic-block-types.json +3 -1
  70. package/src/api/parsers/html/__snapshots__/paste/parse-fake-image-caption.json +3 -1
  71. package/src/api/testUtil/cases/customBlocks.ts +79 -33
  72. package/src/api/testUtil/cases/customInlineContent.ts +1 -1
  73. package/src/api/testUtil/cases/customStyles.ts +1 -1
  74. package/src/api/testUtil/cases/defaultSchema.ts +114 -4
  75. package/src/blocks/AudioBlockContent/AudioBlockContent.ts +162 -0
  76. package/src/blocks/AudioBlockContent/audioBlockHelpers.ts +5 -0
  77. package/src/blocks/FileBlockContent/FileBlockContent.ts +121 -0
  78. package/src/blocks/FileBlockContent/fileBlockHelpers.ts +377 -0
  79. package/src/blocks/ImageBlockContent/ImageBlockContent.ts +135 -356
  80. package/src/blocks/ImageBlockContent/imageBlockHelpers.ts +6 -0
  81. package/src/blocks/VideoBlockContent/VideoBlockContent.ts +182 -0
  82. package/src/blocks/VideoBlockContent/videoBlockHelpers.ts +6 -0
  83. package/src/blocks/defaultBlockTypeGuards.ts +53 -1
  84. package/src/blocks/defaultBlocks.ts +8 -2
  85. package/src/editor/Block.css +67 -27
  86. package/src/editor/BlockNoteEditor.ts +14 -10
  87. package/src/editor/BlockNoteSchema.ts +12 -3
  88. package/src/extensions/{ImagePanel/ImageToolbarPlugin.ts → FilePanel/FilePanelPlugin.ts} +22 -25
  89. package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +14 -2
  90. package/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +59 -2
  91. package/src/i18n/locales/en.ts +102 -11
  92. package/src/i18n/locales/fr.ts +104 -11
  93. package/src/i18n/locales/index.ts +8 -2
  94. package/src/i18n/locales/is.ts +288 -0
  95. package/src/i18n/locales/ja.ts +300 -0
  96. package/src/i18n/locales/ko.ts +292 -0
  97. package/src/i18n/locales/nl.ts +101 -8
  98. package/src/i18n/locales/pl.ts +280 -0
  99. package/src/i18n/locales/pt.ts +281 -0
  100. package/src/i18n/locales/vi.ts +281 -0
  101. package/src/i18n/locales/zh.ts +107 -8
  102. package/src/index.ts +9 -2
  103. package/src/pm-nodes/BlockContainer.ts +2 -2
  104. package/src/schema/blocks/createSpec.ts +1 -0
  105. package/src/schema/blocks/internal.ts +10 -0
  106. package/src/schema/blocks/types.ts +41 -5
  107. package/src/util/string.ts +12 -0
  108. package/types/src/api/testUtil/cases/customBlocks.d.ts +228 -42
  109. package/types/src/api/testUtil/cases/customInlineContent.d.ts +178 -4
  110. package/types/src/api/testUtil/cases/customStyles.d.ts +178 -4
  111. package/types/src/blocks/AudioBlockContent/AudioBlockContent.d.ts +104 -0
  112. package/types/src/blocks/AudioBlockContent/audioBlockHelpers.d.ts +3 -0
  113. package/types/src/blocks/FileBlockContent/FileBlockContent.d.ts +96 -0
  114. package/types/src/blocks/FileBlockContent/fileBlockHelpers.d.ts +30 -0
  115. package/types/src/blocks/ImageBlockContent/ImageBlockContent.d.ts +53 -14
  116. package/types/src/blocks/ImageBlockContent/imageBlockHelpers.d.ts +4 -0
  117. package/types/src/blocks/VideoBlockContent/VideoBlockContent.d.ts +132 -0
  118. package/types/src/blocks/VideoBlockContent/videoBlockHelpers.d.ts +4 -0
  119. package/types/src/blocks/defaultBlockTypeGuards.d.ts +6 -1
  120. package/types/src/blocks/defaultBlocks.d.ts +356 -8
  121. package/types/src/editor/BlockNoteEditor.d.ts +5 -5
  122. package/types/src/extensions/{ImagePanel/ImageToolbarPlugin.d.ts → FilePanel/FilePanelPlugin.d.ts} +9 -12
  123. package/types/src/i18n/locales/en.d.ts +49 -7
  124. package/types/src/i18n/locales/fr.d.ts +2 -184
  125. package/types/src/i18n/locales/index.d.ts +7 -1
  126. package/types/src/i18n/locales/is.d.ts +2 -0
  127. package/types/src/i18n/locales/ja.d.ts +2 -0
  128. package/types/src/i18n/locales/ko.d.ts +2 -0
  129. package/types/src/i18n/locales/pl.d.ts +2 -0
  130. package/types/src/i18n/locales/pt.d.ts +2 -0
  131. package/types/src/i18n/locales/vi.d.ts +2 -0
  132. package/types/src/index.d.ts +8 -2
  133. package/types/src/schema/blocks/internal.d.ts +1 -1
  134. package/types/src/schema/blocks/types.d.ts +26 -1
  135. package/types/src/util/string.d.ts +1 -0
  136. /package/src/blocks/{ImageBlockContent → FileBlockContent}/uploadToTmpFilesDotOrg_DEV_ONLY.ts +0 -0
  137. /package/types/src/blocks/{ImageBlockContent → FileBlockContent}/uploadToTmpFilesDotOrg_DEV_ONLY.d.ts +0 -0
@@ -0,0 +1,162 @@
1
+ import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
2
+ import {
3
+ BlockFromConfig,
4
+ createBlockSpec,
5
+ FileBlockConfig,
6
+ Props,
7
+ PropSchema,
8
+ } from "../../schema";
9
+ import { defaultProps } from "../defaultProps";
10
+
11
+ import {
12
+ createAddFileButton,
13
+ createDefaultFilePreview,
14
+ createFigureWithCaption,
15
+ createFileAndCaptionWrapper,
16
+ createLinkWithCaption,
17
+ parseFigureElement,
18
+ } from "../FileBlockContent/fileBlockHelpers";
19
+ import { parseAudioElement } from "./audioBlockHelpers";
20
+
21
+ export const audioPropSchema = {
22
+ backgroundColor: defaultProps.backgroundColor,
23
+ // File name.
24
+ name: {
25
+ default: "" as const,
26
+ },
27
+ // File url.
28
+ url: {
29
+ default: "" as const,
30
+ },
31
+ // File caption.
32
+ caption: {
33
+ default: "" as const,
34
+ },
35
+
36
+ showPreview: {
37
+ default: true,
38
+ },
39
+ } satisfies PropSchema;
40
+
41
+ export const audioBlockConfig = {
42
+ type: "audio" as const,
43
+ propSchema: audioPropSchema,
44
+ content: "none",
45
+ isFileBlock: true,
46
+ isFileBlockPlaceholder: (block: any) => !block.props.url,
47
+ fileBlockAcceptMimeTypes: ["audio/*"],
48
+ } satisfies FileBlockConfig;
49
+
50
+ export const audioRender = (
51
+ block: BlockFromConfig<typeof audioBlockConfig, any, any>,
52
+ editor: BlockNoteEditor<any, any, any>
53
+ ) => {
54
+ const wrapper = document.createElement("div");
55
+ wrapper.className = "bn-file-block-content-wrapper";
56
+
57
+ if (block.props.url === "") {
58
+ const fileBlockAudioIcon = document.createElement("div");
59
+ fileBlockAudioIcon.innerHTML =
60
+ '<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>';
61
+ const addAudioButton = createAddFileButton(
62
+ block,
63
+ editor,
64
+ editor.dictionary.file_blocks.audio.add_button_text,
65
+ fileBlockAudioIcon.firstElementChild as HTMLElement
66
+ );
67
+ wrapper.appendChild(addAudioButton.dom);
68
+
69
+ return {
70
+ dom: wrapper,
71
+ destroy: () => {
72
+ addAudioButton?.destroy?.();
73
+ },
74
+ };
75
+ } else if (!block.props.showPreview) {
76
+ const file = createDefaultFilePreview(block).dom;
77
+ const element = createFileAndCaptionWrapper(block, file);
78
+
79
+ return {
80
+ dom: element.dom,
81
+ };
82
+ } else {
83
+ const audio = document.createElement("audio");
84
+ audio.className = "bn-audio";
85
+ audio.src = block.props.url;
86
+ audio.controls = true;
87
+ audio.contentEditable = "false";
88
+ audio.draggable = false;
89
+
90
+ const element = createFileAndCaptionWrapper(block, audio);
91
+ wrapper.appendChild(element.dom);
92
+
93
+ return {
94
+ dom: wrapper,
95
+ };
96
+ }
97
+ };
98
+
99
+ export const audioParse = (
100
+ element: HTMLElement
101
+ ): Partial<Props<typeof audioBlockConfig.propSchema>> | undefined => {
102
+ if (element.tagName === "AUDIO") {
103
+ return parseAudioElement(element as HTMLAudioElement);
104
+ }
105
+
106
+ if (element.tagName === "FIGURE") {
107
+ const parsedFigure = parseFigureElement(element, "audio");
108
+ if (!parsedFigure) {
109
+ return undefined;
110
+ }
111
+
112
+ const { targetElement, caption } = parsedFigure;
113
+
114
+ return {
115
+ ...parseAudioElement(targetElement as HTMLAudioElement),
116
+ caption,
117
+ };
118
+ }
119
+
120
+ return undefined;
121
+ };
122
+
123
+ export const audioToExternalHTML = (
124
+ block: BlockFromConfig<typeof audioBlockConfig, any, any>
125
+ ) => {
126
+ if (!block.props.url) {
127
+ const div = document.createElement("p");
128
+ div.textContent = "Add audio";
129
+
130
+ return {
131
+ dom: div,
132
+ };
133
+ }
134
+
135
+ let audio;
136
+ if (block.props.showPreview) {
137
+ audio = document.createElement("audio");
138
+ audio.src = block.props.url;
139
+ } else {
140
+ audio = document.createElement("a");
141
+ audio.href = block.props.url;
142
+ audio.textContent = block.props.name || block.props.url;
143
+ }
144
+
145
+ if (block.props.caption) {
146
+ if (block.props.showPreview) {
147
+ return createFigureWithCaption(audio, block.props.caption);
148
+ } else {
149
+ return createLinkWithCaption(audio, block.props.caption);
150
+ }
151
+ }
152
+
153
+ return {
154
+ dom: audio,
155
+ };
156
+ };
157
+
158
+ export const AudioBlock = createBlockSpec(audioBlockConfig, {
159
+ render: audioRender,
160
+ parse: audioParse,
161
+ toExternalHTML: audioToExternalHTML,
162
+ });
@@ -0,0 +1,5 @@
1
+ export const parseAudioElement = (audioElement: HTMLAudioElement) => {
2
+ const url = audioElement.src || undefined;
3
+
4
+ return { url };
5
+ };
@@ -0,0 +1,121 @@
1
+ import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
2
+ import {
3
+ BlockFromConfig,
4
+ FileBlockConfig,
5
+ PropSchema,
6
+ createBlockSpec,
7
+ } from "../../schema";
8
+ import { defaultProps } from "../defaultProps";
9
+ import {
10
+ createAddFileButton,
11
+ createDefaultFilePreview,
12
+ createFileAndCaptionWrapper,
13
+ parseEmbedElement,
14
+ parseFigureElement,
15
+ createLinkWithCaption,
16
+ } from "./fileBlockHelpers";
17
+
18
+ export const filePropSchema = {
19
+ backgroundColor: defaultProps.backgroundColor,
20
+ // File name.
21
+ name: {
22
+ default: "" as const,
23
+ },
24
+ // File url.
25
+ url: {
26
+ default: "" as const,
27
+ },
28
+ // File caption.
29
+ caption: {
30
+ default: "" as const,
31
+ },
32
+ } satisfies PropSchema;
33
+
34
+ export const fileBlockConfig = {
35
+ type: "file" as const,
36
+ propSchema: filePropSchema,
37
+ content: "none",
38
+ isFileBlock: true,
39
+ isFileBlockPlaceholder: (block: any) => !block.props.url,
40
+ } satisfies FileBlockConfig;
41
+
42
+ export const fileRender = (
43
+ block: BlockFromConfig<typeof fileBlockConfig, any, any>,
44
+ editor: BlockNoteEditor<any, any, any>
45
+ ) => {
46
+ // Wrapper element to set the file alignment, contains both file/file
47
+ // upload dashboard and caption.
48
+ const wrapper = document.createElement("div");
49
+ wrapper.className = "bn-file-block-content-wrapper";
50
+
51
+ if (block.props.url === "") {
52
+ const addFileButton = createAddFileButton(block, editor);
53
+ wrapper.appendChild(addFileButton.dom);
54
+
55
+ return {
56
+ dom: wrapper,
57
+ destroy: addFileButton.destroy,
58
+ };
59
+ } else {
60
+ const file = createDefaultFilePreview(block).dom;
61
+ const element = createFileAndCaptionWrapper(block, file);
62
+ wrapper.appendChild(element.dom);
63
+
64
+ return {
65
+ dom: wrapper,
66
+ };
67
+ }
68
+ };
69
+
70
+ export const fileParse = (element: HTMLElement) => {
71
+ if (element.tagName === "EMBED") {
72
+ return parseEmbedElement(element as HTMLEmbedElement);
73
+ }
74
+
75
+ if (element.tagName === "FIGURE") {
76
+ const parsedFigure = parseFigureElement(element, "embed");
77
+ if (!parsedFigure) {
78
+ return undefined;
79
+ }
80
+
81
+ const { targetElement, caption } = parsedFigure;
82
+
83
+ return {
84
+ ...parseEmbedElement(targetElement as HTMLEmbedElement),
85
+ caption,
86
+ };
87
+ }
88
+
89
+ return undefined;
90
+ };
91
+
92
+ export const fileToExternalHTML = (
93
+ block: BlockFromConfig<typeof fileBlockConfig, any, any>
94
+ ) => {
95
+ if (!block.props.url) {
96
+ const div = document.createElement("p");
97
+ div.textContent = "Add file";
98
+
99
+ return {
100
+ dom: div,
101
+ };
102
+ }
103
+
104
+ const fileSrcLink = document.createElement("a");
105
+ fileSrcLink.href = block.props.url;
106
+ fileSrcLink.textContent = block.props.name || block.props.url;
107
+
108
+ if (block.props.caption) {
109
+ return createLinkWithCaption(fileSrcLink, block.props.caption);
110
+ }
111
+
112
+ return {
113
+ dom: fileSrcLink,
114
+ };
115
+ };
116
+
117
+ export const FileBlock = createBlockSpec(fileBlockConfig, {
118
+ render: fileRender,
119
+ parse: fileParse,
120
+ toExternalHTML: fileToExternalHTML,
121
+ });
@@ -0,0 +1,377 @@
1
+ import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
2
+ import { BlockFromConfig, FileBlockConfig } from "../../schema";
3
+
4
+ // Default file preview, displaying a file icon and file name.
5
+ export const createDefaultFilePreview = (
6
+ block: BlockFromConfig<FileBlockConfig, any, any>
7
+ ): { dom: HTMLElement; destroy?: () => void } => {
8
+ const file = document.createElement("div");
9
+ file.className = "bn-file-default-preview";
10
+
11
+ const icon = document.createElement("div");
12
+ icon.className = "bn-file-default-preview-icon";
13
+ icon.innerHTML =
14
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M3 8L9.00319 2H19.9978C20.5513 2 21 2.45531 21 2.9918V21.0082C21 21.556 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5501 3 20.9932V8ZM10 4V9H5V20H19V4H10Z"></path></svg>';
15
+
16
+ const fileName = document.createElement("p");
17
+ fileName.className = "bn-file-default-preview-name";
18
+ fileName.textContent = block.props.name || "";
19
+
20
+ file.appendChild(icon);
21
+ file.appendChild(fileName);
22
+
23
+ return {
24
+ dom: file,
25
+ };
26
+ };
27
+
28
+ // Wrapper element containing file preview and caption.
29
+ export const createFileAndCaptionWrapper = (
30
+ block: BlockFromConfig<FileBlockConfig, any, any>,
31
+ file: HTMLElement
32
+ ) => {
33
+ const fileAndCaptionWrapper = document.createElement("div");
34
+ fileAndCaptionWrapper.className = "bn-file-and-caption-wrapper";
35
+
36
+ const caption = document.createElement("p");
37
+ caption.className = "bn-file-caption";
38
+ caption.textContent = block.props.caption;
39
+
40
+ fileAndCaptionWrapper.appendChild(file);
41
+ fileAndCaptionWrapper.appendChild(caption);
42
+
43
+ return {
44
+ dom: fileAndCaptionWrapper,
45
+ };
46
+ };
47
+
48
+ // Button element that acts as a placeholder for files with no src.
49
+ export const createAddFileButton = (
50
+ block: BlockFromConfig<FileBlockConfig, any, any>,
51
+ editor: BlockNoteEditor<any, any, any>,
52
+ buttonText?: string,
53
+ buttonIcon?: HTMLElement
54
+ ) => {
55
+ const addFileButton = document.createElement("div");
56
+ addFileButton.className = "bn-add-file-button";
57
+
58
+ const addFileButtonIcon = document.createElement("div");
59
+ addFileButtonIcon.className = "bn-add-file-button-icon";
60
+ if (buttonIcon) {
61
+ addFileButtonIcon.appendChild(buttonIcon);
62
+ } else {
63
+ addFileButtonIcon.innerHTML =
64
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M3 8L9.00319 2H19.9978C20.5513 2 21 2.45531 21 2.9918V21.0082C21 21.556 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5501 3 20.9932V8ZM10 4V9H5V20H19V4H10Z"></path></svg>';
65
+ }
66
+
67
+ const addFileButtonText = document.createElement("p");
68
+ addFileButtonText.className = "bn-add-file-button-text";
69
+ addFileButtonText.innerHTML =
70
+ buttonText || editor.dictionary.file_blocks.file.add_button_text;
71
+
72
+ // Prevents focus from moving to the button.
73
+ const addFileButtonMouseDownHandler = (event: MouseEvent) => {
74
+ event.preventDefault();
75
+ };
76
+ // Opens the file toolbar.
77
+ const addFileButtonClickHandler = () => {
78
+ editor._tiptapEditor.view.dispatch(
79
+ editor._tiptapEditor.state.tr.setMeta(editor.filePanel!.plugin, {
80
+ block: block,
81
+ })
82
+ );
83
+ };
84
+
85
+ addFileButton.appendChild(addFileButtonIcon);
86
+ addFileButton.appendChild(addFileButtonText);
87
+
88
+ addFileButton.addEventListener(
89
+ "mousedown",
90
+ addFileButtonMouseDownHandler,
91
+ true
92
+ );
93
+ addFileButton.addEventListener("click", addFileButtonClickHandler, true);
94
+
95
+ return {
96
+ dom: addFileButton,
97
+ destroy: () => {
98
+ addFileButton.removeEventListener(
99
+ "mousedown",
100
+ addFileButtonMouseDownHandler,
101
+ true
102
+ );
103
+ addFileButton.removeEventListener(
104
+ "click",
105
+ addFileButtonClickHandler,
106
+ true
107
+ );
108
+ },
109
+ };
110
+ };
111
+
112
+ export const parseEmbedElement = (embedElement: HTMLEmbedElement) => {
113
+ const url = embedElement.src || undefined;
114
+
115
+ return { url };
116
+ };
117
+
118
+ export const parseFigureElement = (
119
+ figureElement: HTMLElement,
120
+ targetTag: string
121
+ ) => {
122
+ const targetElement = figureElement.querySelector(
123
+ targetTag
124
+ ) as HTMLElement | null;
125
+ if (!targetElement) {
126
+ return undefined;
127
+ }
128
+
129
+ const captionElement = figureElement.querySelector("figcaption");
130
+ const caption = captionElement?.textContent ?? undefined;
131
+
132
+ return { targetElement, caption };
133
+ };
134
+
135
+ // Wrapper figure element to display file link with caption. Used for external
136
+ // HTML
137
+ export const createLinkWithCaption = (
138
+ element: HTMLElement,
139
+ caption: string
140
+ ) => {
141
+ const wrapper = document.createElement("div");
142
+ const fileCaption = document.createElement("p");
143
+ fileCaption.textContent = caption;
144
+
145
+ wrapper.appendChild(element);
146
+ wrapper.appendChild(fileCaption);
147
+
148
+ return {
149
+ dom: wrapper,
150
+ };
151
+ };
152
+
153
+ // Wrapper figure element to display file preview with caption. Used for
154
+ // external HTML.
155
+ export const createFigureWithCaption = (
156
+ element: HTMLElement,
157
+ caption: string
158
+ ) => {
159
+ const figure = document.createElement("figure");
160
+ const captionElement = document.createElement("figcaption");
161
+ captionElement.textContent = caption;
162
+
163
+ figure.appendChild(element);
164
+ figure.appendChild(captionElement);
165
+
166
+ return { dom: figure };
167
+ };
168
+
169
+ // Wrapper element which adds resize handles & logic for visual media file
170
+ // previews.
171
+ export const createResizeHandlesWrapper = (
172
+ block: BlockFromConfig<FileBlockConfig, any, any>,
173
+ editor: BlockNoteEditor<any, any, any>,
174
+ element: HTMLElement,
175
+ getWidth: () => number,
176
+ setWidth: (width: number) => void
177
+ ): { dom: HTMLElement; destroy: () => void } => {
178
+ if (!block.props.previewWidth) {
179
+ throw new Error("Block must have a `previewWidth` prop.");
180
+ }
181
+
182
+ // Wrapper element for rendered element and resize handles.
183
+ const wrapper = document.createElement("div");
184
+ wrapper.className = "bn-visual-media-wrapper";
185
+
186
+ // Resize handle elements.
187
+ const leftResizeHandle = document.createElement("div");
188
+ leftResizeHandle.className = "bn-visual-media-resize-handle";
189
+ leftResizeHandle.style.left = "4px";
190
+ const rightResizeHandle = document.createElement("div");
191
+ rightResizeHandle.className = "bn-visual-media-resize-handle";
192
+ rightResizeHandle.style.right = "4px";
193
+
194
+ // Temporary parameters set when the user begins resizing the element, used to
195
+ // calculate the new width of the element.
196
+ let resizeParams:
197
+ | {
198
+ handleUsed: "left" | "right";
199
+ initialWidth: number;
200
+ initialClientX: number;
201
+ }
202
+ | undefined;
203
+
204
+ // Updates the element width with an updated width depending on the cursor X
205
+ // offset from when the resize began, and which resize handle is being used.
206
+ const windowMouseMoveHandler = (event: MouseEvent) => {
207
+ if (!resizeParams) {
208
+ if (
209
+ !editor.isEditable &&
210
+ wrapper.contains(leftResizeHandle) &&
211
+ wrapper.contains(rightResizeHandle)
212
+ ) {
213
+ wrapper.removeChild(leftResizeHandle);
214
+ wrapper.removeChild(rightResizeHandle);
215
+ }
216
+
217
+ return;
218
+ }
219
+
220
+ let newWidth: number;
221
+
222
+ if (block.props.textAlignment === "center") {
223
+ if (resizeParams.handleUsed === "left") {
224
+ newWidth =
225
+ resizeParams.initialWidth +
226
+ (resizeParams.initialClientX - event.clientX) * 2;
227
+ } else {
228
+ newWidth =
229
+ resizeParams.initialWidth +
230
+ (event.clientX - resizeParams.initialClientX) * 2;
231
+ }
232
+ } else {
233
+ if (resizeParams.handleUsed === "left") {
234
+ newWidth =
235
+ resizeParams.initialWidth +
236
+ resizeParams.initialClientX -
237
+ event.clientX;
238
+ } else {
239
+ newWidth =
240
+ resizeParams.initialWidth +
241
+ event.clientX -
242
+ resizeParams.initialClientX;
243
+ }
244
+ }
245
+
246
+ // Min element width in px.
247
+ const minWidth = 64;
248
+
249
+ // Ensures the element is not wider than the editor and not smaller than a
250
+ // predetermined minimum width.
251
+ if (newWidth < minWidth) {
252
+ setWidth(minWidth);
253
+ } else if (newWidth > editor.domElement.firstElementChild!.clientWidth) {
254
+ setWidth(editor.domElement.firstElementChild!.clientWidth);
255
+ } else {
256
+ setWidth(newWidth);
257
+ }
258
+ };
259
+ // Stops mouse movements from resizing the element and updates the block's
260
+ // `width` prop to the new value.
261
+ const windowMouseUpHandler = (event: MouseEvent) => {
262
+ // Hides the drag handles if the cursor is no longer over the element.
263
+ if (
264
+ (!event.target ||
265
+ !wrapper.contains(event.target as Node) ||
266
+ !editor.isEditable) &&
267
+ wrapper.contains(leftResizeHandle) &&
268
+ wrapper.contains(rightResizeHandle)
269
+ ) {
270
+ wrapper.removeChild(leftResizeHandle);
271
+ wrapper.removeChild(rightResizeHandle);
272
+ }
273
+
274
+ if (!resizeParams) {
275
+ return;
276
+ }
277
+
278
+ resizeParams = undefined;
279
+
280
+ editor.updateBlock(block, {
281
+ props: {
282
+ previewWidth: getWidth(),
283
+ },
284
+ });
285
+ };
286
+
287
+ // Shows the resize handles when hovering over the element with the cursor.
288
+ const elementMouseEnterHandler = () => {
289
+ if (editor.isEditable) {
290
+ wrapper.appendChild(leftResizeHandle);
291
+ wrapper.appendChild(rightResizeHandle);
292
+ }
293
+ };
294
+ // Hides the resize handles when the cursor leaves the element, unless the
295
+ // cursor moves to one of the resize handles.
296
+ const elementMouseLeaveHandler = (event: MouseEvent) => {
297
+ if (
298
+ event.relatedTarget === leftResizeHandle ||
299
+ event.relatedTarget === rightResizeHandle
300
+ ) {
301
+ return;
302
+ }
303
+
304
+ if (resizeParams) {
305
+ return;
306
+ }
307
+
308
+ if (
309
+ editor.isEditable &&
310
+ wrapper.contains(leftResizeHandle) &&
311
+ wrapper.contains(rightResizeHandle)
312
+ ) {
313
+ wrapper.removeChild(leftResizeHandle);
314
+ wrapper.removeChild(rightResizeHandle);
315
+ }
316
+ };
317
+
318
+ // Sets the resize params, allowing the user to begin resizing the element by
319
+ // moving the cursor left or right.
320
+ const leftResizeHandleMouseDownHandler = (event: MouseEvent) => {
321
+ event.preventDefault();
322
+
323
+ wrapper.appendChild(leftResizeHandle);
324
+ wrapper.appendChild(rightResizeHandle);
325
+
326
+ resizeParams = {
327
+ handleUsed: "left",
328
+ initialWidth: block.props.previewWidth!,
329
+ initialClientX: event.clientX,
330
+ };
331
+ };
332
+ const rightResizeHandleMouseDownHandler = (event: MouseEvent) => {
333
+ event.preventDefault();
334
+
335
+ wrapper.appendChild(leftResizeHandle);
336
+ wrapper.appendChild(rightResizeHandle);
337
+
338
+ resizeParams = {
339
+ handleUsed: "right",
340
+ initialWidth: block.props.previewWidth!,
341
+ initialClientX: event.clientX,
342
+ };
343
+ };
344
+
345
+ wrapper.appendChild(element);
346
+
347
+ window.addEventListener("mousemove", windowMouseMoveHandler);
348
+ window.addEventListener("mouseup", windowMouseUpHandler);
349
+ element.addEventListener("mouseenter", elementMouseEnterHandler);
350
+ element.addEventListener("mouseleave", elementMouseLeaveHandler);
351
+ leftResizeHandle.addEventListener(
352
+ "mousedown",
353
+ leftResizeHandleMouseDownHandler
354
+ );
355
+ rightResizeHandle.addEventListener(
356
+ "mousedown",
357
+ rightResizeHandleMouseDownHandler
358
+ );
359
+
360
+ return {
361
+ dom: wrapper,
362
+ destroy: () => {
363
+ window.removeEventListener("mousemove", windowMouseMoveHandler);
364
+ window.removeEventListener("mouseup", windowMouseUpHandler);
365
+ element.removeEventListener("mouseenter", elementMouseEnterHandler);
366
+ element.removeEventListener("mouseleave", elementMouseLeaveHandler);
367
+ leftResizeHandle.removeEventListener(
368
+ "mousedown",
369
+ leftResizeHandleMouseDownHandler
370
+ );
371
+ rightResizeHandle.removeEventListener(
372
+ "mousedown",
373
+ rightResizeHandleMouseDownHandler
374
+ );
375
+ },
376
+ };
377
+ };