@blocknote/core 0.15.6 → 0.15.9

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 (86) hide show
  1. package/dist/blocknote.js +1486 -1350
  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 +81 -17
  53. package/src/blocks/AudioBlockContent/AudioBlockContent.ts +24 -48
  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 +36 -62
  57. package/src/blocks/VideoBlockContent/VideoBlockContent.ts +34 -59
  58. package/src/editor/BlockNoteEditor.test.ts +13 -0
  59. package/src/editor/BlockNoteEditor.ts +89 -13
  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 +23 -12
  65. package/src/extensions/SuggestionMenu/SuggestionPlugin.ts +22 -10
  66. package/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +6 -1
  67. package/src/extensions/TableHandles/TableHandlesPlugin.ts +5 -1
  68. package/src/extensions/UniqueID/UniqueID.ts +15 -4
  69. package/src/pm-nodes/BlockContainer.ts +1 -2
  70. package/src/schema/blocks/types.ts +1 -1
  71. package/src/schema/inlineContent/createSpec.ts +54 -5
  72. package/types/src/api/testUtil/cases/customBlocks.d.ts +6 -6
  73. package/types/src/api/testUtil/cases/customInlineContent.d.ts +6 -6
  74. package/types/src/api/testUtil/cases/customStyles.d.ts +6 -6
  75. package/types/src/blocks/AudioBlockContent/AudioBlockContent.d.ts +5 -5
  76. package/types/src/blocks/FileBlockContent/FileBlockContent.d.ts +2 -2
  77. package/types/src/blocks/FileBlockContent/fileBlockHelpers.d.ts +11 -1
  78. package/types/src/blocks/ImageBlockContent/ImageBlockContent.d.ts +5 -5
  79. package/types/src/blocks/VideoBlockContent/VideoBlockContent.d.ts +5 -5
  80. package/types/src/blocks/defaultBlocks.d.ts +12 -12
  81. package/types/src/editor/BlockNoteEditor.d.ts +27 -5
  82. package/types/src/editor/BlockNoteExtensions.d.ts +1 -0
  83. package/types/src/extensions/FilePanel/FilePanelPlugin.d.ts +1 -1
  84. package/types/src/extensions/SuggestionMenu/SuggestionPlugin.d.ts +1 -0
  85. package/types/src/schema/blocks/types.d.ts +1 -1
  86. package/types/src/schema/inlineContent/createSpec.d.ts +3 -3
@@ -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
 
@@ -1159,15 +1224,26 @@ export class BlockNoteEditor<
1159
1224
  };
1160
1225
  }
1161
1226
 
1162
- public openSelectionMenu(triggerCharacter: string) {
1227
+ public openSuggestionMenu(
1228
+ triggerCharacter: string,
1229
+ pluginState?: {
1230
+ deleteTriggerCharacter?: boolean;
1231
+ ignoreQueryLength?: boolean;
1232
+ }
1233
+ ) {
1234
+ const tr = this.prosemirrorView.state.tr;
1235
+ const transaction =
1236
+ pluginState && pluginState.deleteTriggerCharacter
1237
+ ? tr.insertText(triggerCharacter)
1238
+ : tr;
1239
+
1163
1240
  this.prosemirrorView.focus();
1164
1241
  this.prosemirrorView.dispatch(
1165
- this.prosemirrorView.state.tr
1166
- .scrollIntoView()
1167
- .setMeta(this.suggestionMenus.plugin, {
1168
- triggerCharacter: triggerCharacter,
1169
- fromUserInput: false,
1170
- })
1242
+ transaction.scrollIntoView().setMeta(this.suggestionMenus.plugin, {
1243
+ triggerCharacter: triggerCharacter,
1244
+ deleteTriggerCharacter: pluginState?.deleteTriggerCharacter || false,
1245
+ ignoreQueryLength: pluginState?.ignoreQueryLength || false,
1246
+ })
1171
1247
  );
1172
1248
  }
1173
1249
  }
@@ -12,8 +12,8 @@ import { Link } from "@tiptap/extension-link";
12
12
  import { Text } from "@tiptap/extension-text";
13
13
  import * as Y from "yjs";
14
14
  import { createCopyToClipboardExtension } from "../api/exporters/copyExtension";
