@blocknote/core 0.47.0 → 0.47.1

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 (37) hide show
  1. package/dist/{BlockNoteSchema-DT4bdXj5.cjs → BlockNoteSchema-CwhtPpVC.cjs} +2 -2
  2. package/dist/{BlockNoteSchema-DT4bdXj5.cjs.map → BlockNoteSchema-CwhtPpVC.cjs.map} +1 -1
  3. package/dist/{BlockNoteSchema-1r-ln0Q0.js → BlockNoteSchema-dmbNkHA-.js} +2 -2
  4. package/dist/{BlockNoteSchema-1r-ln0Q0.js.map → BlockNoteSchema-dmbNkHA-.js.map} +1 -1
  5. package/dist/TrailingNode-DHOdUVUO.cjs +2 -0
  6. package/dist/TrailingNode-DHOdUVUO.cjs.map +1 -0
  7. package/dist/{TrailingNode-DZag-Nvu.js → TrailingNode-F9hX_UlQ.js} +5 -3
  8. package/dist/TrailingNode-F9hX_UlQ.js.map +1 -0
  9. package/dist/blocknote.cjs +4 -4
  10. package/dist/blocknote.cjs.map +1 -1
  11. package/dist/blocknote.js +1169 -954
  12. package/dist/blocknote.js.map +1 -1
  13. package/dist/blocks.cjs +1 -1
  14. package/dist/blocks.js +2 -2
  15. package/dist/{defaultBlocks-D049Pbme.cjs → defaultBlocks-CSB5GiAu.cjs} +5 -5
  16. package/dist/defaultBlocks-CSB5GiAu.cjs.map +1 -0
  17. package/dist/{defaultBlocks-BSOEW3GR.js → defaultBlocks-Caw1U1oV.js} +47 -44
  18. package/dist/defaultBlocks-Caw1U1oV.js.map +1 -0
  19. package/dist/extensions.cjs +1 -1
  20. package/dist/extensions.js +3 -3
  21. package/dist/tsconfig.tsbuildinfo +1 -1
  22. package/dist/webpack-stats.json +1 -1
  23. package/package.json +1 -1
  24. package/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts +30 -7
  25. package/src/extensions/Collaboration/YCursorPlugin.ts +3 -1
  26. package/src/extensions/SuggestionMenu/SuggestionMenu.test.ts +191 -0
  27. package/src/extensions/SuggestionMenu/SuggestionMenu.ts +28 -11
  28. package/src/extensions/tiptap-extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts +433 -53
  29. package/src/schema/blocks/createSpec.ts +2 -0
  30. package/types/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.d.ts +5 -0
  31. package/types/src/extensions/SuggestionMenu/SuggestionMenu.d.ts +12 -3
  32. package/types/src/extensions/SuggestionMenu/SuggestionMenu.test.d.ts +1 -0
  33. package/dist/TrailingNode-DZag-Nvu.js.map +0 -1
  34. package/dist/TrailingNode-tesI8f7N.cjs +0 -2
  35. package/dist/TrailingNode-tesI8f7N.cjs.map +0 -1
  36. package/dist/defaultBlocks-BSOEW3GR.js.map +0 -1
  37. package/dist/defaultBlocks-D049Pbme.cjs.map +0 -1
@@ -1,8 +1,11 @@
1
1
  import { Extension } from "@tiptap/core";
2
-
2
+ import { Fragment, Node } from "prosemirror-model";
3
3
  import { TextSelection } from "prosemirror-state";
4
+
4
5
  import {
5
6
  getBottomNestedBlockInfo,
7
+ getNextBlockInfo,
8
+ getParentBlockInfo,
6
9
  getPrevBlockInfo,
7
10
  mergeBlocksCommand,
8
11
  } from "../../../api/blockManipulation/commands/mergeBlocks/mergeBlocks.js";
@@ -10,7 +13,10 @@ import { nestBlock } from "../../../api/blockManipulation/commands/nestBlock/nes
10
13
  import { fixColumnList } from "../../../api/blockManipulation/commands/replaceBlocks/util/fixColumnList.js";
11
14
  import { splitBlockCommand } from "../../../api/blockManipulation/commands/splitBlock/splitBlock.js";
12
15
  import { updateBlockCommand } from "../../../api/blockManipulation/commands/updateBlock/updateBlock.js";
