@blocknote/core 0.35.0 → 0.36.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 (72) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +7 -25
  3. package/dist/blocknote.cjs +8 -8
  4. package/dist/blocknote.cjs.map +1 -1
  5. package/dist/blocknote.js +1592 -1458
  6. package/dist/blocknote.js.map +1 -1
  7. package/dist/{en-Dx9fwHD4.js → en-CvDoFvhc.js} +4 -1
  8. package/dist/en-CvDoFvhc.js.map +1 -0
  9. package/dist/{en-CsgPjHa4.cjs → en-ub2yVBX0.cjs} +2 -2
  10. package/dist/en-ub2yVBX0.cjs.map +1 -0
  11. package/dist/locales.cjs +1 -1
  12. package/dist/locales.cjs.map +1 -1
  13. package/dist/locales.js +62 -2
  14. package/dist/locales.js.map +1 -1
  15. package/dist/style.css +1 -1
  16. package/dist/tsconfig.tsbuildinfo +1 -1
  17. package/dist/webpack-stats.json +1 -1
  18. package/package.json +17 -16
  19. package/src/api/__snapshots__/blocks-moved-down-twice-in-same-parent.json +44 -0
  20. package/src/api/__snapshots__/blocks-moved-insert-changes-sibling-order.json +26 -0
  21. package/src/api/__snapshots__/blocks-moved-nested-sibling-reorder.json +180 -0
  22. package/src/api/__snapshots__/blocks-moved-up-down-in-same-parent.json +44 -0
  23. package/src/api/__snapshots__/blocks-moved-up-down-in-same-transaction.json +44 -0
  24. package/src/api/blockManipulation/commands/updateBlock/updateBlock.ts +132 -4
  25. package/src/api/clipboard/toClipboard/copyExtension.ts +2 -0
  26. package/src/api/{nodeUtil.test.ts → getBlocksChangedByTransaction.test.ts} +117 -1
  27. package/src/api/getBlocksChangedByTransaction.ts +422 -0
  28. package/src/api/nodeUtil.ts +0 -250
  29. package/src/blocks/ImageBlockContent/parseImageElement.ts +2 -1
  30. package/src/blocks/TableBlockContent/TableBlockContent.ts +31 -4
  31. package/src/blocks/ToggleWrapper/createToggleWrapper.ts +2 -1
  32. package/src/editor/BlockNoteEditor.ts +1 -1
  33. package/src/editor/editor.css +8 -17
  34. package/src/extensions/BlockChange/BlockChangePlugin.ts +4 -2
  35. package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +53 -1
  36. package/src/extensions/SideMenu/SideMenuPlugin.ts +6 -15
  37. package/src/i18n/locales/ar.ts +3 -0
  38. package/src/i18n/locales/de.ts +4 -0
  39. package/src/i18n/locales/en.ts +3 -0
  40. package/src/i18n/locales/es.ts +3 -0
  41. package/src/i18n/locales/fr.ts +3 -0
  42. package/src/i18n/locales/he.ts +3 -0
  43. package/src/i18n/locales/hr.ts +3 -0
  44. package/src/i18n/locales/is.ts +3 -0
  45. package/src/i18n/locales/it.ts +3 -0
  46. package/src/i18n/locales/ja.ts +3 -0
  47. package/src/i18n/locales/ko.ts +4 -1
  48. package/src/i18n/locales/nl.ts +3 -0
  49. package/src/i18n/locales/no.ts +3 -0
  50. package/src/i18n/locales/pl.ts +4 -0
  51. package/src/i18n/locales/pt.ts +3 -0
  52. package/src/i18n/locales/ru.ts +3 -0
  53. package/src/i18n/locales/sk.ts +3 -0
  54. package/src/i18n/locales/uk.ts +3 -0
  55. package/src/i18n/locales/vi.ts +3 -0
  56. package/src/i18n/locales/zh-tw.ts +3 -0
  57. package/src/i18n/locales/zh.ts +3 -0
  58. package/src/index.ts +1 -0
  59. package/types/src/api/blockManipulation/commands/updateBlock/updateBlock.d.ts +16 -3
  60. package/types/src/api/getBlocksChangedByTransaction.d.ts +63 -0
  61. package/types/src/api/nodeUtil.d.ts +0 -63
  62. package/types/src/blocks/ImageBlockContent/parseImageElement.d.ts +1 -0
  63. package/types/src/editor/BlockNoteEditor.d.ts +1 -1
  64. package/types/src/extensions/BlockChange/BlockChangePlugin.d.ts +1 -1
  65. package/types/src/extensions/TableHandles/TableHandlesPlugin.d.ts +2 -2
  66. package/types/src/i18n/locales/en.d.ts +3 -0
  67. package/types/src/i18n/locales/sk.d.ts +3 -0
  68. package/types/src/index.d.ts +1 -0
  69. package/types/src/schema/inlineContent/internal.d.ts +1 -1
  70. package/dist/en-CsgPjHa4.cjs.map +0 -1
  71. package/dist/en-Dx9fwHD4.js.map +0 -1
  72. /package/types/src/api/{nodeUtil.test.d.ts → getBlocksChangedByTransaction.test.d.ts} +0 -0
