@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,244 @@
1
+ import { TextSelection } from "prosemirror-state";
2
+ import { TableMap } from "prosemirror-tables";
3
+
4
+ import { Block } from "../../../blocks/defaultBlocks.js";
5
+ import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor";
6
+ import { Selection } from "../../../editor/selectionTypes.js";
7
+ import {
8
+ BlockIdentifier,
9
+ BlockSchema,
10
+ InlineContentSchema,
11
+ StyleSchema,
12
+ } from "../../../schema/index.js";
13
+ import { getBlockInfo, getNearestBlockPos } from "../../getBlockInfoFromPos.js";
14
+ import { nodeToBlock } from "../../nodeConversions/nodeToBlock.js";
15
+ import { getNodeById } from "../../nodeUtil.js";
16
+
17
+ export function getSelection<
18
+ BSchema extends BlockSchema,
19
+ I extends InlineContentSchema,
20
+ S extends StyleSchema
21
+ >(
22
+ editor: BlockNoteEditor<BSchema, I, S>
23
+ ): Selection<BSchema, I, S> | undefined {
24
+ const state = editor._tiptapEditor.state;
25
+
26
+ const $startBlockBeforePos = state.doc.resolve(
27
+ getNearestBlockPos(state.doc, state.selection.from).posBeforeNode
28
+ );
29
+ const $endBlockBeforePos = state.doc.resolve(
30
+ getNearestBlockPos(state.doc, state.selection.to).posBeforeNode
31
+ );
32
+
33
+ // Return undefined if anchor and head are in the same block.
34
+ if ($startBlockBeforePos.pos === $endBlockBeforePos.pos) {
35
+ return undefined;
36
+ }
37
+
38
+ // Converts the node at the given index and depth around `$startBlockBeforePos`
39
+ // to a block. Used to get blocks at given indices at the shared depth and
40
+ // at the depth of `$startBlockBeforePos`.
41
+ const indexToBlock = (
42
+ index: number,
43
+ depth?: number
44
+ ): Block<BSchema, I, S> => {
45
+ const pos = $startBlockBeforePos.posAtIndex(index, depth);
46
+ const node = state.doc.resolve(pos).nodeAfter;
47
+
48
+ if (!node) {
49
+ throw new Error(
50
+ `Error getting selection - node not found at position ${pos}`
51
+ );
52
+ }
53
+
54
+ return nodeToBlock(
55
+ node,
56
+ editor.schema.blockSchema,
57
+ editor.schema.inlineContentSchema,
58
+ editor.schema.styleSchema,
59
+ editor.blockCache
60
+ );
61
+ };
62
+
63
+ const blocks: Block<BSchema, I, S>[] = [];
64
+ // Minimum depth at which the blocks share a common ancestor.
65
+ const sharedDepth = $startBlockBeforePos.sharedDepth($endBlockBeforePos.pos);
66
+ const startIndex = $startBlockBeforePos.index(sharedDepth);
67
+ const endIndex = $endBlockBeforePos.index(sharedDepth);
68
+
69
+ // In most cases, we want to return the blocks spanned by the selection at the
70
+ // shared depth. However, when the block in which the selection starts is at a
71
+ // higher depth than the shared depth, we omit the first block at the shared
72
+ // depth. Instead, we include the first block at its depth, and any blocks at
73
+ // a higher index up to the shared depth. The following example illustrates
74
+ // this:
75
+ // - id-0
76
+ // - id-1
77
+ // - >|id-2
78
+ // - id-3
79
+ // - id-4
80
+ // - id-5
81
+ // - id-6
82
+ // - id-7
83
+ // - id-8
84
+ // - id-9|<
85
+ // - id-10
86
+ // Here, each block is represented by its ID, and the selection is represented
87
+ // by the `>|` and `|<` markers. So the selection starts in block `id-2` and
88
+ // ends in block `id-8`. In this case, the shared depth is 0, since the blocks
89
+ // `id-6`, `id-7`, and `id-8` set the shared depth, as they are the least
90
+ // nested blocks spanned by the selection. Therefore, these blocks are all
91
+ // added to the `blocks` array. However, the selection starts in block `id-2`,
92
+ // which is at a higher depth than the shared depth. So we add block `id-2` to
93
+ // the `blocks` array, as well as any later siblings (in this case, `id-3`),
94
+ // and move up one level of depth. The ancestor of block `id-2` at this depth
95
+ // is block `id-1`, so we add all its later siblings to the `blocks` array as
96
+ // well, again moving up one level of depth. Since we're now at the shared
97
+ // depth, we are done. The final `blocks` array for this example would be:
98
+ // [ id-2, id-3, id-4, id-6, id-7, id-8, id-9 ]
99
+ if ($startBlockBeforePos.depth > sharedDepth) {
100
+ // Adds the block that the selection starts in.
101
+ blocks.push(
102
+ nodeToBlock(
103
+ $startBlockBeforePos.nodeAfter!,
104
+ editor.schema.blockSchema,
105
+ editor.schema.inlineContentSchema,
106
+ editor.schema.styleSchema,
107
+ editor.blockCache
108
+ )
109
+ );
110
+
111
+ // Traverses all depths from the depth of the block in which the selection
112
+ // starts, up to the shared depth.
113
+ for (let depth = $startBlockBeforePos.depth; depth > sharedDepth; depth--) {
114
+ const parentNode = $startBlockBeforePos.node(depth);
115
+
116
+ if (parentNode.type.isInGroup("childContainer")) {
117
+ const startIndexAtDepth = $startBlockBeforePos.index(depth) + 1;
118
+ const childCountAtDepth = $startBlockBeforePos.node(depth).childCount;
119
+
120
+ // Adds all blocks after the index of the block in which the selection
121
+ // starts (or its ancestors at lower depths).
122
+ for (let i = startIndexAtDepth; i < childCountAtDepth; i++) {
123
+ blocks.push(indexToBlock(i, depth));
124
+ }
125
+ }
126
+ }
127
+ } else {
128
+ // Adds the first block spanned by the selection at the shared depth.
129
+ blocks.push(indexToBlock(startIndex, sharedDepth));
130
+ }
131
+
132
+ // Adds all blocks spanned by the selection at the shared depth, excluding
133
+ // the first.
134
+ for (let i = startIndex + 1; i <= endIndex; i++) {
135
+ blocks.push(indexToBlock(i, sharedDepth));
136
+ }
137
+
138
+ if (blocks.length === 0) {
139
+ throw new Error(
140
+ `Error getting selection - selection doesn't span any blocks (${state.selection})`
141
+ );
142
+ }
143
+
144
+ return {
145
+ blocks,
146
+ };
147
+ }
148
+
149
+ export function setSelection<
150
+ BSchema extends BlockSchema,
151
+ I extends InlineContentSchema,
152
+ S extends StyleSchema
153
+ >(
154
+ editor: BlockNoteEditor<BSchema, I, S>,
155
+ startBlock: BlockIdentifier,
156
+ endBlock: BlockIdentifier
157
+ ) {
158
+ const startBlockId =
159
+ typeof startBlock === "string" ? startBlock : startBlock.id;
160
+ const endBlockId = typeof endBlock === "string" ? endBlock : endBlock.id;
161
+
162
+ if (startBlockId === endBlockId) {
163
+ throw new Error(
164
+ `Attempting to set selection with the same anchor and head blocks (id ${startBlockId})`
165
+ );
166
+ }
167
+
168
+ const doc = editor._tiptapEditor.state.doc;
169
+
170
+ const anchorPosInfo = getNodeById(startBlockId, doc);
171
+ if (!anchorPosInfo) {
172
+ throw new Error(`Block with ID ${startBlockId} not found`);
173
+ }
174
+ const headPosInfo = getNodeById(endBlockId, doc);
175
+ if (!headPosInfo) {
176
+ throw new Error(`Block with ID ${endBlockId} not found`);
177
+ }
178
+
179
+ const anchorBlockInfo = getBlockInfo(anchorPosInfo);
180
+ const headBlockInfo = getBlockInfo(headPosInfo);
181
+
182
+ const anchorBlockConfig =
183
+ editor.schema.blockSchema[
184
+ anchorBlockInfo.blockNoteType as keyof typeof editor.schema.blockSchema
185
+ ];
186
+ const headBlockConfig =
187
+ editor.schema.blockSchema[
188
+ headBlockInfo.blockNoteType as keyof typeof editor.schema.blockSchema
189
+ ];
190
+
191
+ if (
192
+ !anchorBlockInfo.isBlockContainer ||
193
+ anchorBlockConfig.content === "none"
194
+ ) {
195
+ throw new Error(
196
+ `Attempting to set selection anchor in block without content (id ${startBlockId})`
197
+ );
198
+ }
199
+ if (!headBlockInfo.isBlockContainer || headBlockConfig.content === "none") {
200
+ throw new Error(
201
+ `Attempting to set selection anchor in block without content (id ${endBlockId})`
202
+ );
203
+ }
204
+
205
+ let startPos: number;
206
+ let endPos: number;
207
+
208
+ if (anchorBlockConfig.content === "table") {
209
+ const tableMap = TableMap.get(anchorBlockInfo.blockContent.node);
210
+ const firstCellPos =
211
+ anchorBlockInfo.blockContent.beforePos +
212
+ tableMap.positionAt(0, 0, anchorBlockInfo.blockContent.node) +
213
+ 1;
214
+ startPos = firstCellPos + 2;
215
+ } else {
216
+ startPos = anchorBlockInfo.blockContent.beforePos + 1;
217
+ }
218
+
219
+ if (headBlockConfig.content === "table") {
220
+ const tableMap = TableMap.get(headBlockInfo.blockContent.node);
221
+ const lastCellPos =
222
+ headBlockInfo.blockContent.beforePos +
223
+ tableMap.positionAt(
224
+ tableMap.height - 1,
225
+ tableMap.width - 1,
226
+ headBlockInfo.blockContent.node
227
+ ) +
228
+ 1;
229
+ const lastCellNodeSize = doc.resolve(lastCellPos).nodeAfter!.nodeSize;
230
+ endPos = lastCellPos + lastCellNodeSize - 2;
231
+ } else {
232
+ endPos = headBlockInfo.blockContent.afterPos - 1;
233
+ }
234
+
235
+ // TODO: We should polish up the `MultipleNodeSelection` and use that instead.
236
+ // Right now it's missing a few things like a jsonID and styling to show
237
+ // which nodes are selected. `TextSelection` is ok for now, but has the
238
+ // restriction that the start/end blocks must have content.
239
+ editor._tiptapEditor.dispatch(
240
+ editor._tiptapEditor.state.tr.setSelection(
241
+ TextSelection.create(editor._tiptapEditor.state.doc, startPos, endPos)
242
+ )
243
+ );
244
+ }
@@ -96,6 +96,10 @@ export function setTextCursorPosition<
96
96
  const id = typeof targetBlock === "string" ? targetBlock : targetBlock.id;
