@blocknote/core 0.1.0-alpha.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.
Files changed (143) hide show
  1. package/README.md +99 -0
  2. package/dist/blocknote.js +4485 -0
  3. package/dist/blocknote.js.map +1 -0
  4. package/dist/blocknote.umd.cjs +90 -0
  5. package/dist/blocknote.umd.cjs.map +1 -0
  6. package/dist/style.css +1 -0
  7. package/package.json +109 -0
  8. package/src/BlockNoteExtensions.ts +90 -0
  9. package/src/EditorContent.tsx +1 -0
  10. package/src/assets/inter-v12-latin/inter-v12-latin-100.woff +0 -0
  11. package/src/assets/inter-v12-latin/inter-v12-latin-100.woff2 +0 -0
  12. package/src/assets/inter-v12-latin/inter-v12-latin-200.woff +0 -0
  13. package/src/assets/inter-v12-latin/inter-v12-latin-200.woff2 +0 -0
  14. package/src/assets/inter-v12-latin/inter-v12-latin-300.woff +0 -0
  15. package/src/assets/inter-v12-latin/inter-v12-latin-300.woff2 +0 -0
  16. package/src/assets/inter-v12-latin/inter-v12-latin-500.woff +0 -0
  17. package/src/assets/inter-v12-latin/inter-v12-latin-500.woff2 +0 -0
  18. package/src/assets/inter-v12-latin/inter-v12-latin-600.woff +0 -0
  19. package/src/assets/inter-v12-latin/inter-v12-latin-600.woff2 +0 -0
  20. package/src/assets/inter-v12-latin/inter-v12-latin-700.woff +0 -0
  21. package/src/assets/inter-v12-latin/inter-v12-latin-700.woff2 +0 -0
  22. package/src/assets/inter-v12-latin/inter-v12-latin-800.woff +0 -0
  23. package/src/assets/inter-v12-latin/inter-v12-latin-800.woff2 +0 -0
  24. package/src/assets/inter-v12-latin/inter-v12-latin-900.woff +0 -0
  25. package/src/assets/inter-v12-latin/inter-v12-latin-900.woff2 +0 -0
  26. package/src/assets/inter-v12-latin/inter-v12-latin-regular.woff +0 -0
  27. package/src/assets/inter-v12-latin/inter-v12-latin-regular.woff2 +0 -0
  28. package/src/editor.module.css +3 -0
  29. package/src/extensions/Blocks/OrderedListPlugin.ts +46 -0
  30. package/src/extensions/Blocks/PreviousBlockTypePlugin.ts +146 -0
  31. package/src/extensions/Blocks/commands/joinBackward.ts +274 -0
  32. package/src/extensions/Blocks/helpers/findBlock.ts +3 -0
  33. package/src/extensions/Blocks/helpers/setBlockHeading.ts +30 -0
  34. package/src/extensions/Blocks/index.ts +15 -0
  35. package/src/extensions/Blocks/nodes/Block.module.css +226 -0
  36. package/src/extensions/Blocks/nodes/Block.ts +390 -0
  37. package/src/extensions/Blocks/nodes/BlockGroup.ts +28 -0
  38. package/src/extensions/Blocks/nodes/Content.ts +50 -0
  39. package/src/extensions/Blocks/nodes/README.md +26 -0
  40. package/src/extensions/Blocks/rule.ts +48 -0
  41. package/src/extensions/BubbleMenu/BubbleMenuExtension.tsx +28 -0
  42. package/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +245 -0
  43. package/src/extensions/BubbleMenu/component/BubbleMenu.tsx +216 -0
  44. package/src/extensions/BubbleMenu/component/DropdownBlockItem.module.css +13 -0
  45. package/src/extensions/BubbleMenu/component/DropdownBlockItem.tsx +25 -0
  46. package/src/extensions/BubbleMenu/component/LinkToolbarButton.tsx +67 -0
  47. package/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts +15 -0
  48. package/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx +266 -0
  49. package/src/extensions/DraggableBlocks/components/DragHandle.module.css +33 -0
  50. package/src/extensions/DraggableBlocks/components/DragHandle.tsx +108 -0
  51. package/src/extensions/DraggableBlocks/components/DragHandleMenu.module.css +10 -0
  52. package/src/extensions/DraggableBlocks/components/DragHandleMenu.tsx +18 -0
  53. package/src/extensions/Hyperlinks/HyperlinkMark.tsx +16 -0
  54. package/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx +200 -0
  55. package/src/extensions/Hyperlinks/menus/HyperlinkBasicMenu.tsx +59 -0
  56. package/src/extensions/Hyperlinks/menus/HyperlinkEditMenu.tsx +72 -0
  57. package/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInput.tsx +173 -0
  58. package/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInputStyles.ts +36 -0
  59. package/src/extensions/Hyperlinks/menus/atlaskit/README.md +1 -0
  60. package/src/extensions/Hyperlinks/menus/atlaskit/ToolbarComponent.tsx +61 -0
  61. package/src/extensions/Paragraph/FixedParagraph.ts +12 -0
  62. package/src/extensions/Placeholder/PlaceholderExtension.ts +127 -0
  63. package/src/extensions/SlashMenu/SlashMenuExtension.ts +43 -0
  64. package/src/extensions/SlashMenu/SlashMenuItem.ts +56 -0
  65. package/src/extensions/SlashMenu/defaultCommands.tsx +229 -0
  66. package/src/extensions/SlashMenu/index.ts +11 -0
  67. package/src/extensions/TrailingNode/TrailingNodeExtension.ts +70 -0
  68. package/src/extensions/UniqueID/UniqueID.ts +281 -0
  69. package/src/extensions/helpers/formatKeyboardShortcut.ts +9 -0
  70. package/src/fonts-inter.css +94 -0
  71. package/src/globals.css +28 -0
  72. package/src/index.ts +5 -0
  73. package/src/lib/atlaskit/browser.ts +47 -0
  74. package/src/root.module.css +19 -0
  75. package/src/shared/components/toolbar/SimpleToolbarButton.module.css +13 -0
  76. package/src/shared/components/toolbar/SimpleToolbarButton.tsx +56 -0
  77. package/src/shared/components/toolbar/Toolbar.module.css +10 -0
  78. package/src/shared/components/toolbar/Toolbar.tsx +5 -0
  79. package/src/shared/components/toolbar/ToolbarSeparator.module.css +13 -0
  80. package/src/shared/components/toolbar/ToolbarSeparator.tsx +7 -0
  81. package/src/shared/components/tooltip/TooltipContent.module.css +15 -0
  82. package/src/shared/components/tooltip/TooltipContent.tsx +23 -0
  83. package/src/shared/hooks/useEditorForceUpdate.tsx +30 -0
  84. package/src/shared/plugins/suggestion/SuggestionItem.ts +31 -0
  85. package/src/shared/plugins/suggestion/SuggestionListReactRenderer.ts +227 -0
  86. package/src/shared/plugins/suggestion/SuggestionPlugin.ts +365 -0
  87. package/src/shared/plugins/suggestion/components/SuggestionGroup.module.css +45 -0
  88. package/src/shared/plugins/suggestion/components/SuggestionGroup.tsx +134 -0
  89. package/src/shared/plugins/suggestion/components/SuggestionList.module.css +10 -0
  90. package/src/shared/plugins/suggestion/components/SuggestionList.tsx +91 -0
  91. package/src/style.css +7 -0
  92. package/src/useEditor.ts +47 -0
  93. package/src/vite-env.d.ts +1 -0
  94. package/types/src/BlockNoteExtensions.d.ts +4 -0
  95. package/types/src/EditorContent.d.ts +1 -0
  96. package/types/src/extensions/Blocks/OrderedListPlugin.d.ts +2 -0
  97. package/types/src/extensions/Blocks/PreviousBlockTypePlugin.d.ts +13 -0
  98. package/types/src/extensions/Blocks/commands/joinBackward.d.ts +14 -0
  99. package/types/src/extensions/Blocks/helpers/findBlock.d.ts +6 -0
  100. package/types/src/extensions/Blocks/helpers/setBlockHeading.d.ts +5 -0
  101. package/types/src/extensions/Blocks/index.d.ts +1 -0
  102. package/types/src/extensions/Blocks/nodes/Block.d.ts +32 -0
  103. package/types/src/extensions/Blocks/nodes/BlockGroup.d.ts +2 -0
  104. package/types/src/extensions/Blocks/nodes/Content.d.ts +5 -0
  105. package/types/src/extensions/Blocks/rule.d.ts +16 -0
  106. package/types/src/extensions/BubbleMenu/BubbleMenuExtension.d.ts +5 -0
  107. package/types/src/extensions/BubbleMenu/BubbleMenuPlugin.d.ts +46 -0
  108. package/types/src/extensions/BubbleMenu/component/BubbleMenu.d.ts +5 -0
  109. package/types/src/extensions/BubbleMenu/component/DropdownBlockItem.d.ts +10 -0
  110. package/types/src/extensions/BubbleMenu/component/LinkToolbarButton.d.ts +11 -0
  111. package/types/src/extensions/DraggableBlocks/DraggableBlocksExtension.d.ts +7 -0
  112. package/types/src/extensions/DraggableBlocks/DraggableBlocksPlugin.d.ts +18 -0
  113. package/types/src/extensions/DraggableBlocks/components/DragHandle.d.ts +12 -0
  114. package/types/src/extensions/DraggableBlocks/components/DragHandleMenu.d.ts +6 -0
  115. package/types/src/extensions/Hyperlinks/HyperlinkMark.d.ts +7 -0
  116. package/types/src/extensions/Hyperlinks/HyperlinkMenuPlugin.d.ts +2 -0
  117. package/types/src/extensions/Hyperlinks/menus/HyperlinkBasicMenu.d.ts +12 -0
  118. package/types/src/extensions/Hyperlinks/menus/HyperlinkEditMenu.d.ts +10 -0
  119. package/types/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInput.d.ts +39 -0
  120. package/types/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInputStyles.d.ts +1 -0
  121. package/types/src/extensions/Hyperlinks/menus/atlaskit/ToolbarComponent.d.ts +11 -0
  122. package/types/src/extensions/Paragraph/FixedParagraph.d.ts +1 -0
  123. package/types/src/extensions/Placeholder/PlaceholderExtension.d.ts +25 -0
  124. package/types/src/extensions/SlashMenu/SlashMenuExtension.d.ts +10 -0
  125. package/types/src/extensions/SlashMenu/SlashMenuItem.d.ts +43 -0
  126. package/types/src/extensions/SlashMenu/defaultCommands.d.ts +8 -0
  127. package/types/src/extensions/SlashMenu/index.d.ts +5 -0
  128. package/types/src/extensions/TrailingNode/TrailingNodeExtension.d.ts +10 -0
  129. package/types/src/extensions/UniqueID/UniqueID.d.ts +3 -0
  130. package/types/src/extensions/helpers/formatKeyboardShortcut.d.ts +1 -0
  131. package/types/src/index.d.ts +4 -0
  132. package/types/src/lib/atlaskit/browser.d.ts +12 -0
  133. package/types/src/shared/components/toolbar/SimpleToolbarButton.d.ts +16 -0
  134. package/types/src/shared/components/toolbar/Toolbar.d.ts +4 -0
  135. package/types/src/shared/components/toolbar/ToolbarSeparator.d.ts +2 -0
  136. package/types/src/shared/components/tooltip/TooltipContent.d.ts +15 -0
  137. package/types/src/shared/hooks/useEditorForceUpdate.d.ts +2 -0
  138. package/types/src/shared/plugins/suggestion/SuggestionItem.d.ts +29 -0
  139. package/types/src/shared/plugins/suggestion/SuggestionListReactRenderer.d.ts +71 -0
  140. package/types/src/shared/plugins/suggestion/SuggestionPlugin.d.ts +74 -0
  141. package/types/src/shared/plugins/suggestion/components/SuggestionGroup.d.ts +23 -0
  142. package/types/src/shared/plugins/suggestion/components/SuggestionList.d.ts +26 -0
  143. package/types/src/useEditor.d.ts +8 -0
