Skip to content

Commit cefbb70

Browse files
feat: Default image blocks (#331)
* Made formatting toolbar buttons not show if they are not applicable * Made formatting toolbar placement change based on text alignment * Improved typing for blocks without inline content and made them selectable * Updated default blocks with new typing * Added image block (React implementation) * Added image upload and caption formatting toolbar buttons * Added image slash menu item * Temporarily updated `App.tsx` to show image block * Added placeholder `alt` attribute to image * Added `destroy` function so vanilla custom blocks can clean ip between renders * Added vanilla image block * Added Uppy deps * Updated `ReplaceImageButton.tsx` * Small fix in `BlockColorsButton.tsx` * Typing fix for hyperlink toolbar components * Created reusable components for link & caption components * Fixed import issue * Made image selection border also include caption * Added comment * Added new hooks to clean up code * Updated exports * Changed image replacing from Uppy to custom UI and removed `replacing` prop * Fixed z-index for UI elements * Changed image `width` prop to be in px * Removed Uppy dependency * Reverted `App.tsx` * Removed React image block implementation * Fixed exports * Fixed image block imports * Small fixes to tests * Changed image replacement to use image specific toolbar * Made image toolbar open immediately after inserting an image using the slash menu * Fixed replace image button having persistent `open` state * `ImageToolbarPositioner.tsx` cleanup * Made image toolbar design more similar to Notion * Cleaned up block typing * Implemented PR feedback * Removed redundant z-index style from `ColorStyleButton` * Made props able to be any primitive type and cleaned up test snapshots * Extracted image styles to CSS * Refactored `DefaultImageToolbar` * Added editor option for consumers to handle image uploads * Small `FormattingToolbarPositioner` refactor * Fixed image slash menu item bug * Changed image toolbar to use tabs and fixed dark theme * Renamed image `src` prop to `url` * Changed how image width is constrained to editor width * Updated default color names * Added loading overlay to the image toolbar while an image is being uploaded * Fixed heading in block tyupe dropdown * small fixes (#346) * small fixes * Updated `uploadFile` JSDoc * Extracted example file upload function to new file removed it as a default --------- Co-authored-by: Matthew Lipski <[email protected]> * Small hook fix * Added image docs * Updated unit test snapshots * Added image e2e tests * Added error handling for image uploads * Renamed example image upload handler * Added example image upload handler to docs home page * Small fixes * Added test snapshots/screenshots * Improved e2e test reliability --------- Co-authored-by: Yousef <[email protected]>
1 parent 5dd663c commit cefbb70

File tree

181 files changed

+3348
-3123
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

181 files changed

+3348
-3123
lines changed

examples/editor/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import "@blocknote/core/style.css";
33
import { BlockNoteView, useBlockNote } from "@blocknote/react";
44
import styles from "./App.module.css";
5+
import { uploadToTmpFilesDotOrg_DEV_ONLY } from "@blocknote/core";
56

67
type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any };
78

@@ -16,6 +17,7 @@ function App() {
1617
"data-test": "editor",
1718
},
1819
},
20+
uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY,
1921
});
2022

2123
// Give tests a way to get prosemirror instance