97
97
 
98
98
  const posInfo = getNodeById(id, editor._tiptapEditor.state.doc);
99
+ if (!posInfo) {
100
+ throw new Error(`Block with ID ${id} not found`);
101
+ }
102
+
99
103
  const info = getBlockInfo(posInfo);
100
104
 
101
105
  const contentType: "none" | "inline" | "table" =
@@ -6,7 +6,7 @@ import { Node } from "prosemirror-model";
6
6
  export function getNodeById(
7
7
  id: string,
8
8
  doc: Node
9
- ): { node: Node; posBeforeNode: number } {
9
+ ): { node: Node; posBeforeNode: number } | undefined {
10
10
  let targetNode: Node | undefined = undefined;
11
11
  let posBeforeNode: number | undefined = undefined;
12
12
 
@@ -28,7 +28,7 @@ export function getNodeById(
28
28
  });
29
29
 
30
30
  if (targetNode === undefined || posBeforeNode === undefined) {
31
- throw Error("Could not find block in the editor with matching ID.");
31
+ return undefined;
32
32
  }
33
33
 
34
34
  return {
@@ -1,5 +1,5 @@
1
1
  import { callOrReturn, Extension, getExtensionField } from "@tiptap/core";
2
- import { columnResizing, tableEditing } from "prosemirror-tables";
2
+ import { columnResizing, goToNextCell, tableEditing } from "prosemirror-tables";
3
3
 
4
4
  export const RESIZE_MIN_WIDTH = 35;
5
5
  export const EMPTY_CELL_WIDTH = 120;
@@ -53,6 +53,17 @@ export const TableExtension = Extension.create({
53
53
  selectionIsInTableParagraphNode
54
54
  );
55
55
  },
56
+ // Enables navigating cells using the tab key.
57
+ Tab: () => {
58
+ return this.editor.commands.command(({ state, dispatch, view }) =>
59
+ goToNextCell(1)(state, dispatch, view)
60
+ );
61
+ },
62
+ "Shift-Tab": () => {
63
+ return this.editor.commands.command(({ state, dispatch, view }) =>
64
+ goToNextCell(-1)(state, dispatch, view)
65
+ );
66
+ },
56
67
  };