13
- import { getBlockInfoFromSelection } from "../../../api/getBlockInfoFromPos.js";
16
+ import {
17
+ getBlockInfoFromResolvedPos,
18
+ getBlockInfoFromSelection,
19
+ } from "../../../api/getBlockInfoFromPos.js";
14
20
  import { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js";
15
21
  import { FormattingToolbarExtension } from "../../FormattingToolbar/FormattingToolbar.js";
16
22
  import { FilePanelExtension } from "../../FilePanel/FilePanel.js";
@@ -83,6 +89,21 @@ export const KeyboardShortcutsExtension = Extension.create<{
83
89
  }
84
90
  const { bnBlock: blockContainer, blockContent } = blockInfo;
85
91
 
92
+ const prevBlockInfo = getPrevBlockInfo(
93
+ state.doc,
94
+ blockInfo.bnBlock.beforePos,
95
+ );
96
+ // If the previous block has no inline content, it can't be merged.
97
+ // It's instead deleted, which is done later in the chan, so we
98
+ // return early here.
99
+ if (
100
+ !prevBlockInfo ||
101
+ !prevBlockInfo.isBlockContainer ||
102
+ prevBlockInfo.blockContent.node.type.spec.content !== "inline*"
103
+ ) {
104
+ return false;
105
+ }
106
+
86
107
  const selectionAtBlockStart =
87
108
  state.selection.from === blockContent.beforePos + 1;
88
109
  const selectionEmpty = state.selection.empty;
@@ -98,9 +119,46 @@ export const KeyboardShortcutsExtension = Extension.create<{
98
119
 
99
120
  return false;
100
121
  }),
122
+ // If the previous block is a columnList, moves the current block to
123
+ // the end of the last column in it.
124
+ () =>
125
+ commands.command(({ state, tr, dispatch }) => {
126
+ const blockInfo = getBlockInfoFromSelection(state);
127
+ if (!blockInfo.isBlockContainer) {
128
+ return false;
129
+ }
130
+
131
+ const prevBlockInfo = getPrevBlockInfo(
132
+ state.doc,
133
+ blockInfo.bnBlock.beforePos,
134
+ );
135
+ if (!prevBlockInfo || prevBlockInfo.isBlockContainer) {
136
+ return false;
137
+ }
138
+
139
+ if (dispatch) {
140
+ const columnAfterPos = prevBlockInfo.bnBlock.afterPos - 1;
141
+ const $blockAfterPos = tr.doc.resolve(columnAfterPos - 1);
142
+
143
+ tr.delete(
144
+ blockInfo.bnBlock.beforePos,
145
+ blockInfo.bnBlock.afterPos,
146
+ );
147
+ tr.insert($blockAfterPos.pos, blockInfo.bnBlock.node);
148
+ tr.setSelection(
149
+ TextSelection.near(tr.doc.resolve($blockAfterPos.pos + 1)),
150
+ );
151
+
152
+ return true;
153
+ }
154
+
155
+ return false;
156
+ }),
157
+ // If the block is the first in a column, moves it to the end of the
158
+ // previous column. If there is no previous column, moves it above the
159
+ // columnList.
101
160
  () =>
