@blocknote/core 0.11.2 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -17
- package/dist/blocknote.js +1600 -1403
- package/dist/blocknote.js.map +1 -1
- package/dist/blocknote.umd.cjs +6 -6
- package/dist/blocknote.umd.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/dist/webpack-stats.json +1 -1
- package/package.json +7 -3
- package/src/api/blockManipulation/blockManipulation.test.ts +19 -15
- package/src/api/blockManipulation/blockManipulation.ts +107 -17
- package/src/api/exporters/html/externalHTMLExporter.ts +3 -7
- package/src/api/exporters/html/htmlConversion.test.ts +6 -3
- package/src/api/exporters/html/internalHTMLSerializer.ts +3 -7
- package/src/api/exporters/html/util/sharedHTMLConversion.ts +3 -3
- package/src/api/exporters/markdown/markdownExporter.test.ts +7 -3
- package/src/api/exporters/markdown/markdownExporter.ts +2 -6
- package/src/api/nodeConversions/nodeConversions.test.ts +14 -7
- package/src/api/nodeConversions/nodeConversions.ts +1 -2
- package/src/api/parsers/html/parseHTML.test.ts +5 -1
- package/src/api/parsers/html/parseHTML.ts +2 -6
- package/src/api/parsers/html/util/nestedLists.ts +11 -1
- package/src/api/parsers/markdown/parseMarkdown.test.ts +3 -0
- package/src/api/parsers/markdown/parseMarkdown.ts +2 -6
- package/src/api/testUtil/cases/customBlocks.ts +18 -16
- package/src/api/testUtil/cases/customInlineContent.ts +12 -13
- package/src/api/testUtil/cases/customStyles.ts +12 -10
- package/src/api/testUtil/index.ts +4 -2
- package/src/api/testUtil/partialBlockTestUtil.ts +2 -6
- package/src/blocks/ImageBlockContent/ImageBlockContent.ts +1 -2
- package/src/blocks/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY.ts +8 -1
- package/src/blocks/defaultBlockHelpers.ts +3 -3
- package/src/blocks/defaultBlockTypeGuards.ts +84 -0
- package/src/blocks/defaultBlocks.ts +29 -3
- package/src/editor/Block.css +2 -31
- package/src/editor/BlockNoteEditor.ts +218 -262
- package/src/editor/BlockNoteExtensions.ts +5 -2
- package/src/editor/BlockNoteSchema.ts +98 -0
- package/src/editor/BlockNoteTipTapEditor.ts +162 -0
- package/src/editor/cursorPositionTypes.ts +2 -6
- package/src/editor/editor.css +0 -1
- package/src/editor/selectionTypes.ts +2 -6
- package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +22 -29
- package/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +26 -27
- package/src/extensions/ImageToolbar/ImageToolbarPlugin.ts +45 -51
- package/src/extensions/Placeholder/PlaceholderExtension.ts +81 -88
- package/src/extensions/SideMenu/SideMenuPlugin.ts +55 -56
- package/src/extensions/SuggestionMenu/DefaultSuggestionItem.ts +8 -0
- package/src/extensions/SuggestionMenu/SuggestionPlugin.ts +353 -0
- package/src/extensions/{SlashMenu/defaultSlashMenuItems.ts → SuggestionMenu/getDefaultSlashMenuItems.ts} +119 -89
- package/src/extensions/TableHandles/TableHandlesPlugin.ts +62 -45
- package/src/extensions-shared/UiElementPosition.ts +4 -0
- package/src/index.ts +6 -6
- package/src/pm-nodes/BlockContainer.ts +5 -5
- package/src/schema/blocks/types.ts +15 -15
- package/src/schema/inlineContent/createSpec.ts +2 -2
- package/src/schema/inlineContent/types.ts +1 -1
- package/src/util/browser.ts +6 -4
- package/src/util/typescript.ts +7 -4
- package/types/src/api/blockManipulation/blockManipulation.d.ts +6 -1
- package/types/src/api/exporters/html/externalHTMLExporter.d.ts +2 -1
- package/types/src/api/exporters/html/internalHTMLSerializer.d.ts +2 -1
- package/types/src/api/exporters/markdown/markdownExporter.d.ts +2 -1
- package/types/src/api/nodeConversions/nodeConversions.d.ts +2 -1
- package/types/src/api/parsers/html/parseHTML.d.ts +2 -1
- package/types/src/api/parsers/markdown/parseMarkdown.d.ts +2 -1
- package/types/src/api/testUtil/cases/customBlocks.d.ts +72 -13
- package/types/src/api/testUtil/cases/customInlineContent.d.ts +281 -6
- package/types/src/api/testUtil/cases/customStyles.d.ts +247 -13
- package/types/src/api/testUtil/index.d.ts +4 -2
- package/types/src/api/testUtil/partialBlockTestUtil.d.ts +2 -1
- package/types/src/blocks/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY.d.ts +6 -1
- package/types/src/blocks/defaultBlockHelpers.d.ts +2 -2
- package/types/src/blocks/defaultBlockTypeGuards.d.ts +24 -0
- package/types/src/blocks/defaultBlocks.d.ts +21 -15
- package/types/src/editor/BlockNoteEditor.d.ts +48 -53
- package/types/src/editor/BlockNoteExtensions.d.ts +1 -0
- package/types/src/editor/BlockNoteSchema.d.ts +34 -0
- package/types/src/editor/BlockNoteTipTapEditor.d.ts +28 -0
- package/types/src/editor/cursorPositionTypes.d.ts +2 -1
- package/types/src/editor/selectionTypes.d.ts +2 -1
- package/types/src/extensions/FormattingToolbar/FormattingToolbarPlugin.d.ts +5 -6
- package/types/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.d.ts +2 -2
- package/types/src/extensions/ImageToolbar/ImageToolbarPlugin.d.ts +15 -14
- package/types/src/extensions/Placeholder/PlaceholderExtension.d.ts +2 -15
- package/types/src/extensions/SideMenu/SideMenuPlugin.d.ts +8 -7
- package/types/src/extensions/SuggestionMenu/DefaultSuggestionItem.d.ts +8 -0
- package/types/src/extensions/SuggestionMenu/SuggestionPlugin.d.ts +31 -0
- package/types/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.d.ts +10 -0
- package/types/src/extensions/TableHandles/TableHandlesPlugin.d.ts +7 -7
- package/types/src/extensions-shared/UiElementPosition.d.ts +4 -0
- package/types/src/index.d.ts +6 -6
- package/types/src/pm-nodes/BlockContainer.d.ts +3 -2
- package/types/src/pm-nodes/BlockGroup.d.ts +1 -1
- package/types/src/schema/blocks/types.d.ts +15 -15
- package/types/src/schema/inlineContent/types.d.ts +1 -1
- package/types/src/util/browser.d.ts +1 -0
- package/types/src/util/typescript.d.ts +1 -0
- package/src/extensions/SlashMenu/BaseSlashMenuItem.ts +0 -12
- package/src/extensions/SlashMenu/SlashMenuPlugin.ts +0 -53
- package/src/extensions-shared/BaseUiElementTypes.ts +0 -8
- package/src/extensions-shared/README.md +0 -3
- package/src/extensions-shared/suggestion/SuggestionItem.ts +0 -3
- package/src/extensions-shared/suggestion/SuggestionPlugin.ts +0 -448
- package/types/src/extensions/SlashMenu/BaseSlashMenuItem.d.ts +0 -7
- package/types/src/extensions/SlashMenu/SlashMenuPlugin.d.ts +0 -13
- package/types/src/extensions/SlashMenu/defaultSlashMenuItems.d.ts +0 -3
- package/types/src/extensions-shared/BaseUiElementTypes.d.ts +0 -7
- package/types/src/extensions-shared/suggestion/SuggestionItem.d.ts +0 -3
- package/types/src/extensions-shared/suggestion/SuggestionPlugin.d.ts +0 -36
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-100.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-100.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-200.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-200.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-300.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-300.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-500.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-500.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-600.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-600.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-700.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-700.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-800.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-800.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-900.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-900.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-regular.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-regular.woff2 +0 -0
- /package/src/{assets/fonts-inter.css → fonts/inter.css} +0 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { findParentNode } from "@tiptap/core";
|
|
2
|
+
import { EditorState, Plugin, PluginKey } from "prosemirror-state";
|
|
3
|
+
import { Decoration, DecorationSet, EditorView } from "prosemirror-view";
|
|
4
|
+
|
|
5
|
+
import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
|
|
6
|
+
import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema";
|
|
7
|
+
import { UiElementPosition } from "../../extensions-shared/UiElementPosition";
|
|
8
|
+
import { EventEmitter } from "../../util/EventEmitter";
|
|
9
|
+
|
|
10
|
+
const findBlock = findParentNode((node) => node.type.name === "blockContainer");
|
|
11
|
+
|
|
12
|
+
export type SuggestionMenuState = UiElementPosition & {
|
|
13
|
+
query: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
class SuggestionMenuView<
|
|
17
|
+
BSchema extends BlockSchema,
|
|
18
|
+
I extends InlineContentSchema,
|
|
19
|
+
S extends StyleSchema
|
|
20
|
+
> {
|
|
21
|
+
private state?: SuggestionMenuState;
|
|
22
|
+
public emitUpdate: (triggerCharacter: string) => void;
|
|
23
|
+
|
|
24
|
+
pluginState: SuggestionPluginState;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
private readonly editor: BlockNoteEditor<BSchema, I, S>,
|
|
28
|
+
emitUpdate: (menuName: string, state: SuggestionMenuState) => void
|
|
29
|
+
) {
|
|
30
|
+
this.pluginState = undefined;
|
|
31
|
+
|
|
32
|
+
this.emitUpdate = (menuName: string) => {
|
|
33
|
+
if (!this.state) {
|
|
34
|
+
throw new Error("Attempting to update uninitialized suggestions menu");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
emitUpdate(menuName, this.state);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
document.addEventListener("scroll", this.handleScroll);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
handleScroll = () => {
|
|
44
|
+
if (this.state?.show) {
|
|
45
|
+
const decorationNode = document.querySelector(
|
|
46
|
+
`[data-decoration-id="${this.pluginState!.decorationId}"]`
|
|
47
|
+
);
|
|
48
|
+
this.state.referencePos = decorationNode!.getBoundingClientRect();
|
|
49
|
+
this.emitUpdate(this.pluginState!.triggerCharacter!);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
update(view: EditorView, prevState: EditorState) {
|
|
54
|
+
const prev: SuggestionPluginState =
|
|
55
|
+
suggestionMenuPluginKey.getState(prevState);
|
|
56
|
+
const next: SuggestionPluginState = suggestionMenuPluginKey.getState(
|
|
57
|
+
view.state
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// See how the state changed
|
|
61
|
+
const started = prev === undefined && next !== undefined;
|
|
62
|
+
const stopped = prev !== undefined && next === undefined;
|
|
63
|
+
const changed = prev !== undefined && next !== undefined;
|
|
64
|
+
|
|
65
|
+
// Cancel when suggestion isn't active
|
|
66
|
+
if (!started && !changed && !stopped) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.pluginState = stopped ? prev : next;
|
|
71
|
+
|
|
72
|
+
if (stopped || !this.editor.isEditable) {
|
|
73
|
+
this.state!.show = false;
|
|
74
|
+
this.emitUpdate(this.pluginState!.triggerCharacter);
|
|
75
|
+
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const decorationNode = document.querySelector(
|
|
80
|
+
`[data-decoration-id="${this.pluginState!.decorationId}"]`
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (this.editor.isEditable) {
|
|
84
|
+
this.state = {
|
|
85
|
+
show: true,
|
|
86
|
+
referencePos: decorationNode!.getBoundingClientRect(),
|
|
87
|
+
query: this.pluginState!.query,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
this.emitUpdate(this.pluginState!.triggerCharacter!);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
destroy() {
|
|
95
|
+
document.removeEventListener("scroll", this.handleScroll);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
closeMenu = () => {
|
|
99
|
+
this.editor._tiptapEditor.view.dispatch(
|
|
100
|
+
this.editor._tiptapEditor.view.state.tr.setMeta(
|
|
101
|
+
suggestionMenuPluginKey,
|
|
102
|
+
null
|
|
103
|
+
)
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
clearQuery = () => {
|
|
108
|
+
if (this.pluginState === undefined) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.editor._tiptapEditor
|
|
113
|
+
.chain()
|
|
114
|
+
.focus()
|
|
115
|
+
.deleteRange({
|
|
116
|
+
from:
|
|
117
|
+
this.pluginState.queryStartPos! -
|
|
118
|
+
(this.pluginState.fromUserInput
|
|
119
|
+
? this.pluginState.triggerCharacter!.length
|
|
120
|
+
: 0),
|
|
121
|
+
to: this.editor._tiptapEditor.state.selection.from,
|
|
122
|
+
})
|
|
123
|
+
.run();
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
type SuggestionPluginState =
|
|
128
|
+
| {
|
|
129
|
+
triggerCharacter: string;
|
|
130
|
+
fromUserInput: boolean;
|
|
131
|
+
queryStartPos: number;
|
|
132
|
+
query: string;
|
|
133
|
+
decorationId: string;
|
|
134
|
+
}
|
|
135
|
+
| undefined;
|
|
136
|
+
|
|
137
|
+
export const suggestionMenuPluginKey = new PluginKey("SuggestionMenuPlugin");
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A ProseMirror plugin for suggestions, designed to make '/'-commands possible as well as mentions.
|
|
141
|
+
*
|
|
142
|
+
* This is basically a simplified version of TipTap's [Suggestions](https://github.com/ueberdosis/tiptap/tree/db92a9b313c5993b723c85cd30256f1d4a0b65e1/packages/suggestion) plugin.
|
|
143
|
+
*
|
|
144
|
+
* This version is adapted from the aforementioned version in the following ways:
|
|
145
|
+
* - This version supports generic items instead of only strings (to allow for more advanced filtering for example)
|
|
146
|
+
* - This version hides some unnecessary complexity from the user of the plugin.
|
|
147
|
+
* - This version handles key events differently
|
|
148
|
+
*/
|
|
149
|
+
export class SuggestionMenuProseMirrorPlugin<
|
|
150
|
+
BSchema extends BlockSchema,
|
|
151
|
+
I extends InlineContentSchema,
|
|
152
|
+
S extends StyleSchema
|
|
153
|
+
> extends EventEmitter<any> {
|
|
154
|
+
private view: SuggestionMenuView<BSchema, I, S> | undefined;
|
|
155
|
+
public readonly plugin: Plugin;
|
|
156
|
+
|
|
157
|
+
private triggerCharacters: string[] = [];
|
|
158
|
+
|
|
159
|
+
constructor(editor: BlockNoteEditor<BSchema, I, S>) {
|
|
160
|
+
super();
|
|
161
|
+
const triggerCharacters = this.triggerCharacters;
|
|
162
|
+
this.plugin = new Plugin({
|
|
163
|
+
key: suggestionMenuPluginKey,
|
|
164
|
+
|
|
165
|
+
view: () => {
|
|
166
|
+
this.view = new SuggestionMenuView<BSchema, I, S>(
|
|
167
|
+
editor,
|
|
168
|
+
(triggerCharacter, state) => {
|
|
169
|
+
this.emit(`update ${triggerCharacter}`, state);
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
return this.view;
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
state: {
|
|
176
|
+
// Initialize the plugin's internal state.
|
|
177
|
+
init(): SuggestionPluginState {
|
|
178
|
+
return undefined;
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
// Apply changes to the plugin state from an editor transaction.
|
|
182
|
+
apply(transaction, prev, _oldState, newState): SuggestionPluginState {
|
|
183
|
+
// TODO: More clearly define which transactions should be ignored.
|
|
184
|
+
if (transaction.getMeta("orderedListIndexing") !== undefined) {
|
|
185
|
+
return prev;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Either contains the trigger character if the menu should be shown,
|
|
189
|
+
// or null if it should be hidden.
|
|
190
|
+
const suggestionPluginTransactionMeta: {
|
|
191
|
+
triggerCharacter: string;
|
|
192
|
+
fromUserInput?: boolean;
|
|
193
|
+
} | null = transaction.getMeta(suggestionMenuPluginKey);
|
|
194
|
+
|
|
195
|
+
// Only opens a menu of no menu is already open
|
|
196
|
+
if (
|
|
197
|
+
typeof suggestionPluginTransactionMeta === "object" &&
|
|
198
|
+
suggestionPluginTransactionMeta !== null &&
|
|
199
|
+
prev === undefined
|
|
200
|
+
) {
|
|
201
|
+
return {
|
|
202
|
+
triggerCharacter:
|
|
203
|
+
suggestionPluginTransactionMeta.triggerCharacter,
|
|
204
|
+
fromUserInput:
|
|
205
|
+
suggestionPluginTransactionMeta.fromUserInput !== false,
|
|
206
|
+
queryStartPos: newState.selection.from,
|
|
207
|
+
query: "",
|
|
208
|
+
decorationId: `id_${Math.floor(Math.random() * 0xffffffff)}`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Checks if the menu is hidden, in which case it doesn't need to be hidden or updated.
|
|
213
|
+
if (prev === undefined) {
|
|
214
|
+
return prev;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Checks if the menu should be hidden.
|
|
218
|
+
if (
|
|
219
|
+
// Highlighting text should hide the menu.
|
|
220
|
+
newState.selection.from !== newState.selection.to ||
|
|
221
|
+
// Transactions with plugin metadata should hide the menu.
|
|
222
|
+
suggestionPluginTransactionMeta === null ||
|
|
223
|
+
// Certain mouse events should hide the menu.
|
|
224
|
+
// TODO: Change to global mousedown listener.
|
|
225
|
+
transaction.getMeta("focus") ||
|
|
226
|
+
transaction.getMeta("blur") ||
|
|
227
|
+
transaction.getMeta("pointer") ||
|
|
228
|
+
// Moving the caret before the character which triggered the menu should hide it.
|
|
229
|
+
(prev.triggerCharacter !== undefined &&
|
|
230
|
+
newState.selection.from < prev.queryStartPos!)
|
|
231
|
+
) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const next = { ...prev };
|
|
236
|
+
|
|
237
|
+
// Updates the current query.
|
|
238
|
+
next.query = newState.doc.textBetween(
|
|
239
|
+
prev.queryStartPos!,
|
|
240
|
+
newState.selection.from
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
return next;
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
props: {
|
|
248
|
+
handleKeyDown(view, event) {
|
|
249
|
+
const suggestionPluginState: SuggestionPluginState = (
|
|
250
|
+
this as Plugin
|
|
251
|
+
).getState(view.state);
|
|
252
|
+
|
|
253
|
+
if (
|
|
254
|
+
triggerCharacters.includes(event.key) &&
|
|
255
|
+
suggestionPluginState === undefined
|
|
256
|
+
) {
|
|
257
|
+
event.preventDefault();
|
|
258
|
+
|
|
259
|
+
view.dispatch(
|
|
260
|
+
view.state.tr
|
|
261
|
+
.insertText(event.key)
|
|
262
|
+
.scrollIntoView()
|
|
263
|
+
.setMeta(suggestionMenuPluginKey, {
|
|
264
|
+
triggerCharacter: event.key,
|
|
265
|
+
})
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return false;
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
// Setup decorator on the currently active suggestion.
|
|
275
|
+
decorations(state) {
|
|
276
|
+
const suggestionPluginState: SuggestionPluginState = (
|
|
277
|
+
this as Plugin
|
|
278
|
+
).getState(state);
|
|
279
|
+
|
|
280
|
+
if (suggestionPluginState === undefined) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// If the menu was opened programmatically by another extension, it may not use a trigger character. In this
|
|
285
|
+
// case, the decoration is set on the whole block instead, as the decoration range would otherwise be empty.
|
|
286
|
+
if (!suggestionPluginState.fromUserInput) {
|
|
287
|
+
const blockNode = findBlock(state.selection);
|
|
288
|
+
if (blockNode) {
|
|
289
|
+
return DecorationSet.create(state.doc, [
|
|
290
|
+
Decoration.node(
|
|
291
|
+
blockNode.pos,
|
|
292
|
+
blockNode.pos + blockNode.node.nodeSize,
|
|
293
|
+
{
|
|
294
|
+
nodeName: "span",
|
|
295
|
+
class: "bn-suggestion-decorator",
|
|
296
|
+
"data-decoration-id": suggestionPluginState.decorationId,
|
|
297
|
+
}
|
|
298
|
+
),
|
|
299
|
+
]);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Creates an inline decoration around the trigger character.
|
|
303
|
+
return DecorationSet.create(state.doc, [
|
|
304
|
+
Decoration.inline(
|
|
305
|
+
suggestionPluginState.queryStartPos! -
|
|
306
|
+
suggestionPluginState.triggerCharacter!.length,
|
|
307
|
+
suggestionPluginState.queryStartPos!,
|
|
308
|
+
{
|
|
309
|
+
nodeName: "span",
|
|
310
|
+
class: "bn-suggestion-decorator",
|
|
311
|
+
"data-decoration-id": suggestionPluginState.decorationId,
|
|
312
|
+
}
|
|
313
|
+
),
|
|
314
|
+
]);
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
public onUpdate(
|
|
321
|
+
triggerCharacter: string,
|
|
322
|
+
callback: (state: SuggestionMenuState) => void
|
|
323
|
+
) {
|
|
324
|
+
if (!this.triggerCharacters.includes(triggerCharacter)) {
|
|
325
|
+
this.addTriggerCharacter(triggerCharacter);
|
|
326
|
+
}
|
|
327
|
+
// TODO: be able to remove the triggerCharacter
|
|
328
|
+
return this.on(`update ${triggerCharacter}`, callback);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
addTriggerCharacter = (triggerCharacter: string) => {
|
|
332
|
+
this.triggerCharacters.push(triggerCharacter);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// TODO: Should this be called automatically when listeners are removed?
|
|
336
|
+
removeTriggerCharacter = (triggerCharacter: string) => {
|
|
337
|
+
this.triggerCharacters = this.triggerCharacters.filter(
|
|
338
|
+
(c) => c !== triggerCharacter
|
|
339
|
+
);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
closeMenu = () => this.view!.closeMenu();
|
|
343
|
+
|
|
344
|
+
clearQuery = () => this.view!.clearQuery();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function createSuggestionMenu<
|
|
348
|
+
BSchema extends BlockSchema,
|
|
349
|
+
I extends InlineContentSchema,
|
|
350
|
+
S extends StyleSchema
|
|
351
|
+
>(editor: BlockNoteEditor<BSchema, I, S>, triggerCharacter: string) {
|
|
352
|
+
editor.suggestionMenus.addTriggerCharacter(triggerCharacter);
|
|
353
|
+
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { Block, PartialBlock } from "../../blocks/defaultBlocks";
|
|
2
|
+
import { checkDefaultBlockTypeInSchema } from "../../blocks/defaultBlockTypeGuards";
|
|
3
|
+
import { BlockNoteEditor } from "../../editor/BlockNoteEditor";
|
|
3
4
|
import {
|
|
4
|
-
Block,
|
|
5
5
|
BlockSchema,
|
|
6
6
|
InlineContentSchema,
|
|
7
|
-
PartialBlock,
|
|
8
|
-
StyleSchema,
|
|
9
7
|
isStyledTextInlineContent,
|
|
8
|
+
StyleSchema,
|
|
10
9
|
} from "../../schema";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
10
|
+
import { formatKeyboardShortcut } from "../../util/browser";
|
|
11
|
+
import { DefaultSuggestionItem } from "./DefaultSuggestionItem";
|
|
13
12
|
|
|
14
13
|
// Sets the editor's text cursor position to the next content editable block,
|
|
15
14
|
// so either a block with inline content or a table. The last block is always a
|
|
@@ -21,11 +20,11 @@ function setSelectionToNextContentEditableBlock<
|
|
|
21
20
|
S extends StyleSchema
|
|
22
21
|
>(editor: BlockNoteEditor<BSchema, I, S>) {
|
|
23
22
|
let block = editor.getTextCursorPosition().block;
|
|
24
|
-
let contentType = editor.blockSchema[block.type].content;
|
|
23
|
+
let contentType = editor.schema.blockSchema[block.type].content;
|
|
25
24
|
|
|
26
25
|
while (contentType === "none") {
|
|
27
26
|
block = editor.getTextCursorPosition().nextBlock!;
|
|
28
|
-
contentType = editor.blockSchema[block.type].content as
|
|
27
|
+
contentType = editor.schema.blockSchema[block.type].content as
|
|
29
28
|
| "inline"
|
|
30
29
|
| "table"
|
|
31
30
|
| "none";
|
|
@@ -37,7 +36,7 @@ function setSelectionToNextContentEditableBlock<
|
|
|
37
36
|
// updates the current block instead of inserting a new one below. If the new
|
|
38
37
|
// block doesn't contain editable content, the cursor is moved to the next block
|
|
39
38
|
// that does.
|
|
40
|
-
function insertOrUpdateBlock<
|
|
39
|
+
export function insertOrUpdateBlock<
|
|
41
40
|
BSchema extends BlockSchema,
|
|
42
41
|
I extends InlineContentSchema,
|
|
43
42
|
S extends StyleSchema
|
|
@@ -74,94 +73,106 @@ function insertOrUpdateBlock<
|
|
|
74
73
|
return insertedBlock;
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
export
|
|
76
|
+
export function getDefaultSlashMenuItems<
|
|
78
77
|
BSchema extends BlockSchema,
|
|
79
78
|
I extends InlineContentSchema,
|
|
80
79
|
S extends StyleSchema
|
|
81
|
-
>(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
slashMenuItems.push({
|
|
90
|
-
name: "Heading",
|
|
91
|
-
aliases: ["h", "heading1", "h1"],
|
|
92
|
-
execute: (editor) =>
|
|
80
|
+
>(editor: BlockNoteEditor<BSchema, I, S>) {
|
|
81
|
+
const items: DefaultSuggestionItem[] = [];
|
|
82
|
+
|
|
83
|
+
if (checkDefaultBlockTypeInSchema("heading", editor)) {
|
|
84
|
+
items.push(
|
|
85
|
+
{
|
|
86
|
+
title: "Heading 1",
|
|
87
|
+
onItemClick: () => {
|
|
93
88
|
insertOrUpdateBlock(editor, {
|
|
94
89
|
type: "heading",
|
|
95
90
|
props: { level: 1 },
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
subtext: "Used for a top-level heading",
|
|
94
|
+
badge: formatKeyboardShortcut("Mod-Alt-1"),
|
|
95
|
+
aliases: ["h", "heading1", "h1"],
|
|
96
|
+
group: "Headings",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
title: "Heading 2",
|
|
100
|
+
onItemClick: () => {
|
|
106
101
|
insertOrUpdateBlock(editor, {
|
|
107
102
|
type: "heading",
|
|
108
103
|
props: { level: 2 },
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
subtext: "Used for key sections",
|
|
107
|
+
badge: formatKeyboardShortcut("Mod-Alt-2"),
|
|
108
|
+
aliases: ["h2", "heading2", "subheading"],
|
|
109
|
+
group: "Headings",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
title: "Heading 3",
|
|
113
|
+
onItemClick: () => {
|
|
119
114
|
insertOrUpdateBlock(editor, {
|
|
120
115
|
type: "heading",
|
|
121
116
|
props: { level: 3 },
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
subtext: "Used for subsections and group headings",
|
|
120
|
+
badge: formatKeyboardShortcut("Mod-Alt-3"),
|
|
121
|
+
aliases: ["h3", "heading3", "subheading"],
|
|
122
|
+
group: "Headings",
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
if ("
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
execute: (editor) =>
|
|
127
|
+
if (checkDefaultBlockTypeInSchema("numberedListItem", editor)) {
|
|
128
|
+
items.push({
|
|
129
|
+
title: "Numbered List",
|
|
130
|
+
onItemClick: () => {
|
|
132
131
|
insertOrUpdateBlock(editor, {
|
|
133
|
-
type: "
|
|
134
|
-
})
|
|
132
|
+
type: "numberedListItem",
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
subtext: "Used to display a numbered list",
|
|
136
|
+
badge: formatKeyboardShortcut("Mod-Shift-7"),
|
|
137
|
+
aliases: ["ol", "li", "list", "numberedlist", "numbered list"],
|
|
138
|
+
group: "Basic blocks",
|
|
135
139
|
});
|
|
136
140
|
}
|
|
137
141
|
|
|
138
|
-
if ("
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
execute: (editor) =>
|
|
142
|
+
if (checkDefaultBlockTypeInSchema("bulletListItem", editor)) {
|
|
143
|
+
items.push({
|
|
144
|
+
title: "Bullet List",
|
|
145
|
+
onItemClick: () => {
|
|
143
146
|
insertOrUpdateBlock(editor, {
|
|
144
|
-
type: "
|
|
145
|
-
})
|
|
147
|
+
type: "bulletListItem",
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
subtext: "Used to display an unordered list",
|
|
151
|
+
badge: formatKeyboardShortcut("Mod-Shift-8"),
|
|
152
|
+
aliases: ["ul", "li", "list", "bulletlist", "bullet list"],
|
|
153
|
+
group: "Basic blocks",
|
|
146
154
|
});
|
|
147
155
|
}
|
|
148
156
|
|
|
149
|
-
if ("paragraph"
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
execute: (editor) =>
|
|
157
|
+
if (checkDefaultBlockTypeInSchema("paragraph", editor)) {
|
|
158
|
+
items.push({
|
|
159
|
+
title: "Paragraph",
|
|
160
|
+
onItemClick: () => {
|
|
154
161
|
insertOrUpdateBlock(editor, {
|
|
155
162
|
type: "paragraph",
|
|
156
|
-
})
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
subtext: "Used for the body of your document",
|
|
166
|
+
badge: formatKeyboardShortcut("Mod-Alt-0"),
|
|
167
|
+
aliases: ["p", "paragraph"],
|
|
168
|
+
group: "Basic blocks",
|
|
157
169
|
});
|
|
158
170
|
}
|
|
159
171
|
|
|
160
|
-
if ("table"
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
execute: (editor) => {
|
|
172
|
+
if (checkDefaultBlockTypeInSchema("table", editor)) {
|
|
173
|
+
items.push({
|
|
174
|
+
title: "Table",
|
|
175
|
+
onItemClick: () => {
|
|
165
176
|
insertOrUpdateBlock(editor, {
|
|
166
177
|
type: "table",
|
|
167
178
|
content: {
|
|
@@ -175,14 +186,31 @@ export const getDefaultSlashMenuItems = <
|
|
|
175
186
|
},
|
|
176
187
|
],
|
|
177
188
|
},
|
|
178
|
-
}
|
|
189
|
+
});
|
|
179
190
|
},
|
|
191
|
+
subtext: "Used for for tables",
|
|
192
|
+
aliases: ["table"],
|
|
193
|
+
group: "Advanced",
|
|
194
|
+
badge: undefined,
|
|
180
195
|
});
|
|
181
196
|
}
|
|
182
197
|
|
|
183
|
-
if ("image"
|
|
184
|
-
|
|
185
|
-
|
|
198
|
+
if (checkDefaultBlockTypeInSchema("image", editor)) {
|
|
199
|
+
items.push({
|
|
200
|
+
title: "Image",
|
|
201
|
+
onItemClick: () => {
|
|
202
|
+
const insertedBlock = insertOrUpdateBlock(editor, {
|
|
203
|
+
type: "image",
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Immediately open the image toolbar
|
|
207
|
+
editor.prosemirrorView.dispatch(
|
|
208
|
+
editor._tiptapEditor.state.tr.setMeta(editor.imageToolbar!.plugin, {
|
|
209
|
+
block: insertedBlock,
|
|
210
|
+
})
|
|
211
|
+
);
|
|
212
|
+
},
|
|
213
|
+
subtext: "Insert an image",
|
|
186
214
|
aliases: [
|
|
187
215
|
"image",
|
|
188
216
|
"imageUpload",
|
|
@@ -194,20 +222,22 @@ export const getDefaultSlashMenuItems = <
|
|
|
194
222
|
"drive",
|
|
195
223
|
"dropbox",
|
|
196
224
|
],
|
|
197
|
-
|
|
198
|
-
const insertedBlock = insertOrUpdateBlock(editor, {
|
|
199
|
-
type: "image",
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
// Immediately open the image toolbar
|
|
203
|
-
editor._tiptapEditor.view.dispatch(
|
|
204
|
-
editor._tiptapEditor.state.tr.setMeta(imageToolbarPluginKey, {
|
|
205
|
-
block: insertedBlock,
|
|
206
|
-
})
|
|
207
|
-
);
|
|
208
|
-
},
|
|
225
|
+
group: "Media",
|
|
209
226
|
});
|
|
210
227
|
}
|
|
211
228
|
|
|
212
|
-
return
|
|
213
|
-
}
|
|
229
|
+
return items;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function filterSuggestionItems<
|
|
233
|
+
T extends { title: string; aliases?: readonly string[] }
|
|
234
|
+
>(items: T[], query: string) {
|
|
235
|
+
return items.filter(
|
|
236
|
+
({ title, aliases }) =>
|
|
237
|
+
title.toLowerCase().startsWith(query.toLowerCase()) ||
|
|
238
|
+
(aliases &&
|
|
239
|
+
aliases.filter((alias) =>
|
|
240
|
+
alias.toLowerCase().startsWith(query.toLowerCase())
|
|
241
|
+
).length !== 0)
|
|
242
|
+
);
|
|
243
|
+
}
|