Skip to content

Commit 68dcdeb

Browse files
fix: Loading indicator when dropping/pasting files (#1069)
* Added `Loading...` indicator when files are inserted * Refactored to use upload event listeners instead of `loading` prop * Made `uploadFile` only get defined if it's present in editor options * Cleaned up code * Removed old loading prop references * Implemented PR feedback * Small fix * Removed comment
1 parent 78e4412 commit 68dcdeb

File tree

15 files changed

+388
-317
lines changed

15 files changed

+388
-317
lines changed

packages/core/src/api/parsers/handleFileInsertion.ts

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -106,28 +106,22 @@ export async function handleFileInsertion<
106106

107107
const file = items[i].getAsFile();
108108
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>;
110115

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;
121117

122118
if (event.type === "paste") {
123-
editor.insertBlocks(
119+
insertedBlockId = editor.insertBlocks(
124120
[fileBlock],
125121
editor.getTextCursorPosition().block,
126122
"after"
127-
);
128-
}
129-
130-
if (event.type === "drop") {
123+
)[0].id;
124+
} else if (event.type === "drop") {
131125
const coords = {
132126
left: (event as DragEvent).clientX,
133127
top: (event as DragEvent).clientY,
@@ -143,8 +137,27 @@ export async function handleFileInsertion<
143137
pos.pos
144138
);
145139

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;
147147
}
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);
148161
}
149162
}
150163
}

packages/core/src/blocks/AudioBlockContent/AudioBlockContent.ts

Lines changed: 23 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@ import {
99
import { defaultProps } from "../defaultProps";
1010

1111
import {
12-
createAddFileButton,
13-
createDefaultFilePreview,
1412
createFigureWithCaption,
1513
createFileAndCaptionWrapper,
14+
createFileBlockWrapper,
1615
createLinkWithCaption,
1716
parseFigureElement,
1817
} from "../FileBlockContent/fileBlockHelpers";
@@ -50,51 +49,28 @@ export const audioRender = (
5049
block: BlockFromConfig<typeof audioBlockConfig, any, any>,
5150
editor: BlockNoteEditor<any, any, any>
5251
) => {
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+
);
9874
};
9975

10076
export const audioParse = (

packages/core/src/blocks/FileBlockContent/FileBlockContent.ts

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import {
77
} from "../../schema";
88
import { defaultProps } from "../defaultProps";
99
import {
10-
createAddFileButton,
1110
createDefaultFilePreview,
1211
createFileAndCaptionWrapper,
12+
createFileBlockWrapper,
1313
createLinkWithCaption,
1414
parseEmbedElement,
1515
parseFigureElement,
@@ -42,28 +42,10 @@ export const fileRender = (
4242
block: BlockFromConfig<typeof fileBlockConfig, any, any>,
4343
editor: BlockNoteEditor<any, any, any>
4444
) => {
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);
4947

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);
6749
};
6850

6951
export const fileParse = (element: HTMLElement) => {

packages/core/src/blocks/FileBlockContent/fileBlockHelpers.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,76 @@
11
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+
};
374

475
// Default file preview, displaying a file icon and file name.
576
export const createDefaultFilePreview = (

packages/core/src/blocks/ImageBlockContent/ImageBlockContent.ts

Lines changed: 35 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,10 @@ import {
77
PropSchema,
88
} from "../../schema";
99
import { defaultProps } from "../defaultProps";
10-
1110
import {
12-
createAddFileButton,
13-
createDefaultFilePreview,
1411
createFigureWithCaption,
1512
createFileAndCaptionWrapper,
13+
createFileBlockWrapper,
1614
createLinkWithCaption,
1715
createResizeHandlesWrapper,
1816
parseFigureElement,
@@ -56,64 +54,40 @@ export const imageRender = (
5654
block: BlockFromConfig<typeof imageBlockConfig, any, any>,
5755
editor: BlockNoteEditor<any, any, any>
5856
) => {
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+
);
11791
};
11892

11993
export const imageParse = (

0 commit comments

Comments
 (0)