@@ -0,0 +1,365 @@
1
+ import { Editor, Range } from "@tiptap/core";
2
+ import { escapeRegExp, groupBy } from "lodash";
3
+ import { Plugin, PluginKey, Selection } from "prosemirror-state";
4
+ import { Decoration, DecorationSet } from "prosemirror-view";
5
+ import { findBlock } from "../../../extensions/Blocks/helpers/findBlock";
6
+ import SuggestionItem from "./SuggestionItem";
7
+
8
+ import createRenderer, {
9
+ SuggestionRendererProps,
10
+ } from "./SuggestionListReactRenderer";
11
+
12
+ export type SuggestionPluginOptions<T extends SuggestionItem> = {
13
+ /**
14
+ * The name of the plugin.
15
+ *
16
+ * Used for ensuring that the plugin key is unique when more than one instance of the SuggestionPlugin is used.
17
+ */
18
+ pluginKey: PluginKey;
19
+
20
+ /**
21
+ * The TipTap editor.
22
+ */
23
+ editor: Editor;
24
+
25
+ /**
26
+ * The character that should trigger the suggestion menu to pop up (e.g. a '/' for commands)
27
+ */
28
+ char: string;
29
+
30
+ /**
31
+ * The callback that gets executed when an item is selected by the user.
32
+ *
33
+ * **NOTE:** The command text is not removed automatically from the editor by this plugin,
34
+ * this should be done manually. The `editor` and `range` properties passed
35
+ * to the callback function might come in handy when doing this.
36
+ */
37
+ onSelectItem?: (props: { item: T; editor: Editor; range: Range }) => void;
38
+
39
+ /**
40
+ * A function that should supply the plugin with items to suggest, based on a certain query string.
41
+ */
42
+ items?: (query: string) => T[];
43
+
44
+ allow?: (props: { editor: Editor; range: Range }) => boolean;
45
+ };
46
+
47
+ export type MenuType = "slash" | "drag";
48
+
49
+ /**
50
+ * Finds a command: a specified character (e.g. '/') followed by a string of characters (all characters except the specified character are allowed).
51
+ * Returns the string following the specified character or undefined if no command was found.
52
+ *
53
+ * @param char the character that indicates the start of a command
54
+ * @param selection the selection (only works if the selection is empty; i.e. is a blinking cursor).
55
+ * @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.
56
+ */
57
+ export function findCommandBeforeCursor(
58
+ char: string,
59
+ selection: Selection
60
+ ): { range: Range; query: string } | undefined {
61
+ if (!selection.empty) {
62
+ return undefined;
63
+ }
64
+
65
+ // get the text before the cursor as a node
66
+ const node = selection.$anchor.nodeBefore;
67
+ if (!node || !node.text) {
68
+ return undefined;
69
+ }
70
+
71
+ // regex to match anything between with the specified char (e.g. '/') and the end of text (which is the end of selection)
72
+ const regex = new RegExp(`${escapeRegExp(char)}([^${escapeRegExp(char)}]*)$`);
73
+ const match = node.text.match(regex);
74
+
75
+ if (!match) {
76
+ return undefined;
77
+ }
78
+
79
+ return {
80
+ query: match[1],
81
+ range: {
82
+ from: selection.$anchor.pos - match[1].length - char.length,
83
+ to: selection.$anchor.pos,
84
+ },
85
+ };
86
+ }
87
+
88
+ /**
89
+ * A ProseMirror plugin for suggestions, designed to make '/'-commands possible as well as mentions.
90
+ *
91
+ * This is basically a simplified version of TipTap's [Suggestions](https://github.com/ueberdosis/tiptap/tree/db92a9b313c5993b723c85cd30256f1d4a0b65e1/packages/suggestion) plugin.
92
+ *
93
+ * This version is adapted from the aforementioned version in the following ways:
94
+ * - This version supports generic items instead of only strings (to allow for more advanced filtering for example)
95
+ * - This version hides some unnecessary complexity from the user of the plugin.
96
+ * - This version handles key events differently
97
+ *
98
+ * @param options options for configuring the plugin
99
+ * @returns the prosemirror plugin
100
+ */
101
+ export function createSuggestionPlugin<T extends SuggestionItem>({
102
+ pluginKey,
103
+ editor,
104
+ char,
105
+ onSelectItem: selectItemCallback = () => {},
106
+ items = () => [],
107
+ }: SuggestionPluginOptions<T>) {
108
+ // Assertions
109
+ if (char.length !== 1) {
110
+ throw new Error("'char' should be a single character");
111
+ }
112
+
113
+ const renderer = createRenderer<T>(editor);
114
+
115
+ // Plugin key is passed in parameter so it can be exported and used in draghandle
116
+ return new Plugin({
117
+ key: pluginKey,
118
+
119
+ filterTransaction(transaction) {
120
+ // prevent blurring when clicking with the mouse inside the popup menu
121
+ const blurMeta = transaction.getMeta("blur");
122
+ if (blurMeta?.event.relatedTarget) {
123
+ const c = renderer.getComponent();
124
+ if (c?.contains(blurMeta.event.relatedTarget)) {
125
+ return false;
126
+ }
127
+ }
128
+ return true;
129
+ },
130
+
131
+ view() {
132
+ return {
133
+ update: async (view, prevState) => {
134
+ const prev = this.key?.getState(prevState);
135
+ const next = this.key?.getState(view.state);
136
+
137
+ // See how the state changed
138
+ const started = !prev.active && next.active;
139
+ const stopped = prev.active && !next.active;
140
+ const changed = !started && !stopped && prev.query !== next.query;
141
+
142
+ // Cancel when suggestion isn't active
143
+ if (!started && !changed && !stopped) {
144
+ return;
145
+ }
146
+
147
+ const state = stopped ? prev : next;
148
+ const decorationNode = document.querySelector(
149
+ `[data-decoration-id="${state.decorationId}"]`
150
+ );
151
+
152
+ const groups: { [groupName: string]: T[] } = groupBy(
153
+ state.items,
154
+ "groupName"
155
+ );
156
+
157
+ const deactivate = () => {
158
+ view.dispatch(
159
+ view.state.tr.setMeta(pluginKey, { deactivate: true })
160
+ );
161
+ };
162
+
163
+ const rendererProps: SuggestionRendererProps<T> = {
164
+ groups: changed || started ? groups : {},
165
+ count: state.items.length,
166
+ onSelectItem: (item: T) => {
167
+ deactivate();
168
+ selectItemCallback({
169
+ item,
170
+ editor,
171
+ range: state.range,
172
+ });
173
+ },
174
+ // virtual node for popper.js or tippy.js
175
+ // this can be used for building popups without a DOM node
176
+ clientRect: decorationNode
177
+ ? () => decorationNode.getBoundingClientRect()
178
+ : null,
179
+ onClose: () => {
180
+ deactivate();
181
+ renderer.onExit?.(rendererProps);
182
+ },
183
+ };
184
+
185
+ if (stopped) {
186
+ renderer.onExit?.(rendererProps);
187
+ }
188
+
189
+ if (changed) {
190
+ renderer.onUpdate?.(rendererProps);
191
+ }
192
+
193
+ if (started) {
194
+ renderer.onStart?.(rendererProps);
195
+ }
196
+ },
197
+ };
198
+ },
199
+
200
+ state: {
201
+ // Initialize the plugin's internal state.
202
+ init() {
203
+ return {
204
+ active: false,
205
+ range: {} as any, // TODO
206
+ query: null as string | null,
207
+ notFoundCount: 0,
208
+ items: [] as T[],
209
+ type: "slash",
210
+ decorationId: null as string | null,
211
+ };
212
+ },
213
+
214
+ // Apply changes to the plugin state from a view transaction.
215
+ apply(transaction, prev, _oldState, _newState) {
216
+ const { selection } = transaction;
217
+ const next = { ...prev };
218
+
219
+ if (
220
+ // only show popup if selection is a blinking cursor
221
+ selection.from === selection.to &&
222
+ // deactivate popup from view (e.g.: choice has been made or esc has been pressed)
223
+ !transaction.getMeta(pluginKey)?.deactivate &&
224
+ // deactivate because a mouse event occurs (user clicks somewhere else in the document)
225
+ !transaction.getMeta("focus") &&
226
+ !transaction.getMeta("blur") &&
227
+ !transaction.getMeta("pointer")
228
+ ) {
229
+ // Reset active state if we just left the previous suggestion range (e.g.: key arrows moving before /)
230
+ if (prev.active && selection.from <= prev.range.from) {
231
+ next.active = false;
232
+ } else if (transaction.getMeta(pluginKey)?.activate) {
233
+ // Start showing suggestions. activate has been set after typing a "/" (or whatever the specified character is), so let's create the decoration and initialize
234
+ const newDecorationId = `id_${Math.floor(
235
+ Math.random() * 0xffffffff
236
+ )}`;
237
+ next.decorationId = newDecorationId;
238
+ next.range = {
239
+ from: selection.from - 1,
240
+ to: selection.to,
241
+ };
242
+ next.query = "";
243
+ next.active = true;
244
+ next.type = transaction.getMeta(pluginKey)?.type;
245
+ } else if (prev.active) {
246
+ // Try to match against where our cursor currently is
247
+ // if the type is slash we get the command after the character
248
+ // otherwise we get the whole query
249
+ const match = findCommandBeforeCursor(
250
+ prev.type === "slash" ? char : "",
251
+ selection
252
+ );
253
+ if (!match) {
254
+ throw new Error("active but no match (suggestions)");
255
+ }
256
+
257
+ next.range = match.range;
258
+ next.active = true;
259
+ next.decorationId = prev.decorationId;
260
+ next.query = match.query;
261
+ }
262
+ } else {
263
+ next.active = false;
264
+ }
265
+
266
+ if (next.active) {
267
+ next.items = items(next.query!);
268
+ if (next.items.length) {
269
+ next.notFoundCount = 0;
270
+ } else {
271
+ // Update the "notFoundCount",
272
+ // which indicates how many characters have been typed after showing no results
273
+ if (next.range.to > prev.range.to) {
274
+ // Text has been entered (selection moved to right), but still no items found, update Count
275
+ next.notFoundCount = prev.notFoundCount + 1;
276
+ } else {
277
+ // No text has been entered in this tr, keep not found count
278
+ // (e.g.: user hits backspace after no results)
279
+ next.notFoundCount = prev.notFoundCount;
280
+ }
281
+ }
282
+
283
+ if (next.notFoundCount > 3) {
284
+ next.active = false;
285
+ }
286
+ }
287
+
288
+ // Make sure to empty the range if suggestion is inactive
289
+ if (!next.active) {
290
+ next.decorationId = null;
291
+ next.range = {};
292
+ next.query = null;
293
+ next.notFoundCount = 0;
294
+ next.items = [];
295
+ }
296
+
297
+ return next;
298
+ },
299
+ },
300
+
301
+ props: {
302
+ handleKeyDown(view, event) {
303
+ const { active } = (this as Plugin).getState(view.state);
304
+
305
+ if (!active) {
306
+ // activate the popup on 'char' keypress (e.g. '/')
307
+ if (event.key === char) {
308
+ view.dispatch(
309
+ view.state.tr
310
+ .insertText(char)
311
+ .scrollIntoView()
312
+ .setMeta(pluginKey, { activate: true, type: "slash" })
313
+ );
314
+ // return true to cancel the original event, as we insert / ourselves
315
+ return true;
316
+ }
317
+ return false;
318
+ }
319
+
320
+ // pass the key event onto the renderer (to handle arrow keys, enter and escape)
321
+ // return true if the event got handled by the renderer or false otherwise
322
+ return renderer.onKeyDown?.(event) || false;
323
+ },
324
+
325
+ // Setup decorator on the currently active suggestion.
326
+ decorations(state) {
327
+ const { active, range, decorationId, type } = (this as Plugin).getState(
328
+ state
329
+ );
330
+
331
+ if (!active) {
332
+ return null;
333
+ }
334
+
335
+ // If type in meta is drag, create decoration node that wraps block
336
+ // Because the block does not have content yet (slash menu has the '/' in its content),
337
+ // so we can't use an inline decoration.
338
+ if (type === "drag") {
339
+ const blockNode = findBlock(state.selection);
340
+ if (blockNode) {
341
+ return DecorationSet.create(state.doc, [
342
+ Decoration.node(
343
+ blockNode.pos,
344
+ blockNode.pos + blockNode.node.nodeSize,
345
+ {
346
+ nodeName: "span",
347
+ class: "suggestion-decorator",
348
+ "data-decoration-id": decorationId,
349
+ }
350
+ ),
351
+ ]);
352
+ }
353
+ }
354
+ // Create inline decoration that wraps / or whatever the specified character is
355
+ return DecorationSet.create(state.doc, [
356
+ Decoration.inline(range.from, range.to, {
357
+ nodeName: "span",
358
+ class: "suggestion-decorator",
359
+ "data-decoration-id": decorationId,
360
+ }),
361
+ ]);
362
+ },
363
+ },
364
+ });
365
+ }
@@ -0,0 +1,45 @@
1
+ .suggestionWrapper {
2
+ display: flex;
3
+ flex-flow: row nowrap;
4
+ justify-content: space-between;
5
+ white-space: initial;
6
+ padding: 0px;
7
+ }
8
+
9
+ .buttonName {
10
+ font-size: small;
11
+ margin-bottom: 4px;
12
+ }
13
+ .buttonHint {
14
+ font-size: smaller;
15
+ color: rgb(128, 128, 128);
16
+ }
17
+ .buttonShortcut {
18
+ font-size: x-small;
19
+ color: rgb(128, 128, 128);
20
+ background-color: rgba(128, 128, 128, 0.2);
21
+ vertical-align: top;
22
+ padding: 3px;
23
+ border-radius: 3px;
24
+ }
25
+ .iconWrapper {
26
+ border: 1px solid rgba(128, 128, 128, 0.5);
27
+ border-radius: 4px;
28
+ background-color: white;
29
+ width: 40px;
30
+ height: 40px;
31
+ padding: 10px;
32
+ }
33
+ .icon {
34
+ width: 20px;
35
+ height: 20px;
36
+ fill: var(--N800);
37
+ }
38
+ .icon path[fill="none"] {
39
+ stroke: none;
40
+ }
41
+
42
+ .selectedIcon path {
43
+ stroke: var(--N800);
44
+ stroke-width: 1px;
45
+ }
@@ -0,0 +1,134 @@
1
+ import { ButtonItem, Section } from "@atlaskit/menu";
2
+ import React from "react";
3
+ import SuggestionItem from "../SuggestionItem";
4
+ import styles from "./SuggestionGroup.module.css";
5
+
6
+ const MIN_LEFT_MARGIN = 5;
7
+
8
+ function SuggestionContent<T extends SuggestionItem>(props: { item: T }) {
9
+ return (
10
+ <div className={styles.suggestionWrapper}>
11
+ <div>
12
+ <div className={styles.buttonName}>{props.item.name}</div>
13
+ {props.item.hint && (
14
+ <div className={styles.buttonHint}>{props.item.hint}</div>
15
+ )}
16
+ </div>
17
+ {props.item.shortcut && (
18
+ <div>
19
+ <div className={styles.buttonShortcut}>{props.item.shortcut}</div>
20
+ </div>
21
+ )}
22
+ </div>
23
+ );
24
+ }
25
+
26
+ function getIcon<T extends SuggestionItem>(
27
+ item: T,
28
+ isButtonSelected: boolean
29
+ ): JSX.Element | undefined {
30
+ const Icon = item.icon;
31
+ return (
32
+ Icon && (
33
+ <div className={styles.iconWrapper}>
34
+ <Icon
35
+ className={
36
+ styles.icon + " " + (isButtonSelected ? styles.selectedIcon : "")
37
+ }
38
+ />
39
+ </div>
40
+ )
41
+ );
42
+ }
43
+
44
+ type SuggestionComponentProps<T> = {
45
+ item: T;
46
+ index: number;
47
+ selectedIndex?: number;
48
+ clickItem: (item: T) => void;
49
+ };
50
+
51
+ function SuggestionComponent<T extends SuggestionItem>(
52
+ props: SuggestionComponentProps<T>
53
+ ) {
54
+ let isButtonSelected =
55
+ props.selectedIndex !== undefined && props.selectedIndex === props.index;
56
+
57
+ const buttonRef = React.useRef<HTMLElement>(null);
58
+ React.useEffect(() => {
59
+ if (
60
+ isButtonSelected &&
61
+ buttonRef.current &&
62
+ buttonRef.current.getBoundingClientRect().left > MIN_LEFT_MARGIN //TODO: Kinda hacky, fix
63
+ // This check is needed because initially the menu is initialized somewhere above outside the screen (with left = 1)
64
+ // scrollIntoView() is called before the menu is set in the right place, and without the check would scroll to the top of the page every time
65
+ ) {
66
+ buttonRef.current.scrollIntoView({
67
+ behavior: "smooth",
68
+ block: "nearest",
69
+ });
70
+ }
71
+ }, [isButtonSelected]);
72
+
73
+ return (
74
+ <div className={styles.buttonItem}>
75
+ <ButtonItem
76
+ isSelected={isButtonSelected} // This is needed to navigate with the keyboard
77
+ iconBefore={getIcon(props.item, isButtonSelected)}
78
+ onClick={(_e) => {
79
+ setTimeout(() => {
80
+ props.clickItem(props.item);
81
+ }, 0);
82
+
83
+ // e.stopPropagation();
84
+ // e.preventDefault();
85
+ }}
86
+ ref={buttonRef}>
87
+ <SuggestionContent item={props.item} />
88
+ </ButtonItem>
89
+ </div>
90
+ );
91
+ }
92
+
93
+ type SuggestionGroupProps<T> = {
94
+ /**
95
+ * Name of the group
96
+ */
97
+ name: string;
98
+
99
+ /**
100
+ * The list of items
101
+ */
102
+ items: T[];
103
+
104
+ /**
105
+ * Index of the selected item in this group; relative to this item group (so 0 refers to the first item in this group)
106
+ * This should be 'undefined' if none of the items in this group are selected
107
+ */
108
+ selectedIndex?: number;
109
+
110
+ /**
111
+ * Callback that gets executed when an item is clicked on.
112
+ */
113
+ clickItem: (item: T) => void;
114
+ };
115
+
116
+ export function SuggestionGroup<T extends SuggestionItem>(
117
+ props: SuggestionGroupProps<T>
118
+ ) {
119
+ return (
120
+ <Section title={props.name}>
121
+ {props.items.map((item, index) => {
122
+ return (
123
+ <SuggestionComponent
124
+ item={item}
125
+ key={index} // TODO: using index as key is not ideal for performance, better have ids on suggestionItems
126
+ index={index}
127
+ selectedIndex={props.selectedIndex}
128
+ clickItem={props.clickItem}
129
+ />
130
+ );
131
+ })}
132
+ </Section>
133
+ );
134
+ }
@@ -0,0 +1,10 @@
1
+ .menuList {
2
+ color: var(--N800);
3
+ background-color: white;
4
+ border: 1px solid var(--N40);
5
+ box-shadow: 0px 4px 8px rgba(9, 30, 66, 0.25),
6
+ 0px 0px 1px rgba(9, 30, 66, 0.31);
7
+ border-radius: 4px;
8
+ max-width: 320;
9
+ margin: 16px auto;
10
+ }
@@ -0,0 +1,91 @@
1
+ import { MenuGroup, Section } from "@atlaskit/menu";
2
+ import styles from "./SuggestionList.module.css";
3
+ import rootStyles from "../../../../root.module.css";
4
+ import { SuggestionGroup } from "./SuggestionGroup";
5
+ import SuggestionItem from "../SuggestionItem";
6
+
7
+ export type SuggestionListProps<T> = {
8
+ /**
9
+ * Object containing all suggestion items, grouped by their `groupName`.
10
+ */
11
+ groups: {
12
+ [groupName: string]: T[];
13
+ };
14
+
15
+ /**
16
+ * The total number of suggestion-items
17
+ */
18
+ count: number;
19
+
20
+ /**
21
+ * This callback gets executed whenever an item is clicked on
22
+ */
23
+ onSelectItem: (item: T) => void;
24
+
25
+ /**
26
+ * The index of the item that is currently selected (but not yet clicked on)
27
+ */
28
+ selectedIndex: number;
29
+ };
30
+
31
+ /**
32
+ * Stateless component that renders the suggestion list
33
+ */
34
+ export function SuggestionList<T extends SuggestionItem>(
35
+ props: SuggestionListProps<T>
36
+ ) {
37
+ const renderedGroups = [];
38
+
39
+ let currentGroupIndex = 0;
40
+
41
+ for (const groupName in props.groups) {
42
+ const items = props.groups[groupName];
43
+
44
+ renderedGroups.push(
45
+ <SuggestionGroup
46
+ key={groupName}
47
+ name={groupName}
48
+ items={items}
49
+ selectedIndex={
50
+ props.selectedIndex >= currentGroupIndex
51
+ ? props.selectedIndex - currentGroupIndex
52
+ : undefined
53
+ }
54
+ clickItem={props.onSelectItem}></SuggestionGroup>
55
+ );
56
+
57
+ currentGroupIndex += items.length;
58
+ }
59
+
60
+ return (
61
+ <div className={styles.menuList + " " + rootStyles.bnRoot}>
62
+ <MenuGroup>
63
+ {renderedGroups.length > 0 ? (
64
+ renderedGroups
65
+ ) : (
66
+ <Section title={"No match found"}> </Section>
67
+ )}
68
+ </MenuGroup>
69
+ </div>
70
+
71
+ // doesn't work well yet, maybe https://github.com/atomiks/tippyjs-react/issues/173
72
+ // We now render the tippy element manually in SuggestionListReactRenderer
73
+ // <Tippy
74
+ // visible={true}
75
+ // placement="bottom-start"
76
+ // content={
77
+ // <div className={styles.menuList}>
78
+ // <PopupMenuGroup maxWidth="250px" maxHeight="400px">
79
+ // {renderedGroups.length > 0 ? (
80
+ // renderedGroups
81
+ // ) : (
82
+ // <Section title={"No match found"}> </Section>
83
+ // )}
84
+ // </PopupMenuGroup>
85
+ // </div>
86
+ // }
87
+ // interactive={false}>
88
+ // <div></div>
89
+ // </Tippy>
90
+ );
91
+ }
package/src/style.css ADDED
@@ -0,0 +1,7 @@
1
+ /*
2
+ This is an empty placeholder file, which should NOT contain CSS code.
3
+ It's here so DEV environment doesn't show a 404
4
+
5
+ - In DEV environment, examples/editor loads this stub file, but actual CSS is loaded from CSS modules directly
6
+ - In PROD environment, the actual dist/style.css file is built from the CSS modules
7
+ */
@@ -0,0 +1,47 @@
1
+ import { EditorOptions, useEditor as useEditorTiptap } from "@tiptap/react";
2
+
3
+ import { DependencyList } from "react";
4
+ import { getBlockNoteExtensions } from "./BlockNoteExtensions";
5
+ import styles from "./editor.module.css";
6
+ import rootStyles from "./root.module.css";
7
+
8
+ type BlockNoteEditorOptions = EditorOptions & {
9
+ enableBlockNoteExtensions: boolean;
10
+ disableHistoryExtension: boolean;
11
+ };
12
+
13
+ const blockNoteExtensions = getBlockNoteExtensions();
14
+
15
+ const blockNoteOptions = {
16
+ enableInputRules: true,
17
+ enablePasteRules: true,
18
+ enableCoreExtensions: false,
19
+ };
20
+ export const useEditor = (
21
+ options: Partial<BlockNoteEditorOptions> = {},
22
+ deps: DependencyList = []
23
+ ) => {
24
+ const extensions = options.disableHistoryExtension
25
+ ? blockNoteExtensions.filter((e) => e.name !== "history")
26
+ : blockNoteExtensions;
27
+
28
+ const tiptapOptions = {
29
+ ...blockNoteOptions,
30
+ ...options,
31
+ extensions:
32
+ options.enableBlockNoteExtensions === false
33
+ ? options.extensions
34
+ : [...(options.extensions || []), ...extensions],
35
+ editorProps: {
36
+ attributes: {
37
+ ...(options.editorProps?.attributes || {}),
38
+ class: [
39
+ styles.bnEditor,
40
+ rootStyles.bnRoot,
41
+ (options.editorProps?.attributes as any)?.class || "",
42
+ ].join(" "),
43
+ },
44
+ },
45
+ };
46
+ return useEditorTiptap(tiptapOptions, deps);
47
+ };
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />