@blocknote/core 0.2.2 → 0.2.4-alpha.7

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 (89) hide show
  1. package/dist/blocknote.js +1061 -936
  2. package/dist/blocknote.js.map +1 -1
  3. package/dist/blocknote.umd.cjs +1 -1
  4. package/dist/blocknote.umd.cjs.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +22 -29
  7. package/src/BlockNoteExtensions.ts +11 -10
  8. package/src/extensions/BackgroundColor/BackgroundColorExtension.ts +61 -0
  9. package/src/extensions/BackgroundColor/BackgroundColorMark.ts +62 -0
  10. package/src/extensions/Blocks/PreviousBlockTypePlugin.ts +112 -106
  11. package/src/extensions/Blocks/apiTypes.ts +48 -0
  12. package/src/extensions/Blocks/helpers/findBlock.ts +3 -1
  13. package/src/extensions/Blocks/helpers/getBlockInfoFromPos.ts +1 -1
  14. package/src/extensions/Blocks/index.ts +10 -8
  15. package/src/extensions/Blocks/nodes/Block.module.css +122 -35
  16. package/src/extensions/Blocks/{BlockAttributes.ts → nodes/BlockAttributes.ts} +0 -0
  17. package/src/extensions/Blocks/nodes/{Block.ts → BlockContainer.ts} +113 -119
  18. package/src/extensions/Blocks/nodes/{BlockTypes/HeadingBlock/HeadingContent.ts → BlockContent/HeadingBlockContent/HeadingBlockContent.ts} +16 -24
  19. package/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +76 -0
  20. package/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts +47 -0
  21. package/src/extensions/Blocks/nodes/{BlockTypes/ListItemBlock/OrderedListItemIndexPlugin.ts → BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListIndexingPlugin.ts} +10 -14
  22. package/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +95 -0
  23. package/src/extensions/Blocks/nodes/{BlockTypes/TextBlock/TextContent.ts → BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts} +7 -12
  24. package/src/extensions/Blocks/nodes/BlockGroup.ts +4 -4
  25. package/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts +9 -1
  26. package/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts +87 -42
  27. package/src/extensions/{Blocks → DraggableBlocks}/MultipleNodeSelection.ts +0 -0
  28. package/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts +20 -7
  29. package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +51 -12
  30. package/src/extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes.ts +1 -1
  31. package/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +3 -1
  32. package/src/extensions/Placeholder/PlaceholderExtension.ts +1 -1
  33. package/src/extensions/SlashMenu/SlashMenuExtension.ts +1 -1
  34. package/src/extensions/SlashMenu/SlashMenuItem.ts +3 -28
  35. package/src/extensions/SlashMenu/defaultCommands.tsx +36 -55
  36. package/src/extensions/SlashMenu/index.ts +1 -6
  37. package/src/extensions/TextAlignment/TextAlignmentExtension.ts +75 -0
  38. package/src/extensions/TextColor/TextColorExtension.ts +54 -0
  39. package/src/extensions/TextColor/TextColorMark.ts +62 -0
  40. package/src/extensions/TrailingNode/TrailingNodeExtension.ts +4 -4
  41. package/src/extensions/UniqueID/UniqueID.ts +6 -0
  42. package/src/index.ts +2 -1
  43. package/src/shared/EditorElement.ts +12 -6
  44. package/src/shared/plugins/suggestion/SuggestionItem.ts +0 -9
  45. package/src/shared/plugins/suggestion/SuggestionPlugin.ts +191 -228
  46. package/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts +2 -2
  47. package/types/src/BlockNoteEditor.d.ts +1 -1
  48. package/types/src/BlockNoteExtensions.d.ts +1 -3
  49. package/types/src/api/Document.d.ts +5 -0
  50. package/types/src/extensions/BackgroundColor/BackgroundColorExtension.d.ts +9 -0
  51. package/types/src/extensions/BackgroundColor/BackgroundColorMark.d.ts +9 -0
  52. package/types/src/extensions/Blocks/PreviousBlockTypePlugin.d.ts +3 -2
  53. package/types/src/extensions/Blocks/apiTypes.d.ts +16 -0
  54. package/types/src/extensions/Blocks/helpers/getBlockInfoFromPos.d.ts +1 -1
  55. package/types/src/extensions/Blocks/nodes/BlockAttributes.d.ts +2 -0
  56. package/types/src/extensions/Blocks/nodes/BlockContainer.d.ts +21 -0
  57. package/types/src/extensions/Blocks/nodes/BlockContent/BlockContentTypes.d.ts +4 -0
  58. package/types/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContent.d.ts +2 -0
  59. package/types/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContentTypes.d.ts +4 -0
  60. package/types/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.d.ts +2 -0
  61. package/types/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContentTypes.d.ts +2 -0
  62. package/types/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.d.ts +2 -0
  63. package/types/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListIndexingPlugin.d.ts +2 -0
  64. package/types/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.d.ts +2 -0
  65. package/types/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContentTypes.d.ts +2 -0
  66. package/types/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.d.ts +2 -0
  67. package/types/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContentTypes.d.ts +2 -0
  68. package/types/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.d.ts +9 -5
  69. package/types/src/extensions/DraggableBlocks/DraggableBlocksExtension.d.ts +1 -1
  70. package/types/src/extensions/DraggableBlocks/DraggableBlocksPlugin.d.ts +6 -11
  71. package/types/src/extensions/DraggableBlocks/MultipleNodeSelection.d.ts +24 -0
  72. package/types/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.d.ts +18 -8
  73. package/types/src/extensions/FormattingToolbar/FormattingToolbarPlugin.d.ts +1 -1
  74. package/types/src/extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes.d.ts +5 -5
  75. package/types/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.d.ts +2 -2
  76. package/types/src/extensions/SlashMenu/SlashMenuExtension.d.ts +1 -1
  77. package/types/src/extensions/SlashMenu/SlashMenuItem.d.ts +2 -19
  78. package/types/src/extensions/SlashMenu/defaultSlashCommands.d.ts +5 -0
  79. package/types/src/extensions/SlashMenu/index.d.ts +1 -2
  80. package/types/src/extensions/TextAlignment/TextAlignmentExtension.d.ts +9 -0
  81. package/types/src/extensions/TextColor/TextColorExtension.d.ts +9 -0
  82. package/types/src/extensions/TextColor/TextColorMark.d.ts +9 -0
  83. package/types/src/index.d.ts +2 -1
  84. package/types/src/shared/EditorElement.d.ts +6 -2
  85. package/types/src/shared/plugins/suggestion/SuggestionItem.d.ts +0 -6
  86. package/types/src/shared/plugins/suggestion/SuggestionPlugin.d.ts +11 -25
  87. package/types/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.d.ts +6 -6
  88. package/src/extensions/Blocks/nodes/BlockTypes/ListItemBlock/ListItemContent.ts +0 -177
  89. package/src/extensions/Paragraph/FixedParagraph.ts +0 -12
