@blocknote/core 0.11.1 → 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.
Files changed (129) hide show
  1. package/README.md +13 -17
  2. package/dist/blocknote.js +1611 -1408
  3. package/dist/blocknote.js.map +1 -1
  4. package/dist/blocknote.umd.cjs +6 -6
  5. package/dist/blocknote.umd.cjs.map +1 -1
  6. package/dist/style.css +1 -1
  7. package/dist/webpack-stats.json +1 -1
  8. package/package.json +8 -4
  9. package/src/api/blockManipulation/blockManipulation.test.ts +19 -15
  10. package/src/api/blockManipulation/blockManipulation.ts +107 -17
  11. package/src/api/exporters/html/externalHTMLExporter.ts +3 -7
  12. package/src/api/exporters/html/htmlConversion.test.ts +6 -3
  13. package/src/api/exporters/html/internalHTMLSerializer.ts +3 -7
  14. package/src/api/exporters/html/util/sharedHTMLConversion.ts +3 -3
  15. package/src/api/exporters/markdown/markdownExporter.test.ts +7 -3
  16. package/src/api/exporters/markdown/markdownExporter.ts +2 -6
  17. package/src/api/nodeConversions/nodeConversions.test.ts +14 -7
  18. package/src/api/nodeConversions/nodeConversions.ts +1 -2
  19. package/src/api/parsers/html/parseHTML.test.ts +5 -1
  20. package/src/api/parsers/html/parseHTML.ts +2 -6
  21. package/src/api/parsers/html/util/nestedLists.ts +11 -1
  22. package/src/api/parsers/markdown/parseMarkdown.test.ts +3 -0
  23. package/src/api/parsers/markdown/parseMarkdown.ts +2 -6
  24. package/src/api/testUtil/cases/customBlocks.ts +18 -16
  25. package/src/api/testUtil/cases/customInlineContent.ts +12 -13
  26. package/src/api/testUtil/cases/customStyles.ts +12 -10
  27. package/src/api/testUtil/index.ts +4 -2
  28. package/src/api/testUtil/partialBlockTestUtil.ts +2 -6
  29. package/src/blocks/ImageBlockContent/ImageBlockContent.ts +1 -2
  30. package/src/blocks/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY.ts +8 -1
  31. package/src/blocks/ParagraphBlockContent/ParagraphBlockContent.ts +13 -0
  32. package/src/blocks/defaultBlockHelpers.ts +3 -3
  33. package/src/blocks/defaultBlockTypeGuards.ts +84 -0
  34. package/src/blocks/defaultBlocks.ts +29 -3
  35. package/src/editor/Block.css +2 -31
  36. package/src/editor/BlockNoteEditor.ts +219 -263
  37. package/src/editor/BlockNoteExtensions.ts +5 -2
  38. package/src/editor/BlockNoteSchema.ts +98 -0
  39. package/src/editor/BlockNoteTipTapEditor.ts +162 -0
  40. package/src/editor/cursorPositionTypes.ts +2 -6
  41. package/src/editor/editor.css +0 -1
  42. package/src/editor/selectionTypes.ts +2 -6
  43. package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +22 -29
  44. package/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +26 -27
  45. package/src/extensions/ImageToolbar/ImageToolbarPlugin.ts +45 -51
  46. package/src/extensions/Placeholder/PlaceholderExtension.ts +81 -88
  47. package/src/extensions/SideMenu/SideMenuPlugin.ts +55 -56
  48. package/src/extensions/SuggestionMenu/DefaultSuggestionItem.ts +8 -0
  49. package/src/extensions/SuggestionMenu/SuggestionPlugin.ts +353 -0
  50. package/src/extensions/{SlashMenu/defaultSlashMenuItems.ts → SuggestionMenu/getDefaultSlashMenuItems.ts} +119 -89
  51. package/src/extensions/TableHandles/TableHandlesPlugin.ts +62 -45
  52. package/src/extensions-shared/UiElementPosition.ts +4 -0
  53. package/src/index.ts +6 -6
  54. package/src/pm-nodes/BlockContainer.ts +5 -9
  55. package/src/schema/blocks/types.ts +15 -15
  56. package/src/schema/inlineContent/createSpec.ts +2 -2
  57. package/src/schema/inlineContent/types.ts +1 -1
  58. package/src/util/browser.ts +6 -4
  59. package/src/util/typescript.ts +7 -4
  60. package/types/src/api/blockManipulation/blockManipulation.d.ts +6 -1
  61. package/types/src/api/exporters/html/externalHTMLExporter.d.ts +2 -1
  62. package/types/src/api/exporters/html/internalHTMLSerializer.d.ts +2 -1
  63. package/types/src/api/exporters/markdown/markdownExporter.d.ts +2 -1
  64. package/types/src/api/nodeConversions/nodeConversions.d.ts +2 -1
  65. package/types/src/api/parsers/html/parseHTML.d.ts +2 -1
  66. package/types/src/api/parsers/markdown/parseMarkdown.d.ts +2 -1
  67. package/types/src/api/testUtil/cases/customBlocks.d.ts +72 -13
  68. package/types/src/api/testUtil/cases/customInlineContent.d.ts +281 -6
  69. package/types/src/api/testUtil/cases/customStyles.d.ts +247 -13
  70. package/types/src/api/testUtil/index.d.ts +4 -2
  71. package/types/src/api/testUtil/partialBlockTestUtil.d.ts +2 -1
  72. package/types/src/blocks/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY.d.ts +6 -1
  73. package/types/src/blocks/defaultBlockHelpers.d.ts +2 -2
  74. package/types/src/blocks/defaultBlockTypeGuards.d.ts +24 -0
  75. package/types/src/blocks/defaultBlocks.d.ts +21 -15
  76. package/types/src/editor/BlockNoteEditor.d.ts +48 -53
  77. package/types/src/editor/BlockNoteExtensions.d.ts +1 -0
  78. package/types/src/editor/BlockNoteSchema.d.ts +34 -0
  79. package/types/src/editor/BlockNoteTipTapEditor.d.ts +28 -0
  80. package/types/src/editor/cursorPositionTypes.d.ts +2 -1
  81. package/types/src/editor/selectionTypes.d.ts +2 -1
  82. package/types/src/extensions/FormattingToolbar/FormattingToolbarPlugin.d.ts +5 -6
  83. package/types/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.d.ts +2 -2
  84. package/types/src/extensions/ImageToolbar/ImageToolbarPlugin.d.ts +15 -14
  85. package/types/src/extensions/Placeholder/PlaceholderExtension.d.ts +2 -15
  86. package/types/src/extensions/SideMenu/SideMenuPlugin.d.ts +8 -7
  87. package/types/src/extensions/SuggestionMenu/DefaultSuggestionItem.d.ts +8 -0
  88. package/types/src/extensions/SuggestionMenu/SuggestionPlugin.d.ts +31 -0
  89. package/types/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.d.ts +10 -0
  90. package/types/src/extensions/TableHandles/TableHandlesPlugin.d.ts +7 -7
  91. package/types/src/extensions-shared/UiElementPosition.d.ts +4 -0
  92. package/types/src/index.d.ts +6 -6
  93. package/types/src/pm-nodes/BlockContainer.d.ts +3 -2
  94. package/types/src/pm-nodes/BlockGroup.d.ts +1 -1
  95. package/types/src/schema/blocks/types.d.ts +15 -15
  96. package/types/src/schema/inlineContent/types.d.ts +1 -1
  97. package/types/src/util/browser.d.ts +1 -0
  98. package/types/src/util/typescript.d.ts +1 -0
  99. package/src/extensions/SlashMenu/BaseSlashMenuItem.ts +0 -12
  100. package/src/extensions/SlashMenu/SlashMenuPlugin.ts +0 -53
  101. package/src/extensions-shared/BaseUiElementTypes.ts +0 -8
  102. package/src/extensions-shared/README.md +0 -3
  103. package/src/extensions-shared/suggestion/SuggestionItem.ts +0 -3
  104. package/src/extensions-shared/suggestion/SuggestionPlugin.ts +0 -448
  105. package/types/src/extensions/SlashMenu/BaseSlashMenuItem.d.ts +0 -7
  106. package/types/src/extensions/SlashMenu/SlashMenuPlugin.d.ts +0 -13
  107. package/types/src/extensions/SlashMenu/defaultSlashMenuItems.d.ts +0 -3
  108. package/types/src/extensions-shared/BaseUiElementTypes.d.ts +0 -7
  109. package/types/src/extensions-shared/suggestion/SuggestionItem.d.ts +0 -3
  110. package/types/src/extensions-shared/suggestion/SuggestionPlugin.d.ts +0 -36
  111. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-100.woff +0 -0
  112. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-100.woff2 +0 -0
  113. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-200.woff +0 -0
  114. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-200.woff2 +0 -0
  115. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-300.woff +0 -0
  116. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-300.woff2 +0 -0
  117. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-500.woff +0 -0
  118. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-500.woff2 +0 -0
  119. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-600.woff +0 -0
  120. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-600.woff2 +0 -0
  121. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-700.woff +0 -0
  122. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-700.woff2 +0 -0
  123. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-800.woff +0 -0
  124. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-800.woff2 +0 -0
  125. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-900.woff +0 -0
  126. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-900.woff2 +0 -0
  127. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-regular.woff +0 -0
  128. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-regular.woff2 +0 -0
  129. /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 { defaultBlockSchema } from "../../blocks/defaultBlocks";