15
- import { createPasteFromClipboardExtension } from "../api/parsers/pasteExtension";
16
15
  import { createDropFileExtension } from "../api/parsers/fileDropExtension";
16
+ import { createPasteFromClipboardExtension } from "../api/parsers/pasteExtension";
17
17
  import { BackgroundColorExtension } from "../extensions/BackgroundColor/BackgroundColorExtension";
18
18
  import { TextAlignmentExtension } from "../extensions/TextAlignment/TextAlignmentExtension";
19
19
  import { TextColorExtension } from "../extensions/TextColor/TextColorExtension";
@@ -55,6 +55,7 @@ export const getBlockNoteExtensions = <
55
55
  renderCursor?: (user: any) => HTMLElement;
56
56
  };
57
57
  disableExtensions: string[] | undefined;
58
+ setIdAttribute?: boolean;
58
59
  }) => {
59
60
  const ret: Extensions = [
60
61
  extensions.ClipboardTextSerializer,
@@ -69,6 +70,7 @@ export const getBlockNoteExtensions = <
69
70
  // DropCursor,
70
71
  UniqueID.configure({
71
72
  types: ["blockContainer"],
73
+ setIdAttribute: opts.setIdAttribute,
72
74
  }),
73
75
  HardBreak.extend({ priority: 10 }),
74
76
  // Comments,
@@ -197,5 +199,5 @@ export const getBlockNoteExtensions = <
197
199
  }
198
200
 
199
201
  const disableExtensions: string[] = opts.disableExtensions || [];
200
- return ret.filter(ex => !disableExtensions.includes(ex.name));
202
+ return ret.filter((ex) => !disableExtensions.includes(ex.name));
201
203
  };
@@ -168,8 +168,11 @@ export class BlockNoteTipTapEditor extends TiptapEditor {
168
168
  };
169
169
  }
170
170
 
171
- (BlockNoteTipTapEditor.prototype as any).createView = () => {
171
+ (BlockNoteTipTapEditor.prototype as any).createView = function () {
172
172
  // no-op
173
173
  // Disable default call to `createView` in the Editor constructor.
174
174
  // We should call `createView` manually only when a DOM element is available
175
+
176
+ // additional fix because onPaste and onDrop depend on installing plugins in constructor which we don't support
177
+ this.options.onPaste = this.options.onDrop = undefined;
175
178
  };
@@ -71,8 +71,10 @@ export class FilePanelView<I extends InlineContentSchema, S extends StyleSchema>
71
71
  if (this.state?.show) {
72
72
  const blockElement = this.pmView.root.querySelector(
73
73
  `[data-node-type="blockContainer"][data-id="${this.state.block.id}"]`
74
- )!;
75
-
74
+ );
75
+ if (!blockElement) {
76
+ return;
77
+ }
76
78
  this.state.referencePos = blockElement.getBoundingClientRect();
77
79
  this.emitUpdate();
78
80
  }