@@ -1,6 +1,5 @@
1
1
  import { Editor, Range } from "@tiptap/core";
2
- import { escapeRegExp } from "lodash";
3
- import { EditorState, Plugin, PluginKey, Selection } from "prosemirror-state";
2
+ import { EditorState, Plugin, PluginKey } from "prosemirror-state";
4
3
  import { Decoration, DecorationSet, EditorView } from "prosemirror-view";
5
4
  import { findBlock } from "../../../extensions/Blocks/helpers/findBlock";
6
5
  import {
@@ -25,9 +24,9 @@ export type SuggestionPluginOptions<T extends SuggestionItem> = {
25
24
  editor: Editor;
26
25
 
27
26
  /**
28
- * The character that should trigger the suggestion menu to pop up (e.g. a '/' for commands)
27
+ * The character that should trigger the suggestion menu to pop up (e.g. a '/' for commands), when typed by the user.
29
28
  */
30
- char: string;
29
+ defaultTriggerCharacter: string;
31
30
 
32
31
  suggestionsMenuFactory: SuggestionsMenuFactory<T>;
33
32
 
@@ -49,16 +48,38 @@ export type SuggestionPluginOptions<T extends SuggestionItem> = {
49
48
  };
50
49
 
51
50
  type SuggestionPluginState<T extends SuggestionItem> = {
51
+ // True when the menu is shown, false when hidden.
52
52
  active: boolean;
53
- range: Range | null;
54
- query: string | null;
55
- notFoundCount: number;
53
+ // The character that triggered the menu being shown. Allowing the trigger to be different to the default
54
+ // trigger allows other extensions to open it programmatically.
55
+ triggerCharacter: string | undefined;
56
+ // The editor position just after the trigger character, i.e. where the user query begins. Used to figure out
57
+ // which menu items to show and can also be used to delete the trigger character.
58
+ queryStartPos: number | undefined;
59
+ // The items that should be shown in the menu.
56
60
  items: T[];
57
- selectedItemIndex: number;
58
- type: string;
59
- decorationId: string | null;
61
+ // The index of the item in the menu that's currently hovered using the keyboard.
62
+ keyboardHoveredItemIndex: number | undefined;
63
+ // The number of characters typed after the last query that matched with at least 1 item. Used to close the
64
+ // menu if the user keeps entering queries that don't return any results.
65
+ notFoundCount: number | undefined;
66
+ decorationId: string | undefined;
60
67
  };
61
68
 
69
+ function getDefaultPluginState<
70
+ T extends SuggestionItem
71
+ >(): SuggestionPluginState<T> {
72
+ return {
73
+ active: false,
74
+ triggerCharacter: undefined,
75
+ queryStartPos: undefined,
76
+ items: [] as T[],
77
+ keyboardHoveredItemIndex: undefined,
78
+ notFoundCount: 0,
79
+ decorationId: undefined,
80
+ };
81
+ }
82
+
62
83
  type SuggestionPluginViewOptions<T extends SuggestionItem> = {
63
84
  editor: Editor;
64
85
  pluginKey: PluginKey;
@@ -66,47 +87,6 @@ type SuggestionPluginViewOptions<T extends SuggestionItem> = {
66
87
  suggestionsMenuFactory: SuggestionsMenuFactory<T>;
67
88
  };
68
89
 
69
- export type MenuType = "slash" | "drag";
70
-
71
- /**
72
- * Finds a command: a specified character (e.g. '/') followed by a string of characters (all characters except the specified character are allowed).
73
- * Returns the string following the specified character or undefined if no command was found.
74
- *
75
- * @param char the character that indicates the start of a command
76
- * @param selection the selection (only works if the selection is empty; i.e. is a blinking cursor).
77
- * @returns an object containing the matching word (excluding the specified character) and the range of the match (including the specified character) or undefined if there is no match.
78
- */
79
- export function findCommandBeforeCursor(
80
- char: string,
81
- selection: Selection
82
- ): { range: Range; query: string } | undefined {
83
- if (!selection.empty) {
84
- return undefined;
85
- }
86
-
87
- // get the text before the cursor as a node
88
- const node = selection.$anchor.nodeBefore;
89
- if (!node || !node.text) {
90
- return undefined;
91
- }
92
-
93
- // regex to match anything between with the specified char (e.g. '/') and the end of text (which is the end of selection)
94
- const regex = new RegExp(`${escapeRegExp(char)}([^${escapeRegExp(char)}]*)$`);
95
- const match = node.text.match(regex);
96
-
97
- if (!match) {
98
- return undefined;
99
- }
100
-
101
- return {
102
- query: match[1],
103
- range: {
104
- from: selection.$anchor.pos - match[1].length - char.length,
105
- to: selection.$anchor.pos,
106
- },
107
- };
108
- }
109
-
110
90
  class SuggestionPluginView<T extends SuggestionItem> {
111
91
  editor: Editor;
112
92
  pluginKey: PluginKey;
@@ -125,22 +105,18 @@ class SuggestionPluginView<T extends SuggestionItem> {
125
105
  this.editor = editor;
126
106
  this.pluginKey = pluginKey;
127
107
 
128
- this.pluginState = {
129
- active: false,
130
- range: null,
131
- query: null,
132
- notFoundCount: 0,
133
- items: [],
134
- selectedItemIndex: 0,
135
- type: "slash",
136
- decorationId: null,
137
- };
108
+ this.pluginState = getDefaultPluginState<T>();
138
109
 
139
110
  this.itemCallback = (item: T) =>
140
111
  selectItemCallback({
141
112
  item: item,
142
113
  editor: editor,
143
- range: this.pluginState.range as Range,
114
+ range: {
115
+ from:
116
+ this.pluginState.queryStartPos! -
117
+ this.pluginState.triggerCharacter!.length,
118
+ to: editor.state.selection.from,
119
+ },
144
120
  });
145
121
 
146
122
  this.suggestionsMenu = suggestionsMenuFactory(this.getStaticParams());
@@ -200,8 +176,8 @@ class SuggestionPluginView<T extends SuggestionItem> {
200
176
 
201
177
  return {
202
178
  items: this.pluginState.items,
203
- selectedItemIndex: this.pluginState.selectedItemIndex,
204
- queryStartBoundingBox: decorationNode!.getBoundingClientRect(),
179
+ keyboardHoveredItemIndex: this.pluginState.keyboardHoveredItemIndex!,
180
+ referenceRect: decorationNode!.getBoundingClientRect(),
205
181
  };
206
182
  }
207
183
  }
@@ -222,13 +198,13 @@ class SuggestionPluginView<T extends SuggestionItem> {
222
198
  export function createSuggestionPlugin<T extends SuggestionItem>({
223
199
  pluginKey,
224
200
  editor,
225
- char,
201
+ defaultTriggerCharacter,
226
202
  suggestionsMenuFactory,
227
203
  onSelectItem: selectItemCallback = () => {},
228
204
  items = () => [],
229
205
  }: SuggestionPluginOptions<T>) {
230
206
  // Assertions
231
- if (char.length !== 1) {
207
+ if (defaultTriggerCharacter.length !== 1) {
232
208
  throw new Error("'char' should be a single character");
233
209
  }
234
210
 
@@ -236,7 +212,7 @@ export function createSuggestionPlugin<T extends SuggestionItem>({
236
212
  view.dispatch(view.state.tr.setMeta(pluginKey, { deactivate: true }));
237
213
  };
238
214
 
239
- // Plugin key is passed in parameter so it can be exported and used in draghandle
215
+ // Plugin key is passed in as a parameter, so it can be exported and used in the DraggableBlocksPlugin.
240
216
  return new Plugin({
241
217
  key: pluginKey,
242
218
 
@@ -254,126 +230,93 @@ export function createSuggestionPlugin<T extends SuggestionItem>({
254
230
  state: {
255
231
  // Initialize the plugin's internal state.
256
232
  init(): SuggestionPluginState<T> {
257
- return {
258
- active: false,
259
- range: null, // TODO
260
- query: null,
261
- notFoundCount: 0,
262
- items: [] as T[],
263
- selectedItemIndex: 0,
264
- type: "slash",
265
- decorationId: null,
266
- };
233
+ return getDefaultPluginState<T>();
267
234
  },
268
235
 
269
- // Apply changes to the plugin state from a view transaction.
270
- apply(transaction, prev, _oldState, newState) {
271
- const { selection } = transaction;
272
- const next = { ...prev };
273
-
274
- // TODO: More clearly define which transactions should be ignored and which should deactivate the menu.
236
+ // Apply changes to the plugin state from an editor transaction.
237
+ apply(transaction, prev, oldState, newState): SuggestionPluginState<T> {
238
+ // TODO: More clearly define which transactions should be ignored.
275
239
  if (transaction.getMeta("orderedListIndexing") !== undefined) {
276
- return next;
240
+ return prev;
277
241
  }
278
242
 
279
- // Handles transactions created by navigating the menu using the up/down arrow keys.
280
- if (
281
- transaction.getMeta(pluginKey)?.selectedItemIndexChanged !== undefined
282
- ) {
283
- let newIndex =
284
- transaction.getMeta(pluginKey).selectedItemIndexChanged;
243
+ // Checks if the menu should be shown.
244
+ if (transaction.getMeta(pluginKey)?.activate) {
245
+ return {
246
+ active: true,
247
+ triggerCharacter:
248
+ transaction.getMeta(pluginKey)?.triggerCharacter || "",
249
+ queryStartPos: newState.selection.from,
250
+ items: items(""),
251
+ keyboardHoveredItemIndex: 0,
252
+ // TODO: Maybe should be 1 if the menu has no possible items? Probably redundant since a menu with no items
253
+ // is useless in practice.
254
+ notFoundCount: 0,
255
+ decorationId: `id_${Math.floor(Math.random() * 0xffffffff)}`,
256
+ };
257
+ }
285
258
 
286
- if (newIndex < 0) {
287
- newIndex = prev.items.length - 1;
288
- }
259
+ // Checks if the menu is hidden, in which case it doesn't need to be hidden or updated.
260
+ if (!prev.active) {
261
+ return prev;
262
+ }
289
263
 
290
- if (newIndex >= prev.items.length) {
291
- newIndex = 0;
292
- }
264
+ const next = { ...prev };
293
265
 
294
- next.selectedItemIndex = newIndex;
266
+ // Updates which menu items to show by checking which items the current query (the text between the trigger
267
+ // character and caret) matches with.
268
+ next.items = items(
269
+ newState.doc.textBetween(prev.queryStartPos!, newState.selection.from)
270
+ );
295
271
 
296
- return next;
272
+ // Updates notFoundCount if the query doesn't match any items.
273
+ next.notFoundCount = 0;
274
+ if (next.items.length === 0) {
275
+ // Checks how many characters were typed or deleted since the last transaction, and updates the notFoundCount
276
+ // accordingly. Also ensures the notFoundCount does not become negative.
277
+ next.notFoundCount = Math.max(
278
+ 0,
279
+ prev.notFoundCount! +
280
+ (newState.selection.from - oldState.selection.from)
281
+ );
297
282
  }
298
283
 
284
+ // Hides the menu. This is done after items and notFoundCount are already updated as notFoundCount is needed to
285
+ // check if the menu should be hidden.
299
286
  if (
300
- // only show popup if selection is a blinking cursor
301
- selection.from === selection.to &&
302
- // deactivate popup from view (e.g.: choice has been made or esc has been pressed)
303
- !transaction.getMeta(pluginKey)?.deactivate &&
304
- // deactivate because a mouse event occurs (user clicks somewhere else in the document)
305
- !transaction.getMeta("focus") &&
306
- !transaction.getMeta("blur") &&
307
- !transaction.getMeta("pointer")
287
+ // Highlighting text should hide the menu.
288
+ newState.selection.from !== newState.selection.to ||
289
+ // Transactions with plugin metadata {deactivate: true} should hide the menu.
290
+ transaction.getMeta(pluginKey)?.deactivate ||
291
+ // Certain mouse events should hide the menu.
292
+ // TODO: Change to global mousedown listener.
293
+ transaction.getMeta("focus") ||
294
+ transaction.getMeta("blur") ||
295
+ transaction.getMeta("pointer") ||
296
+ // Moving the caret before the character which triggered the menu should hide it.
297
+ (prev.active && newState.selection.from < prev.queryStartPos!) ||
298
+ // Entering more than 3 characters, after the last query that matched with at least 1 menu item, should hide
299
+ // the menu.
300
+ next.notFoundCount > 3
308
301
  ) {
309
- // Reset active state if we just left the previous suggestion range (e.g.: key arrows moving before /)
310
- if (prev.active && selection.from <= prev.range!.from) {
311
- next.active = false;
312
- } else if (transaction.getMeta(pluginKey)?.activate) {
313
- // Start showing suggestions. activate has been set after typing a "/" (or whatever the specified character is), so let's create the decoration and initialize
314
- const newDecorationId = `id_${Math.floor(
315
- Math.random() * 0xffffffff
316
- )}`;
317
- next.decorationId = newDecorationId;
318
- next.range = {
319
- from: selection.from - 1,
320
- to: selection.to,
321
- };
322
- next.query = "";
323
- next.active = true;
324
- next.type = transaction.getMeta(pluginKey)?.type;
325
- next.selectedItemIndex = 0;
326
- } else if (prev.active) {
327
- // Try to match against where our cursor currently is
328
- // if the type is slash we get the command after the character
329
- // otherwise we get the whole query
330
- const match = findCommandBeforeCursor(
331
- prev.type === "slash" ? char : "",
332
- newState.selection
333
- );
334
- if (!match) {
335
- throw new Error("active but no match (suggestions)");
336
- }
337
-
338
- next.range = match.range;
339
- next.active = true;
340
- next.decorationId = prev.decorationId;
341
- next.query = match.query;
342
- next.selectedItemIndex = 0;
343
- }
344
- } else {
345
- next.active = false;
302
+ return getDefaultPluginState<T>();
346
303
  }
347
304
 
348
- if (next.active) {
349
- next.items = items(next.query!);
350
- if (next.items.length) {
351
- next.notFoundCount = 0;
352
- } else {
353
- // Update the "notFoundCount",
354
- // which indicates how many characters have been typed after showing no results
355
- if (next.range!.to > prev.range!.to) {
356
- // Text has been entered (selection moved to right), but still no items found, update Count
357
- next.notFoundCount = prev.notFoundCount + 1;
358
- } else {
359
- // No text has been entered in this tr, keep not found count
360
- // (e.g.: user hits backspace after no results)
361
- next.notFoundCount = prev.notFoundCount;
362
- }
363
- }
305
+ // Updates keyboardHoveredItemIndex if necessary.
306
+ if (
307
+ transaction.getMeta(pluginKey)?.selectedItemIndexChanged !== undefined
308
+ ) {
309
+ let newIndex =
310
+ transaction.getMeta(pluginKey).selectedItemIndexChanged;
364
311
 
365
- if (next.notFoundCount > 3) {
366
- next.active = false;
312
+ // Allows selection to jump between first and last items.
313
+ if (newIndex < 0) {
314
+ newIndex = prev.items.length - 1;
315
+ } else if (newIndex >= prev.items.length) {
316
+ newIndex = 0;
367
317
  }
368
- }
369
318
 
370
- // Make sure to empty the range if suggestion is inactive
371
- if (!next.active) {
372
- next.decorationId = null;
373
- next.range = null;
374
- next.query = null;
375
- next.notFoundCount = 0;
376
- next.items = [];
319
+ next.keyboardHoveredItemIndex = newIndex;
377
320
  }
378
321
 
379
322
  return next;
@@ -382,57 +325,74 @@ export function createSuggestionPlugin<T extends SuggestionItem>({
382
325
 
383
326
  props: {
384
327
  handleKeyDown(view, event) {
385
- const { active } = (this as Plugin).getState(view.state);
386
-
387
- if (!active) {
388
- // activate the popup on 'char' keypress (e.g. '/')
389
- if (event.key === char) {
390
- view.dispatch(
391
- view.state.tr
392
- .insertText(char)
393
- .scrollIntoView()
394
- .setMeta(pluginKey, { activate: true, type: "slash" })
395
- );
396
- // return true to cancel the original event, as we insert / ourselves
397
- return true;
398
- }
399
- } else {
400
- const { items, range, selectedItemIndex } = pluginKey.getState(
401
- view.state
328
+ const menuIsActive = (this as Plugin).getState(view.state).active;
329
+
330
+ // Shows the menu if the default trigger character was pressed and the menu isn't active.
331
+ if (event.key === defaultTriggerCharacter && !menuIsActive) {
332
+ view.dispatch(
333
+ view.state.tr
334
+ .insertText(defaultTriggerCharacter)
335
+ .scrollIntoView()
336
+ .setMeta(pluginKey, {
337
+ activate: true,
338
+ triggerCharacter: defaultTriggerCharacter,
339
+ })
402
340
  );
403
341
 
404
- if (event.key === "ArrowUp") {
405
- view.dispatch(
406
- view.state.tr.setMeta(pluginKey, {
407
- selectedItemIndexChanged: selectedItemIndex - 1,
408
- })
409
- );
410
- return true;
411
- }
342
+ return true;
343
+ }
412
344
 
413
- if (event.key === "ArrowDown") {
414
- view.dispatch(
415
- view.state.tr.setMeta(pluginKey, {
416
- selectedItemIndexChanged: selectedItemIndex + 1,
417
- })
418
- );
419
- return true;
420
- }
345
+ // Doesn't handle other keystrokes if the menu isn't active.
346
+ if (!menuIsActive) {
347
+ return false;
348
+ }
421
349
 
422
- if (event.key === "Enter") {
423
- deactivate(view);
424
- selectItemCallback({
425
- item: items[selectedItemIndex],
426
- editor: editor,
427
- range: range,
428
- });
429
- return true;
430
- }
350
+ // Handles keystrokes for navigating the menu.
351
+ const {
352
+ triggerCharacter,
353
+ queryStartPos,
354
+ items,
355
+ keyboardHoveredItemIndex,
356
+ } = pluginKey.getState(view.state);
357
+
358
+ // Moves the keyboard selection to the previous item.
359
+ if (event.key === "ArrowUp") {
360
+ view.dispatch(
361
+ view.state.tr.setMeta(pluginKey, {
362
+ selectedItemIndexChanged: keyboardHoveredItemIndex - 1,
363
+ })
364
+ );
365
+ return true;
366
+ }
431
367
 
432
- if (event.key === "Escape") {
433
- deactivate(view);
434
- return true;
435
- }
368
+ // Moves the keyboard selection to the next item.
369
+ if (event.key === "ArrowDown") {
370
+ view.dispatch(
371
+ view.state.tr.setMeta(pluginKey, {
372
+ selectedItemIndexChanged: keyboardHoveredItemIndex + 1,
373
+ })
374
+ );
375
+ return true;
376
+ }
377
+
378
+ // Selects an item and closes the menu.
379
+ if (event.key === "Enter") {
380
+ deactivate(view);
381
+ selectItemCallback({
382
+ item: items[keyboardHoveredItemIndex],
383
+ editor: editor,
384
+ range: {
385
+ from: queryStartPos - triggerCharacter.length,
386
+ to: view.state.selection.from,
387
+ },
388
+ });
389
+ return true;
390
+ }
391
+
392
+ // Closes the menu.
393
+ if (event.key === "Escape") {
394
+ deactivate(view);
395
+ return true;
436
396
  }
437
397
 
438
398
  return false;
@@ -445,18 +405,17 @@ export function createSuggestionPlugin<T extends SuggestionItem>({
445
405
 
446
406
  // Setup decorator on the currently active suggestion.
447
407
  decorations(state) {
448
- const { active, range, decorationId, type } = (this as Plugin).getState(
449
- state
450
- );
408
+ const { active, decorationId, queryStartPos, triggerCharacter } = (
409
+ this as Plugin
410
+ ).getState(state);
451
411
 
452
412
  if (!active) {
453
413
  return null;
454
414
  }
455
415
 
456
- // If type in meta is drag, create decoration node that wraps block
457
- // Because the block does not have content yet (slash menu has the '/' in its content),
458
- // so we can't use an inline decoration.
459
- if (type === "drag") {
416
+ // If the menu was opened programmatically by another extension, it may not use a trigger character. In this
417
+ // case, the decoration is set on the whole block instead, as the decoration range would otherwise be empty.
418
+ if (triggerCharacter === "") {
460
419
  const blockNode = findBlock(state.selection);
461
420
  if (blockNode) {
462
421
  return DecorationSet.create(state.doc, [
@@ -472,13 +431,17 @@ export function createSuggestionPlugin<T extends SuggestionItem>({
472
431
  ]);
473
432
  }
474
433
  }
475
- // Create inline decoration that wraps / or whatever the specified character is
434
+ // Creates an inline decoration around the trigger character.
476
435
  return DecorationSet.create(state.doc, [
477
- Decoration.inline(range.from, range.to, {
478
- nodeName: "span",
479
- class: "suggestion-decorator",
480
- "data-decoration-id": decorationId,
481
- }),
436
+ Decoration.inline(
437
+ queryStartPos - triggerCharacter.length,
438
+ queryStartPos,
439
+ {
440
+ nodeName: "span",
441
+ class: "suggestion-decorator",
442
+ "data-decoration-id": decorationId,
443
+ }
444
+ ),
482
445
  ]);
483
446
  },
484
447
  },
@@ -7,9 +7,9 @@ export type SuggestionsMenuStaticParams<T extends SuggestionItem> = {
7
7
 
8
8
  export type SuggestionsMenuDynamicParams<T extends SuggestionItem> = {
9
9
  items: T[];
10
- selectedItemIndex: number;
10
+ keyboardHoveredItemIndex: number;
11
11
 
12
- queryStartBoundingBox: DOMRect;
12
+ referenceRect: DOMRect;
13
13
  };
14
14
 
15
15
  export type SuggestionsMenu<T extends SuggestionItem> = EditorElement<
@@ -1,6 +1,6 @@
1
1
  import { Editor, EditorOptions } from "@tiptap/core";
2
2
  import { UiFactories } from "./BlockNoteExtensions";
3
- export declare type BlockNoteEditorOptions = EditorOptions & {
3
+ export type BlockNoteEditorOptions = EditorOptions & {
4
4
  enableBlockNoteExtensions: boolean;
5
5
  disableHistoryExtension: boolean;
6
6
  uiFactories: UiFactories;
@@ -1,12 +1,10 @@
1
1
  import { Extensions } from "@tiptap/core";
2
- import { Node } from "@tiptap/core";
3
2
  import { FormattingToolbarFactory } from "./extensions/FormattingToolbar/FormattingToolbarFactoryTypes";
4
3
  import { HyperlinkToolbarFactory } from "./extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes";
5
4
  import { SuggestionsMenuFactory } from "./shared/plugins/suggestion/SuggestionsMenuFactoryTypes";
6
5
  import { BlockSideMenuFactory } from "./extensions/DraggableBlocks/BlockSideMenuFactoryTypes";
7
6
  import { SlashMenuItem } from "./extensions/SlashMenu/SlashMenuItem";
8
- export declare const Document: Node<any, any>;
9
- export declare type UiFactories = Partial<{
7
+ export type UiFactories = Partial<{
10
8
  formattingToolbarFactory: FormattingToolbarFactory;
11
9
  hyperlinkToolbarFactory: HyperlinkToolbarFactory;
12
10
  slashMenuFactory: SuggestionsMenuFactory<SlashMenuItem>;
@@ -0,0 +1,5 @@
1
+ import { Node } from "prosemirror-model";
2
+ import { EditorState } from "prosemirror-state";
3
+ import { Block, Document } from "../extensions/Blocks/apiTypes";
4
+ export declare function BlockFromPMNode(node: Node): Block;
5
+ export declare function DocumentFromPMState(_state: EditorState): Document;
@@ -0,0 +1,9 @@
1
+ import { Extension } from "@tiptap/core";
2
+ declare module "@tiptap/core" {
3
+ interface Commands<ReturnType> {
4
+ blockBackgroundColor: {
5
+ setBlockBackgroundColor: (posInBlock: number, color: string) => ReturnType;
6
+ };
7
+ }
8
+ }
9
+ export declare const BackgroundColorExtension: Extension<any, any>;
@@ -0,0 +1,9 @@
1
+ import { Mark } from "@tiptap/core";
2
+ declare module "@tiptap/core" {
3
+ interface Commands<ReturnType> {
4
+ backgroundColor: {
5
+ setBackgroundColor: (color: string) => ReturnType;
6
+ };
7
+ }
8
+ }
9
+ export declare const BackgroundColorMark: Mark<any, any>;
@@ -8,6 +8,7 @@ import { Plugin } from "prosemirror-state";
8
8
  * Solution: When attributes change on a node, this plugin sets a data-* attribute with the "previous" value. This way we can still use CSS transitions. (See block.module.css)
9
9
  */
10
10
  export declare const PreviousBlockTypePlugin: () => Plugin<{
11
- prevBlockAttrs: any;
12
- needsUpdate: boolean;
11
+ prevTransactionOldBlockAttrs: any;
12
+ currentTransactionOldBlockAttrs: any;
13
+ updatedBlocks: Set<string>;
13
14
  }>;
@@ -0,0 +1,16 @@
1
+ export type BlockSpec<Type extends string, Props extends Record<string, string>> = {
2
+ type: Type;
3
+ props: Props;
4
+ };
5
+ export type BlockSpecUpdate<Spec> = Spec extends BlockSpec<infer Type, infer Props> ? {
6
+ type: Type;
7
+ props?: Partial<Props>;
8
+ } : never;
9
+ export type NumberedListItemBlock = BlockSpec<"numberedListItem", {}>;
10
+ export type BulletListItemBlock = BlockSpec<"bulletListItem", {}>;
11
+ export type HeadingBlock = BlockSpec<"heading", {
12
+ level: "1" | "2" | "3";
13
+ }>;
14
+ export type ParagraphBlock = BlockSpec<"paragraph", {}>;
15
+ export type Block = ParagraphBlock | HeadingBlock | BulletListItemBlock | NumberedListItemBlock;
16
+ export type BlockUpdate = BlockSpecUpdate<Block>;
@@ -1,5 +1,5 @@
1
1
  import { Node, NodeType } from "prosemirror-model";
2
- export declare type BlockInfo = {
2
+ export type BlockInfo = {
3
3
  id: string;
4
4
  node: Node;
5
5
  contentNode: Node;
@@ -0,0 +1,2 @@
1
+ declare const BlockAttributes: Record<string, string>;
2
+ export default BlockAttributes;