2
- import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
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 { imageToolbarPluginKey } from "../ImageToolbar/ImageToolbarPlugin";
12
- import { BaseSlashMenuItem } from "./BaseSlashMenuItem";
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 const getDefaultSlashMenuItems = <
76
+ export function getDefaultSlashMenuItems<
78
77
  BSchema extends BlockSchema,
79
78
  I extends InlineContentSchema,
80
79
  S extends StyleSchema
81
- >(
82
- schema: BSchema = defaultBlockSchema as unknown as BSchema
83
- ) => {
84
- const slashMenuItems: BaseSlashMenuItem<BSchema, I, S>[] = [];
85
-
86
- if ("heading" in schema && "level" in schema.heading.propSchema) {
87
- // Command for creating a level 1 heading
88
- if (schema.heading.propSchema.level.values?.includes(1)) {
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
- } as PartialBlock<BSchema, I, S>),
97
- });
98
- }
99
-
100
- // Command for creating a level 2 heading
101
- if (schema.heading.propSchema.level.values?.includes(2)) {
102
- slashMenuItems.push({
103
- name: "Heading 2",
104
- aliases: ["h2", "heading2", "subheading"],
105
- execute: (editor) =>
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
- } as PartialBlock<BSchema, I, S>),
110
- });
111
- }
112
-
113
- // Command for creating a level 3 heading
114
- if (schema.heading.propSchema.level.values?.includes(3)) {
115
- slashMenuItems.push({
116
- name: "Heading 3",
117
- aliases: ["h3", "heading3", "subheading"],
118
- execute: (editor) =>
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
- } as PartialBlock<BSchema, I, S>),
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 ("bulletListItem" in schema) {
128
- slashMenuItems.push({
129
- name: "Bullet List",
130
- aliases: ["ul", "list", "bulletlist", "bullet list"],
131
- execute: (editor) =>
127
+ if (checkDefaultBlockTypeInSchema("numberedListItem", editor)) {
128
+ items.push({
129
+ title: "Numbered List",
130
+ onItemClick: () => {
132
131
  insertOrUpdateBlock(editor, {
133
- type: "bulletListItem",
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 ("numberedListItem" in schema) {
139
- slashMenuItems.push({
140
- name: "Numbered List",
141
- aliases: ["li", "list", "numberedlist", "numbered list"],
142
- execute: (editor) =>
142
+ if (checkDefaultBlockTypeInSchema("bulletListItem", editor)) {
143
+ items.push({
144
+ title: "Bullet List",
145
+ onItemClick: () => {
143
146
  insertOrUpdateBlock(editor, {
144
- type: "numberedListItem",
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" in schema) {
150
- slashMenuItems.push({
151
- name: "Paragraph",
152
- aliases: ["p"],
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" in schema) {
161
- slashMenuItems.push({
162
- name: "Table",
163
- aliases: ["table"],
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
- } as PartialBlock<BSchema, I, S>);
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" in schema) {
184
- slashMenuItems.push({
185
- name: "Image",
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
- execute: (editor) => {
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 slashMenuItems;
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
+ }