@blocknote/core 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +4 -0
  2. package/dist/blocknote.js +1787 -1834
  3. package/dist/blocknote.js.map +1 -1
  4. package/dist/blocknote.umd.cjs +4 -4
  5. package/dist/blocknote.umd.cjs.map +1 -1
  6. package/dist/style.css +1 -1
  7. package/package.json +3 -3
  8. package/src/BlockNoteEditor.ts +102 -38
  9. package/src/BlockNoteExtensions.ts +1 -58
  10. package/src/api/formatConversions/__snapshots__/formatConversions.test.ts.snap +10 -10
  11. package/src/api/formatConversions/formatConversions.test.ts +587 -605
  12. package/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +15 -15
  13. package/src/api/nodeConversions/nodeConversions.test.ts +90 -94
  14. package/src/extensions/Blocks/api/blockTypes.ts +3 -2
  15. package/src/extensions/Blocks/helpers/getBlockInfoFromPos.ts +6 -0
  16. package/src/extensions/Blocks/nodes/BlockContainer.ts +12 -3
  17. package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +92 -104
  18. package/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +178 -134
  19. package/src/extensions/Placeholder/PlaceholderExtension.ts +2 -2
  20. package/src/extensions/{DraggableBlocks/DraggableBlocksPlugin.ts → SideMenu/SideMenuPlugin.ts} +173 -163
  21. package/src/extensions/SlashMenu/BaseSlashMenuItem.ts +7 -30
  22. package/src/extensions/SlashMenu/SlashMenuPlugin.ts +51 -0
  23. package/src/extensions/SlashMenu/defaultSlashMenuItems.ts +109 -0
  24. package/src/extensions/UniqueID/UniqueID.ts +29 -30
  25. package/src/index.ts +9 -8
  26. package/src/node_modules/.vitest/results.json +1 -0
  27. package/src/shared/BaseUiElementTypes.ts +8 -0
  28. package/src/shared/EditorElement.ts +0 -16
  29. package/src/shared/EventEmitter.ts +58 -0
  30. package/src/shared/plugins/suggestion/SuggestionItem.ts +3 -6
  31. package/src/shared/plugins/suggestion/SuggestionPlugin.ts +333 -389
  32. package/types/src/BlockNoteEditor.d.ts +18 -10
  33. package/types/src/BlockNoteExtensions.d.ts +0 -19
  34. package/types/src/EventEmitter.d.ts +11 -0
  35. package/types/src/extensions/Blocks/api/blockTypes.d.ts +3 -2
  36. package/types/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.d.ts +0 -17
  37. package/types/src/extensions/DraggableBlocks/DraggableBlocksPlugin.d.ts +25 -19
  38. package/types/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.d.ts +2 -3
  39. package/types/src/extensions/FormattingToolbar/FormattingToolbarPlugin.d.ts +17 -24
  40. package/types/src/extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes.d.ts +0 -12
  41. package/types/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.d.ts +37 -10
  42. package/types/src/extensions/SideMenu/MultipleNodeSelection.d.ts +24 -0
  43. package/types/src/extensions/SideMenu/SideMenuPlugin.d.ts +79 -0
  44. package/types/src/extensions/SlashMenu/BaseSlashMenuItem.d.ts +5 -18
  45. package/types/src/extensions/SlashMenu/SlashMenuPlugin.d.ts +13 -0
  46. package/types/src/extensions/SlashMenu/defaultSlashMenuItems.d.ts +1 -69
  47. package/types/src/extensions/SlashMenu/index.d.ts +2 -3
  48. package/types/src/index.d.ts +9 -8
  49. package/types/src/shared/BaseUiElementTypes.d.ts +7 -0
  50. package/types/src/shared/EditorElement.d.ts +0 -10
  51. package/types/src/shared/EventEmitter.d.ts +11 -0
  52. package/types/src/shared/plugins/suggestion/SuggestionItem.d.ts +2 -7
  53. package/types/src/shared/plugins/suggestion/SuggestionPlugin.d.ts +12 -43
  54. package/types/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.d.ts +1 -1
  55. package/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts +0 -29
  56. package/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts +0 -37
  57. package/src/extensions/FormattingToolbar/FormattingToolbarExtension.ts +0 -37
  58. package/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts +0 -20
  59. package/src/extensions/HyperlinkToolbar/HyperlinkMark.ts +0 -28
  60. package/src/extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes.ts +0 -19
  61. package/src/extensions/SlashMenu/SlashMenuExtension.ts +0 -53
  62. package/src/extensions/SlashMenu/defaultSlashMenuItems.tsx +0 -195
  63. package/src/extensions/SlashMenu/index.ts +0 -5
  64. package/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts +0 -21
  65. package/types/src/CustomBlock.d.ts +0 -15
  66. package/types/src/extensions/Blocks/nodes/BlockContent/TableContent/TableCol.d.ts +0 -2
  67. package/types/src/extensions/Blocks/nodes/BlockContent/TableContent/TableContent.d.ts +0 -2
  68. package/types/src/extensions/Blocks/nodes/BlockContent/TableContent/TableRow.d.ts +0 -2
  69. package/types/src/extensions/Placeholder/localisation/index.d.ts +0 -2
  70. package/types/src/extensions/Placeholder/localisation/translation.d.ts +0 -51
  71. /package/src/extensions/{DraggableBlocks → SideMenu}/MultipleNodeSelection.ts +0 -0
