@blocknote/core 0.46.2 → 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 (54) hide show
  1. package/dist/{BlockNoteSchema-DmFDeA0n.cjs → BlockNoteSchema-CwhtPpVC.cjs} +2 -2
  2. package/dist/{BlockNoteSchema-DmFDeA0n.cjs.map → BlockNoteSchema-CwhtPpVC.cjs.map} +1 -1
  3. package/dist/{BlockNoteSchema-BkXw8HJ6.js → BlockNoteSchema-dmbNkHA-.js} +2 -2
  4. package/dist/{BlockNoteSchema-BkXw8HJ6.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-CxM966vN.js → TrailingNode-F9hX_UlQ.js} +451 -445
  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 +1624 -1370
  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-DosClM5E.cjs → defaultBlocks-CSB5GiAu.cjs} +4 -4
  16. package/dist/defaultBlocks-CSB5GiAu.cjs.map +1 -0
  17. package/dist/{defaultBlocks-DE5GNdJH.js → defaultBlocks-Caw1U1oV.js} +49 -46
  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/locales.cjs +1 -1
  22. package/dist/locales.cjs.map +1 -1
  23. package/dist/locales.js +813 -28
  24. package/dist/locales.js.map +1 -1
  25. package/dist/style.css +1 -1
  26. package/dist/tsconfig.tsbuildinfo +1 -1
  27. package/dist/webpack-stats.json +1 -1
  28. package/package.json +1 -1
  29. package/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts +30 -7
  30. package/src/blocks/ListItem/CheckListItem/block.test.ts +61 -0
  31. package/src/blocks/ListItem/CheckListItem/block.ts +4 -0
  32. package/src/editor/Block.css +2 -2
  33. package/src/editor/transformPasted.ts +69 -0
  34. package/src/extensions/Collaboration/YCursorPlugin.ts +3 -1
  35. package/src/extensions/SideMenu/SideMenu.ts +44 -0
  36. package/src/extensions/SuggestionMenu/SuggestionMenu.test.ts +191 -0
  37. package/src/extensions/SuggestionMenu/SuggestionMenu.ts +28 -11
  38. package/src/extensions/tiptap-extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts +470 -64
  39. package/src/i18n/locales/fa.ts +390 -0
  40. package/src/i18n/locales/index.ts +2 -0
  41. package/src/i18n/locales/uz.ts +421 -0
  42. package/src/schema/blocks/createSpec.ts +2 -0
  43. package/types/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.d.ts +5 -0
  44. package/types/src/blocks/ListItem/CheckListItem/block.test.d.ts +1 -0
  45. package/types/src/extensions/SuggestionMenu/SuggestionMenu.d.ts +12 -3
  46. package/types/src/extensions/SuggestionMenu/SuggestionMenu.test.d.ts +1 -0
  47. package/types/src/i18n/locales/fa.d.ts +320 -0
  48. package/types/src/i18n/locales/index.d.ts +2 -0
  49. package/types/src/i18n/locales/uz.d.ts +2 -0
  50. package/dist/TrailingNode-CxM966vN.js.map +0 -1
  51. package/dist/TrailingNode-D-CZ76FS.cjs +0 -2
  52. package/dist/TrailingNode-D-CZ76FS.cjs.map +0 -1
  53. package/dist/defaultBlocks-DE5GNdJH.js.map +0 -1
  54. package/dist/defaultBlocks-DosClM5E.cjs.map +0 -1