57
68
  },
58
69
 
@@ -9,11 +9,17 @@ import {
9
9
  import { Node, Schema } from "prosemirror-model";
10
10
  // import "./blocknote.css";
11
11
  import * as Y from "yjs";
12
+ import {
13
+ getBlock,
14
+ getNextBlock,
15
+ getParentBlock,
16
+ getPrevBlock,
17
+ } from "../api/blockManipulation/getBlock/getBlock.js";
12
18
  import { insertBlocks } from "../api/blockManipulation/commands/insertBlocks/insertBlocks.js";
13
19
  import {
14
- moveBlockDown,
15
- moveBlockUp,
16
- } from "../api/blockManipulation/commands/moveBlock/moveBlock.js";
20
+ moveBlocksDown,
21
+ moveBlocksUp,
22
+ } from "../api/blockManipulation/commands/moveBlocks/moveBlocks.js";
17
23
  import {
18
24
  canNestBlock,
19
25
  canUnnestBlock,
@@ -28,6 +34,10 @@ import {
28
34
  getTextCursorPosition,
29
35
  setTextCursorPosition,
30
36
  } from "../api/blockManipulation/selections/textCursorPosition/textCursorPosition.js";
37
+ import {
38
+ getSelection,
39
+ setSelection,
40
+ } from "../api/blockManipulation/selections/selection.js";
31
41
  import { createExternalHTMLExporter } from "../api/exporters/html/externalHTMLExporter.js";
32
42
  import { blocksToMarkdown } from "../api/exporters/markdown/markdownExporter.js";
33
43
  import { HTMLToBlocks } from "../api/parsers/html/parseHTML.js";
@@ -219,6 +229,21 @@ export type BlockNoteEditorOptions<
219
229
  setIdAttribute?: boolean;
220
230
 
221
231
  dropCursor?: (opts: any) => Plugin;
232
+
233
+ /**
234
+ Select desired behavior when pressing `Tab` (or `Shift-Tab`). Specifically,
235
+ what should happen when a user has selected multiple blocks while a toolbar
236
+ is open:
237
+ - `"prefer-navigate-ui"`: Change focus to the toolbar. The user needs to
238
+ first press `Escape` to close the toolbar, and can then indent multiple
239
+ blocks. Better for keyboard accessibility.
240
+ - `"prefer-indent"`: Regardless of whether toolbars are open, indent the
241
+ selection of blocks. In this case, it's not possible to navigate toolbars
242
+ with the keyboard.
243
+
244
+ @default "prefer-navigate-ui"
245
+ */
246
+ tabBehavior: "prefer-navigate-ui" | "prefer-indent";
222
247
  };
