@blocknote/core 0.19.2 → 0.20.0

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 (87) hide show
  1. package/dist/blocknote.js +1721 -1453
  2. package/dist/blocknote.js.map +1 -1
  3. package/dist/blocknote.umd.cjs +7 -7
  4. package/dist/blocknote.umd.cjs.map +1 -1
  5. package/dist/src/api/blockManipulation/commands/insertBlocks/insertBlocks.js +6 -3
  6. package/dist/src/api/blockManipulation/commands/insertBlocks/insertBlocks.js.map +1 -1
  7. package/dist/src/api/blockManipulation/commands/moveBlocks/moveBlocks.js +219 -0
  8. package/dist/src/api/blockManipulation/commands/moveBlocks/moveBlocks.js.map +1 -0
  9. package/dist/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.js +175 -0
  10. package/dist/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.js.map +1 -0
  11. package/dist/src/api/blockManipulation/commands/splitBlock/splitBlock.test.js +3 -0
  12. package/dist/src/api/blockManipulation/commands/splitBlock/splitBlock.test.js.map +1 -1
  13. package/dist/src/api/blockManipulation/commands/updateBlock/updateBlock.js +6 -3
  14. package/dist/src/api/blockManipulation/commands/updateBlock/updateBlock.js.map +1 -1
  15. package/dist/src/api/blockManipulation/getBlock/getBlock.js +56 -0
  16. package/dist/src/api/blockManipulation/getBlock/getBlock.js.map +1 -0
  17. package/dist/src/api/blockManipulation/selections/selection.js +149 -0
  18. package/dist/src/api/blockManipulation/selections/selection.js.map +1 -0
  19. package/dist/src/api/blockManipulation/selections/selection.test.js +39 -0
  20. package/dist/src/api/blockManipulation/selections/selection.test.js.map +1 -0
  21. package/dist/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.js +3 -0
  22. package/dist/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.js.map +1 -1
  23. package/dist/src/api/nodeUtil.js +1 -1
  24. package/dist/src/api/nodeUtil.js.map +1 -1
  25. package/dist/src/blocks/TableBlockContent/TableExtension.js +8 -1
  26. package/dist/src/blocks/TableBlockContent/TableExtension.js.map +1 -1
  27. package/dist/src/editor/BlockNoteEditor.js +56 -57
  28. package/dist/src/editor/BlockNoteEditor.js.map +1 -1
  29. package/dist/src/editor/BlockNoteExtensions.js +1 -0
  30. package/dist/src/editor/BlockNoteExtensions.js.map +1 -1
  31. package/dist/src/extensions/FormattingToolbar/FormattingToolbarPlugin.js +4 -2
  32. package/dist/src/extensions/FormattingToolbar/FormattingToolbarPlugin.js.map +1 -1
  33. package/dist/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.js +10 -8
  34. package/dist/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.js.map +1 -1
  35. package/dist/src/extensions/LinkToolbar/LinkToolbarPlugin.js +7 -3
  36. package/dist/src/extensions/LinkToolbar/LinkToolbarPlugin.js.map +1 -1
  37. package/dist/src/extensions/Placeholder/PlaceholderPlugin.js +13 -7
  38. package/dist/src/extensions/Placeholder/PlaceholderPlugin.js.map +1 -1
  39. package/dist/src/extensions/SideMenu/dragging.js +5 -1
  40. package/dist/src/extensions/SideMenu/dragging.js.map +1 -1
  41. package/dist/src/extensions/TableHandles/TableHandlesPlugin.js +25 -8
  42. package/dist/src/extensions/TableHandles/TableHandlesPlugin.js.map +1 -1
  43. package/dist/src/i18n/locales/ru.js +1 -1
  44. package/dist/tsconfig.tsbuildinfo +1 -1
  45. package/dist/webpack-stats.json +1 -1
  46. package/package.json +3 -3
  47. package/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts +6 -6
  48. package/src/api/blockManipulation/commands/moveBlocks/__snapshots__/moveBlocks.test.ts.snap +9506 -0
  49. package/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts +295 -0
  50. package/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts +338 -0
  51. package/src/api/blockManipulation/commands/splitBlock/splitBlock.test.ts +4 -0
  52. package/src/api/blockManipulation/commands/updateBlock/updateBlock.ts +11 -3
  53. package/src/api/blockManipulation/getBlock/getBlock.ts +141 -0
  54. package/src/api/blockManipulation/selections/__snapshots__/selection.test.ts.snap +660 -0
  55. package/src/api/blockManipulation/selections/selection.test.ts +56 -0
  56. package/src/api/blockManipulation/selections/selection.ts +244 -0
  57. package/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.ts +4 -0
  58. package/src/api/nodeUtil.ts +2 -2
  59. package/src/blocks/TableBlockContent/TableExtension.ts +12 -1
  60. package/src/editor/BlockNoteEditor.ts +87 -85
  61. package/src/editor/BlockNoteExtensions.ts +2 -0
  62. package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +4 -2
  63. package/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts +11 -8
  64. package/src/extensions/LinkToolbar/LinkToolbarPlugin.ts +11 -4
  65. package/src/extensions/Placeholder/PlaceholderPlugin.ts +23 -15
  66. package/src/extensions/SideMenu/dragging.ts +5 -1
  67. package/src/extensions/TableHandles/TableHandlesPlugin.ts +34 -9
  68. package/src/i18n/locales/ru.ts +1 -1
  69. package/types/src/api/blockManipulation/commands/moveBlocks/moveBlocks.d.ts +15 -0
  70. package/types/src/api/blockManipulation/getBlock/getBlock.d.ts +7 -0
  71. package/types/src/api/blockManipulation/selections/selection.d.ts +5 -0
  72. package/types/src/api/blockManipulation/selections/selection.test.d.ts +1 -0
  73. package/types/src/api/nodeUtil.d.ts +1 -1
  74. package/types/src/editor/BlockNoteEditor.d.ts +54 -10
  75. package/types/src/editor/BlockNoteExtensions.d.ts +1 -0
  76. package/types/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.d.ts +1 -0
  77. package/types/src/pm-nodes/BlockContainer.d.ts +2 -2
  78. package/types/src/pm-nodes/BlockGroup.d.ts +2 -2
  79. package/dist/src/api/blockManipulation/commands/moveBlock/moveBlock.js +0 -116
  80. package/dist/src/api/blockManipulation/commands/moveBlock/moveBlock.js.map +0 -1
  81. package/dist/src/api/blockManipulation/commands/moveBlock/moveBlock.test.js +0 -110
  82. package/dist/src/api/blockManipulation/commands/moveBlock/moveBlock.test.js.map +0 -1
  83. package/src/api/blockManipulation/commands/moveBlock/__snapshots__/moveBlock.test.ts.snap +0 -3799
  84. package/src/api/blockManipulation/commands/moveBlock/moveBlock.test.ts +0 -196
  85. package/src/api/blockManipulation/commands/moveBlock/moveBlock.ts +0 -176
  86. package/types/src/api/blockManipulation/commands/moveBlock/moveBlock.d.ts +0 -5
  87. /package/types/src/api/blockManipulation/commands/{moveBlock/moveBlock.test.d.ts → moveBlocks/moveBlocks.test.d.ts} +0 -0
