@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.
- package/dist/{BlockNoteSchema-DmFDeA0n.cjs → BlockNoteSchema-CwhtPpVC.cjs} +2 -2
- package/dist/{BlockNoteSchema-DmFDeA0n.cjs.map → BlockNoteSchema-CwhtPpVC.cjs.map} +1 -1
- package/dist/{BlockNoteSchema-BkXw8HJ6.js → BlockNoteSchema-dmbNkHA-.js} +2 -2
- package/dist/{BlockNoteSchema-BkXw8HJ6.js.map → BlockNoteSchema-dmbNkHA-.js.map} +1 -1
- package/dist/TrailingNode-DHOdUVUO.cjs +2 -0
- package/dist/TrailingNode-DHOdUVUO.cjs.map +1 -0
- package/dist/{TrailingNode-CxM966vN.js → TrailingNode-F9hX_UlQ.js} +451 -445
- package/dist/TrailingNode-F9hX_UlQ.js.map +1 -0
- package/dist/blocknote.cjs +4 -4
- package/dist/blocknote.cjs.map +1 -1
- package/dist/blocknote.js +1624 -1370
- package/dist/blocknote.js.map +1 -1
- package/dist/blocks.cjs +1 -1
- package/dist/blocks.js +2 -2
- package/dist/{defaultBlocks-DosClM5E.cjs → defaultBlocks-CSB5GiAu.cjs} +4 -4
- package/dist/defaultBlocks-CSB5GiAu.cjs.map +1 -0
- package/dist/{defaultBlocks-DE5GNdJH.js → defaultBlocks-Caw1U1oV.js} +49 -46
- package/dist/defaultBlocks-Caw1U1oV.js.map +1 -0
- package/dist/extensions.cjs +1 -1
- package/dist/extensions.js +3 -3
- package/dist/locales.cjs +1 -1
- package/dist/locales.cjs.map +1 -1
- package/dist/locales.js +813 -28
- package/dist/locales.js.map +1 -1
- package/dist/style.css +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/webpack-stats.json +1 -1
- package/package.json +1 -1
- package/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts +30 -7
- package/src/blocks/ListItem/CheckListItem/block.test.ts +61 -0
- package/src/blocks/ListItem/CheckListItem/block.ts +4 -0
- package/src/editor/Block.css +2 -2
- package/src/editor/transformPasted.ts +69 -0
- package/src/extensions/Collaboration/YCursorPlugin.ts +3 -1
- package/src/extensions/SideMenu/SideMenu.ts +44 -0
- package/src/extensions/SuggestionMenu/SuggestionMenu.test.ts +191 -0
- package/src/extensions/SuggestionMenu/SuggestionMenu.ts +28 -11
- package/src/extensions/tiptap-extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts +470 -64
- package/src/i18n/locales/fa.ts +390 -0
- package/src/i18n/locales/index.ts +2 -0
- package/src/i18n/locales/uz.ts +421 -0
- package/src/schema/blocks/createSpec.ts +2 -0
- package/types/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.d.ts +5 -0
- package/types/src/blocks/ListItem/CheckListItem/block.test.d.ts +1 -0
- package/types/src/extensions/SuggestionMenu/SuggestionMenu.d.ts +12 -3
- package/types/src/extensions/SuggestionMenu/SuggestionMenu.test.d.ts +1 -0
- package/types/src/i18n/locales/fa.d.ts +320 -0
- package/types/src/i18n/locales/index.d.ts +2 -0
- package/types/src/i18n/locales/uz.d.ts +2 -0
- package/dist/TrailingNode-CxM966vN.js.map +0 -1
- package/dist/TrailingNode-D-CZ76FS.cjs +0 -2
- package/dist/TrailingNode-D-CZ76FS.cjs.map +0 -1
- package/dist/defaultBlocks-DE5GNdJH.js.map +0 -1
- 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
|
|
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
|
-
|
|
174
|
-
|
|
183
|
+
addSuggestionMenu: (options: SuggestionMenuOptions) => {
|
|
184
|
+
suggestionMenus.set(options.triggerCharacter, options);
|
|
175
185
|
},
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
339
|
+
for (const [triggerChar, menuOptions] of suggestionMenus) {
|
|
330
340
|
const snippet =
|
|
331
|
-
|
|
332
|
-
? doc.textBetween(from -
|
|
341
|
+
triggerChar.length > 1
|
|
342
|
+
? doc.textBetween(from - triggerChar.length, from) + text
|
|
333
343
|
: text;
|
|
334
344
|
|
|
335
|
-
if (
|
|
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
|