223
248
 
224
249
  const blockNoteTipTapOptions = {
@@ -395,6 +420,7 @@ export class BlockNoteEditor<
395
420
  tableHandles: checkDefaultBlockTypeInSchema("table", this),
396
421
  dropCursor: this.options.dropCursor ?? dropCursor,
397
422
  placeholders: newOptions.placeholders,
423
+ tabBehavior: newOptions.tabBehavior,
398
424
  });
399
425
 
400
426
  // add extensions from _tiptapOptions
@@ -610,39 +636,57 @@ export class BlockNoteEditor<
610
636
 
611
637
  /**
612
638
  * Gets a snapshot of an existing block from the editor.
613
- * @param blockIdentifier The identifier of an existing block that should be retrieved.
614
- * @returns The block that matches the identifier, or `undefined` if no matching block was found.
639
+ * @param blockIdentifier The identifier of an existing block that should be
640
+ * retrieved.
641
+ * @returns The block that matches the identifier, or `undefined` if no
642
+ * matching block was found.
615
643
  */
616
644
  public getBlock(
617
645
  blockIdentifier: BlockIdentifier
618
646
  ): Block<BSchema, ISchema, SSchema> | undefined {
619
- const id =
620
- typeof blockIdentifier === "string"
621
- ? blockIdentifier
622
- : blockIdentifier.id;
623
- let newBlock: Block<BSchema, ISchema, SSchema> | undefined = undefined;
624
-
625
- this._tiptapEditor.state.doc.firstChild!.descendants((node) => {
626
- if (typeof newBlock !== "undefined") {
627
- return false;
628
- }
629
-
630
- if (node.type.name !== "blockContainer" || node.attrs.id !== id) {
631
- return true;
632
- }
647
+ return getBlock(this, blockIdentifier);
648
+ }
633
649
 
634
- newBlock = nodeToBlock(
635
- node,
636
- this.schema.blockSchema,
637
- this.schema.inlineContentSchema,
638
- this.schema.styleSchema,
639
- this.blockCache
640
- );
650
+ /**
651
+ * Gets a snapshot of the previous sibling of an existing block from the
652
+ * editor.
653
+ * @param blockIdentifier The identifier of an existing block for which the
654
+ * previous sibling should be retrieved.
655
+ * @returns The previous sibling of the block that matches the identifier.
656
+ * `undefined` if no matching block was found, or it's the first child/block
657
+ * in the document.
658
+ */
659
+ public getPrevBlock(
660
+ blockIdentifier: BlockIdentifier
661
+ ): Block<BSchema, ISchema, SSchema> | undefined {
662
+ return getPrevBlock(this, blockIdentifier);
663
+ }
641
664
 
642
- return false;
643
- });
665
+ /**
666
+ * Gets a snapshot of the next sibling of an existing block from the editor.
667
+ * @param blockIdentifier The identifier of an existing block for which the
668
+ * next sibling should be retrieved.
669
+ * @returns The next sibling of the block that matches the identifier.
670
+ * `undefined` if no matching block was found, or it's the last child/block in
671
+ * the document.
672
+ */
673
+ public getNextBlock(
674
+ blockIdentifier: BlockIdentifier
675
+ ): Block<BSchema, ISchema, SSchema> | undefined {
676
+ return getNextBlock(this, blockIdentifier);
677
+ }
644
678
 
645
- return newBlock;
679
+ /**
680
+ * Gets a snapshot of the parent of an existing block from the editor.
681
+ * @param blockIdentifier The identifier of an existing block for which the
682
+ * parent should be retrieved.
683
+ * @returns The parent of the block that matches the identifier. `undefined`
684
+ * if no matching block was found, or the block isn't nested.
685
+ */
686
+ public getParentBlock(
687
+ blockIdentifier: BlockIdentifier
688
+ ): Block<BSchema, ISchema, SSchema> | undefined {
689
+ return getParentBlock(this, blockIdentifier);
646
690
  }
647
691
 
648
692
  /**
@@ -728,53 +772,11 @@ export class BlockNoteEditor<
728
772
  * Gets a snapshot of the current selection.
729
773
  */
730
774
  public getSelection(): Selection<BSchema, ISchema, SSchema> | undefined {
731
- // Either the TipTap selection is empty, or it's a node selection. In either
732
- // case, it only spans one block, so we return undefined.
733
- if (
734
- this._tiptapEditor.state.selection.from ===
735
- this._tiptapEditor.state.selection.to ||
736
- "node" in this._tiptapEditor.state.selection
737
- ) {
738
- return undefined;
739
- }
740
-
741
- const blocks: Block<BSchema, ISchema, SSchema>[] = [];
742
-
743
- // TODO: This adds all child blocks to the same array. Needs to find min
744
- // depth and only add blocks at that depth.
745
- this._tiptapEditor.state.doc.descendants((node, pos) => {
746
- if (node.type.spec.group !== "blockContent") {
747
- return true;
748
- }
749
-
750
- // Fixed the block pos and size
751
- // all block is wrapped with a blockContent wrapper
752
- // and blockContent wrapper pos = inner block pos - 1
753
- // blockContent wrapper end = inner block pos + nodeSize + 1
754
- // need to add 1 to start and -1 to end
755
- const end = pos + node.nodeSize - 1;
756
- const start = pos + 1;
757
- if (
758
- end <= this._tiptapEditor.state.selection.from ||
759
- start >= this._tiptapEditor.state.selection.to
760
- ) {
761
- return true;
762
- }
763
-
764
- blocks.push(
765
- nodeToBlock(
766
- this._tiptapEditor.state.doc.resolve(pos).node(),
767
- this.schema.blockSchema,
768
- this.schema.inlineContentSchema,
769
- this.schema.styleSchema,
770
- this.blockCache
771
- )
772
- );
773
-
774
- return false;
775
- });
775
+ return getSelection(this);
776
+ }
776
777
 