@@ -86,8 +88,10 @@ export class FilePanelView<I extends InlineContentSchema, S extends StyleSchema>
86
88
  if (!this.state?.show && pluginState.block && this.editor.isEditable) {
87
89
  const blockElement = this.pmView.root.querySelector(
88
90
  `[data-node-type="blockContainer"][data-id="${pluginState.block.id}"]`
89
- )!;
90
-
91
+ );
92
+ if (!blockElement) {
93
+ return;
94
+ }
91
95
  this.state = {
92
96
  show: true,
93
97
  referencePos: blockElement.getBoundingClientRect(),
@@ -157,7 +161,7 @@ export class FilePanelProsemirrorPlugin<
157
161
  props: {
158
162
  handleKeyDown: (_view, event: KeyboardEvent) => {
159
163
  if (event.key === "Escape" && this.shown) {
160
- this.view!.closeMenu();
164
+ this.view?.closeMenu();
161
165
  return true;
162
166
  }
163
167
  return false;
@@ -189,5 +193,5 @@ export class FilePanelProsemirrorPlugin<
189
193
  return this.on("update", callback);
190
194
  }
191
195
 
192
- public closeMenu = () => this.view!.closeMenu();
196
+ public closeMenu = () => this.view?.closeMenu();
193
197
  }
@@ -205,7 +205,6 @@ export class FormattingToolbarView implements PluginView {
205
205
 
206
206
  if (isNodeSelection(selection)) {
207
207
  const node = this.pmView.nodeDOM(from) as HTMLElement;
208
-
209
208
  if (node) {
210
209
  return node.getBoundingClientRect();
211
210
  }
@@ -267,7 +267,7 @@ export class SideMenuView<
267
267
  // When false, the drag handle with be just to the left of the element
268
268
  // TODO: Is there any case where we want this to be false?
269
269
  private horizontalPosAnchoredAtRoot: boolean;
270
- private horizontalPosAnchor: number;
270
+ private horizontalPosAnchor: number | undefined;
271
271
 
272
272
  private hoveredBlock: HTMLElement | undefined;
273
273
 
@@ -290,9 +290,12 @@ export class SideMenuView<
290
290
  };
291
291
 
292
292
  this.horizontalPosAnchoredAtRoot = true;
293
- this.horizontalPosAnchor = (
294
- this.pmView.dom.firstChild! as HTMLElement
295
- ).getBoundingClientRect().x;
293
+
294
+ if (this.pmView.dom.firstChild) {
295
+ this.horizontalPosAnchor = (
296
+ this.pmView.dom.firstChild as HTMLElement
297
+ ).getBoundingClientRect().x;
298
+ }
296
299
 
297
300
  this.pmView.root.addEventListener(
298
301
  "drop",
@@ -337,8 +340,12 @@ export class SideMenuView<
337
340
  // size/position, so we get the boundingRect of the first child (i.e. the
338
341
  // blockGroup that wraps all blocks in the editor) for more accurate side
339
342
  // menu placement.
343
+ if (!this.pmView.dom.firstChild) {
344
+ return;
345
+ }
346
+
340
347
  const editorBoundingBox = (
341
- this.pmView.dom.firstChild! as HTMLElement
348
+ this.pmView.dom.firstChild as HTMLElement
342
349
  ).getBoundingClientRect();
343
350
 
344
351
  this.horizontalPosAnchor = editorBoundingBox.x;
@@ -441,7 +448,7 @@ export class SideMenuView<
441
448
  if (!pos || pos.inside === -1) {
442
449
  const evt = new Event("drop", event) as any;
443
450
  const editorBoundingBox = (
444
- this.pmView.dom.firstChild! as HTMLElement
451
+ this.pmView.dom.firstChild as HTMLElement
445
452
  ).getBoundingClientRect();
446
453
  evt.clientX =
447
454
  event.clientX < editorBoundingBox.left ||
@@ -474,10 +481,10 @@ export class SideMenuView<
474
481
  top: event.clientY,
475
482
  });
476
483
 
477
- if (!pos || pos.inside === -1) {
484
+ if (!pos || (pos.inside === -1 && this.pmView.dom.firstChild)) {
478
485
  const evt = new Event("dragover", event) as any;
479
486
  const editorBoundingBox = (
480
- this.pmView.dom.firstChild! as HTMLElement
487
+ this.pmView.dom.firstChild as HTMLElement
481
488
  ).getBoundingClientRect();
482
489
  evt.clientX = editorBoundingBox.left + editorBoundingBox.width / 2;
483
490
  evt.clientY = event.clientY;
@@ -555,8 +562,8 @@ export class SideMenuView<
555
562
  };
556
563
 
557
564
  onScroll = () => {
558
- if (this.state?.show) {
559
- const blockContent = this.hoveredBlock!.firstChild as HTMLElement;
565
+ if (this.state?.show && this.hoveredBlock?.firstChild) {
566
+ const blockContent = this.hoveredBlock.firstChild as HTMLElement;
560
567
  const blockContentBoundingBox = blockContent.getBoundingClientRect();
561
568
 
562
569
  this.state.referencePos = new DOMRect(
@@ -624,7 +631,11 @@ export class SideMenuView<
624
631
  this.emitUpdate(this.state);
625
632
  }
626
633
 
627
- const blockContent = this.hoveredBlock!.firstChild! as HTMLElement;
634
+ if (!this.hoveredBlock?.firstChild) {
635
+ return;
636
+ }
637
+
638
+ const blockContent = this.hoveredBlock.firstChild as HTMLElement;
628
639
  const blockContentBoundingBox = blockContent.getBoundingClientRect();
629
640
 
630
641
  const pos = this.pmView.posAtCoords({
@@ -664,7 +675,7 @@ export class SideMenuView<
664
675
  }
665
676
 
666
677
  // Focuses and activates the slash menu.
667
- this.editor.openSelectionMenu("/");
678
+ this.editor.openSuggestionMenu("/");
668
679
  }
669
680
  }
670
681
 
@@ -11,6 +11,7 @@ const findBlock = findParentNode((node) => node.type.name === "blockContainer");
11
11
 
12
12
  export type SuggestionMenuState = UiElementPosition & {
13
13
  query: string;
14
+ ignoreQueryLength?: boolean;
14
15
  };
15
16
 
16
17
  class SuggestionMenuView<
@@ -34,7 +35,10 @@ class SuggestionMenuView<
34
35
  throw new Error("Attempting to update uninitialized suggestions menu");
35
36
  }
36
37
 
37
- emitUpdate(menuName, this.state);
38
+ emitUpdate(menuName, {
39
+ ...this.state,
40
+ ignoreQueryLength: this.pluginState?.ignoreQueryLength,
41
+ });
38
42
  };
39
43
 
40
44
  this.rootEl = this.editor._tiptapEditor.view.root;
@@ -50,7 +54,10 @@ class SuggestionMenuView<
50
54
  const decorationNode = this.rootEl?.querySelector(
51
55
  `[data-decoration-id="${this.pluginState!.decorationId}"]`
52
56
  );
53
- this.state.referencePos = decorationNode!.getBoundingClientRect();
57
+ if (!decorationNode) {
58
+ return;
59
+ }
60
+ this.state.referencePos = decorationNode.getBoundingClientRect();
54
61
  this.emitUpdate(this.pluginState!.triggerCharacter!);
55
62
  }
56
63
  };
@@ -85,10 +92,10 @@ class SuggestionMenuView<
85
92
  `[data-decoration-id="${this.pluginState!.decorationId}"]`
86
93
  );