@@ -1,27 +1,22 @@
1
- import { Editor, getMarkRange, posToDOMRect, Range } from "@tiptap/core";
1
+ import { getMarkRange, posToDOMRect, Range } from "@tiptap/core";
2
+ import { EditorView } from "@tiptap/pm/view";
2
3
  import { Mark } from "prosemirror-model";
3
4
  import { Plugin, PluginKey } from "prosemirror-state";
4
- import {
5
- HyperlinkToolbar,
6
- HyperlinkToolbarDynamicParams,
7
- HyperlinkToolbarFactory,
8
- HyperlinkToolbarStaticParams,
9
- } from "./HyperlinkToolbarFactoryTypes";
10
- const PLUGIN_KEY = new PluginKey("HyperlinkToolbarPlugin");
11
-
12
- export type HyperlinkToolbarPluginProps = {
13
- hyperlinkToolbarFactory: HyperlinkToolbarFactory;
5
+ import { BlockNoteEditor } from "../../BlockNoteEditor";
6
+ import { BaseUiElementState } from "../../shared/BaseUiElementTypes";
7
+ import { EventEmitter } from "../../shared/EventEmitter";
8
+ import { BlockSchema } from "../Blocks/api/blockTypes";
9
+
10
+ export type HyperlinkToolbarState = BaseUiElementState & {
11
+ // The hovered hyperlink's URL, and the text it's displayed with in the
12
+ // editor.
13
+ url: string;
14
+ text: string;
14
15
  };
15
16
 
16
- export type HyperlinkToolbarViewProps = {
17
- editor: Editor;
18
- hyperlinkToolbarFactory: HyperlinkToolbarFactory;
19
- };
20
-
21
- class HyperlinkToolbarView {
22
- editor: Editor;
23
-
24
- hyperlinkToolbar: HyperlinkToolbar;
17
+ class HyperlinkToolbarView<BSchema extends BlockSchema> {
18
+ private hyperlinkToolbarState?: HyperlinkToolbarState;
19
+ public updateHyperlinkToolbar: () => void;
25
20
 
26
21
  menuUpdateTimer: NodeJS.Timeout | undefined;
27
22
  startMenuUpdateTimer: () => void;
@@ -36,10 +31,20 @@ class HyperlinkToolbarView {
36
31
  hyperlinkMark: Mark | undefined;
37
32
  hyperlinkMarkRange: Range | undefined;
38
33
 
39
- constructor({ editor, hyperlinkToolbarFactory }: HyperlinkToolbarViewProps) {
40
- this.editor = editor;
34
+ constructor(
35
+ private readonly editor: BlockNoteEditor<BSchema>,
36
+ private readonly pmView: EditorView,
37
+ updateHyperlinkToolbar: (
38
+ hyperlinkToolbarState: HyperlinkToolbarState
39
+ ) => void
40
+ ) {
41
+ this.updateHyperlinkToolbar = () => {
42
+ if (!this.hyperlinkToolbarState) {
43
+ throw new Error("Attempting to update uninitialized hyperlink toolbar");
44
+ }
41
45
 
42
- this.hyperlinkToolbar = hyperlinkToolbarFactory(this.getStaticParams());
46
+ updateHyperlinkToolbar(this.hyperlinkToolbarState);
47
+ };
43
48
 
44
49
  this.startMenuUpdateTimer = () => {
45
50
  this.menuUpdateTimer = setTimeout(() => {
@@ -56,7 +61,7 @@ class HyperlinkToolbarView {
56
61
  return false;
57
62
  };
58
63
 
59
- this.editor.view.dom.addEventListener("mouseover", this.mouseOverHandler);
64
+ this.pmView.dom.addEventListener("mouseover", this.mouseOverHandler);
60
65
  document.addEventListener("click", this.clickHandler, true);
61
66
  document.addEventListener("scroll", this.scrollHandler);
62
67
  }
@@ -76,14 +81,16 @@ class HyperlinkToolbarView {
76
81
  // mouseHoveredHyperlinkMarkRange.
77
82
  const hoveredHyperlinkElement = event.target;
78
83
  const posInHoveredHyperlinkMark =
79
- this.editor.view.posAtDOM(hoveredHyperlinkElement, 0) + 1;
80
- const resolvedPosInHoveredHyperlinkMark = this.editor.state.doc.resolve(
84
+ this.pmView.posAtDOM(hoveredHyperlinkElement, 0) + 1;
85
+ const resolvedPosInHoveredHyperlinkMark = this.pmView.state.doc.resolve(
81
86
  posInHoveredHyperlinkMark
82
87
  );
83
88
  const marksAtPos = resolvedPosInHoveredHyperlinkMark.marks();
84
89
 
85
90
  for (const mark of marksAtPos) {
86
- if (mark.type.name === this.editor.schema.mark("link").type.name) {
91
+ if (
92
+ mark.type.name === this.pmView.state.schema.mark("link").type.name
93
+ ) {
87
94
  this.mouseHoveredHyperlinkMark = mark;
88
95
  this.mouseHoveredHyperlinkMarkRange =
89
96
  getMarkRange(
@@ -103,31 +110,80 @@ class HyperlinkToolbarView {
103
110
  };
104
111
 
105
112
  clickHandler = (event: MouseEvent) => {
113
+ const editorWrapper = this.pmView.dom.parentElement!;
114
+
106
115
  if (
107
116
  // Toolbar is open.
108
117
  this.hyperlinkMark &&
109
118
  // An element is clicked.
110
119
  event &&
111
120
  event.target &&
112
- // Element is outside the editor.
113
- this.editor.view.dom !== (event.target as Node) &&
114
- !this.editor.view.dom.contains(event.target as Node) &&
115
- // Element is outside the toolbar.
116
- this.hyperlinkToolbar.element !== (event.target as Node) &&
117
- !this.hyperlinkToolbar.element?.contains(event.target as Node)
121
+ // The clicked element is not the editor.
122
+ !(
123
+ editorWrapper === (event.target as Node) ||
124
+ editorWrapper.contains(event.target as Node)
125
+ )
118
126
  ) {
119
- this.hyperlinkToolbar.hide();
127
+ if (this.hyperlinkToolbarState?.show) {
128
+ this.hyperlinkToolbarState.show = false;
129
+ this.updateHyperlinkToolbar();
130
+ }
120
131
  }
121
132
  };
122
133
 
123
134
  scrollHandler = () => {
124
135
  if (this.hyperlinkMark !== undefined) {
125
- this.hyperlinkToolbar.render(this.getDynamicParams(), false);
136
+ if (this.hyperlinkToolbarState?.show) {
137
+ this.hyperlinkToolbarState.referencePos = posToDOMRect(
138
+ this.pmView,
139
+ this.hyperlinkMarkRange!.from,
140
+ this.hyperlinkMarkRange!.to
141
+ );
142
+ this.updateHyperlinkToolbar();
143
+ }
126
144
  }
127
145
  };
128
146
 
147
+ editHyperlink(url: string, text: string) {
148
+ const tr = this.pmView.state.tr.insertText(
149
+ text,
150
+ this.hyperlinkMarkRange!.from,
151
+ this.hyperlinkMarkRange!.to
152
+ );
153
+ tr.addMark(
154
+ this.hyperlinkMarkRange!.from,
155
+ this.hyperlinkMarkRange!.from + text.length,
156
+ this.pmView.state.schema.mark("link", { href: url })
157
+ );
158
+ this.pmView.dispatch(tr);
159
+ this.pmView.focus();
160
+
161
+ if (this.hyperlinkToolbarState?.show) {
162
+ this.hyperlinkToolbarState.show = false;
163
+ this.updateHyperlinkToolbar();
164
+ }
165
+ }
166
+
167
+ deleteHyperlink() {
168
+ this.pmView.dispatch(
169
+ this.pmView.state.tr
170
+ .removeMark(
171
+ this.hyperlinkMarkRange!.from,
172
+ this.hyperlinkMarkRange!.to,
173
+ this.hyperlinkMark!.type
174
+ )
175
+ .setMeta("preventAutolink", true)
176
+ );
177
+ this.pmView.focus();
178
+
179
+ if (this.hyperlinkToolbarState?.show) {
180
+ this.hyperlinkToolbarState.show = false;
181
+ this.updateHyperlinkToolbar();
182
+ }
183
+ }
184
+
129
185
  update() {
130
- if (!this.editor.view.hasFocus()) {
186
+ if (!this.pmView.hasFocus()) {
131
187
  return;
132
188
  }
133
189
 
@@ -144,15 +200,17 @@ class HyperlinkToolbarView {
144
200
 
145
201
  // Finds link mark at the editor selection's position to update keyboardHoveredHyperlinkMark and
146
202
  // keyboardHoveredHyperlinkMarkRange.
147
- if (this.editor.state.selection.empty) {
148
- const marksAtPos = this.editor.state.selection.$from.marks();
203
+ if (this.pmView.state.selection.empty) {
204
+ const marksAtPos = this.pmView.state.selection.$from.marks();
149
205
 
150
206
  for (const mark of marksAtPos) {
151
- if (mark.type.name === this.editor.schema.mark("link").type.name) {
207
+ if (
208
+ mark.type.name === this.pmView.state.schema.mark("link").type.name
209
+ ) {
152
210
  this.keyboardHoveredHyperlinkMark = mark;
153
211
  this.keyboardHoveredHyperlinkMarkRange =
154
212
  getMarkRange(
155
- this.editor.state.selection.$from,
213
+ this.pmView.state.selection.$from,
156
214
  mark.type,
157
215
  mark.attrs
158
216
  ) || undefined;
@@ -174,116 +232,102 @@ class HyperlinkToolbarView {
174
232
  }
175
233
 
176
234
  if (this.hyperlinkMark && this.editor.isEditable) {
177
- this.getDynamicParams();
178
-
179
- // Shows menu.
180
- if (!prevHyperlinkMark) {
181
- this.hyperlinkToolbar.render(this.getDynamicParams(), true);
182
-
183
- this.hyperlinkToolbar.element?.addEventListener(
184
- "mouseleave",
185
- this.startMenuUpdateTimer
186
- );
187
- this.hyperlinkToolbar.element?.addEventListener(
188
- "mouseenter",
189
- this.stopMenuUpdateTimer
190
- );
191
-
192
- return;
193
- }
194
-
195
- // Updates menu.
196
- this.hyperlinkToolbar.render(this.getDynamicParams(), false);
235
+ this.hyperlinkToolbarState = {
236
+ show: true,
237
+ referencePos: posToDOMRect(
238
+ this.pmView,
239
+ this.hyperlinkMarkRange!.from,
240
+ this.hyperlinkMarkRange!.to
241
+ ),
242
+ url: this.hyperlinkMark!.attrs.href,
243
+ text: this.pmView.state.doc.textBetween(
244
+ this.hyperlinkMarkRange!.from,
245
+ this.hyperlinkMarkRange!.to
246
+ ),
247
+ };
248
+ this.updateHyperlinkToolbar();
197
249
 
198
250
  return;
199
251
  }
200
252
 
201
253
  // Hides menu.
202
- if (prevHyperlinkMark && (!this.hyperlinkMark || !this.editor.isEditable)) {
203
- this.hyperlinkToolbar.element?.removeEventListener(
204
- "mouseleave",
205
- this.startMenuUpdateTimer
206
- );
207
- this.hyperlinkToolbar.element?.removeEventListener(
208
- "mouseenter",
209
- this.stopMenuUpdateTimer
210
- );
211
-
212
- this.hyperlinkToolbar.hide();
254
+ if (
255
+ this.hyperlinkToolbarState?.show &&
256
+ prevHyperlinkMark &&
257
+ (!this.hyperlinkMark || !this.editor.isEditable)
258
+ ) {
259
+ this.hyperlinkToolbarState.show = false;
260
+ this.updateHyperlinkToolbar();
213
261
 
214
262
  return;
215
263
  }
216
264
  }
217
265
 
218
266
  destroy() {
219
- this.editor.view.dom.removeEventListener(
220
- "mouseover",
221
- this.mouseOverHandler
222
- );
267
+ this.pmView.dom.removeEventListener("mouseover", this.mouseOverHandler);
223
268
  document.removeEventListener("scroll", this.scrollHandler);
269
+ document.removeEventListener("click", this.clickHandler, true);
224
270
  }
271
+ }
225
272
 
226
- getStaticParams(): HyperlinkToolbarStaticParams {
227
- return {
228
- editHyperlink: (url: string, text: string) => {
229
- const tr = this.editor.view.state.tr.insertText(
230
- text,
231
- this.hyperlinkMarkRange!.from,
232
- this.hyperlinkMarkRange!.to
233
- );
234
- tr.addMark(
235
- this.hyperlinkMarkRange!.from,
236
- this.hyperlinkMarkRange!.from + text.length,
237
- this.editor.schema.mark("link", { href: url })
238
- );
239
- this.editor.view.dispatch(tr);
240
- this.editor.view.focus();
241
-
242
- this.hyperlinkToolbar.hide();
243
- },
244
- deleteHyperlink: () => {
245
- this.editor.view.dispatch(
246
- this.editor.view.state.tr
247
- .removeMark(
248
- this.hyperlinkMarkRange!.from,
249
- this.hyperlinkMarkRange!.to,
250
- this.hyperlinkMark!.type
251
- )
252
- .setMeta("preventAutolink", true)
253
- );
254
- this.editor.view.focus();
255
-
256
- this.hyperlinkToolbar.hide();
273
+ export const hyperlinkToolbarPluginKey = new PluginKey(
274
+ "HyperlinkToolbarPlugin"
275
+ );
276
+
277
+ export class HyperlinkToolbarProsemirrorPlugin<
278
+ BSchema extends BlockSchema
279
+ > extends EventEmitter<any> {
280
+ private view: HyperlinkToolbarView<BSchema> | undefined;
281
+ public readonly plugin: Plugin;
282
+
283
+ constructor(editor: BlockNoteEditor<BSchema>) {
284
+ super();
285
+ this.plugin = new Plugin({
286
+ key: hyperlinkToolbarPluginKey,
287
+ view: (editorView) => {
288
+ this.view = new HyperlinkToolbarView(editor, editorView, (state) => {
289
+ this.emit("update", state);
290
+ });
291
+ return this.view;
257
292
  },
258
- };
293
+ });
259
294
  }
260
295
 
261
- getDynamicParams(): HyperlinkToolbarDynamicParams {
262
- return {
263
- url: this.hyperlinkMark!.attrs.href,
264
- text: this.editor.view.state.doc.textBetween(
265
- this.hyperlinkMarkRange!.from,
266
- this.hyperlinkMarkRange!.to
267
- ),
268
- referenceRect: posToDOMRect(
269
- this.editor.view,
270
- this.hyperlinkMarkRange!.from,
271
- this.hyperlinkMarkRange!.to
272
- ),
273
- };
296
+ public onUpdate(callback: (state: HyperlinkToolbarState) => void) {
297
+ return this.on("update", callback);
274
298
  }
275
- }
276
299
 
277
- export const createHyperlinkToolbarPlugin = (
278
- editor: Editor,
279
- options: HyperlinkToolbarPluginProps
280
- ) => {
281
- return new Plugin({
282
- key: PLUGIN_KEY,
283
- view: () =>
284
- new HyperlinkToolbarView({
285
- editor: editor,
286
- hyperlinkToolbarFactory: options.hyperlinkToolbarFactory,
287
- }),
288
- });
289
- };
300
+ /**
301
+ * Edit the currently hovered hyperlink.
302
+ */
303
+ public editHyperlink = (url: string, text: string) => {
304
+ this.view!.editHyperlink(url, text);
305
+ };
306
+
307
+ /**
308
+ * Delete the currently hovered hyperlink.
309
+ */
310
+ public deleteHyperlink = () => {
311
+ this.view!.deleteHyperlink();
312
+ };
313
+
314
+ /**
315
+ * When hovering on/off hyperlinks using the mouse cursor, the hyperlink
316
+ * toolbar will open & close with a delay.
317
+ *
318
+ * This function starts the delay timer, and should be used for when the mouse cursor enters the hyperlink toolbar.
319
+ */
320
+ public startHideTimer = () => {
321
+ this.view!.startMenuUpdateTimer();
322
+ };
323
+
324
+ /**
325
+ * When hovering on/off hyperlinks using the mouse cursor, the hyperlink
326
+ * toolbar will open & close with a delay.
327
+ *
328
+ * This function stops the delay timer, and should be used for when the mouse cursor exits the hyperlink toolbar.
329
+ */
330
+ public stopHideTimer = () => {
331
+ this.view!.stopMenuUpdateTimer();
332
+ };
333
+ }
@@ -2,7 +2,7 @@ import { Editor, Extension } from "@tiptap/core";
2
2
  import { Node as ProsemirrorNode } from "prosemirror-model";
3
3
  import { Plugin, PluginKey } from "prosemirror-state";
4
4
  import { Decoration, DecorationSet } from "prosemirror-view";
5
- import { SlashMenuPluginKey } from "../SlashMenu/SlashMenuExtension";
5
+ import { slashMenuPluginKey } from "../SlashMenu/SlashMenuPlugin";
6
6
 
7
7
  const PLUGIN_KEY = new PluginKey(`blocknote-placeholder`);
8
8
 
@@ -55,7 +55,7 @@ export const Placeholder = Extension.create<PlaceholderOptions>({
55
55
  decorations: (state) => {
56
56
  const { doc, selection } = state;
57
57
  // Get state of slash menu
58
- const menuState = SlashMenuPluginKey.getState(state);
58
+ const menuState = slashMenuPluginKey.getState(state);
59
59
  const active =
60
60
  this.editor.isEditable || !this.options.showOnlyWhenEditable;
61
61
  const { anchor } = selection;