777
- return { blocks: blocks };
778
+ public setSelection(startBlock: BlockIdentifier, endBlock: BlockIdentifier) {
779
+ setSelection(this, startBlock, endBlock);
778
780
  }
779
781
 
780
782
  /**
@@ -1032,21 +1034,21 @@ export class BlockNoteEditor<
1032
1034
  }
1033
1035
 
1034
1036
  /**
1035
- * Moves the block containing the text cursor up. If the previous block has
1036
- * children, moves it to the end of its children. If there is no previous
1037
- * block, but the current block is nested, moves it out of & before its parent.
1037
+ * Moves the selected blocks up. If the previous block has children, moves
1038
+ * them to the end of its children. If there is no previous block, but the
1039
+ * current blocks share a common parent, moves them out of & before it.
1038
1040
  */
1039
- public moveBlockUp() {
1040
- moveBlockUp(this);
1041
+ public moveBlocksUp() {
1042
+ moveBlocksUp(this);
1041
1043
  }
1042
1044
 
1043
1045
  /**
1044
- * Moves the block containing the text cursor down. If the next block has
1045
- * children, moves it to the start of its children. If there is no next block,
1046
- * but the current block is nested, moves it out of & after its parent.
1046
+ * Moves the selected blocks down. If the next block has children, moves
1047
+ * them to the start of its children. If there is no next block, but the
1048
+ * current blocks share a common parent, moves them out of & after it.
1047
1049
  */
1048
- public moveBlockDown() {
1049
- moveBlockDown(this);
1050
+ public moveBlocksDown() {
1051
+ moveBlocksDown(this);
1050
1052
  }
1051
1053
 
1052
1054
  /**
@@ -67,6 +67,7 @@ type ExtensionOptions<
67
67
  tableHandles: boolean;
68
68
  dropCursor: (opts: any) => Plugin;
69
69
  placeholders: Record<string | "default", string>;
70
+ tabBehavior?: "prefer-navigate-ui" | "prefer-indent";
70
71
  };
71
72
 
72
73
  /**
@@ -200,6 +201,7 @@ const getTipTapExtensions = <
200
201
  }),
201
202
  KeyboardShortcutsExtension.configure({
202
203
  editor: opts.editor,
204
+ tabBehavior: opts.tabBehavior,
203
205
  }),
204
206
  BlockGroup.configure({
205
207
  domAttributes: opts.domAttributes,
@@ -139,9 +139,11 @@ export class FormattingToolbarView implements PluginView {
139
139
  // Wrapping in a setTimeout gives enough time to wait for the blur event to
140
140
  // occur before updating the toolbar.
141
141
  const { state, composing } = view;
142
- const { doc, selection } = state;
142
+ const { selection } = state;
143
143
  const isSame =
144
- oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection);
144
+ oldState &&
145
+ oldState.selection.from === state.selection.from &&
146
+ oldState.selection.to === state.selection.to;
145
147
 
146
148
  if (composing || isSame) {
147
149
  return;
@@ -16,6 +16,7 @@ import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
16
16
 
17
17
  export const KeyboardShortcutsExtension = Extension.create<{
18
18
  editor: BlockNoteEditor<any, any, any>;
19
+ tabBehavior: "prefer-navigate-ui" | "prefer-indent";
19
20
  }>({
20
21
  priority: 50,
21
22
 
@@ -479,9 +480,10 @@ export const KeyboardShortcutsExtension = Extension.create<{
479
480
  // editor since the browser will try to use tab for keyboard navigation.
480
481
  Tab: () => {
481
482
  if (
482
- this.options.editor.formattingToolbar?.shown ||
483
- this.options.editor.linkToolbar?.shown ||
484
- this.options.editor.filePanel?.shown
483
+ this.options.tabBehavior !== "prefer-indent" &&
484
+ (this.options.editor.formattingToolbar?.shown ||
485
+ this.options.editor.linkToolbar?.shown ||
486
+ this.options.editor.filePanel?.shown)
485
487
  ) {
486
488
  // don't handle tabs if a toolbar is shown, so we can tab into / out of it
487
489
  return false;
@@ -491,9 +493,10 @@ export const KeyboardShortcutsExtension = Extension.create<{
491
493
  },
492
494
  "Shift-Tab": () => {
493
495
  if (
494
- this.options.editor.formattingToolbar?.shown ||
495
- this.options.editor.linkToolbar?.shown ||
496
- this.options.editor.filePanel?.shown
496
+ this.options.tabBehavior !== "prefer-indent" &&
497
+ (this.options.editor.formattingToolbar?.shown ||
498
+ this.options.editor.linkToolbar?.shown ||
499
+ this.options.editor.filePanel?.shown)
497
500
  ) {
498
501
  // don't handle tabs if a toolbar is shown, so we can tab into / out of it
499
502
  return false;
@@ -502,11 +505,11 @@ export const KeyboardShortcutsExtension = Extension.create<{
502
505
  return true;
503
506
  },
504
507
  "Shift-Mod-ArrowUp": () => {
505
- this.options.editor.moveBlockUp();
508
+ this.options.editor.moveBlocksUp();
506
509
  return true;
507
510
  },
508
511
  "Shift-Mod-ArrowDown": () => {
509
- this.options.editor.moveBlockDown();
512
+ this.options.editor.moveBlocksDown();
510
513
  return true;
511
514
  },
512
515
  };
@@ -2,7 +2,7 @@ import { getMarkRange, posToDOMRect, Range } from "@tiptap/core";
2
2
 
3
3
  import { EditorView } from "@tiptap/pm/view";
4
4
  import { Mark } from "prosemirror-model";
5
- import { Plugin, PluginKey, PluginView } from "prosemirror-state";
5
+ import { EditorState, Plugin, PluginKey, PluginView } from "prosemirror-state";
6
6
 
7
7
  import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
8
8
  import { UiElementPosition } from "../../extensions-shared/UiElementPosition.js";
@@ -52,7 +52,7 @@ class LinkToolbarView implements PluginView {
52
52
 
53
53
  this.startMenuUpdateTimer = () => {
54
54
  this.menuUpdateTimer = setTimeout(() => {
55
- this.update();
55
+ this.update(this.pmView);
56
56
  }, 250);
57
57
  };
58
58
 
@@ -190,8 +190,15 @@ class LinkToolbarView implements PluginView {
190
190
  }
191
191
  }
192
192
 
193
- update() {
194
- if (!this.pmView.hasFocus()) {
193
+ update(view: EditorView, oldState?: EditorState) {
194
+ const { state } = view;
195
+
196
+ const isSame =
197
+ oldState &&
198
+ oldState.selection.from === state.selection.from &&
199
+ oldState.selection.to === state.selection.to;
200
+
201
+ if (isSame || !this.pmView.hasFocus()) {
195
202
  return;
196
203
  }
197
204