@blocknote/core 0.8.2 → 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 +1777 -1849
- 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 +2 -2
- package/src/BlockNoteEditor.ts +89 -39
- 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/FormattingToolbar/FormattingToolbarPlugin.ts +101 -114
- package/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +184 -149
- package/src/extensions/Placeholder/PlaceholderExtension.ts +2 -2
- package/src/extensions/{DraggableBlocks/DraggableBlocksPlugin.ts → SideMenu/SideMenuPlugin.ts} +181 -164
- 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 +341 -403
- package/types/src/BlockNoteEditor.d.ts +18 -11
- 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 +26 -20
- package/types/src/extensions/FormattingToolbar/FormattingToolbarPlugin.d.ts +18 -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/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 -18
- 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,146 +1,59 @@
|
|
|
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
16
|
|
|
88
|
-
|
|
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
|
-
|
|
98
|
-
class SuggestionPluginView<
|
|
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
|
-
private lastPosition: DOMRect | undefined;
|
|
111
|
-
|
|
112
|
-
constructor({
|
|
113
|
-
editor,
|
|
114
|
-
pluginKey,
|
|
115
|
-
onSelectItem: selectItemCallback = () => {},
|
|
116
|
-
suggestionsMenuFactory,
|
|
117
|
-
}: SuggestionPluginViewOptions<T, BSchema>) {
|
|
118
|
-
this.editor = editor;
|
|
119
|
-
this.pluginKey = pluginKey;
|
|
120
25
|
|
|
26
|
+
constructor(
|
|
27
|
+
private readonly editor: BlockNoteEditor<BSchema>,
|
|
28
|
+
private readonly pluginKey: PluginKey,
|
|
29
|
+
updateSuggestionsMenu: (
|
|
30
|
+
suggestionsMenuState: SuggestionsMenuState<T>
|
|
31
|
+
) => void = () => {}
|
|
32
|
+
) {
|
|
121
33
|
this.pluginState = getDefaultPluginState<T>();
|
|
122
34
|
|
|
123
|
-
this.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
.deleteRange({
|
|
128
|
-
from:
|
|
129
|
-
this.pluginState.queryStartPos! -
|
|
130
|
-
this.pluginState.triggerCharacter!.length,
|
|
131
|
-
to: editor._tiptapEditor.state.selection.from,
|
|
132
|
-
})
|
|
133
|
-
.run();
|
|
35
|
+
this.updateSuggestionsMenu = () => {
|
|
36
|
+
if (!this.suggestionsMenuState) {
|
|
37
|
+
throw new Error("Attempting to update uninitialized suggestions menu");
|
|
38
|
+
}
|
|
134
39
|
|
|
135
|
-
|
|
136
|
-
item: item,
|
|
137
|
-
editor: editor,
|
|
138
|
-
});
|
|
40
|
+
updateSuggestionsMenu(this.suggestionsMenuState);
|
|
139
41
|
};
|
|
140
42
|
|
|
141
|
-
|
|
43
|
+
document.addEventListener("scroll", this.handleScroll);
|
|
142
44
|
}
|
|
143
45
|
|
|
46
|
+
handleScroll = () => {
|
|
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();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
144
57
|
update(view: EditorView, prevState: EditorState) {
|
|
145
58
|
const prev = this.pluginKey.getState(prevState);
|
|
146
59
|
const next = this.pluginKey.getState(view.state);
|
|
@@ -160,61 +73,64 @@ class SuggestionPluginView<
|
|
|
160
73
|
this.pluginState = stopped ? prev : next;
|
|
161
74
|
|
|
162
75
|
if (stopped || !this.editor.isEditable) {
|
|
163
|
-
this.
|
|
76
|
+
this.suggestionsMenuState!.show = false;
|
|
77
|
+
this.updateSuggestionsMenu();
|
|
164
78
|
|
|
165
|
-
|
|
166
|
-
this.suggestionsMenu.element!.removeEventListener("mousedown", (event) =>
|
|
167
|
-
event.preventDefault()
|
|
168
|
-
);
|
|
79
|
+
return;
|
|
169
80
|
}
|
|
170
81
|
|
|
171
|
-
|
|
172
|
-
this.
|
|
173
|
-
|
|
82
|
+
const decorationNode = document.querySelector(
|
|
83
|
+
`[data-decoration-id="${this.pluginState.decorationId}"]`
|
|
84
|
+
);
|
|
174
85
|
|
|
175
|
-
if (
|
|
176
|
-
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
|
+
};
|
|
177
93
|
|
|
178
|
-
|
|
179
|
-
this.suggestionsMenu.element!.addEventListener("mousedown", (event) =>
|
|
180
|
-
event.preventDefault()
|
|
181
|
-
);
|
|
94
|
+
this.updateSuggestionsMenu();
|
|
182
95
|
}
|
|
183
96
|
}
|
|
184
97
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
itemCallback: (item: T) => this.itemCallback(item),
|
|
188
|
-
getReferenceRect: () => {
|
|
189
|
-
const decorationNode = document.querySelector(
|
|
190
|
-
`[data-decoration-id="${this.pluginState.decorationId}"]`
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
if (!decorationNode) {
|
|
194
|
-
if (this.lastPosition === undefined) {
|
|
195
|
-
throw new Error(
|
|
196
|
-
"Attempted to access trigger character reference rect before rendering suggestions menu."
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return this.lastPosition;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const triggerCharacterBoundingBox =
|
|
204
|
-
decorationNode.getBoundingClientRect();
|
|
205
|
-
this.lastPosition = triggerCharacterBoundingBox;
|
|
206
|
-
|
|
207
|
-
return triggerCharacterBoundingBox;
|
|
208
|
-
},
|
|
209
|
-
};
|
|
98
|
+
destroy() {
|
|
99
|
+
document.removeEventListener("scroll", this.handleScroll);
|
|
210
100
|
}
|
|
101
|
+
}
|
|
211
102
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
+
};
|
|
121
|
+
|
|
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
|
+
};
|
|
218
134
|
}
|
|
219
135
|
|
|
220
136
|
/**
|
|
@@ -226,271 +142,293 @@ class SuggestionPluginView<
|
|
|
226
142
|
* - This version supports generic items instead of only strings (to allow for more advanced filtering for example)
|
|
227
143
|
* - This version hides some unnecessary complexity from the user of the plugin.
|
|
228
144
|
* - This version handles key events differently
|
|
229
|
-
*
|
|
230
|
-
* @param options options for configuring the plugin
|
|
231
|
-
* @returns the prosemirror plugin
|
|
232
145
|
*/
|
|
233
|
-
export
|
|
146
|
+
export const setupSuggestionsMenu = <
|
|
234
147
|
T extends SuggestionItem,
|
|
235
148
|
BSchema extends BlockSchema
|
|
236
|
-
>(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
+
) => {
|
|
244
163
|
// Assertions
|
|
245
164
|
if (defaultTriggerCharacter.length !== 1) {
|
|
246
165
|
throw new Error("'char' should be a single character");
|
|
247
166
|
}
|
|
248
167
|
|
|
168
|
+
let suggestionsPluginView: SuggestionsMenuView<T, BSchema>;
|
|
169
|
+
|
|
249
170
|
const deactivate = (view: EditorView) => {
|
|
250
171
|
view.dispatch(view.state.tr.setMeta(pluginKey, { deactivate: true }));
|
|
251
172
|
};
|
|
252
173
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
174
|
+
return {
|
|
175
|
+
plugin: new Plugin({
|
|
176
|
+
key: pluginKey,
|
|
256
177
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
onSelectItem: (props: {
|
|
262
|
-
item: T;
|
|
263
|
-
editor: BlockNoteEditor<BSchema>;
|
|
264
|
-
}) => {
|
|
265
|
-
deactivate(view);
|
|
266
|
-
selectItemCallback(props);
|
|
267
|
-
},
|
|
268
|
-
suggestionsMenuFactory: suggestionsMenuFactory,
|
|
269
|
-
}),
|
|
178
|
+
view: () => {
|
|
179
|
+
suggestionsPluginView = new SuggestionsMenuView<T, BSchema>(
|
|
180
|
+
editor,
|
|
181
|
+
pluginKey,
|
|
270
182
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
return getDefaultPluginState<T>();
|
|
183
|
+
updateSuggestionsMenu
|
|
184
|
+
);
|
|
185
|
+
return suggestionsPluginView;
|
|
275
186
|
},
|
|
276
187
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
newState.doc.textBetween(prev.queryStartPos!, newState.selection.from)
|
|
311
|
-
);
|
|
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
|
+
}
|
|
216
|
+
|
|
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
|
+
}
|
|
312
221
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
+
)
|
|
322
231
|
);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
transaction.getMeta("focus") ||
|
|
335
|
-
transaction.getMeta("blur") ||
|
|
336
|
-
transaction.getMeta("pointer") ||
|
|
337
|
-
// Moving the caret before the character which triggered the menu should hide it.
|
|
338
|
-
(prev.active && newState.selection.from < prev.queryStartPos!) ||
|
|
339
|
-
// Entering more than 3 characters, after the last query that matched with at least 1 menu item, should hide
|
|
340
|
-
// the menu.
|
|
341
|
-
next.notFoundCount > 3
|
|
342
|
-
) {
|
|
343
|
-
return getDefaultPluginState<T>();
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Updates keyboardHoveredItemIndex if necessary.
|
|
347
|
-
if (
|
|
348
|
-
transaction.getMeta(pluginKey)?.selectedItemIndexChanged !== undefined
|
|
349
|
-
) {
|
|
350
|
-
let newIndex =
|
|
351
|
-
transaction.getMeta(pluginKey).selectedItemIndexChanged;
|
|
352
|
-
|
|
353
|
-
// Allows selection to jump between first and last items.
|
|
354
|
-
if (newIndex < 0) {
|
|
355
|
-
newIndex = prev.items.length - 1;
|
|
356
|
-
} else if (newIndex >= prev.items.length) {
|
|
357
|
-
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
|
+
);
|
|
358
243
|
}
|
|
359
244
|
|
|
360
|
-
|
|
361
|
-
|
|
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
|
+
}
|
|
362
265
|
|
|
363
|
-
|
|
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
|
+
}
|
|
286
|
+
|
|
287
|
+
return next;
|
|
288
|
+
},
|
|
364
289
|
},
|
|
365
|
-
},
|
|
366
290
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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,
|
|
380
328
|
})
|
|
381
|
-
|
|
329
|
+
);
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
|
|
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
|
+
}
|
|
382
342
|
|
|
383
|
-
|
|
384
|
-
|
|
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
|
+
}
|
|
385
368
|
|
|
386
|
-
// Doesn't handle other keystrokes if the menu isn't active.
|
|
387
|
-
if (!menuIsActive) {
|
|
388
369
|
return false;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Handles keystrokes for navigating the menu.
|
|
392
|
-
const {
|
|
393
|
-
triggerCharacter,
|
|
394
|
-
queryStartPos,
|
|
395
|
-
items,
|
|
396
|
-
keyboardHoveredItemIndex,
|
|
397
|
-
} = pluginKey.getState(view.state);
|
|
398
|
-
|
|
399
|
-
// Moves the keyboard selection to the previous item.
|
|
400
|
-
if (event.key === "ArrowUp") {
|
|
401
|
-
view.dispatch(
|
|
402
|
-
view.state.tr.setMeta(pluginKey, {
|
|
403
|
-
selectedItemIndexChanged: keyboardHoveredItemIndex - 1,
|
|
404
|
-
})
|
|
405
|
-
);
|
|
406
|
-
return true;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Moves the keyboard selection to the next item.
|
|
410
|
-
if (event.key === "ArrowDown") {
|
|
411
|
-
view.dispatch(
|
|
412
|
-
view.state.tr.setMeta(pluginKey, {
|
|
413
|
-
selectedItemIndexChanged: keyboardHoveredItemIndex + 1,
|
|
414
|
-
})
|
|
415
|
-
);
|
|
416
|
-
return true;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Selects an item and closes the menu.
|
|
420
|
-
if (event.key === "Enter") {
|
|
421
|
-
deactivate(view);
|
|
422
|
-
editor._tiptapEditor
|
|
423
|
-
.chain()
|
|
424
|
-
.focus()
|
|
425
|
-
.deleteRange({
|
|
426
|
-
from: queryStartPos! - triggerCharacter!.length,
|
|
427
|
-
to: editor._tiptapEditor.state.selection.from,
|
|
428
|
-
})
|
|
429
|
-
.run();
|
|
430
|
-
|
|
431
|
-
selectItemCallback({
|
|
432
|
-
item: items[keyboardHoveredItemIndex],
|
|
433
|
-
editor: editor,
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
return true;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Closes the menu.
|
|
440
|
-
if (event.key === "Escape") {
|
|
441
|
-
deactivate(view);
|
|
442
|
-
return true;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
return false;
|
|
446
|
-
},
|
|
370
|
+
},
|
|
447
371
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
372
|
+
// Setup decorator on the currently active suggestion.
|
|
373
|
+
decorations(state) {
|
|
374
|
+
const { active, decorationId, queryStartPos, triggerCharacter } = (
|
|
375
|
+
this as Plugin
|
|
376
|
+
).getState(state);
|
|
452
377
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const { active, decorationId, queryStartPos, triggerCharacter } = (
|
|
456
|
-
this as Plugin
|
|
457
|
-
).getState(state);
|
|
458
|
-
|
|
459
|
-
if (!active) {
|
|
460
|
-
return null;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// If the menu was opened programmatically by another extension, it may not use a trigger character. In this
|
|
464
|
-
// case, the decoration is set on the whole block instead, as the decoration range would otherwise be empty.
|
|
465
|
-
if (triggerCharacter === "") {
|
|
466
|
-
const blockNode = findBlock(state.selection);
|
|
467
|
-
if (blockNode) {
|
|
468
|
-
return DecorationSet.create(state.doc, [
|
|
469
|
-
Decoration.node(
|
|
470
|
-
blockNode.pos,
|
|
471
|
-
blockNode.pos + blockNode.node.nodeSize,
|
|
472
|
-
{
|
|
473
|
-
nodeName: "span",
|
|
474
|
-
class: "suggestion-decorator",
|
|
475
|
-
"data-decoration-id": decorationId,
|
|
476
|
-
}
|
|
477
|
-
),
|
|
478
|
-
]);
|
|
378
|
+
if (!active) {
|
|
379
|
+
return null;
|
|
479
380
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
+
]);
|
|
490
398
|
}
|
|
491
|
-
|
|
492
|
-
|
|
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
|
+
},
|
|
493
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
|
+
});
|
|
494
432
|
},
|
|
495
|
-
}
|
|
496
|
-
}
|
|
433
|
+
};
|
|
434
|
+
};
|