packages/core/src/BlockNoteEditor.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import {
1111
updateBlock,
1212
} from "./api/blockManipulation/blockManipulation";
1313
import {
14+
HTMLToBlocks,
1415
blocksToHTML,
1516
blocksToMarkdown,
16-
HTMLToBlocks,
1717
markdownToBlocks,
1818
} from "./api/formatConversions/formatConversions";
1919
import {
@@ -44,6 +44,7 @@ import { getBlockInfoFromPos } from "./extensions/Blocks/helpers/getBlockInfoFro
4444

4545
import { FormattingToolbarProsemirrorPlugin } from "./extensions/FormattingToolbar/FormattingToolbarPlugin";
4646
import { HyperlinkToolbarProsemirrorPlugin } from "./extensions/HyperlinkToolbar/HyperlinkToolbarPlugin";
47+
import { ImageToolbarProsemirrorPlugin } from "./extensions/ImageToolbar/ImageToolbarPlugin";
4748
import { SideMenuProsemirrorPlugin } from "./extensions/SideMenu/SideMenuPlugin";
4849
import { BaseSlashMenuItem } from "./extensions/SlashMenu/BaseSlashMenuItem";
4950
import { SlashMenuProsemirrorPlugin } from "./extensions/SlashMenu/SlashMenuPlugin";
@@ -106,6 +107,13 @@ export type BlockNoteEditorOptions<BSchema extends BlockSchema> = {
106107
*/
107108
blockSchema: BSchema;
108109

110+
/**
111+
* A custom function to handle file uploads.
112+
* @param file The file that should be uploaded.
113+
* @returns The URL of the uploaded file.
114+
*/
115+
uploadFile: (file: File) => Promise<string>;
116+
109117
/**
110118
* When enabled, allows for collaboration between multiple users.
111119
*/
@@ -151,6 +159,9 @@ export class BlockNoteEditor<BSchema extends BlockSchema = DefaultBlockSchema> {
151159
public readonly formattingToolbar: FormattingToolbarProsemirrorPlugin<BSchema>;
152160
public readonly slashMenu: SlashMenuProsemirrorPlugin<BSchema, any>;
153161
public readonly hyperlinkToolbar: HyperlinkToolbarProsemirrorPlugin<BSchema>;
162+
public readonly imageToolbar: ImageToolbarProsemirrorPlugin<BSchema>;
163+
164+
public readonly uploadFile: ((file: File) => Promise<string>) | undefined;
154165

155166
constructor(
156167
private readonly options: Partial<BlockNoteEditorOptions<BSchema>> = {}
@@ -178,6 +189,7 @@ export class BlockNoteEditor<BSchema extends BlockSchema = DefaultBlockSchema> {
178189
getDefaultSlashMenuItems(newOptions.blockSchema)
179190
);
180191
this.hyperlinkToolbar = new HyperlinkToolbarProsemirrorPlugin(this);
192+
this.imageToolbar = new ImageToolbarProsemirrorPlugin(this);
181193

182194
const extensions = getBlockNoteExtensions<BSchema>({
183195
editor: this,
@@ -195,13 +207,16 @@ export class BlockNoteEditor<BSchema extends BlockSchema = DefaultBlockSchema> {
195207
this.formattingToolbar.plugin,
196208
this.slashMenu.plugin,
197209
this.hyperlinkToolbar.plugin,
210+
this.imageToolbar.plugin,
198211
];
199212
},
200213
});
201214
extensions.push(blockNoteUIExtension);
202215

203216
this.schema = newOptions.blockSchema;
204217

218+
this.uploadFile = newOptions.uploadFile;
219+
205220
const initialContent =
206221
newOptions.initialContent ||
207222
(options.collaboration
@@ -468,6 +483,12 @@ export class BlockNoteEditor<BSchema extends BlockSchema = DefaultBlockSchema> {
468483
posBeforeNode + 2
469484
)!;
470485

486+
// For blocks without inline content
487+
if (contentNode.type.spec.content === "") {
488+
this._tiptapEditor.commands.setNodeSelection(startPos);
489+
return;
490+
}
491+
471492
if (placement === "start") {
472493
this._tiptapEditor.commands.setTextSelection(startPos + 1);
473494
} else {
@@ -481,9 +502,12 @@ export class BlockNoteEditor<BSchema extends BlockSchema = DefaultBlockSchema> {
481502
* Gets a snapshot of the current selection.
482503
*/
483504
public getSelection(): Selection<BSchema> | undefined {
505+
// Either the TipTap selection is empty, or it's a node selection. In either
506+
// case, it only spans one block, so we return undefined.
484507
if (
485508
this._tiptapEditor.state.selection.from ===
486-
this._tiptapEditor.state.selection.to
509+
this._tiptapEditor.state.selection.to ||
510+
"node" in this._tiptapEditor.state.selection
487511
) {
488512
return undefined;
489513
}

packages/core/src/api/blockManipulation/__snapshots__/blockManipulation.test.ts.snap

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Vitest Snapshot v1
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

33
exports[`Insert, Update, & Delete Blocks > Insert, update, & delete multiple blocks 1`] = `
44
[
@@ -16,7 +16,7 @@ exports[`Insert, Update, & Delete Blocks > Insert, update, & delete multiple blo
1616
"id": "2",
1717
"props": {
1818
"backgroundColor": "default",
19-
"level": "1",
19+
"level": 1,
2020
"textAlignment": "left",
2121
"textColor": "default",
2222
},
@@ -33,7 +33,7 @@ exports[`Insert, Update, & Delete Blocks > Insert, update, & delete multiple blo
3333
"id": "1",
3434
"props": {
3535
"backgroundColor": "default",
36-
"level": "1",
36+
"level": 1,
3737
"textAlignment": "left",
3838
"textColor": "default",
3939
},
@@ -53,7 +53,7 @@ exports[`Insert, Update, & Delete Blocks > Insert, update, & delete multiple blo
5353
"id": "4",
5454
"props": {
5555
"backgroundColor": "default",
56-
"level": "2",
56+
"level": 2,
5757
"textAlignment": "left",
5858
"textColor": "default",
5959
},
@@ -70,7 +70,7 @@ exports[`Insert, Update, & Delete Blocks > Insert, update, & delete multiple blo
7070
"id": "3",
7171
"props": {
7272
"backgroundColor": "default",
73-
"level": "2",
73+
"level": 2,
7474
"textAlignment": "left",
7575
"textColor": "default",
7676
},
@@ -106,7 +106,7 @@ exports[`Insert, Update, & Delete Blocks > Insert, update, & delete multiple blo
106106
"id": "2",
107107
"props": {
108108
"backgroundColor": "default",
109-
"level": "1",
109+
"level": 1,
110110
"textAlignment": "left",
111111
"textColor": "default",
112112
},
@@ -142,7 +142,7 @@ exports[`Insert, Update, & Delete Blocks > Insert, update, & delete multiple blo
142142
"id": "4",
143143
"props": {
144144
"backgroundColor": "default",
145-
"level": "2",
145+
"level": 2,
146146
"textAlignment": "left",
147147
"textColor": "default",
148148
},
@@ -159,7 +159,7 @@ exports[`Insert, Update, & Delete Blocks > Insert, update, & delete multiple blo
159159
"id": "3",
160160
"props": {
161161
"backgroundColor": "default",
162-
"level": "2",
162+
"level": 2,
163163
"textAlignment": "left",
164164
"textColor": "default",
165165
},
@@ -286,7 +286,7 @@ exports[`Insert, Update, & Delete Blocks > Insert, update, & delete single block
286286
"id": "1",
287287
"props": {
288288
"backgroundColor": "default",
289-
"level": "3",
289+
"level": 3,
290290
"textAlignment": "right",
291291
"textColor": "default",
292292
},
@@ -349,7 +349,7 @@ exports[`Inserting Blocks with Different Placements > Insert after existing bloc
349349
"id": "2",
350350
"props": {
351351
"backgroundColor": "default",
352-
"level": "1",
352+
"level": 1,
353353
"textAlignment": "left",
354354
"textColor": "default",
355355
},
@@ -366,7 +366,7 @@ exports[`Inserting Blocks with Different Placements > Insert after existing bloc
366366
"id": "1",
367367
"props": {
368368
"backgroundColor": "default",
369-
"level": "1",
369+
"level": 1,
370370
"textAlignment": "left",
371371
"textColor": "default",
372372
},
@@ -386,7 +386,7 @@ exports[`Inserting Blocks with Different Placements > Insert after existing bloc
386386
"id": "4",
387387
"props": {
388388
"backgroundColor": "default",
389-
"level": "2",
389+
"level": 2,
390390
"textAlignment": "left",
391391
"textColor": "default",
392392
},
@@ -403,7 +403,7 @@ exports[`Inserting Blocks with Different Placements > Insert after existing bloc
403403
"id": "3",
404404
"props": {
405405
"backgroundColor": "default",
406-
"level": "2",
406+
"level": 2,
407407
"textAlignment": "left",
408408
"textColor": "default",
409409
},
@@ -439,7 +439,7 @@ exports[`Inserting Blocks with Different Placements > Insert before existing blo
439439
"id": "2",
440440
"props": {
441441
"backgroundColor": "default",
442-
"level": "1",
442+
"level": 1,
443443
"textAlignment": "left",
444444
"textColor": "default",
445445
},
@@ -456,7 +456,7 @@ exports[`Inserting Blocks with Different Placements > Insert before existing blo
456456
"id": "1",
457457
"props": {
458458
"backgroundColor": "default",
459-
"level": "1",
459+
"level": 1,
460460
"textAlignment": "left",
461461
"textColor": "default",
462462
},
@@ -476,7 +476,7 @@ exports[`Inserting Blocks with Different Placements > Insert before existing blo
476476
"id": "4",
477477
"props": {
478478
"backgroundColor": "default",
479-
"level": "2",
479+
"level": 2,
480480
"textAlignment": "left",
481481
"textColor": "default",
482482
},
@@ -493,7 +493,7 @@ exports[`Inserting Blocks with Different Placements > Insert before existing blo
493493
"id": "3",
494494
"props": {
495495
"backgroundColor": "default",
496-
"level": "2",
496+
"level": 2,
497497
"textAlignment": "left",
498498
"textColor": "default",
499499
},
@@ -531,7 +531,7 @@ exports[`Inserting Blocks with Different Placements > Insert nested inside exist
531531
"id": "2",
532532
"props": {
533533
"backgroundColor": "default",
534-
"level": "1",
534+
"level": 1,
535535
"textAlignment": "left",
536536
"textColor": "default",
537537
},
@@ -548,7 +548,7 @@ exports[`Inserting Blocks with Different Placements > Insert nested inside exist
548548
"id": "1",
549549
"props": {
550550
"backgroundColor": "default",
551-
"level": "1",
551+
"level": 1,
552552
"textAlignment": "left",
553553
"textColor": "default",
554554
},
@@ -568,7 +568,7 @@ exports[`Inserting Blocks with Different Placements > Insert nested inside exist
568568
"id": "4",
569569
"props": {
570570
"backgroundColor": "default",
571-
"level": "2",
571+
"level": 2,
572572
"textAlignment": "left",
573573
"textColor": "default",
574574
},
@@ -585,7 +585,7 @@ exports[`Inserting Blocks with Different Placements > Insert nested inside exist
585585
"id": "3",
586586
"props": {
587587
"backgroundColor": "default",
588-
"level": "2",
588+
"level": 2,
589589
"textAlignment": "left",
590590
"textColor": "default",
591591
},

packages/core/src/api/blockManipulation/blockManipulation.test.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { afterEach, beforeEach, describe, expect, it } from "vitest";
22
import { Block, BlockNoteEditor, PartialBlock } from "../..";
3-
import { DefaultBlockSchema } from "../../extensions/Blocks/api/defaultBlocks";
43

54
let editor: BlockNoteEditor;
65

@@ -15,13 +14,11 @@ function waitForEditor() {
1514
});
1615
}
1716

18-
let singleBlock: PartialBlock<DefaultBlockSchema>;
17+
let singleBlock: PartialBlock;
1918

20-
let multipleBlocks: PartialBlock<DefaultBlockSchema>[];
19+
let multipleBlocks: PartialBlock[];
2120

22-
let insert: (
23-
placement: "before" | "nested" | "after"
24-
) => Block<DefaultBlockSchema>[];
21+
let insert: (placement: "before" | "nested" | "after") => Block[];
2522

2623
beforeEach(() => {
2724
(window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS = {};
@@ -37,14 +34,14 @@ beforeEach(() => {
3734
{
3835
type: "heading",
3936
props: {
40-
level: "1",
37+
level: 1,
4138
},
4239
content: "Heading 1",
4340
children: [
4441
{
4542
type: "heading",
4643
props: {
47-
level: "1",
44+
level: 1,
4845
},
4946
content: "Nested Heading 1",
5047
},
@@ -53,14 +50,14 @@ beforeEach(() => {
5350
{
5451
type: "heading",
5552
props: {
56-
level: "2",
53+
level: 2,
5754
},
5855
content: "Heading 2",
5956
children: [
6057
{
6158
type: "heading",
6259
props: {
63-
level: "2",
60+
level: 2,
6461
},
6562
content: "Nested Heading 2",
6663
},
@@ -123,7 +120,7 @@ describe("Insert, Update, & Delete Blocks", () => {
123120
type: "heading",
124121
props: {
125122
textAlignment: "right",
126-
level: "3",
123+
level: 3,
127124
},
128125
content: [
129126
{

packages/core/src/api/formatConversions/__snapshots__/formatConversions.test.ts.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Vitest Snapshot v1
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

33
exports[`Complex Block/HTML/Markdown Conversions > Convert complex blocks to HTML 1`] = `"<h1>Heading 1</h1><h2>Heading 2</h2><h3>Heading 3</h3><p><span data-text-color=\\"purple\\"><span data-background-color=\\"green\\">Paragraph</span></span></p><p>P<strong>ara</strong><em>grap</em>h</p><p>P<u>ara</u><s>grap</s>h</p><ul><li><p>Bullet List Item</p></li><li><p>Bullet List Item</p><ul><li><p>Bullet List Item</p><ul><li><p>Bullet List Item</p></li></ul><p>Paragraph</p><ol><li><p>Numbered List Item</p></li><li><p>Numbered List Item</p></li><li><p>Numbered List Item</p><ol><li><p>Numbered List Item</p></li></ol></li></ol><ul><li><p>Bullet List Item</p></li></ul></li><li><p>Bullet List Item</p></li></ul></li><li><p>Bullet List Item</p></li></ul>"`;
44
@@ -68,7 +68,7 @@ exports[`Non-Nested Block/HTML/Markdown Conversions > Convert non-nested HTML to
6868
"id": "1",
6969
"props": {
7070
"backgroundColor": "default",
71-
"level": "1",
71+
"level": 1,
7272
"textAlignment": "left",
7373
"textColor": "default",
7474
},
@@ -142,7 +142,7 @@ exports[`Non-Nested Block/HTML/Markdown Conversions > Convert non-nested Markdow
142142
"id": "1",
143143
"props": {
144144
"backgroundColor": "default",
145-
"level": "1",
145+
"level": 1,
146146
"textAlignment": "left",
147147
"textColor": "default",
148148
},

0 commit comments

Comments
 (0)