@@ -0,0 +1,295 @@
1
+ import { NodeSelection, TextSelection } from "prosemirror-state";
2
+ import { CellSelection } from "prosemirror-tables";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ import { getBlockInfoFromSelection } from "../../../getBlockInfoFromPos.js";
6
+ import { setupTestEnv } from "../../setupTestEnv.js";
7
+ import {
8
+ moveBlocksDown,
9
+ moveBlocksUp,
10
+ moveSelectedBlocksAndSelection,
11
+ } from "./moveBlocks.js";
12
+
13
+ const getEditor = setupTestEnv();
14
+
15
+ function makeSelectionSpanContent(selectionType: "text" | "node" | "cell") {
16
+ const blockInfo = getBlockInfoFromSelection(getEditor()._tiptapEditor.state);
17
+ if (!blockInfo.isBlockContainer) {
18
+ throw new Error(
19
+ `Selection points to a ${blockInfo.blockNoteType} node, not a blockContainer node`
20
+ );
21
+ }
22
+ const { blockContent } = blockInfo;
23
+
24
+ if (selectionType === "cell") {
25
+ getEditor()._tiptapEditor.view.dispatch(
26
+ getEditor()._tiptapEditor.state.tr.setSelection(
27
+ CellSelection.create(
28
+ getEditor()._tiptapEditor.state.doc,
29
+ getEditor()
30
+ ._tiptapEditor.state.doc.resolve(blockContent.beforePos + 3)
31
+ .before(),
32
+ getEditor()
33
+ ._tiptapEditor.state.doc.resolve(blockContent.afterPos - 3)
34
+ .before()
35
+ )
36
+ )
37
+ );
38
+ } else if (selectionType === "node") {
39
+ getEditor()._tiptapEditor.view.dispatch(
40
+ getEditor()._tiptapEditor.state.tr.setSelection(
41
+ NodeSelection.create(
42
+ getEditor()._tiptapEditor.state.doc,
43
+ blockContent.beforePos
44
+ )
45
+ )
46
+ );
47
+ } else {
48
+ getEditor()._tiptapEditor.view.dispatch(
49
+ getEditor()._tiptapEditor.state.tr.setSelection(
50
+ TextSelection.create(
51
+ getEditor()._tiptapEditor.state.doc,
52
+ blockContent.beforePos + 1,
53
+ blockContent.afterPos - 1
54
+ )
55
+ )
56
+ );
57
+ }
58
+ }
59
+
60
+ describe("Test moveSelectedBlockAndSelection", () => {
61
+ it("Text selection", () => {
62
+ getEditor().setTextCursorPosition("paragraph-1");
63
+ makeSelectionSpanContent("text");
64
+
65
+ moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before");
66
+
67
+ const selection = getEditor()._tiptapEditor.state.selection;
68
+ getEditor().setTextCursorPosition("paragraph-1");
69
+ makeSelectionSpanContent("text");
70
+
71
+ expect(
72
+ selection.eq(getEditor()._tiptapEditor.state.selection)
73
+ ).toBeTruthy();
74
+ });
75
+
76
+ it("Node selection", () => {
77
+ getEditor().setTextCursorPosition("image-0");
78
+ makeSelectionSpanContent("node");
79
+
80
+ moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before");
81
+
82
+ const selection = getEditor()._tiptapEditor.state.selection;
83
+ getEditor().setTextCursorPosition("image-0");
84
+ makeSelectionSpanContent("node");
85
+
86
+ expect(
87
+ selection.eq(getEditor()._tiptapEditor.state.selection)
88
+ ).toBeTruthy();
89
+ });
90
+
91
+ it("Cell selection", () => {
92
+ getEditor().setTextCursorPosition("table-0");
93
+ makeSelectionSpanContent("cell");
94
+
95
+ moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before");
96
+
97
+ const selection = getEditor()._tiptapEditor.state.selection;
98
+ getEditor().setTextCursorPosition("table-0");
99
+ makeSelectionSpanContent("cell");
100
+
101
+ expect(
102
+ selection.eq(getEditor()._tiptapEditor.state.selection)
103
+ ).toBeTruthy();
104
+ });
105
+
106
+ it("Multiple block selection", () => {
107
+ getEditor().setSelection("paragraph-1", "paragraph-2");
108
+
109
+ moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before");
110
+
111
+ const selection = getEditor()._tiptapEditor.state.selection;
112
+ getEditor().setSelection("paragraph-1", "paragraph-2");
113
+
114
+ expect(
115
+ selection.eq(getEditor()._tiptapEditor.state.selection)
116
+ ).toBeTruthy();
117
+ });
118
+
119
+ it("Multiple block selection with table", () => {
120
+ getEditor().setSelection("paragraph-6", "table-0");
121
+
122
+ moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before");
123
+
124
+ const selection = getEditor()._tiptapEditor.state.selection;
125
+ getEditor().setSelection("paragraph-6", "table-0");
126
+
127
+ expect(
128
+ selection.eq(getEditor()._tiptapEditor.state.selection)
129
+ ).toBeTruthy();
130
+ });
131
+ });
132
+
133
+ describe("Test moveBlocksUp", () => {
134
+ it("Basic", () => {
135
+ getEditor().setTextCursorPosition("paragraph-1");
136
+
137
+ moveBlocksUp(getEditor());
138
+
139
+ expect(getEditor().document).toMatchSnapshot();
140
+ });
141
+
142
+ it("Into children", () => {
143
+ getEditor().setTextCursorPosition("paragraph-2");
144
+
145
+ moveBlocksUp(getEditor());
146
+
147
+ expect(getEditor().document).toMatchSnapshot();
148
+ });
149
+
150
+ it("Out of children", () => {
151
+ getEditor().setTextCursorPosition("nested-paragraph-1");
152
+
153
+ moveBlocksUp(getEditor());
154
+
155
+ expect(getEditor().document).toMatchSnapshot();
156
+ });
157
+
158
+ it("First block", () => {
159
+ getEditor().setTextCursorPosition("paragraph-0");
160
+
161
+ moveBlocksUp(getEditor());
162
+
163
+ expect(getEditor().document).toMatchSnapshot();
164
+ });
165
+
166
+ it("Multiple blocks", () => {
167
+ getEditor().setSelection("paragraph-1", "paragraph-2");
168
+
169
+ moveBlocksUp(getEditor());
170
+
171
+ expect(getEditor().document).toMatchSnapshot();
172
+ });
173
+
174
+ it("Multiple blocks starting in block with children", () => {
175
+ getEditor().setSelection("paragraph-with-children", "paragraph-2");
176
+
177
+ moveBlocksUp(getEditor());
178
+
179
+ expect(getEditor().document).toMatchSnapshot();
180
+ });
181
+
182
+ it("Multiple blocks starting in nested block", () => {
183
+ getEditor().setSelection("nested-paragraph-0", "paragraph-2");
184
+
185
+ moveBlocksUp(getEditor());
186
+
187
+ expect(getEditor().document).toMatchSnapshot();
188
+ });
189
+
190
+ it("Multiple blocks ending in block with children", () => {
191
+ getEditor().setSelection("paragraph-1", "paragraph-with-children");
192
+
193
+ moveBlocksUp(getEditor());
194
+
195
+ expect(getEditor().document).toMatchSnapshot();
196
+ });
197
+
198
+ it("Multiple blocks ending in nested block", () => {
199
+ getEditor().setSelection("paragraph-1", "nested-paragraph-0");
200
+
201
+ moveBlocksUp(getEditor());
202
+
203
+ expect(getEditor().document).toMatchSnapshot();
204
+ });
205
+
206
+ it("Multiple blocks starting and ending in nested block", () => {
207
+ getEditor().setSelection("nested-paragraph-0", "nested-paragraph-1");
208
+
209
+ moveBlocksUp(getEditor());
210
+
211
+ expect(getEditor().document).toMatchSnapshot();
212
+ });
213
+ });
214
+
215
+ describe("Test moveBlocksDown", () => {
216
+ it("Basic", () => {
217
+ getEditor().setTextCursorPosition("paragraph-0");
218
+
219
+ moveBlocksDown(getEditor());
220
+
221
+ expect(getEditor().document).toMatchSnapshot();
222
+ });
223
+
224
+ it("Into children", () => {
225
+ getEditor().setTextCursorPosition("paragraph-1");
226
+
227
+ moveBlocksDown(getEditor());
228
+
229
+ expect(getEditor().document).toMatchSnapshot();
230
+ });
231
+
232
+ it("Out of children", () => {
233
+ getEditor().setTextCursorPosition("nested-paragraph-1");
234
+
235
+ moveBlocksDown(getEditor());
236
+
237
+ expect(getEditor().document).toMatchSnapshot();
238
+ });
239
+
240
+ it("Last block", () => {
241
+ getEditor().setTextCursorPosition("trailing-paragraph");
242
+
243
+ moveBlocksDown(getEditor());
244
+
245
+ expect(getEditor().document).toMatchSnapshot();
246
+ });
247
+
248
+ it("Multiple blocks", () => {
249
+ getEditor().setSelection("paragraph-1", "paragraph-2");
250
+
251
+ moveBlocksDown(getEditor());
252
+
253
+ expect(getEditor().document).toMatchSnapshot();
254
+ });
255
+
256
+ it("Multiple blocks starting in block with children", () => {
257
+ getEditor().setSelection("paragraph-with-children", "paragraph-2");
258
+
259
+ moveBlocksDown(getEditor());
260
+
261
+ expect(getEditor().document).toMatchSnapshot();
262
+ });
263
+
264
+ it("Multiple blocks starting in nested block", () => {
265
+ getEditor().setSelection("nested-paragraph-0", "paragraph-2");
266
+
267
+ moveBlocksDown(getEditor());
268
+
269
+ expect(getEditor().document).toMatchSnapshot();
270
+ });
271
+
272
+ it("Multiple blocks ending in block with children", () => {
273
+ getEditor().setSelection("paragraph-1", "paragraph-with-children");
274
+
275
+ moveBlocksDown(getEditor());
276
+
277
+ expect(getEditor().document).toMatchSnapshot();
278
+ });
279
+
280
+ it("Multiple blocks ending in nested block", () => {
281
+ getEditor().setSelection("paragraph-1", "nested-paragraph-0");
282
+
283
+ moveBlocksDown(getEditor());
284
+
285
+ expect(getEditor().document).toMatchSnapshot();
286
+ });
287
+
288
+ it("Multiple blocks starting and ending in nested block", () => {
289
+ getEditor().setSelection("nested-paragraph-0", "nested-paragraph-1");
290
+
291
+ moveBlocksDown(getEditor());
292
+
293
+ expect(getEditor().document).toMatchSnapshot();
294
+ });
295
+ });
@@ -0,0 +1,338 @@
1
+ import { NodeSelection, Selection, TextSelection } from "prosemirror-state";
2
+ import { CellSelection } from "prosemirror-tables";
3
+
4
+ import { Block } from "../../../../blocks/defaultBlocks.js";
5
+ import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor";
6
+ import { BlockIdentifier } from "../../../../schema/index.js";
7
+ import { getNearestBlockPos } from "../../../getBlockInfoFromPos.js";
8
+ import { getNodeById } from "../../../nodeUtil.js";
9
+
10
+ type BlockSelectionData = (
11
+ | {
12
+ type: "text";
13
+ headBlockId: string;
14
+ anchorOffset: number;
15
+ headOffset: number;
16
+ }
17
+ | {
18
+ type: "node";
19
+ }
20
+ | {
21
+ type: "cell";
22
+ anchorCellOffset: number;
23
+ headCellOffset: number;
24
+ }
25
+ ) & {
26
+ anchorBlockId: string;
27
+ };
28
+
29
+ /**
30
+ * `getBlockSelectionData` and `updateBlockSelectionFromData` are used to save
31
+ * and restore the selection within a block, when the block is moved. This is
32
+ * done by first saving the offsets of the anchor and head from the before
33
+ * positions of their surrounding blocks, as well as the IDs of those blocks. We
34
+ * can then recreate the selection by finding the blocks with those IDs, getting
35
+ * their before positions, and adding the offsets to those positions.
36
+ * @param editor The BlockNote editor instance to get the selection data from.
37
+ */
38
+ function getBlockSelectionData(
39
+ editor: BlockNoteEditor<any, any, any>
40
+ ): BlockSelectionData {
41
+ const state = editor._tiptapEditor.state;
42
+ const selection = state.selection;
43
+
44
+ const anchorBlockPosInfo = getNearestBlockPos(state.doc, selection.anchor);
45
+
46
+ if (selection instanceof CellSelection) {
47
+ return {
48
+ type: "cell" as const,
49
+ anchorBlockId: anchorBlockPosInfo.node.attrs.id,
50
+ anchorCellOffset:
51
+ selection.$anchorCell.pos - anchorBlockPosInfo.posBeforeNode,
52
+ headCellOffset:
53
+ selection.$headCell.pos - anchorBlockPosInfo.posBeforeNode,
54
+ };
55
+ } else if (editor._tiptapEditor.state.selection instanceof NodeSelection) {
56
+ return {
57
+ type: "node" as const,
58
+ anchorBlockId: anchorBlockPosInfo.node.attrs.id,
59
+ };
60
+ } else {
61
+ const headBlockPosInfo = getNearestBlockPos(state.doc, selection.head);
62
+
63
+ return {
64
+ type: "text" as const,
65
+ anchorBlockId: anchorBlockPosInfo.node.attrs.id,
66
+ headBlockId: headBlockPosInfo.node.attrs.id,
67
+ anchorOffset: selection.anchor - anchorBlockPosInfo.posBeforeNode,
68
+ headOffset: selection.head - headBlockPosInfo.posBeforeNode,
69
+ };
70
+ }
71
+ }
72
+
73
+ /**
74
+ * `getBlockSelectionData` and `updateBlockSelectionFromData` are used to save
75
+ * and restore the selection within a block, when the block is moved. This is
76
+ * done by first saving the offsets of the anchor and head from the before
77
+ * positions of their surrounding blocks, as well as the IDs of those blocks. We
78
+ * can then recreate the selection by finding the blocks with those IDs, getting
79
+ * their before positions, and adding the offsets to those positions.
80
+ * @param editor The BlockNote editor instance to update the selection in.
81
+ * @param data The selection data to update the selection with (generated by
82
+ * `getBlockSelectionData`).
83
+ */
84
+ function updateBlockSelectionFromData(
85
+ editor: BlockNoteEditor<any, any, any>,
86
+ data: BlockSelectionData
87
+ ) {
88
+ const anchorBlockPos = getNodeById(
89
+ data.anchorBlockId,
90
+ editor._tiptapEditor.state.doc
91
+ )?.posBeforeNode;
92
+ if (anchorBlockPos === undefined) {
93
+ throw new Error(
94
+ `Could not find block with ID ${data.anchorBlockId} to update selection`
95
+ );
96
+ }
97
+
98
+ let selection: Selection;
99
+ if (data.type === "cell") {
100
+ selection = CellSelection.create(
101
+ editor._tiptapEditor.state.doc,
102
+ anchorBlockPos + data.anchorCellOffset,
103
+ anchorBlockPos + data.headCellOffset
104
+ );
105
+ } else if (data.type === "node") {
106
+ selection = NodeSelection.create(
107
+ editor._tiptapEditor.state.doc,
108
+ anchorBlockPos + 1
109
+ );
110
+ } else {
111
+ const headBlockPos = getNodeById(
112
+ data.headBlockId,
113
+ editor._tiptapEditor.state.doc
114
+ )?.posBeforeNode;
115
+ if (headBlockPos === undefined) {
116
+ throw new Error(
117
+ `Could not find block with ID ${data.headBlockId} to update selection`
118
+ );
119
+ }
120
+
121
+ selection = TextSelection.create(
122
+ editor._tiptapEditor.state.doc,
123
+ anchorBlockPos + data.anchorOffset,
124
+ headBlockPos + data.headOffset
125
+ );
126
+ }
127
+
128
+ editor._tiptapEditor.view.dispatch(
129
+ editor._tiptapEditor.state.tr.setSelection(selection)
130
+ );
131
+ }
132
+
133
+ /**
134
+ * Replaces any `columnList` blocks with the children of their columns. This is
135
+ * done here instead of in `getSelection` as we still need to remove the entire
136
+ * `columnList` node but only insert the `blockContainer` nodes inside it.
137
+ * @param blocks The blocks to flatten.
138
+ */
139
+ function flattenColumns(
140
+ blocks: Block<any, any, any>[]
141
+ ): Block<any, any, any>[] {
142
+ return blocks
143
+ .map((block) => {
144
+ if (block.type === "columnList") {
145
+ return block.children
146
+ .map((column) => flattenColumns(column.children))
147
+ .flat();
148
+ }
149
+
150
+ return {
151
+ ...block,
152
+ children: flattenColumns(block.children),
153
+ };
154
+ })
155
+ .flat();
156
+ }
157
+
158
+ /**
159
+ * Removes the selected blocks from the editor, then inserts them before/after a
160
+ * reference block. Also updates the selection to match the original selection
161
+ * using `getBlockSelectionData` and `updateBlockSelectionFromData`.
162
+ * @param editor The BlockNote editor instance to move the blocks in.
163
+ * @param referenceBlock The reference block to insert the selected blocks
164
+ * before/after.
165
+ * @param placement Whether to insert the selected blocks before or after the
166
+ * reference block.
167
+ */
168
+ export function moveSelectedBlocksAndSelection(
169
+ editor: BlockNoteEditor<any, any, any>,
170
+ referenceBlock: BlockIdentifier,
171
+ placement: "before" | "after"
172
+ ) {
173
+ const blocks = editor.getSelection()?.blocks || [
174
+ editor.getTextCursorPosition().block,
175
+ ];
176
+ const selectionData = getBlockSelectionData(editor);
177
+
178
+ editor.removeBlocks(blocks);
179
+ editor.insertBlocks(flattenColumns(blocks), referenceBlock, placement);
180
+
181
+ updateBlockSelectionFromData(editor, selectionData);
182
+ }
183
+
184
+ // Checks if a block is in a valid place after being moved. This check is
185
+ // primitive at the moment and only returns false if the block's parent is a
186
+ // `columnList` block. This is because regular blocks cannot be direct children
187
+ // of `columnList` blocks.
188
+ function checkPlacementIsValid(parentBlock?: Block<any, any, any>): boolean {
189
+ return !parentBlock || parentBlock.type !== "columnList";
190
+ }
191
+
192
+ // Gets the placement for moving a block up. This has 3 cases:
193
+ // 1. If the block has a previous sibling without children, the placement is
194
+ // before it.
195
+ // 2. If the block has a previous sibling with children, the placement is after
196
+ // the last child.
197
+ // 3. If the block has no previous sibling, but is nested, the placement is
198
+ // before its parent.
199
+ // If the placement is invalid, the function is called recursively until a valid
200
+ // placement is found. Returns undefined if no valid placement is found, meaning
201
+ // the block is already at the top of the document.
202
+ function getMoveUpPlacement(
203
+ editor: BlockNoteEditor<any, any, any>,
204
+ prevBlock?: Block<any, any, any>,
205
+ parentBlock?: Block<any, any, any>
206
+ ):
207
+ | { referenceBlock: BlockIdentifier; placement: "before" | "after" }
208
+ | undefined {
209
+ let referenceBlock: Block<any, any, any> | undefined;
210
+ let placement: "before" | "after" | undefined;
211
+
212
+ if (!prevBlock) {
213
+ if (parentBlock) {
214
+ referenceBlock = parentBlock;
215
+ placement = "before";
216
+ }
217
+ } else if (prevBlock.children.length > 0) {
218
+ referenceBlock = prevBlock.children[prevBlock.children.length - 1];
219
+ placement = "after";
220
+ } else {
221
+ referenceBlock = prevBlock;
222
+ placement = "before";
223
+ }
224
+
225
+ // Case when the block is already at the top of the document.
226
+ if (!referenceBlock || !placement) {
227
+ return undefined;
228
+ }
229
+
230
+ const referenceBlockParent = editor.getParentBlock(referenceBlock);
231
+ if (!checkPlacementIsValid(referenceBlockParent)) {
232
+ return getMoveUpPlacement(
233
+ editor,
234
+ placement === "after"
235
+ ? referenceBlock
236
+ : editor.getPrevBlock(referenceBlock),
237
+ referenceBlockParent
238
+ );
239
+ }
240
+
241
+ return { referenceBlock, placement };
242
+ }
243
+
244
+ // Gets the placement for moving a block down. This has 3 cases:
245
+ // 1. If the block has a next sibling without children, the placement is after
246
+ // it.
247
+ // 2. If the block has a next sibling with children, the placement is before the
248
+ // first child.
249
+ // 3. If the block has no next sibling, but is nested, the placement is
250
+ // after its parent.
251
+ // If the placement is invalid, the function is called recursively until a valid
252
+ // placement is found. Returns undefined if no valid placement is found, meaning
253
+ // the block is already at the bottom of the document.
254
+ function getMoveDownPlacement(
255
+ editor: BlockNoteEditor<any, any, any>,
256
+ nextBlock?: Block<any, any, any>,
257
+ parentBlock?: Block<any, any, any>
258
+ ):
259
+ | { referenceBlock: BlockIdentifier; placement: "before" | "after" }
260
+ | undefined {
261
+ let referenceBlock: Block<any, any, any> | undefined;
262
+ let placement: "before" | "after" | undefined;
263
+
264
+ if (!nextBlock) {
265
+ if (parentBlock) {
266
+ referenceBlock = parentBlock;
267
+ placement = "after";
268
+ }
269
+ } else if (nextBlock.children.length > 0) {
270
+ referenceBlock = nextBlock.children[0];
271
+ placement = "before";
272
+ } else {
273
+ referenceBlock = nextBlock;
274
+ placement = "after";
275
+ }
276
+
277
+ // Case when the block is already at the bottom of the document.
278
+ if (!referenceBlock || !placement) {
279
+ return undefined;
280
+ }
281
+
282
+ const referenceBlockParent = editor.getParentBlock(referenceBlock);
283
+ if (!checkPlacementIsValid(referenceBlockParent)) {
284
+ return getMoveDownPlacement(
285
+ editor,
286
+ placement === "before"
287
+ ? referenceBlock
288
+ : editor.getNextBlock(referenceBlock),
289
+ referenceBlockParent
290
+ );
291
+ }
292
+
293
+ return { referenceBlock, placement };
294
+ }
295
+
296
+ export function moveBlocksUp(editor: BlockNoteEditor<any, any, any>) {
297
+ const selection = editor.getSelection();
298
+ const block = selection?.blocks[0] || editor.getTextCursorPosition().block;
299
+
300
+ const moveUpPlacement = getMoveUpPlacement(
301
+ editor,
302
+ editor.getPrevBlock(block),
303
+ editor.getParentBlock(block)
304
+ );
305
+
306
+ if (!moveUpPlacement) {
307
+ return;
308
+ }
309
+
310
+ moveSelectedBlocksAndSelection(
311
+ editor,
312
+ moveUpPlacement.referenceBlock,
313
+ moveUpPlacement.placement
314
+ );
315
+ }
316
+
317
+ export function moveBlocksDown(editor: BlockNoteEditor<any, any, any>) {
318
+ const selection = editor.getSelection();
319
+ const block =
320
+ selection?.blocks[selection?.blocks.length - 1] ||
321
+ editor.getTextCursorPosition().block;
322
+
323
+ const moveDownPlacement = getMoveDownPlacement(
324
+ editor,
325
+ editor.getNextBlock(block),
326
+ editor.getParentBlock(block)
327
+ );
328
+
329
+ if (!moveDownPlacement) {
330
+ return;
331
+ }
332
+
333
+ moveSelectedBlocksAndSelection(
334
+ editor,
335
+ moveDownPlacement.referenceBlock,
336
+ moveDownPlacement.placement
337
+ );
338
+ }
@@ -28,6 +28,10 @@ function setSelectionWithOffset(
28
28
  offset: number
29
29
  ) {
30
30
  const posInfo = getNodeById(targetBlockId, doc);
31
+ if (!posInfo) {
32
+ throw new Error(`Block with ID ${targetBlockId} not found`);
33
+ }
34
+
31
35
  const info = getBlockInfo(posInfo);
32
36
 
33
37
  if (!info.isBlockContainer) {
@@ -263,15 +263,23 @@ export function updateBlock<
263
263
 
264
264
  const id =
265
265
  typeof blockToUpdate === "string" ? blockToUpdate : blockToUpdate.id;
266
- const { posBeforeNode } = getNodeById(id, ttEditor.state.doc);
266
+
267
+ const posInfo = getNodeById(id, ttEditor.state.doc);
268
+ if (!posInfo) {
269
+ throw new Error(`Block with ID ${id} not found`);
270
+ }
267
271
 
268
272
  ttEditor.commands.command(({ state, dispatch }) => {
269
- updateBlockCommand(editor, posBeforeNode, update)({ state, dispatch });
273
+ updateBlockCommand(
274
+ editor,
275
+ posInfo.posBeforeNode,
276
+ update
277
+ )({ state, dispatch });
270
278
  return true;
271
279
  });
272
280
 
273
281
  const blockContainerNode = ttEditor.state.doc
274
- .resolve(posBeforeNode + 1) // TODO: clean?
282
+ .resolve(posInfo.posBeforeNode + 1) // TODO: clean?
275
283
  .node();
276
284
 
277
285
  return nodeToBlock(