@blocknote/core 0.8.1 → 0.8.3
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/README.md +4 -0
- package/dist/blocknote.js +1787 -1834
- package/dist/blocknote.js.map +1 -1
- package/dist/blocknote.umd.cjs +4 -4
- package/dist/blocknote.umd.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +3 -3
- package/src/BlockNoteEditor.ts +102 -38
- package/src/BlockNoteExtensions.ts +1 -58
- package/src/api/formatConversions/__snapshots__/formatConversions.test.ts.snap +10 -10
- package/src/api/formatConversions/formatConversions.test.ts +587 -605
- package/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +15 -15
- package/src/api/nodeConversions/nodeConversions.test.ts +90 -94
- package/src/extensions/Blocks/api/blockTypes.ts +3 -2
- package/src/extensions/Blocks/helpers/getBlockInfoFromPos.ts +6 -0
- package/src/extensions/Blocks/nodes/BlockContainer.ts +12 -3
- package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +92 -104
- package/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +178 -134
- package/src/extensions/Placeholder/PlaceholderExtension.ts +2 -2
- package/src/extensions/{DraggableBlocks/DraggableBlocksPlugin.ts → SideMenu/SideMenuPlugin.ts} +173 -163
- package/src/extensions/SlashMenu/BaseSlashMenuItem.ts +7 -30
- package/src/extensions/SlashMenu/SlashMenuPlugin.ts +51 -0
- package/src/extensions/SlashMenu/defaultSlashMenuItems.ts +109 -0
- package/src/extensions/UniqueID/UniqueID.ts +29 -30
- package/src/index.ts +9 -8
- package/src/node_modules/.vitest/results.json +1 -0
- package/src/shared/BaseUiElementTypes.ts +8 -0
- package/src/shared/EditorElement.ts +0 -16
- package/src/shared/EventEmitter.ts +58 -0
- package/src/shared/plugins/suggestion/SuggestionItem.ts +3 -6
- package/src/shared/plugins/suggestion/SuggestionPlugin.ts +333 -389
- package/types/src/BlockNoteEditor.d.ts +18 -10
- package/types/src/BlockNoteExtensions.d.ts +0 -19
- package/types/src/EventEmitter.d.ts +11 -0
- package/types/src/extensions/Blocks/api/blockTypes.d.ts +3 -2
- package/types/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.d.ts +0 -17
- package/types/src/extensions/DraggableBlocks/DraggableBlocksPlugin.d.ts +25 -19
- package/types/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.d.ts +2 -3
- package/types/src/extensions/FormattingToolbar/FormattingToolbarPlugin.d.ts +17 -24
- package/types/src/extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes.d.ts +0 -12
- package/types/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.d.ts +37 -10
- package/types/src/extensions/SideMenu/MultipleNodeSelection.d.ts +24 -0
- package/types/src/extensions/SideMenu/SideMenuPlugin.d.ts +79 -0
- package/types/src/extensions/SlashMenu/BaseSlashMenuItem.d.ts +5 -18
- package/types/src/extensions/SlashMenu/SlashMenuPlugin.d.ts +13 -0
- package/types/src/extensions/SlashMenu/defaultSlashMenuItems.d.ts +1 -69
- package/types/src/extensions/SlashMenu/index.d.ts +2 -3
- package/types/src/index.d.ts +9 -8
- package/types/src/shared/BaseUiElementTypes.d.ts +7 -0
- package/types/src/shared/EditorElement.d.ts +0 -10
- package/types/src/shared/EventEmitter.d.ts +11 -0
- package/types/src/shared/plugins/suggestion/SuggestionItem.d.ts +2 -7
- package/types/src/shared/plugins/suggestion/SuggestionPlugin.d.ts +12 -43
- package/types/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.d.ts +1 -1
- package/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts +0 -29
- package/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts +0 -37
- package/src/extensions/FormattingToolbar/FormattingToolbarExtension.ts +0 -37
- package/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts +0 -20
- package/src/extensions/HyperlinkToolbar/HyperlinkMark.ts +0 -28
- package/src/extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes.ts +0 -19
- package/src/extensions/SlashMenu/SlashMenuExtension.ts +0 -53
- package/src/extensions/SlashMenu/defaultSlashMenuItems.tsx +0 -195
- package/src/extensions/SlashMenu/index.ts +0 -5
- package/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts +0 -21
- package/types/src/CustomBlock.d.ts +0 -15
- package/types/src/extensions/Blocks/nodes/BlockContent/TableContent/TableCol.d.ts +0 -2
- package/types/src/extensions/Blocks/nodes/BlockContent/TableContent/TableContent.d.ts +0 -2
- package/types/src/extensions/Blocks/nodes/BlockContent/TableContent/TableRow.d.ts +0 -2
- package/types/src/extensions/Placeholder/localisation/index.d.ts +0 -2
- package/types/src/extensions/Placeholder/localisation/translation.d.ts +0 -51
- /package/src/extensions/{DraggableBlocks → SideMenu}/MultipleNodeSelection.ts +0 -0
|
@@ -1,149 +1,56 @@
|
|
|
1
|
-
import { Editor, Range } from "@tiptap/core";
|
|
2
1
|
import { EditorState, Plugin, PluginKey } from "prosemirror-state";
|
|
3
2
|
import { Decoration, DecorationSet, EditorView } from "prosemirror-view";
|
|
4
|
-
import { findBlock } from "../../../extensions/Blocks/helpers/findBlock";
|
|
5
|
-
import {
|
|
6
|
-
SuggestionsMenu,
|
|
7
|
-
SuggestionsMenuDynamicParams,
|
|
8
|
-
SuggestionsMenuFactory,
|
|
9
|
-
SuggestionsMenuStaticParams,
|
|
10
|
-
} from "./SuggestionsMenuFactoryTypes";
|
|
11
|
-
import { SuggestionItem } from "./SuggestionItem";
|
|
12
3
|
import { BlockNoteEditor } from "../../../BlockNoteEditor";
|
|
13
4
|
import { BlockSchema } from "../../../extensions/Blocks/api/blockTypes";
|
|
5
|
+
import { findBlock } from "../../../extensions/Blocks/helpers/findBlock";
|
|
6
|
+
import { BaseUiElementState } from "../../BaseUiElementTypes";
|
|
7
|
+
import { SuggestionItem } from "./SuggestionItem";
|
|
14
8
|
|
|
15
|
-
export type
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
*
|
|
22
|
-
* Used for ensuring that the plugin key is unique when more than one instance of the SuggestionPlugin is used.
|
|
23
|
-
*/
|
|
24
|
-
pluginKey: PluginKey;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* The BlockNote editor.
|
|
28
|
-
*/
|
|
29
|
-
editor: BlockNoteEditor<BSchema>;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* The character that should trigger the suggestion menu to pop up (e.g. a '/' for commands), when typed by the user.
|
|
33
|
-
*/
|
|
34
|
-
defaultTriggerCharacter: string;
|
|
35
|
-
|
|
36
|
-
suggestionsMenuFactory: SuggestionsMenuFactory<T>;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The callback that gets executed when an item is selected by the user.
|
|
40
|
-
*
|
|
41
|
-
* **NOTE:** The command text is not removed automatically from the editor by this plugin,
|
|
42
|
-
* this should be done manually. The `editor` and `range` properties passed
|
|
43
|
-
* to the callback function might come in handy when doing this.
|
|
44
|
-
*/
|
|
45
|
-
onSelectItem?: (props: { item: T; editor: BlockNoteEditor<BSchema> }) => void;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* A function that should supply the plugin with items to suggest, based on a certain query string.
|
|
49
|
-
*/
|
|
50
|
-
items?: (query: string) => T[];
|
|
51
|
-
|
|
52
|
-
allow?: (props: { editor: Editor; range: Range }) => boolean;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
type SuggestionPluginState<T extends SuggestionItem> = {
|
|
56
|
-
// True when the menu is shown, false when hidden.
|
|
57
|
-
active: boolean;
|
|
58
|
-
// The character that triggered the menu being shown. Allowing the trigger to be different to the default
|
|
59
|
-
// trigger allows other extensions to open it programmatically.
|
|
60
|
-
triggerCharacter: string | undefined;
|
|
61
|
-
// The editor position just after the trigger character, i.e. where the user query begins. Used to figure out
|
|
62
|
-
// which menu items to show and can also be used to delete the trigger character.
|
|
63
|
-
queryStartPos: number | undefined;
|
|
64
|
-
// The items that should be shown in the menu.
|
|
65
|
-
items: T[];
|
|
66
|
-
// The index of the item in the menu that's currently hovered using the keyboard.
|
|
67
|
-
keyboardHoveredItemIndex: number | undefined;
|
|
68
|
-
// The number of characters typed after the last query that matched with at least 1 item. Used to close the
|
|
69
|
-
// menu if the user keeps entering queries that don't return any results.
|
|
70
|
-
notFoundCount: number | undefined;
|
|
71
|
-
decorationId: string | undefined;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
function getDefaultPluginState<
|
|
75
|
-
T extends SuggestionItem
|
|
76
|
-
>(): SuggestionPluginState<T> {
|
|
77
|
-
return {
|
|
78
|
-
active: false,
|
|
79
|
-
triggerCharacter: undefined,
|
|
80
|
-
queryStartPos: undefined,
|
|
81
|
-
items: [] as T[],
|
|
82
|
-
keyboardHoveredItemIndex: undefined,
|
|
83
|
-
notFoundCount: 0,
|
|
84
|
-
decorationId: undefined,
|
|
9
|
+
export type SuggestionsMenuState<T extends SuggestionItem> =
|
|
10
|
+
BaseUiElementState & {
|
|
11
|
+
// The suggested items to display.
|
|
12
|
+
filteredItems: T[];
|
|
13
|
+
// The index of the suggested item that's currently hovered by the keyboard.
|
|
14
|
+
keyboardHoveredItemIndex: number;
|
|
85
15
|
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
type SuggestionPluginViewOptions<
|
|
89
|
-
T extends SuggestionItem,
|
|
90
|
-
BSchema extends BlockSchema
|
|
91
|
-
> = {
|
|
92
|
-
editor: BlockNoteEditor<BSchema>;
|
|
93
|
-
pluginKey: PluginKey;
|
|
94
|
-
onSelectItem: (props: { item: T; editor: BlockNoteEditor<BSchema> }) => void;
|
|
95
|
-
suggestionsMenuFactory: SuggestionsMenuFactory<T>;
|
|
96
|
-
};
|
|
97
16
|
|
|
98
|
-
class
|
|
17
|
+
class SuggestionsMenuView<
|
|
99
18
|
T extends SuggestionItem,
|
|
100
19
|
BSchema extends BlockSchema
|
|
101
20
|
> {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
suggestionsMenu: SuggestionsMenu<T>;
|
|
21
|
+
private suggestionsMenuState?: SuggestionsMenuState<T>;
|
|
22
|
+
public updateSuggestionsMenu: () => void;
|
|
106
23
|
|
|
107
24
|
pluginState: SuggestionPluginState<T>;
|
|
108
|
-
itemCallback: (item: T) => void;
|
|
109
|
-
|
|
110
|
-
constructor({
|
|
111
|
-
editor,
|
|
112
|
-
pluginKey,
|
|
113
|
-
onSelectItem: selectItemCallback = () => {},
|
|
114
|
-
suggestionsMenuFactory,
|
|
115
|
-
}: SuggestionPluginViewOptions<T, BSchema>) {
|
|
116
|
-
this.editor = editor;
|
|
117
|
-
this.pluginKey = pluginKey;
|
|
118
25
|
|
|
26
|
+
constructor(
|
|
27
|
+
private readonly editor: BlockNoteEditor<BSchema>,
|
|
28
|
+
private readonly pluginKey: PluginKey,
|
|
29
|
+
updateSuggestionsMenu: (
|
|
30
|
+
suggestionsMenuState: SuggestionsMenuState<T>
|
|
31
|
+
) => void = () => {}
|
|
32
|
+
) {
|
|
119
33
|
this.pluginState = getDefaultPluginState<T>();
|
|
120
34
|
|
|
121
|
-
this.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
.deleteRange({
|
|
126
|
-
from:
|
|
127
|
-
this.pluginState.queryStartPos! -
|
|
128
|
-
this.pluginState.triggerCharacter!.length,
|
|
129
|
-
to: editor._tiptapEditor.state.selection.from,
|
|
130
|
-
})
|
|
131
|
-
.run();
|
|
35
|
+
this.updateSuggestionsMenu = () => {
|
|
36
|
+
if (!this.suggestionsMenuState) {
|
|
37
|
+
throw new Error("Attempting to update uninitialized suggestions menu");
|
|
38
|
+
}
|
|
132
39
|
|
|
133
|
-
|
|
134
|
-
item: item,
|
|
135
|
-
editor: editor,
|
|
136
|
-
});
|
|
40
|
+
updateSuggestionsMenu(this.suggestionsMenuState);
|
|
137
41
|
};
|
|
138
42
|
|
|
139
|
-
this.suggestionsMenu = suggestionsMenuFactory(this.getStaticParams());
|
|
140
|
-
|
|
141
43
|
document.addEventListener("scroll", this.handleScroll);
|
|
142
44
|
}
|
|
143
45
|
|
|
144
46
|
handleScroll = () => {
|
|
145
|
-
if (this.
|
|
146
|
-
|
|
47
|
+
if (this.suggestionsMenuState?.show) {
|
|
48
|
+
const decorationNode = document.querySelector(
|
|
49
|
+
`[data-decoration-id="${this.pluginState.decorationId}"]`
|
|
50
|
+
);
|
|
51
|
+
this.suggestionsMenuState.referencePos =
|
|
52
|
+
decorationNode!.getBoundingClientRect();
|
|
53
|
+
this.updateSuggestionsMenu();
|
|
147
54
|
}
|
|
148
55
|
};
|
|
149
56
|
|
|
@@ -166,49 +73,64 @@ class SuggestionPluginView<
|
|
|
166
73
|
this.pluginState = stopped ? prev : next;
|
|
167
74
|
|
|
168
75
|
if (stopped || !this.editor.isEditable) {
|
|
169
|
-
this.
|
|
76
|
+
this.suggestionsMenuState!.show = false;
|
|
77
|
+
this.updateSuggestionsMenu();
|
|
170
78
|
|
|
171
|
-
|
|
172
|
-
this.suggestionsMenu.element!.removeEventListener("mousedown", (event) =>
|
|
173
|
-
event.preventDefault()
|
|
174
|
-
);
|
|
79
|
+
return;
|
|
175
80
|
}
|
|
176
81
|
|
|
177
|
-
|
|
178
|
-
this.
|
|
179
|
-
|
|
82
|
+
const decorationNode = document.querySelector(
|
|
83
|
+
`[data-decoration-id="${this.pluginState.decorationId}"]`
|
|
84
|
+
);
|
|
180
85
|
|
|
181
|
-
if (
|
|
182
|
-
this.
|
|
86
|
+
if (this.editor.isEditable) {
|
|
87
|
+
this.suggestionsMenuState = {
|
|
88
|
+
show: true,
|
|
89
|
+
referencePos: decorationNode!.getBoundingClientRect(),
|
|
90
|
+
filteredItems: this.pluginState.items,
|
|
91
|
+
keyboardHoveredItemIndex: this.pluginState.keyboardHoveredItemIndex!,
|
|
92
|
+
};
|
|
183
93
|
|
|
184
|
-
|
|
185
|
-
this.suggestionsMenu.element!.addEventListener("mousedown", (event) =>
|
|
186
|
-
event.preventDefault()
|
|
187
|
-
);
|
|
94
|
+
this.updateSuggestionsMenu();
|
|
188
95
|
}
|
|
189
96
|
}
|
|
190
97
|
|
|
191
98
|
destroy() {
|
|
192
99
|
document.removeEventListener("scroll", this.handleScroll);
|
|
193
100
|
}
|
|
101
|
+
}
|
|
194
102
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
103
|
+
type SuggestionPluginState<T extends SuggestionItem> = {
|
|
104
|
+
// True when the menu is shown, false when hidden.
|
|
105
|
+
active: boolean;
|
|
106
|
+
// The character that triggered the menu being shown. Allowing the trigger to be different to the default
|
|
107
|
+
// trigger allows other extensions to open it programmatically.
|
|
108
|
+
triggerCharacter: string | undefined;
|
|
109
|
+
// The editor position just after the trigger character, i.e. where the user query begins. Used to figure out
|
|
110
|
+
// which menu items to show and can also be used to delete the trigger character.
|
|
111
|
+
queryStartPos: number | undefined;
|
|
112
|
+
// The items that should be shown in the menu.
|
|
113
|
+
items: T[];
|
|
114
|
+
// The index of the item in the menu that's currently hovered using the keyboard.
|
|
115
|
+
keyboardHoveredItemIndex: number | undefined;
|
|
116
|
+
// The number of characters typed after the last query that matched with at least 1 item. Used to close the
|
|
117
|
+
// menu if the user keeps entering queries that don't return any results.
|
|
118
|
+
notFoundCount: number | undefined;
|
|
119
|
+
decorationId: string | undefined;
|
|
120
|
+
};
|
|
205
121
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
122
|
+
function getDefaultPluginState<
|
|
123
|
+
T extends SuggestionItem
|
|
124
|
+
>(): SuggestionPluginState<T> {
|
|
125
|
+
return {
|
|
126
|
+
active: false,
|
|
127
|
+
triggerCharacter: undefined,
|
|
128
|
+
queryStartPos: undefined,
|
|
129
|
+
items: [] as T[],
|
|
130
|
+
keyboardHoveredItemIndex: undefined,
|
|
131
|
+
notFoundCount: 0,
|
|
132
|
+
decorationId: undefined,
|
|
133
|
+
};
|
|
212
134
|
}
|
|
213
135
|
|
|
214
136
|
/**
|
|
@@ -220,271 +142,293 @@ class SuggestionPluginView<
|
|
|
220
142
|
* - This version supports generic items instead of only strings (to allow for more advanced filtering for example)
|
|
221
143
|
* - This version hides some unnecessary complexity from the user of the plugin.
|
|
222
144
|
* - This version handles key events differently
|
|
223
|
-
*
|
|
224
|
-
* @param options options for configuring the plugin
|
|
225
|
-
* @returns the prosemirror plugin
|
|
226
145
|
*/
|
|
227
|
-
export
|
|
146
|
+
export const setupSuggestionsMenu = <
|
|
228
147
|
T extends SuggestionItem,
|
|
229
148
|
BSchema extends BlockSchema
|
|
230
|
-
>(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
149
|
+
>(
|
|
150
|
+
editor: BlockNoteEditor<BSchema>,
|
|
151
|
+
updateSuggestionsMenu: (
|
|
152
|
+
suggestionsMenuState: SuggestionsMenuState<T>
|
|
153
|
+
) => void,
|
|
154
|
+
|
|
155
|
+
pluginKey: PluginKey,
|
|
156
|
+
defaultTriggerCharacter: string,
|
|
157
|
+
items: (query: string) => T[] = () => [],
|
|
158
|
+
onSelectItem: (props: {
|
|
159
|
+
item: T;
|
|
160
|
+
editor: BlockNoteEditor<BSchema>;
|
|
161
|
+
}) => void = () => {}
|
|
162
|
+
) => {
|
|
238
163
|
// Assertions
|
|
239
164
|
if (defaultTriggerCharacter.length !== 1) {
|
|
240
165
|
throw new Error("'char' should be a single character");
|
|
241
166
|
}
|
|
242
167
|
|
|
168
|
+
let suggestionsPluginView: SuggestionsMenuView<T, BSchema>;
|
|
169
|
+
|
|
243
170
|
const deactivate = (view: EditorView) => {
|
|
244
171
|
view.dispatch(view.state.tr.setMeta(pluginKey, { deactivate: true }));
|
|
245
172
|
};
|
|
246
173
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
174
|
+
return {
|
|
175
|
+
plugin: new Plugin({
|
|
176
|
+
key: pluginKey,
|
|
250
177
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
onSelectItem: (props: {
|
|
256
|
-
item: T;
|
|
257
|
-
editor: BlockNoteEditor<BSchema>;
|
|
258
|
-
}) => {
|
|
259
|
-
deactivate(view);
|
|
260
|
-
selectItemCallback(props);
|
|
261
|
-
},
|
|
262
|
-
suggestionsMenuFactory: suggestionsMenuFactory,
|
|
263
|
-
}),
|
|
178
|
+
view: () => {
|
|
179
|
+
suggestionsPluginView = new SuggestionsMenuView<T, BSchema>(
|
|
180
|
+
editor,
|
|
181
|
+
pluginKey,
|
|
264
182
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
return getDefaultPluginState<T>();
|
|
183
|
+
updateSuggestionsMenu
|
|
184
|
+
);
|
|
185
|
+
return suggestionsPluginView;
|
|
269
186
|
},
|
|
270
187
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const next = { ...prev };
|
|
300
|
-
|
|
301
|
-
// Updates which menu items to show by checking which items the current query (the text between the trigger
|
|
302
|
-
// character and caret) matches with.
|
|
303
|
-
next.items = items(
|
|
304
|
-
newState.doc.textBetween(prev.queryStartPos!, newState.selection.from)
|
|
305
|
-
);
|
|
188
|
+
state: {
|
|
189
|
+
// Initialize the plugin's internal state.
|
|
190
|
+
init(): SuggestionPluginState<T> {
|
|
191
|
+
return getDefaultPluginState<T>();
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
// Apply changes to the plugin state from an editor transaction.
|
|
195
|
+
apply(transaction, prev, oldState, newState): SuggestionPluginState<T> {
|
|
196
|
+
// TODO: More clearly define which transactions should be ignored.
|
|
197
|
+
if (transaction.getMeta("orderedListIndexing") !== undefined) {
|
|
198
|
+
return prev;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Checks if the menu should be shown.
|
|
202
|
+
if (transaction.getMeta(pluginKey)?.activate) {
|
|
203
|
+
return {
|
|
204
|
+
active: true,
|
|
205
|
+
triggerCharacter:
|
|
206
|
+
transaction.getMeta(pluginKey)?.triggerCharacter || "",
|
|
207
|
+
queryStartPos: newState.selection.from,
|
|
208
|
+
items: items(""),
|
|
209
|
+
keyboardHoveredItemIndex: 0,
|
|
210
|
+
// TODO: Maybe should be 1 if the menu has no possible items? Probably redundant since a menu with no items
|
|
211
|
+
// is useless in practice.
|
|
212
|
+
notFoundCount: 0,
|
|
213
|
+
decorationId: `id_${Math.floor(Math.random() * 0xffffffff)}`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
306
216
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
next
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
217
|
+
// Checks if the menu is hidden, in which case it doesn't need to be hidden or updated.
|
|
218
|
+
if (!prev.active) {
|
|
219
|
+
return prev;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const next = { ...prev };
|
|
223
|
+
|
|
224
|
+
// Updates which menu items to show by checking which items the current query (the text between the trigger
|
|
225
|
+
// character and caret) matches with.
|
|
226
|
+
next.items = items(
|
|
227
|
+
newState.doc.textBetween(
|
|
228
|
+
prev.queryStartPos!,
|
|
229
|
+
newState.selection.from
|
|
230
|
+
)
|
|
316
231
|
);
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
transaction.getMeta("focus") ||
|
|
329
|
-
transaction.getMeta("blur") ||
|
|
330
|
-
transaction.getMeta("pointer") ||
|
|
331
|
-
// Moving the caret before the character which triggered the menu should hide it.
|
|
332
|
-
(prev.active && newState.selection.from < prev.queryStartPos!) ||
|
|
333
|
-
// Entering more than 3 characters, after the last query that matched with at least 1 menu item, should hide
|
|
334
|
-
// the menu.
|
|
335
|
-
next.notFoundCount > 3
|
|
336
|
-
) {
|
|
337
|
-
return getDefaultPluginState<T>();
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Updates keyboardHoveredItemIndex if necessary.
|
|
341
|
-
if (
|
|
342
|
-
transaction.getMeta(pluginKey)?.selectedItemIndexChanged !== undefined
|
|
343
|
-
) {
|
|
344
|
-
let newIndex =
|
|
345
|
-
transaction.getMeta(pluginKey).selectedItemIndexChanged;
|
|
346
|
-
|
|
347
|
-
// Allows selection to jump between first and last items.
|
|
348
|
-
if (newIndex < 0) {
|
|
349
|
-
newIndex = prev.items.length - 1;
|
|
350
|
-
} else if (newIndex >= prev.items.length) {
|
|
351
|
-
newIndex = 0;
|
|
232
|
+
|
|
233
|
+
// Updates notFoundCount if the query doesn't match any items.
|
|
234
|
+
next.notFoundCount = 0;
|
|
235
|
+
if (next.items.length === 0) {
|
|
236
|
+
// Checks how many characters were typed or deleted since the last transaction, and updates the notFoundCount
|
|
237
|
+
// accordingly. Also ensures the notFoundCount does not become negative.
|
|
238
|
+
next.notFoundCount = Math.max(
|
|
239
|
+
0,
|
|
240
|
+
prev.notFoundCount! +
|
|
241
|
+
(newState.selection.from - oldState.selection.from)
|
|
242
|
+
);
|
|
352
243
|
}
|
|
353
244
|
|
|
354
|
-
|
|
355
|
-
|
|
245
|
+
// Hides the menu. This is done after items and notFoundCount are already updated as notFoundCount is needed to
|
|
246
|
+
// check if the menu should be hidden.
|
|
247
|
+
if (
|
|
248
|
+
// Highlighting text should hide the menu.
|
|
249
|
+
newState.selection.from !== newState.selection.to ||
|
|
250
|
+
// Transactions with plugin metadata {deactivate: true} should hide the menu.
|
|
251
|
+
transaction.getMeta(pluginKey)?.deactivate ||
|
|
252
|
+
// Certain mouse events should hide the menu.
|
|
253
|
+
// TODO: Change to global mousedown listener.
|
|
254
|
+
transaction.getMeta("focus") ||
|
|
255
|
+
transaction.getMeta("blur") ||
|
|
256
|
+
transaction.getMeta("pointer") ||
|
|
257
|
+
// Moving the caret before the character which triggered the menu should hide it.
|
|
258
|
+
(prev.active && newState.selection.from < prev.queryStartPos!) ||
|
|
259
|
+
// Entering more than 3 characters, after the last query that matched with at least 1 menu item, should hide
|
|
260
|
+
// the menu.
|
|
261
|
+
next.notFoundCount > 3
|
|
262
|
+
) {
|
|
263
|
+
return getDefaultPluginState<T>();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Updates keyboardHoveredItemIndex if the up or down arrow key was
|
|
267
|
+
// pressed, or resets it if the keyboard cursor moved.
|
|
268
|
+
if (
|
|
269
|
+
transaction.getMeta(pluginKey)?.selectedItemIndexChanged !==
|
|
270
|
+
undefined
|
|
271
|
+
) {
|
|
272
|
+
let newIndex =
|
|
273
|
+
transaction.getMeta(pluginKey).selectedItemIndexChanged;
|
|
274
|
+
|
|
275
|
+
// Allows selection to jump between first and last items.
|
|
276
|
+
if (newIndex < 0) {
|
|
277
|
+
newIndex = prev.items.length - 1;
|
|
278
|
+
} else if (newIndex >= prev.items.length) {
|
|
279
|
+
newIndex = 0;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
next.keyboardHoveredItemIndex = newIndex;
|
|
283
|
+
} else if (oldState.selection.from !== newState.selection.from) {
|
|
284
|
+
next.keyboardHoveredItemIndex = 0;
|
|
285
|
+
}
|
|
356
286
|
|
|
357
|
-
|
|
287
|
+
return next;
|
|
288
|
+
},
|
|
358
289
|
},
|
|
359
|
-
},
|
|
360
290
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
291
|
+
props: {
|
|
292
|
+
handleKeyDown(view, event) {
|
|
293
|
+
const menuIsActive = (this as Plugin).getState(view.state).active;
|
|
294
|
+
|
|
295
|
+
// Shows the menu if the default trigger character was pressed and the menu isn't active.
|
|
296
|
+
if (event.key === defaultTriggerCharacter && !menuIsActive) {
|
|
297
|
+
view.dispatch(
|
|
298
|
+
view.state.tr
|
|
299
|
+
.insertText(defaultTriggerCharacter)
|
|
300
|
+
.scrollIntoView()
|
|
301
|
+
.setMeta(pluginKey, {
|
|
302
|
+
activate: true,
|
|
303
|
+
triggerCharacter: defaultTriggerCharacter,
|
|
304
|
+
})
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Doesn't handle other keystrokes if the menu isn't active.
|
|
311
|
+
if (!menuIsActive) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Handles keystrokes for navigating the menu.
|
|
316
|
+
const {
|
|
317
|
+
triggerCharacter,
|
|
318
|
+
queryStartPos,
|
|
319
|
+
items,
|
|
320
|
+
keyboardHoveredItemIndex,
|
|
321
|
+
} = pluginKey.getState(view.state);
|
|
322
|
+
|
|
323
|
+
// Moves the keyboard selection to the previous item.
|
|
324
|
+
if (event.key === "ArrowUp") {
|
|
325
|
+
view.dispatch(
|
|
326
|
+
view.state.tr.setMeta(pluginKey, {
|
|
327
|
+
selectedItemIndexChanged: keyboardHoveredItemIndex - 1,
|
|
374
328
|
})
|
|
375
|
-
|
|
329
|
+
);
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
376
332
|
|
|
377
|
-
|
|
378
|
-
|
|
333
|
+
// Moves the keyboard selection to the next item.
|
|
334
|
+
if (event.key === "ArrowDown") {
|
|
335
|
+
view.dispatch(
|
|
336
|
+
view.state.tr.setMeta(pluginKey, {
|
|
337
|
+
selectedItemIndexChanged: keyboardHoveredItemIndex + 1,
|
|
338
|
+
})
|
|
339
|
+
);
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Selects an item and closes the menu.
|
|
344
|
+
if (event.key === "Enter") {
|
|
345
|
+
deactivate(view);
|
|
346
|
+
editor._tiptapEditor
|
|
347
|
+
.chain()
|
|
348
|
+
.focus()
|
|
349
|
+
.deleteRange({
|
|
350
|
+
from: queryStartPos! - triggerCharacter!.length,
|
|
351
|
+
to: editor._tiptapEditor.state.selection.from,
|
|
352
|
+
})
|
|
353
|
+
.run();
|
|
354
|
+
|
|
355
|
+
onSelectItem({
|
|
356
|
+
item: items[keyboardHoveredItemIndex],
|
|
357
|
+
editor: editor,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Closes the menu.
|
|
364
|
+
if (event.key === "Escape") {
|
|
365
|
+
deactivate(view);
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
379
368
|
|
|
380
|
-
// Doesn't handle other keystrokes if the menu isn't active.
|
|
381
|
-
if (!menuIsActive) {
|
|
382
369
|
return false;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Handles keystrokes for navigating the menu.
|
|
386
|
-
const {
|
|
387
|
-
triggerCharacter,
|
|
388
|
-
queryStartPos,
|
|
389
|
-
items,
|
|
390
|
-
keyboardHoveredItemIndex,
|
|
391
|
-
} = pluginKey.getState(view.state);
|
|
392
|
-
|
|
393
|
-
// Moves the keyboard selection to the previous item.
|
|
394
|
-
if (event.key === "ArrowUp") {
|
|
395
|
-
view.dispatch(
|
|
396
|
-
view.state.tr.setMeta(pluginKey, {
|
|
397
|
-
selectedItemIndexChanged: keyboardHoveredItemIndex - 1,
|
|
398
|
-
})
|
|
399
|
-
);
|
|
400
|
-
return true;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Moves the keyboard selection to the next item.
|
|
404
|
-
if (event.key === "ArrowDown") {
|
|
405
|
-
view.dispatch(
|
|
406
|
-
view.state.tr.setMeta(pluginKey, {
|
|
407
|
-
selectedItemIndexChanged: keyboardHoveredItemIndex + 1,
|
|
408
|
-
})
|
|
409
|
-
);
|
|
410
|
-
return true;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Selects an item and closes the menu.
|
|
414
|
-
if (event.key === "Enter") {
|
|
415
|
-
deactivate(view);
|
|
416
|
-
editor._tiptapEditor
|
|
417
|
-
.chain()
|
|
418
|
-
.focus()
|
|
419
|
-
.deleteRange({
|
|
420
|
-
from: queryStartPos! - triggerCharacter!.length,
|
|
421
|
-
to: editor._tiptapEditor.state.selection.from,
|
|
422
|
-
})
|
|
423
|
-
.run();
|
|
424
|
-
|
|
425
|
-
selectItemCallback({
|
|
426
|
-
item: items[keyboardHoveredItemIndex],
|
|
427
|
-
editor: editor,
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
return true;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Closes the menu.
|
|
434
|
-
if (event.key === "Escape") {
|
|
435
|
-
deactivate(view);
|
|
436
|
-
return true;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
return false;
|
|
440
|
-
},
|
|
370
|
+
},
|
|
441
371
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
372
|
+
// Setup decorator on the currently active suggestion.
|
|
373
|
+
decorations(state) {
|
|
374
|
+
const { active, decorationId, queryStartPos, triggerCharacter } = (
|
|
375
|
+
this as Plugin
|
|
376
|
+
).getState(state);
|
|
446
377
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const { active, decorationId, queryStartPos, triggerCharacter } = (
|
|
450
|
-
this as Plugin
|
|
451
|
-
).getState(state);
|
|
452
|
-
|
|
453
|
-
if (!active) {
|
|
454
|
-
return null;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// If the menu was opened programmatically by another extension, it may not use a trigger character. In this
|
|
458
|
-
// case, the decoration is set on the whole block instead, as the decoration range would otherwise be empty.
|
|
459
|
-
if (triggerCharacter === "") {
|
|
460
|
-
const blockNode = findBlock(state.selection);
|
|
461
|
-
if (blockNode) {
|
|
462
|
-
return DecorationSet.create(state.doc, [
|
|
463
|
-
Decoration.node(
|
|
464
|
-
blockNode.pos,
|
|
465
|
-
blockNode.pos + blockNode.node.nodeSize,
|
|
466
|
-
{
|
|
467
|
-
nodeName: "span",
|
|
468
|
-
class: "suggestion-decorator",
|
|
469
|
-
"data-decoration-id": decorationId,
|
|
470
|
-
}
|
|
471
|
-
),
|
|
472
|
-
]);
|
|
378
|
+
if (!active) {
|
|
379
|
+
return null;
|
|
473
380
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
381
|
+
|
|
382
|
+
// If the menu was opened programmatically by another extension, it may not use a trigger character. In this
|
|
383
|
+
// case, the decoration is set on the whole block instead, as the decoration range would otherwise be empty.
|
|
384
|
+
if (triggerCharacter === "") {
|
|
385
|
+
const blockNode = findBlock(state.selection);
|
|
386
|
+
if (blockNode) {
|
|
387
|
+
return DecorationSet.create(state.doc, [
|
|
388
|
+
Decoration.node(
|
|
389
|
+
blockNode.pos,
|
|
390
|
+
blockNode.pos + blockNode.node.nodeSize,
|
|
391
|
+
{
|
|
392
|
+
nodeName: "span",
|
|
393
|
+
class: "suggestion-decorator",
|
|
394
|
+
"data-decoration-id": decorationId,
|
|
395
|
+
}
|
|
396
|
+
),
|
|
397
|
+
]);
|
|
484
398
|
}
|
|
485
|
-
|
|
486
|
-
|
|
399
|
+
}
|
|
400
|
+
// Creates an inline decoration around the trigger character.
|
|
401
|
+
return DecorationSet.create(state.doc, [
|
|
402
|
+
Decoration.inline(
|
|
403
|
+
queryStartPos - triggerCharacter.length,
|
|
404
|
+
queryStartPos,
|
|
405
|
+
{
|
|
406
|
+
nodeName: "span",
|
|
407
|
+
class: "suggestion-decorator",
|
|
408
|
+
"data-decoration-id": decorationId,
|
|
409
|
+
}
|
|
410
|
+
),
|
|
411
|
+
]);
|
|
412
|
+
},
|
|
487
413
|
},
|
|
414
|
+
}),
|
|
415
|
+
itemCallback: (item: T) => {
|
|
416
|
+
deactivate(editor._tiptapEditor.view);
|
|
417
|
+
editor._tiptapEditor
|
|
418
|
+
.chain()
|
|
419
|
+
.focus()
|
|
420
|
+
.deleteRange({
|
|
421
|
+
from:
|
|
422
|
+
suggestionsPluginView.pluginState.queryStartPos! -
|
|
423
|
+
suggestionsPluginView.pluginState.triggerCharacter!.length,
|
|
424
|
+
to: editor._tiptapEditor.state.selection.from,
|
|
425
|
+
})
|
|
426
|
+
.run();
|
|
427
|
+
|
|
428
|
+
onSelectItem({
|
|
429
|
+
item: item,
|
|
430
|
+
editor: editor,
|
|
431
|
+
});
|
|
488
432
|
},
|
|
489
|
-
}
|
|
490
|
-
}
|
|
433
|
+
};
|
|
434
|
+
};
|