102
161
  commands.command(({ state, tr, dispatch }) => {
103
- // when at the start of a first block in a column
104
162
  const blockInfo = getBlockInfoFromSelection(state);
105
163
  if (!blockInfo.isBlockContainer) {
106
164
  return false;
@@ -116,7 +174,6 @@ export const KeyboardShortcutsExtension = Extension.create<{
116
174
 
117
175
  const prevBlock = $pos.nodeBefore;
118
176
  if (prevBlock) {
119
- // should be no previous block
120
177
  return false;
121
178
  }
122
179
 
@@ -130,31 +187,22 @@ export const KeyboardShortcutsExtension = Extension.create<{
130
187
  const columnListPos = $columnPos.before();
131
188
 
132
189
  if (dispatch) {
133
- const fragment = tr.doc.slice(
134
- blockInfo.bnBlock.beforePos,
135
- blockInfo.bnBlock.afterPos,
136
- ).content;
137
-
138
190
  tr.delete(
139
191
  blockInfo.bnBlock.beforePos,
140
192
  blockInfo.bnBlock.afterPos,
141
193
  );
194
+ fixColumnList(tr, columnListPos);
142
195
 
143
- if ($columnPos.index() === 0) {
144
- // Fix `columnList` and insert the block before it.
145
- fixColumnList(tr, columnListPos);
146
- tr.insert(columnListPos, fragment);
196
+ if ($columnPos.pos === columnListPos + 1) {
197
+ tr.insert(columnListPos, blockInfo.bnBlock.node);
147
198
  tr.setSelection(
148
199
  TextSelection.near(tr.doc.resolve(columnListPos)),
149
200
  );
150
201
  } else {
151
- // Insert the block at the end of the first column and fix
152
- // `columnList`.
153
- tr.insert($columnPos.pos - 1, fragment);
202
+ tr.insert($columnPos.pos - 1, blockInfo.bnBlock.node);
154
203
  tr.setSelection(
155
- TextSelection.near(tr.doc.resolve($columnPos.pos - 1)),
204
+ TextSelection.near(tr.doc.resolve($columnPos.pos)),
156
205
  );
157
- fixColumnList(tr, columnListPos);
158
206
  }
159
207
  }
160
208
 
@@ -208,12 +256,8 @@ export const KeyboardShortcutsExtension = Extension.create<{
208
256
  } else if (
209
257
  prevBlockInfo.blockContent.node.type.spec.content === ""
210
258
  ) {
211
- const nonEditableBlockContentStartPos =
212
- prevBlockInfo.blockContent.afterPos -
213
- prevBlockInfo.blockContent.node.nodeSize;
214
-
215
259
  chainedCommands = chainedCommands.setNodeSelection(
216
- nonEditableBlockContentStartPos,
260
+ prevBlockInfo.blockContent.beforePos,
217
261
  );
218
262
  } else {
219
263
  const blockContentStartPos =
@@ -242,8 +286,7 @@ export const KeyboardShortcutsExtension = Extension.create<{
242
286
  const blockInfo = getBlockInfoFromSelection(state);
243
287
 
244
288
  if (!blockInfo.isBlockContainer) {
245
- // TODO
246
- throw new Error(`todo`);
289
+ return false;
247
290
  }
248
291
 
249
292
  const selectionAtBlockStart =
@@ -262,8 +305,7 @@ export const KeyboardShortcutsExtension = Extension.create<{
262
305
  );
263
306
 
264
307
  if (!bottomBlock.isBlockContainer) {
265
- // TODO
266
- throw new Error(`todo`);
308
+ return false;
267
309
  }
268
310
 
269
311
  const prevBlockNotTableAndNoContent =
@@ -294,50 +336,388 @@ export const KeyboardShortcutsExtension = Extension.create<{
294
336
  ]);
295
337
 
296
338
  const handleDelete = () =>
297
- this.editor.commands.first(({ commands }) => [
339
+ this.editor.commands.first(({ chain, commands }) => [
298
340
  // Deletes the selection if it's not empty.
299
341
  () => commands.deleteSelection(),
342
+ // Deletes the first child block and un-nests its children, if the
343
+ // selection is empty and at the end of the current block. If both the
344
+ // parent and child blocks have inline content, the child block's
345
+ // content is appended to the parent's. The child block's own children
346
+ // are unindented before it's deleted.
347
+ () =>
348
+ commands.command(({ state }) => {
349
+ const blockInfo = getBlockInfoFromSelection(state);
350
+ if (!blockInfo.isBlockContainer || !blockInfo.childContainer) {
351
+ return false;
352
+ }
353
+ const { blockContent, childContainer } = blockInfo;
354
+
355
+ const selectionAtBlockEnd =
356
+ state.selection.from === blockContent.afterPos - 1;
357
+ const selectionEmpty = state.selection.empty;
358
+
359
+ const firstChildBlockInfo = getBlockInfoFromResolvedPos(
360
+ state.doc.resolve(childContainer.beforePos + 1),
361
+ );
362
+ if (!firstChildBlockInfo.isBlockContainer) {
363
+ return false;
364
+ }
365
+
366
+ if (selectionAtBlockEnd && selectionEmpty) {
367
+ const firstChildBlockContent =
368
+ firstChildBlockInfo.blockContent.node;
369
+ const firstChildBlockHasInlineContent =
370
+ firstChildBlockContent.type.spec.content === "inline*";
371
+ const blockHasInlineContent =
372
+ blockContent.node.type.spec.content === "inline*";
373
+
374
+ return (
375
+ chain()
376
+ // Un-nests child block's children if necessary.
377
+ .insertContentAt(
378
+ firstChildBlockInfo.bnBlock.afterPos,
379
+ firstChildBlockInfo.childContainer?.node.content ||
380
+ Fragment.empty,
381
+ )
382
+ .deleteRange(
383
+ // Deletes whole child container if there's only one child.
384
+ childContainer.node.childCount === 1
385
+ ? {
386
+ from: childContainer.beforePos,
387
+ to: childContainer.afterPos,
388
+ }
389
+ : {
390
+ from: firstChildBlockInfo.bnBlock.beforePos,
391
+ to: firstChildBlockInfo.bnBlock.afterPos,
392
+ },
393
+ )
394
+ // Appends inline content from child block if possible.
395
+ .insertContentAt(
396
+ state.selection.from,
397
+ firstChildBlockHasInlineContent && blockHasInlineContent
398
+ ? firstChildBlockContent.content
399
+ : null,
400
+ )
401
+ .setTextSelection(state.selection.from)
402
+ .scrollIntoView()
403
+ .run()
404
+ );
405
+ }
406
+
407
+ return false;
408
+ }),
300
409
  // Merges block with the next one (at the same nesting level or lower),
301
410
  // if one exists, the block has no children, and the selection is at the
302
411
  // end of the block.
303
412
  () =>
304
413
  commands.command(({ state }) => {
305
- // TODO: Change this to not rely on offsets & schema assumptions
306
414
  const blockInfo = getBlockInfoFromSelection(state);
307
415
  if (!blockInfo.isBlockContainer) {
308
416
  return false;
309
417
  }
310
- const {
311
- bnBlock: blockContainer,
312
- blockContent,
313
- childContainer,
314
- } = blockInfo;
418
+ const { bnBlock: blockContainer, blockContent } = blockInfo;
419
+
420
+ const nextBlockInfo = getNextBlockInfo(
421
+ state.doc,
422
+ blockInfo.bnBlock.beforePos,
423
+ );
424
+ if (!nextBlockInfo || !nextBlockInfo.isBlockContainer) {
425
+ return false;
426
+ }
315
427
 
316
- const { depth } = state.doc.resolve(blockContainer.beforePos);
317
- const blockAtDocEnd =
318
- blockContainer.afterPos === state.doc.nodeSize - 3;
319
428
  const selectionAtBlockEnd =
320
429
  state.selection.from === blockContent.afterPos - 1;
321
430
  const selectionEmpty = state.selection.empty;
322
- const hasChildBlocks = childContainer !== undefined;
323
431
 
324
- if (
325
- !blockAtDocEnd &&
326
- selectionAtBlockEnd &&
327
- selectionEmpty &&
328
- !hasChildBlocks
329
- ) {
330
- let oldDepth = depth;
331
- let newPos = blockContainer.afterPos + 1;
332
- let newDepth = state.doc.resolve(newPos).depth;
333
-
334
- while (newDepth < oldDepth) {
335
- oldDepth = newDepth;
336
- newPos += 2;
337
- newDepth = state.doc.resolve(newPos).depth;
432
+ const posBetweenBlocks = blockContainer.afterPos;
433
+
434
+ if (selectionAtBlockEnd && selectionEmpty) {
435
+ return chain()
436
+ .command(mergeBlocksCommand(posBetweenBlocks))
437
+ .scrollIntoView()
438
+ .run();
439
+ }
440
+
441
+ return false;
442
+ }),
443
+ // If the previous block is a columnList, moves the current block to
444
+ // the end of the last column in it.
445
+ () =>
446
+ commands.command(({ state, tr, dispatch }) => {
447
+ const blockInfo = getBlockInfoFromSelection(state);
448
+ if (!blockInfo.isBlockContainer) {
449
+ return false;
450
+ }
451
+
452
+ const nextBlockInfo = getNextBlockInfo(
453
+ state.doc,
454
+ blockInfo.bnBlock.beforePos,
455
+ );
456
+ if (!nextBlockInfo || nextBlockInfo.isBlockContainer) {
457
+ return false;
458
+ }
459
+
460
+ if (dispatch) {
461
+ const columnBeforePos = nextBlockInfo.bnBlock.beforePos + 1;
462
+ const $blockBeforePos = tr.doc.resolve(columnBeforePos + 1);
463
+
464
+ tr.delete(
465
+ $blockBeforePos.pos,
466
+ $blockBeforePos.pos + $blockBeforePos.nodeAfter!.nodeSize,
467
+ );
468
+ fixColumnList(tr, nextBlockInfo.bnBlock.beforePos);
469
+ tr.insert(blockInfo.bnBlock.afterPos, $blockBeforePos.nodeAfter!);
470
+ tr.setSelection(
471
+ TextSelection.near(tr.doc.resolve($blockBeforePos.pos)),
472
+ );
473
+
474
+ return true;
475
+ }
476
+
477
+ return false;
478
+ }),
479
+ // If the block is the last in a column, moves it to the start of the
480
+ // next column. If there is no next column, moves it below the
481
+ // columnList.
482
+ () =>
483
+ commands.command(({ state, tr, dispatch }) => {
484
+ const blockInfo = getBlockInfoFromSelection(state);
485
+ if (!blockInfo.isBlockContainer) {
486
+ return false;
487
+ }
488
+
489
+ const selectionAtBlockEnd =
490
+ tr.selection.from === blockInfo.blockContent.afterPos - 1;
491
+ if (!selectionAtBlockEnd) {
492
+ return false;
493
+ }
494
+
495
+ const $pos = tr.doc.resolve(blockInfo.bnBlock.afterPos);
496
+
497
+ const nextBlock = $pos.nodeAfter;
498
+ if (nextBlock) {
499
+ return false;
500
+ }
501
+
502
+ const parentBlock = $pos.node();
503
+ if (parentBlock.type.name !== "column") {
504
+ return false;
505
+ }
506
+
507
+ const $blockEndPos = tr.doc.resolve(blockInfo.bnBlock.afterPos);
508
+ const $columnEndPos = tr.doc.resolve($blockEndPos.after());
509
+ const columnListEndPos = $columnEndPos.after();
510
+
511
+ if (dispatch) {
512
+ // Position before first block in next column, or first block
513
+ // after columnList if there is no next column.
514
+ const nextBlockBeforePos =
515
+ $columnEndPos.pos === columnListEndPos - 1
516
+ ? columnListEndPos
517
+ : $columnEndPos.pos + 1;
518
+ const nextBlockInfo = getBlockInfoFromResolvedPos(
519
+ tr.doc.resolve(nextBlockBeforePos),
520
+ );
521
+
522
+ tr.delete(
523
+ nextBlockInfo.bnBlock.beforePos,
524
+ nextBlockInfo.bnBlock.afterPos,
525
+ );
526
+ fixColumnList(
527
+ tr,
528
+ columnListEndPos - $columnEndPos.node().nodeSize,
529
+ );
530
+ tr.insert($blockEndPos.pos, nextBlockInfo.bnBlock.node);
531
+ tr.setSelection(
532
+ TextSelection.near(tr.doc.resolve(nextBlockBeforePos)),
533
+ );
534
+ }
535
+
536
+ return true;
537
+ }),
538
+ // Deletes the next block at either the same or lower nesting level, if
539
+ // the selection is empty and at the end of the block. If both the
540
+ // current and next blocks have inline content, the next block's
541
+ // content is appended to the current block's. The next block's own
542
+ // children are unindented before it's deleted.
543
+ () =>
544
+ commands.command(({ state }) => {
545
+ const blockInfo = getBlockInfoFromSelection(state);
546
+ if (!blockInfo.isBlockContainer) {
547
+ return false;
548
+ }
549
+ const { blockContent } = blockInfo;
550
+
551
+ const selectionAtBlockEnd =
552
+ state.selection.from === blockContent.afterPos - 1;
553
+ const selectionEmpty = state.selection.empty;
554
+
555
+ if (selectionAtBlockEnd && selectionEmpty) {
556
+ const getNextBlockInfoAtAnyLevel = (
557
+ doc: Node,
558
+ beforePos: number,
559
+ ) => {
560
+ const nextBlockInfo = getNextBlockInfo(doc, beforePos);
561
+ if (nextBlockInfo) {
562
+ return nextBlockInfo;
563
+ }
564
+
565
+ const parentBlockInfo = getParentBlockInfo(doc, beforePos);
566
+ if (!parentBlockInfo) {
567
+ return undefined;
568
+ }
569
+
570
+ return getNextBlockInfoAtAnyLevel(
571
+ doc,
572
+ parentBlockInfo.bnBlock.beforePos,
573
+ );
574
+ };
575
+
576
+ const nextBlockInfo = getNextBlockInfoAtAnyLevel(
577
+ state.doc,
578
+ blockInfo.bnBlock.beforePos,
579
+ );
580
+ if (!nextBlockInfo || !nextBlockInfo.isBlockContainer) {
581
+ return false;
338
582
  }
339
583
 
340
- return commands.command(mergeBlocksCommand(newPos - 1));
584
+ const nextBlockContent = nextBlockInfo.blockContent.node;
585
+ const nextBlockHasInlineContent =
586
+ nextBlockContent.type.spec.content === "inline*";
587
+ const blockHasInlineContent =
588
+ blockContent.node.type.spec.content === "inline*";
589
+
590
+ return (
591
+ chain()
592
+ // Un-nests next block's children if necessary.
593
+ .insertContentAt(
594
+ nextBlockInfo.bnBlock.afterPos,
595
+ nextBlockInfo.childContainer?.node.content ||
596
+ Fragment.empty,
597
+ )
598
+ .deleteRange({
599
+ from: nextBlockInfo.bnBlock.beforePos,
600
+ to: nextBlockInfo.bnBlock.afterPos,
601
+ })
602
+ // Appends inline content from child block if possible.
603
+ .insertContentAt(
604
+ state.selection.from,
605
+ nextBlockHasInlineContent && blockHasInlineContent
606
+ ? nextBlockContent.content
607
+ : null,
608
+ )
609
+ .setTextSelection(state.selection.from)
610
+ .scrollIntoView()
611
+ .run()
612
+ );
613
+ }
614
+
615
+ return false;
616
+ }),
617
+ // Deletes the current block if it's an empty block with inline content,
618
+ // and moves the selection to the next block.
619
+ () =>
620
+ commands.command(({ state }) => {
621
+ const blockInfo = getBlockInfoFromSelection(state);
622
+ if (!blockInfo.isBlockContainer) {
623
+ return false;
624
+ }
625
+
626
+ const blockEmpty =
627
+ blockInfo.blockContent.node.childCount === 0 &&
628
+ blockInfo.blockContent.node.type.spec.content === "inline*";
629
+
630
+ if (blockEmpty) {
631
+ const nextBlockInfo = getNextBlockInfo(
632
+ state.doc,
633
+ blockInfo.bnBlock.beforePos,
634
+ );
635
+ if (!nextBlockInfo || !nextBlockInfo.isBlockContainer) {
636
+ return false;
637
+ }
638
+
639
+ let chainedCommands = chain();
640
+
641
+ if (
642
+ nextBlockInfo.blockContent.node.type.spec.content ===
643
+ "tableRow+"
644
+ ) {
645
+ const tableBlockStartPos = blockInfo.bnBlock.afterPos + 1;
646
+ const tableBlockContentStartPos = tableBlockStartPos + 1;
647
+ const firstRowStartPos = tableBlockContentStartPos + 1;
648
+ const firstCellStartPos = firstRowStartPos + 1;
649
+ const firstCellParagraphStartPos = firstCellStartPos + 1;
650
+
651
+ chainedCommands = chainedCommands.setTextSelection(
652
+ firstCellParagraphStartPos,
653
+ );
654
+ } else if (
655
+ nextBlockInfo.blockContent.node.type.spec.content === ""
656
+ ) {
657
+ chainedCommands = chainedCommands.setNodeSelection(
658
+ nextBlockInfo.blockContent.beforePos,
659
+ );
660
+ } else {
661
+ chainedCommands = chainedCommands.setTextSelection(
662
+ nextBlockInfo.blockContent.beforePos + 1,
663
+ );
664
+ }
665
+
666
+ return chainedCommands
667
+ .deleteRange({
668
+ from: blockInfo.bnBlock.beforePos,
669
+ to: blockInfo.bnBlock.afterPos,
670
+ })
671
+ .scrollIntoView()
672
+ .run();
673
+ }
674
+
675
+ return false;
676
+ }),
677
+ // Deletes next block if it contains no content and isn't a table,
678
+ // when the selection is empty and at the end of the block. Moves the
679
+ // current block into the deleted block's place.
680
+ () =>
681
+ commands.command(({ state }) => {
682
+ const blockInfo = getBlockInfoFromSelection(state);
683
+
684
+ if (!blockInfo.isBlockContainer) {
685
+ return false;
686
+ }
687
+
688
+ const selectionAtBlockEnd =
689
+ state.selection.from === blockInfo.blockContent.afterPos - 1;
690
+ const selectionEmpty = state.selection.empty;
691
+
692
+ const nextBlockInfo = getNextBlockInfo(
693
+ state.doc,
694
+ blockInfo.bnBlock.beforePos,
695
+ );
696
+ if (!nextBlockInfo) {
697
+ return false;
698
+ }
699
+ if (!nextBlockInfo.isBlockContainer) {
700
+ return false;
701
+ }
702
+
703
+ if (nextBlockInfo && selectionAtBlockEnd && selectionEmpty) {
704
+ const nextBlockNotTableAndNoContent =
705
+ nextBlockInfo.blockContent.node.type.spec.content === "" ||
706
+ (nextBlockInfo.blockContent.node.type.spec.content ===
707
+ "inline*" &&
708
+ nextBlockInfo.blockContent.node.childCount === 0);
709
+
710
+ if (nextBlockNotTableAndNoContent) {
711
+ const childBlocks =
712
+ nextBlockInfo.bnBlock.node.lastChild!.content;
713
+ return chain()
714
+ .deleteRange({
715
+ from: nextBlockInfo.bnBlock.beforePos,
716
+ to: nextBlockInfo.bnBlock.afterPos,
717
+ })
718
+ .insertContentAt(blockInfo.bnBlock.afterPos, nextBlockInfo.bnBlock.node.childCount === 2 ? childBlocks : null)
719
+ .run();
720
+ }
341
721
  }
342
722
 
343
723
  return false;
@@ -210,6 +210,8 @@ export function addNodeAndExtensionsToSpec<
210
210
 
211
211
  // See explanation for why `update` is not implemented for NodeViews
212
212
  // https://github.com/TypeCellOS/BlockNote/pull/1904#discussion_r2313461464
213
+ // TODO: in a future version, we might want to implement updates so that
214
+ // vanilla blocks don't always re-render entirely (https://github.com/TypeCellOS/BlockNote/issues/220)
213
215
  return nodeView;
214
216
  };
215
217
  },
@@ -11,6 +11,11 @@ export declare const getParentBlockInfo: (doc: Node, beforePos: number) => Block
11
11
  * or undefined if the given block is the first sibling.
12
12
  */
13
13
  export declare const getPrevBlockInfo: (doc: Node, beforePos: number) => BlockInfo | undefined;
14
+ /**
15
+ * Returns the block info from the sibling block after (below) the given block,
16
+ * or undefined if the given block is the last sibling.
17
+ */
18
+ export declare const getNextBlockInfo: (doc: Node, beforePos: number) => BlockInfo | undefined;
14
19
  /**
15
20
  * If a block has children like this:
16
21
  * A
@@ -1,4 +1,4 @@
1
- import { Plugin } from "prosemirror-state";
1
+ import { Plugin, Transaction } from "prosemirror-state";
2
2
  import { UiElementPosition } from "../../extensions-shared/UiElementPosition.js";
3
3
  export type SuggestionMenuState = UiElementPosition & {
4
4
  query: string;
@@ -12,6 +12,15 @@ type SuggestionPluginState = {
12
12
  decorationId: string;
13
13
  ignoreQueryLength?: boolean;
14
14
  } | undefined;
15
+ export type SuggestionMenuOptions = {
16
+ triggerCharacter: string;
17
+ /**
18
+ * Optional callback to determine whether the suggestion menu should be
19
+ * opened in the current editor state. Return `false` to prevent the
20
+ * menu from opening (e.g. when the cursor is inside table content).
21
+ */
22
+ shouldOpen?: (tr: Transaction) => boolean;
23
+ };
15
24
  /**
16
25
  * A ProseMirror plugin for suggestions, designed to make '/'-commands possible as well as mentions.
17
26
  *
@@ -40,8 +49,8 @@ export declare const SuggestionMenu: (options?: any) => import("../../index.js")
40
49
  } & {
41
50
  triggerCharacter: string;
42
51
  }) | undefined>;
43
- readonly addTriggerCharacter: (triggerCharacter: string) => void;
44
- readonly removeTriggerCharacter: (triggerCharacter: string) => void;
52
+ readonly addSuggestionMenu: (options: SuggestionMenuOptions) => void;
53
+ readonly removeSuggestionMenu: (triggerCharacter: string) => void;
45
54
  readonly closeMenu: () => void;
46
55
  readonly clearQuery: () => void;
47
56
  readonly shown: () => boolean;