@@ -0,0 +1,191 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
4
+ import { SuggestionMenu } from "./SuggestionMenu.js";
5
+
6
+ /**
7
+ * @vitest-environment jsdom
8
+ */
9
+
10
+ /**
11
+ * Find the SuggestionMenu ProseMirror plugin instance from the editor state.
12
+ * We need to do this because the PluginKey is not exported, and creating a new
13
+ * PluginKey with the same name gives a different instance.
14
+ */
15
+ function findSuggestionPlugin(editor: BlockNoteEditor) {
16
+ const state = editor._tiptapEditor.state;
17
+ const plugin = state.plugins.find(
18
+ (p) => (p as any).key === "SuggestionMenuPlugin$",
19
+ );
20
+ if (!plugin) {
21
+ throw new Error("SuggestionMenuPlugin not found in editor state");
22
+ }
23
+ return plugin;
24
+ }
25
+
26
+ function getSuggestionPluginState(editor: BlockNoteEditor) {
27
+ const plugin = findSuggestionPlugin(editor);
28
+ return plugin.getState(editor._tiptapEditor.state);
29
+ }
30
+
31
+ /**
32
+ * Calls the `handleTextInput` prop of the SuggestionMenu plugin directly,
33
+ * which mirrors what ProseMirror would do when the user types a character.
34
+ * This allows us to test the `shouldTrigger` filtering path.
35
+ */
36
+ function simulateTextInput(editor: BlockNoteEditor, char: string): boolean {
37
+ const plugin = findSuggestionPlugin(editor);
38
+ const view = editor._tiptapEditor.view;
39
+ const from = view.state.selection.from;
40
+ const to = view.state.selection.to;
41
+ const handler = plugin.props.handleTextInput;
42
+ if (!handler) {
43
+ throw new Error("handleTextInput not found on SuggestionMenu plugin");
44
+ }
45
+ return (handler as any)(view, from, to, char) as boolean;
46
+ }
47
+
48
+ function createEditor() {
49
+ const editor = BlockNoteEditor.create();
50
+ const div = document.createElement("div");
51
+ editor.mount(div);
52
+ return editor;
53
+ }
54
+
55
+ describe("SuggestionMenu", () => {
56
+ it("should open suggestion menu in a paragraph", () => {
57
+ const editor = createEditor();
58
+ const sm = editor.getExtension(SuggestionMenu)!;
59
+
60
+ // Register "/" trigger character (no filter)
61
+ sm.addSuggestionMenu({ triggerCharacter: "/" });
62
+
63
+ editor.replaceBlocks(editor.document, [
64
+ {
65
+ id: "paragraph-0",
66
+ type: "paragraph",
67
+ content: "Hello world",
68
+ },
69
+ ]);
70
+
71
+ editor.setTextCursorPosition("paragraph-0", "end");
72
+
73
+ // Verify we start with no active suggestion menu
74
+ expect(getSuggestionPluginState(editor)).toBeUndefined();
75
+
76
+ // Simulate typing "/" — handleTextInput should trigger the menu
77
+ const handled = simulateTextInput(editor, "/");
78
+
79
+ // The input should be handled (menu opened)
80
+ expect(handled).toBe(true);
81
+
82
+ // Plugin state should now be defined (menu opened)
83
+ const pluginState = getSuggestionPluginState(editor);
84
+ expect(pluginState).toBeDefined();
85
+ expect(pluginState.triggerCharacter).toBe("/");
86
+
87
+ editor._tiptapEditor.destroy();
88
+ });
89
+
90
+ it("should not open suggestion menu in table content when shouldTrigger returns false", () => {
91
+ const editor = createEditor();
92
+ const sm = editor.getExtension(SuggestionMenu)!;
93
+
94
+ // Register "/" with a shouldTrigger filter that blocks table content.
95
+ // This mirrors what BlockNoteDefaultUI does.
96
+ sm.addSuggestionMenu({
97
+ triggerCharacter: "/",
98
+ shouldOpen: (tr) =>
99
+ !tr.selection.$from.parent.type.isInGroup("tableContent"),
100
+ });
101
+
102
+ editor.replaceBlocks(editor.document, [
103
+ {
104
+ id: "table-0",
105
+ type: "table",
106
+ content: {
107
+ type: "tableContent",
108
+ rows: [
109
+ {
110
+ cells: ["Cell 1", "Cell 2", "Cell 3"],
111
+ },
112
+ {
113
+ cells: ["Cell 4", "Cell 5", "Cell 6"],
114
+ },
115
+ ],
116
+ },
117
+ },
118
+ ]);
119
+
120
+ // Place cursor inside a table cell
121
+ editor.setTextCursorPosition("table-0", "start");
122
+
123
+ // Verify the cursor is inside table content
124
+ const $from = editor._tiptapEditor.state.selection.$from;
125
+ expect($from.parent.type.isInGroup("tableContent")).toBe(true);
126
+
127
+ // Verify we start with no active suggestion menu
128
+ expect(getSuggestionPluginState(editor)).toBeUndefined();
129
+
130
+ // Simulate typing "/" — shouldTrigger should prevent the menu from opening
131
+ const handled = simulateTextInput(editor, "/");
132
+
133
+ // handleTextInput should return false (not handled) because
134
+ // shouldTrigger rejected the context
135
+ expect(handled).toBe(false);
136
+
137
+ // Plugin state should remain undefined
138
+ expect(getSuggestionPluginState(editor)).toBeUndefined();
139
+
140
+ editor._tiptapEditor.destroy();
141
+ });
142
+
143
+ it("should still allow suggestion menus without shouldTrigger in table content", () => {
144
+ const editor = createEditor();
145
+ const sm = editor.getExtension(SuggestionMenu)!;
146
+
147
+ // Register "@" WITHOUT a shouldTrigger filter — should still work in tables
148
+ sm.addSuggestionMenu({ triggerCharacter: "@" });
149
+
150
+ editor.replaceBlocks(editor.document, [
151
+ {
152
+ id: "table-0",
153
+ type: "table",
154
+ content: {
155
+ type: "tableContent",
156
+ rows: [
157
+ {
158
+ cells: ["Cell 1", "Cell 2", "Cell 3"],
159
+ },
160
+ {
161
+ cells: ["Cell 4", "Cell 5", "Cell 6"],
162
+ },
163
+ ],
164
+ },
165
+ },
166
+ ]);
167
+
168
+ // Place cursor inside a table cell
169
+ editor.setTextCursorPosition("table-0", "start");
170
+
171
+ // Verify the cursor is inside table content
172
+ const $from = editor._tiptapEditor.state.selection.$from;
173
+ expect($from.parent.type.isInGroup("tableContent")).toBe(true);
174
+
175
+ // Verify we start with no active suggestion menu
176
+ expect(getSuggestionPluginState(editor)).toBeUndefined();
177
+
178
+ // Simulate typing "@" — no shouldTrigger filter, so it should still work
179
+ const handled = simulateTextInput(editor, "@");
180
+
181
+ // The input should be handled (menu opened)
182
+ expect(handled).toBe(true);
183
+
184
+ // Plugin state should now be defined
185
+ const pluginState = getSuggestionPluginState(editor);
186
+ expect(pluginState).toBeDefined();
187
+ expect(pluginState.triggerCharacter).toBe("@");
188
+
189
+ editor._tiptapEditor.destroy();
190
+ });
191
+ });
@@ -1,14 +1,14 @@
1
1
  import { findParentNode } from "@tiptap/core";
2
- import { EditorState, Plugin, PluginKey } from "prosemirror-state";
2
+ import { EditorState, Plugin, PluginKey, Transaction } from "prosemirror-state";
3
3
  import { Decoration, DecorationSet, EditorView } from "prosemirror-view";
4
4
 
5
5
  import { trackPosition } from "../../api/positionMapping.js";
6
+ import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
6
7
  import {
7
8
  createExtension,
8
9
  createStore,
9
10
  } from "../../editor/BlockNoteExtension.js";
10
11
  import { UiElementPosition } from "../../extensions-shared/UiElementPosition.js";
11
- import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
12
12
 
13
13
  const findBlock = findParentNode((node) => node.type.name === "blockContainer");
14
14
 
@@ -149,6 +149,16 @@ type SuggestionPluginState =
149
149
  }
150
150
  | undefined;
151
151
 
152
+ export type SuggestionMenuOptions = {
153
+ triggerCharacter: string;
154
+ /**
155
+ * Optional callback to determine whether the suggestion menu should be
156
+ * opened in the current editor state. Return `false` to prevent the
157
+ * menu from opening (e.g. when the cursor is inside table content).
158
+ */
159
+ shouldOpen?: (tr: Transaction) => boolean;
160
+ };
161
+
152
162
  const suggestionMenuPluginKey = new PluginKey("SuggestionMenuPlugin");
153
163
 
154
164
  /**
@@ -162,7 +172,7 @@ const suggestionMenuPluginKey = new PluginKey("SuggestionMenuPlugin");
162
172
  * - This version handles key events differently
163
173
  */
164
174
  export const SuggestionMenu = createExtension(({ editor }) => {
165
- const triggerCharacters: string[] = [];
175
+ const suggestionMenus = new Map<string, SuggestionMenuOptions>();
166
176
  let view: SuggestionMenuView | undefined = undefined;
167
177
  const store = createStore<
168
178
  (SuggestionMenuState & { triggerCharacter: string }) | undefined
@@ -170,11 +180,11 @@ export const SuggestionMenu = createExtension(({ editor }) => {
170
180
  return {
171
181
  key: "suggestionMenu",
172
182
  store,
173
- addTriggerCharacter: (triggerCharacter: string) => {
174
- triggerCharacters.push(triggerCharacter);
183
+ addSuggestionMenu: (options: SuggestionMenuOptions) => {
184
+ suggestionMenus.set(options.triggerCharacter, options);
175
185
  },
176
- removeTriggerCharacter: (triggerCharacter: string) => {
177
- triggerCharacters.splice(triggerCharacters.indexOf(triggerCharacter), 1);
186
+ removeSuggestionMenu: (triggerCharacter: string) => {
187
+ suggestionMenus.delete(triggerCharacter);
178
188
  },
179
189
  closeMenu: () => {
180
190
  view?.closeMenu();
@@ -326,13 +336,20 @@ export const SuggestionMenu = createExtension(({ editor }) => {
326
336
  // only on insert
327
337
  if (from === to) {
328
338
  const doc = view.state.doc;
329
- for (const str of triggerCharacters) {
339
+ for (const [triggerChar, menuOptions] of suggestionMenus) {
330
340
  const snippet =
331
- str.length > 1
332
- ? doc.textBetween(from - str.length, from) + text
341
+ triggerChar.length > 1
342
+ ? doc.textBetween(from - triggerChar.length, from) + text
333
343
  : text;
334
344
 
335
- if (str === snippet) {
345
+ if (triggerChar === snippet) {
346
+ // Check the per-suggestion-menu filter before activating.
347
+ if (
348
+ menuOptions.shouldOpen &&
349
+ !menuOptions.shouldOpen(view.state.tr)
350
+ ) {
351
+ continue;
352
+ }
336
353
  view.dispatch(view.state.tr.insertText(text));
337
354
  view.dispatch(
338
355
  view.state.tr