@@ -4,9 +4,10 @@ import {
4
4
  type Node as PMNode,
5
5
  Slice,
6
6
  } from "prosemirror-model";
7
- import type { Transaction } from "prosemirror-state";
8
-
7
+ import { TextSelection, Transaction } from "prosemirror-state";
8
+ import { TableMap } from "prosemirror-tables";
9
9
  import { ReplaceStep, Transform } from "prosemirror-transform";
10
+
10
11
  import type { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js";
11
12
  import type {
12
13
  BlockIdentifier,
@@ -56,7 +57,7 @@ export function updateBlockTr<
56
57
  I extends InlineContentSchema,
57
58
  S extends StyleSchema,
58
59
  >(
59
- tr: Transform,
60
+ tr: Transform | Transaction,
60
61
  posBeforeBlock: number,
61
62
  block: PartialBlock<BSchema, I, S>,
62
63
  replaceFromPos?: number,
@@ -64,6 +65,11 @@ export function updateBlockTr<
64
65
  ) {
65
66
  const blockInfo = getBlockInfoFromResolvedPos(tr.doc.resolve(posBeforeBlock));
66
67
 
68
+ let cellAnchor: CellAnchor | null = null;
69
+ if (blockInfo.blockNoteType === "table") {
70
+ cellAnchor = captureCellAnchor(tr);
71
+ }
72
+
67
73
  const pmSchema = getPmSchema(tr);
68
74
 
69
75
  if (
@@ -143,6 +149,10 @@ export function updateBlockTr<
143
149
  ...blockInfo.bnBlock.node.attrs,
144
150
  ...block.props,
145
151
  });
152
+
153
+ if (cellAnchor) {
154
+ restoreCellAnchor(tr, blockInfo, cellAnchor);
155
+ }
146
156
  }
147
157
 
148
158
  function updateBlockContentNode<
@@ -301,7 +311,7 @@ export function updateBlock<
301
311
  I extends InlineContentSchema = any,
302
312
  S extends StyleSchema = any,
303
313
  >(
304
- tr: Transaction,
314
+ tr: Transform,
305
315
  blockToUpdate: BlockIdentifier,
306
316
  update: PartialBlock<BSchema, I, S>,
307
317
  replaceFromPos?: number,
@@ -329,3 +339,121 @@ export function updateBlock<
329
339
  const pmSchema = getPmSchema(tr);
330
340
  return nodeToBlock(blockContainerNode, pmSchema);
331
341
  }
342
+
343
+ type CellAnchor = { row: number; col: number; offset: number };
344
+
345
+ /**
346
+ * Captures the cell anchor from the current selection.
347
+ * @param tr - The transaction to capture the cell anchor from.
348
+ *
349
+ * @returns The cell anchor, or null if no cell is selected.
350
+ */
351
+ export function captureCellAnchor(tr: Transform): CellAnchor | null {
352
+ const sel = "selection" in tr ? tr.selection : null;
353
+ if (!(sel instanceof TextSelection)) {
354
+ return null;
355
+ }
356
+
357
+ const $head = tr.doc.resolve(sel.head);
358
+ // Find enclosing cell and table
359
+ let cellDepth = -1;
360
+ let tableDepth = -1;
361
+ for (let d = $head.depth; d >= 0; d--) {
362
+ const name = $head.node(d).type.name;
363
+ if (cellDepth < 0 && (name === "tableCell" || name === "tableHeader")) {
364
+ cellDepth = d;
365
+ }
366
+ if (name === "table") {
367
+ tableDepth = d;
368
+ break;
369
+ }
370
+ }
371
+ if (cellDepth < 0 || tableDepth < 0) {
372
+ return null;
373
+ }
374
+
375
+ // Absolute positions (before the cell)
376
+ const cellPos = $head.before(cellDepth);
377
+ const tablePos = $head.before(tableDepth);
378
+ const table = tr.doc.nodeAt(tablePos);
379
+ if (!table || table.type.name !== "table") {
380
+ return null;
381
+ }
382
+
383
+ // Visual grid position via TableMap (handles spans)
384
+ const map = TableMap.get(table);
385
+ const rel = cellPos - (tablePos + 1); // relative to inside table
386
+ const idx = map.map.indexOf(rel);
387
+ if (idx < 0) {
388
+ return null;
389
+ }
390
+
391
+ const row = Math.floor(idx / map.width);
392
+ const col = idx % map.width;
393
+
394
+ // Caret offset relative to the start of paragraph text
395
+ const paraPos = cellPos + 1; // pos BEFORE tableParagraph
396
+ const textStart = paraPos + 1; // start of paragraph text
397
+ const offset = Math.max(0, sel.head - textStart);
398
+
399
+ return { row, col, offset };
400
+ }
401
+
402
+ function restoreCellAnchor(
403
+ tr: Transform | Transaction,
404
+ blockInfo: BlockInfo,
405
+ a: CellAnchor,
406
+ ): boolean {
407
+ if (blockInfo.blockNoteType !== "table") {
408
+ return false;
409
+ }
410
+
411
+ // 1) Resolve the table node in the current document
412
+ let tablePos = -1;
413
+
414
+ if (blockInfo.isBlockContainer) {
415
+ // Prefer the blockContent position when available (points directly at the PM table node)
416
+ tablePos = tr.mapping.map(blockInfo.blockContent.beforePos);
417
+ } else {
418
+ // Fallback: scan within the mapped bnBlock range to find the inner table node
419
+ const start = tr.mapping.map(blockInfo.bnBlock.beforePos);
420
+ const end = start + (tr.doc.nodeAt(start)?.nodeSize || 0);
421
+ tr.doc.nodesBetween(start, end, (node, pos) => {
422
+ if (node.type.name === "table") {
423
+ tablePos = pos;
424
+ return false;
425
+ }
426
+ return true;
427
+ });
428
+ }
429
+
430
+ const table = tablePos >= 0 ? tr.doc.nodeAt(tablePos) : null;
431
+ if (!table || table.type.name !== "table") {
432
+ return false;
433
+ }
434
+
435
+ // 2) Clamp row/col to the table’s current grid
436
+ const map = TableMap.get(table);
437
+ const row = Math.max(0, Math.min(a.row, map.height - 1));
438
+ const col = Math.max(0, Math.min(a.col, map.width - 1));
439
+
440
+ // 3) Compute the absolute position of the target cell (pos BEFORE the cell)
441
+ const cellIndex = row * map.width + col;
442
+ const relCellPos = map.map[cellIndex]; // relative to (tablePos + 1)
443
+ if (relCellPos == null) {
444
+ return false;
445
+ }
446
+ const cellPos = tablePos + 1 + relCellPos;
447
+
448
+ // 4) Place the caret inside the cell, clamping the text offset
449
+ const textPos = cellPos + 1;
450
+ const textNode = tr.doc.nodeAt(textPos);
451
+ const textStart = textPos + 1;
452
+ const max = textNode ? textNode.content.size : 0;
453
+ const head = textStart + Math.max(0, Math.min(a.offset, max));
454
+
455
+ if ("selection" in tr) {
456
+ tr.setSelection(TextSelection.create(tr.doc, head));
457
+ }
458
+ return true;
459
+ }
@@ -17,6 +17,7 @@ import {
17
17
  contentNodeToInlineContent,
18
18
  contentNodeToTableContent,
19
19
  } from "../../nodeConversions/nodeToBlock.js";
20
+ import { initializeESMDependencies } from "../../../util/esmDependencies.js";
20
21
 
21
22
  function fragmentToExternalHTML<
22
23
  BSchema extends BlockSchema,
@@ -208,6 +209,7 @@ export const createCopyToClipboardExtension = <
208
209
  Extension.create<{ editor: BlockNoteEditor<BSchema, I, S> }, undefined>({
209
210
  name: "copyToClipboard",
210
211
  addProseMirrorPlugins() {
212
+ initializeESMDependencies();
211
213
  return [
212
214
  new Plugin({
213
215
  props: {
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it, beforeEach } from "vitest";
2
2
 
3
3
  import { setupTestEnv } from "./blockManipulation/setupTestEnv.js";
4
- import { getBlocksChangedByTransaction } from "./nodeUtil.js";
4
+ import { getBlocksChangedByTransaction } from "./getBlocksChangedByTransaction.js";
5
5
  import { BlockNoteEditor } from "../editor/BlockNoteEditor.js";
6
6
 
7
7
  const getEditor = setupTestEnv();
@@ -452,4 +452,120 @@ describe("getBlocksChangedByTransaction", () => {
452
452
  "__snapshots__/blocks-moved-multiple-in-same-transaction.json",
453
453
  );
454
454
  });
455
+
456
+ it("should return blocks which have been moved up or down in the same transaction", async () => {
457
+ editor.replaceBlocks(editor.document, [
458
+ {
459
+ id: "top",
460
+ type: "paragraph",
461
+ content: "Top",
462
+ },
463
+ {
464
+ id: "middle",
465
+ type: "paragraph",
466
+ content: "Middle",
467
+ },
468
+ {
469
+ id: "bottom",
470
+ type: "paragraph",
471
+ content: "Bottom",
472
+ },
473
+ ]);
474
+
475
+ const blocksChanged = editor.transact((tr) => {
476
+ editor.setTextCursorPosition("top");
477
+ editor.moveBlocksDown();
478
+
479
+ return getBlocksChangedByTransaction(tr);
480
+ });
481
+
482
+ // Should report a single minimal move within the same parent
483
+ await expect(blocksChanged).toMatchFileSnapshot(
484
+ "__snapshots__/blocks-moved-up-down-in-same-transaction.json",
485
+ );
486
+ });
487
+
488
+ it("should detect moving the bottom block up within the same parent", async () => {
489
+ editor.replaceBlocks(editor.document, [
490
+ { id: "top", type: "paragraph", content: "Top" },
491
+ { id: "middle", type: "paragraph", content: "Middle" },
492
+ { id: "bottom", type: "paragraph", content: "Bottom" },
493
+ ]);
494
+
495
+ const blocksChanged = editor.transact((tr) => {
496
+ editor.setTextCursorPosition("bottom");
497
+ editor.moveBlocksUp();
498
+ return getBlocksChangedByTransaction(tr);
499
+ });
500
+
501
+ await expect(blocksChanged).toMatchFileSnapshot(
502
+ "__snapshots__/blocks-moved-up-down-in-same-parent.json",
503
+ );
504
+ });
505
+
506
+ it("should detect moving a block down twice within the same parent as a single move", async () => {
507
+ editor.replaceBlocks(editor.document, [
508
+ { id: "a", type: "paragraph", content: "A" },
509
+ { id: "b", type: "paragraph", content: "B" },
510
+ { id: "c", type: "paragraph", content: "C" },
511
+ ]);
512
+
513
+ const blocksChanged = editor.transact((tr) => {
514
+ editor.setTextCursorPosition("a");
515
+ editor.moveBlocksDown();
516
+ editor.moveBlocksDown();
517
+ return getBlocksChangedByTransaction(tr);
518
+ });
519
+
520
+ await expect(blocksChanged).toMatchFileSnapshot(
521
+ "__snapshots__/blocks-moved-down-twice-in-same-parent.json",
522
+ );
523
+ });
524
+
525
+ it("should detect nested sibling reorder within the same parent", async () => {
526
+ editor.replaceBlocks(editor.document, [
527
+ {
528
+ id: "parent",
529
+ type: "paragraph",
530
+ content: "Parent",
531
+ children: [
532
+ { id: "child-a", type: "paragraph", content: "A" },
533
+ { id: "child-b", type: "paragraph", content: "B" },
534
+ { id: "child-c", type: "paragraph", content: "C" },
535
+ ],
536
+ },
537
+ { id: "sibling", type: "paragraph", content: "S" },
538
+ ]);
539
+
540
+ const blocksChanged = editor.transact((tr) => {
541
+ editor.setTextCursorPosition("child-a");
542
+ editor.moveBlocksDown();
543
+ return getBlocksChangedByTransaction(tr);
544
+ });
545
+
546
+ await expect(blocksChanged).toMatchFileSnapshot(
547
+ "__snapshots__/blocks-moved-nested-sibling-reorder.json",
548
+ );
549
+ });
550
+
551
+ it("should not report moves when an insert changes sibling order", async () => {
552
+ editor.replaceBlocks(editor.document, [
553
+ { id: "a", type: "paragraph", content: "A" },
554
+ { id: "b", type: "paragraph", content: "B" },
555
+ { id: "c", type: "paragraph", content: "C" },
556
+ ]);
557
+
558
+ const blocksChanged = editor.transact((tr) => {
559
+ editor.insertBlocks(
560
+ [{ id: "x", type: "paragraph", content: "X" }],
561
+ "a",
562
+ "after",
563
+ );
564
+ return getBlocksChangedByTransaction(tr);
565
+ });
566
+
567
+ await expect(blocksChanged).toMatchFileSnapshot(
568
+ "__snapshots__/blocks-moved-insert-changes-sibling-order.json",
569
+ );
570
+ });
455
571
  });