87
94
 
88
- if (this.editor.isEditable) {
95
+ if (this.editor.isEditable && decorationNode) {
89
96
  this.state = {
90
97
  show: true,
91
- referencePos: decorationNode!.getBoundingClientRect(),
98
+ referencePos: decorationNode.getBoundingClientRect(),
92
99
  query: this.pluginState!.query,
93
100
  };
94
101
 
@@ -120,7 +127,7 @@ class SuggestionMenuView<
120
127
  .deleteRange({
121
128
  from:
122
129
  this.pluginState.queryStartPos! -
123
- (this.pluginState.fromUserInput
130
+ (this.pluginState.deleteTriggerCharacter
124
131
  ? this.pluginState.triggerCharacter!.length
125
132
  : 0),
126
133
  to: this.editor._tiptapEditor.state.selection.from,
@@ -132,10 +139,11 @@ class SuggestionMenuView<
132
139
  type SuggestionPluginState =
133
140
  | {
134
141
  triggerCharacter: string;
135
- fromUserInput: boolean;
142
+ deleteTriggerCharacter: boolean;
136
143
  queryStartPos: number;
137
144
  query: string;
138
145
  decorationId: string;
146
+ ignoreQueryLength?: boolean;
139
147
  }
140
148
  | undefined;
141
149
 
@@ -194,7 +202,8 @@ export class SuggestionMenuProseMirrorPlugin<
194
202
  // or null if it should be hidden.
195
203
  const suggestionPluginTransactionMeta: {
196
204
  triggerCharacter: string;
197
- fromUserInput?: boolean;
205
+ deleteTriggerCharacter?: boolean;
206
+ ignoreQueryLength?: boolean;
198
207
  } | null = transaction.getMeta(suggestionMenuPluginKey);
199
208
 
200
209
  // Only opens a menu of no menu is already open
@@ -206,11 +215,14 @@ export class SuggestionMenuProseMirrorPlugin<
206
215
  return {
207
216
  triggerCharacter:
208
217
  suggestionPluginTransactionMeta.triggerCharacter,
209
- fromUserInput:
210
- suggestionPluginTransactionMeta.fromUserInput !== false,
218
+ deleteTriggerCharacter:
219
+ suggestionPluginTransactionMeta.deleteTriggerCharacter !==
220
+ false,
211
221
  queryStartPos: newState.selection.from,
212
222
  query: "",
213
223
  decorationId: `id_${Math.floor(Math.random() * 0xffffffff)}`,
224
+ ignoreQueryLength:
225
+ suggestionPluginTransactionMeta?.ignoreQueryLength,
214
226
  };
215
227
  }
216
228
 
@@ -285,7 +297,7 @@ export class SuggestionMenuProseMirrorPlugin<
285
297
 
286
298
  // If the menu was opened programmatically by another extension, it may not use a trigger character. In this
287
299
  // case, the decoration is set on the whole block instead, as the decoration range would otherwise be empty.
288
- if (!suggestionPluginState.fromUserInput) {
300
+ if (!suggestionPluginState.deleteTriggerCharacter) {
289
301
  const blockNode = findBlock(state.selection);
290
302
  if (blockNode) {
291
303
  return DecorationSet.create(state.doc, [
@@ -272,7 +272,12 @@ export function getDefaultSlashMenuItems<
272
272
  }
273
273
 
274
274
  items.push({
275
- onItemClick: () => editor.openSelectionMenu(":"),
275
+ onItemClick: () => {
276
+ editor.openSuggestionMenu(":", {
277
+ deleteTriggerCharacter: true,
278
+ ignoreQueryLength: true,
279
+ });
280
+ },
276
281
  key: "emoji",
277
282
  ...editor.dictionary.slash_menu.emoji,
278
283
  });
@@ -159,7 +159,11 @@ export class TableHandlesView<
159
159
  const rowIndex = getChildIndex(target.parentElement!);
160
160
  const cellRect = target.getBoundingClientRect();
161
161
  const tableRect =
162
- target.parentElement!.parentElement!.getBoundingClientRect();
162
+ target.parentElement?.parentElement?.getBoundingClientRect();
163
+
164
+ if (!tableRect) {
165
+ return;
166
+ }
163
167
 
164
168
  const blockEl = getDraggableBlockFromElement(target, this.pmView);
165
169
  if (!blockEl) {
@@ -50,6 +50,7 @@ const UniqueID = Extension.create({
50
50
  return {
51
51
  attributeName: "id",
52
52
  types: [],
53
+ setIdAttribute: false,
53
54
  generateID: () => {
54
55
  // Use mock ID if tests are running.
55
56
  if (typeof window !== "undefined" && (window as any).__TEST_OPTIONS) {
@@ -77,10 +78,20 @@ const UniqueID = Extension.create({
77
78
  default: null,
78
79
  parseHTML: (element) =>
79
80
  element.getAttribute(`data-${this.options.attributeName}`),
80
- renderHTML: (attributes) => ({
81
- [`data-${this.options.attributeName}`]:
82
- attributes[this.options.attributeName],
83
- }),
81
+ renderHTML: (attributes) => {
82
+ const defaultIdAttributes = {
83
+ [`data-${this.options.attributeName}`]:
84
+ attributes[this.options.attributeName],
85
+ };
86
+ if (this.options.setIdAttribute) {
87
+ return {
88
+ ...defaultIdAttributes,
89
+ id: attributes[this.options.attributeName],
90
+ };
91
+ } else {
92
+ return defaultIdAttributes;
93
+ }
94
+ },
84
95
  },
85
96
  },
86
97
  },
@@ -11,7 +11,6 @@ import {
11
11
  import { PartialBlock } from "../blocks/defaultBlocks";
12
12
  import type { BlockNoteEditor } from "../editor/BlockNoteEditor";
13
13
  import { NonEditableBlockPlugin } from "../extensions/NonEditableBlocks/NonEditableBlockPlugin";
14
- import { PreviousBlockTypePlugin } from "../extensions/PreviousBlockType/PreviousBlockTypePlugin";
15
14
  import {
16
15
  BlockNoteDOMAttributes,
17
16
  BlockSchema,
@@ -492,7 +491,7 @@ export const BlockContainer = Node.create<{
492
491
  },
493
492
 
494
493
  addProseMirrorPlugins() {
495
- return [PreviousBlockTypePlugin(), NonEditableBlockPlugin()];
494
+ return [NonEditableBlockPlugin()];
496
495
  },
497
496
 
498
497
  addKeyboardShortcuts() {
@@ -50,7 +50,7 @@ export type FileBlockConfig = {
50
50
  };
51
51
  content: "none";
52
52
  isFileBlock: true;
53
- fileBlockAcceptMimeTypes?: string[];
53
+ fileBlockAccept?: string[];
54
54
  };
55
55
 
56
56
  // BlockConfig contains the "schema" info about a Block type
@@ -1,6 +1,9 @@
1
1
  import { Node } from "@tiptap/core";
2
2
  import { TagParseRule } from "@tiptap/pm/model";
3
- import { nodeToCustomInlineContent } from "../../api/nodeConversions/nodeConversions";
3
+ import {
4
+ inlineContentToNodes,
5
+ nodeToCustomInlineContent,
6
+ } from "../../api/nodeConversions/nodeConversions";
4
7
  import { propsToAttributes } from "../blocks/internal";
5
8
  import { Props } from "../propTypes";
6
9
  import { StyleSchema } from "../styles/types";
@@ -11,15 +14,15 @@ import {
11
14
  } from "./internal";
12
15
  import {
13
16
  CustomInlineContentConfig,
14
- InlineContentConfig,
15
17
  InlineContentFromConfig,
16
18
  InlineContentSpec,
19
+ PartialCustomInlineContentFromConfig,
17
20
  } from "./types";
18
21
 
19
22
  // TODO: support serialization
20
23
 
21
24
  export type CustomInlineContentImplementation<
22
- T extends InlineContentConfig,
25
+ T extends CustomInlineContentConfig,
23
26
  // B extends BlockSchema,
24
27
  // I extends InlineContentSchema,
25
28
  S extends StyleSchema
@@ -28,7 +31,10 @@ export type CustomInlineContentImplementation<
28
31
  /**
29
32
  * The custom inline content to render
30
33
  */
31
- inlineContent: InlineContentFromConfig<T, S>
34
+ inlineContent: InlineContentFromConfig<T, S>,
35
+ updateInlineContent: (
36
+ update: PartialCustomInlineContentFromConfig<T, S>
37
+ ) => void
32
38
  /**
33
39
  * The BlockNote editor instance
34
40
  * This is typed generically. If you want an editor with your custom schema, you need to
@@ -100,7 +106,10 @@ export function createInlineContentSpec<
100
106
  node,
101
107
  editor.schema.inlineContentSchema,
102
108
  editor.schema.styleSchema
103
- ) as any as InlineContentFromConfig<T, S> // TODO: fix cast
109
+ ) as any as InlineContentFromConfig<T, S>, // TODO: fix cast
110
+ () => {
111
+ // No-op
112
+ }
104
113
  );
105
114
 
106
115
  return addInlineContentAttributes(
@@ -110,6 +119,46 @@ export function createInlineContentSpec<
110
119
  inlineContentConfig.propSchema
111
120
  );
112
121
  },
122
+
123
+ addNodeView() {
124
+ return ({ node, getPos }) => {
125
+ const editor = this.options.editor;
126
+
127
+ const output = inlineContentImplementation.render(
128
+ nodeToCustomInlineContent(
129
+ node,
130
+ editor.schema.inlineContentSchema,
131
+ editor.schema.styleSchema
132
+ ) as any as InlineContentFromConfig<T, S>, // TODO: fix cast
133
+ (update) => {
134
+ if (typeof getPos === "boolean") {
135
+ return;
136
+ }
137
+
138
+ const content = inlineContentToNodes(
139
+ [update],
140
+ editor._tiptapEditor.schema,
141
+ editor.schema.styleSchema
142
+ );
143
+
144
+ editor._tiptapEditor.view.dispatch(
145
+ editor._tiptapEditor.view.state.tr.replaceWith(
146
+ getPos(),
147
+ getPos() + node.nodeSize,
148
+ content
149
+ )
150
+ );
151
+ }
152
+ );
153
+
154
+ return addInlineContentAttributes(
155
+ output,
156
+ inlineContentConfig.type,
157
+ node.attrs as Props<T["propSchema"]>,
158
+ inlineContentConfig.propSchema
159
+ );
160
+ };
161
+ },
113
162
  });
114
163
 
115
164
  return createInlineContentSpecFromTipTapNode(