@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,27 @@
1
- import { Editor } from "@tiptap/core";
1
+ import { PluginView } from "@tiptap/pm/state";
2
2
  import { Node } from "prosemirror-model";
3
3
  import { NodeSelection, Plugin, PluginKey, Selection } from "prosemirror-state";
4
4
  import * as pv from "prosemirror-view";
5
5
  import { EditorView } from "prosemirror-view";
6
+ import { BlockNoteEditor } from "../../BlockNoteEditor";
6
7
  import styles from "../../editor.module.css";
8
+ import { BaseUiElementState } from "../../shared/BaseUiElementTypes";
9
+ import { EventEmitter } from "../../shared/EventEmitter";
10
+ import { Block, BlockSchema } from "../Blocks/api/blockTypes";
7
11
  import { getBlockInfoFromPos } from "../Blocks/helpers/getBlockInfoFromPos";
8
- import { SlashMenuPluginKey } from "../SlashMenu/SlashMenuExtension";
9
- import {
10
- BlockSideMenu,
11
- BlockSideMenuDynamicParams,
12
- BlockSideMenuFactory,
13
- BlockSideMenuStaticParams,
14
- } from "./BlockSideMenuFactoryTypes";
15
- import { DraggableBlocksOptions } from "./DraggableBlocksExtension";
12
+ import { slashMenuPluginKey } from "../SlashMenu/SlashMenuPlugin";
16
13
  import { MultipleNodeSelection } from "./MultipleNodeSelection";
17
- import { BlockNoteEditor } from "../../BlockNoteEditor";
18
- import { BlockSchema } from "../Blocks/api/blockTypes";
19
14
 
20
15
  const serializeForClipboard = (pv as any).__serializeForClipboard;
21
16
  // code based on https://github.com/ueberdosis/tiptap/issues/323#issuecomment-506637799
22
17
 
23
18
  let dragImageElement: Element | undefined;
24
19
 
20
+ export type SideMenuState<BSchema extends BlockSchema> = BaseUiElementState & {
21
+ // The block that the side menu is attached to.
22
+ block: Block<BSchema>;
23
+ };
24
+
25
25
  function getDraggableBlockFromCoords(
26
26
  coords: { left: number; top: number },
27
27
  view: EditorView
@@ -176,7 +176,10 @@ function unsetDragImage() {
176
176
  }
177
177
  }
178
178
 
179
- function dragStart(e: DragEvent, view: EditorView) {
179
+ function dragStart(
180
+ e: { dataTransfer: DataTransfer | null; clientY: number },
181
+ view: EditorView
182
+ ) {
180
183
  if (!e.dataTransfer) {
181
184
  return;
182
185
  }
@@ -224,50 +227,37 @@ function dragStart(e: DragEvent, view: EditorView) {
224
227
  }
225
228
  }
226
229
 
227
- export type BlockMenuViewProps<BSchema extends BlockSchema> = {
228
- tiptapEditor: Editor;
229
- editor: BlockNoteEditor<BSchema>;
230
- blockMenuFactory: BlockSideMenuFactory<BSchema>;
231
- horizontalPosAnchoredAtRoot: boolean;
232
- };
233
-
234
- export class BlockMenuView<BSchema extends BlockSchema> {
235
- editor: BlockNoteEditor<BSchema>;
236
- private ttEditor: Editor;
230
+ export class SideMenuView<BSchema extends BlockSchema> implements PluginView {
231
+ private sideMenuState?: SideMenuState<BSchema>;
237
232
 
238
233
  // When true, the drag handle with be anchored at the same level as root elements
239
234
  // When false, the drag handle with be just to the left of the element
240
- horizontalPosAnchoredAtRoot: boolean;
235
+ // TODO: Is there any case where we want this to be false?
236
+ private horizontalPosAnchoredAtRoot: boolean;
237
+ private horizontalPosAnchor: number;
241
238
 
242
- horizontalPosAnchor: number;
239
+ private hoveredBlock: HTMLElement | undefined;
243
240
 
244
- blockMenu: BlockSideMenu<BSchema>;
241
+ // Used to check if currently dragged content comes from this editor instance.
242
+ public isDragging = false;
245
243
 
246
- hoveredBlock: HTMLElement | undefined;
244
+ public menuFrozen = false;
247
245
 
248
- // Used to check if currently dragged content comes from this editor instance.
249
- isDragging = false;
250
- menuOpen = false;
251
- menuFrozen = false;
252
-
253
- constructor({
254
- tiptapEditor,
255
- editor,
256
- blockMenuFactory,
257
- horizontalPosAnchoredAtRoot,
258
- }: BlockMenuViewProps<BSchema>) {
259
- this.editor = editor;
260
- this.ttEditor = tiptapEditor;
261
- this.horizontalPosAnchoredAtRoot = horizontalPosAnchoredAtRoot;
246
+ constructor(
247
+ private readonly editor: BlockNoteEditor<BSchema>,
248
+ private readonly pmView: EditorView,
249
+ private readonly updateSideMenu: (
250
+ sideMenuState: SideMenuState<BSchema>
251
+ ) => void
252
+ ) {
253
+ this.horizontalPosAnchoredAtRoot = true;
262
254
  this.horizontalPosAnchor = (
263
- this.ttEditor.view.dom.firstChild! as HTMLElement
255
+ this.pmView.dom.firstChild! as HTMLElement
264
256
  ).getBoundingClientRect().x;
265
257
 
266
- this.blockMenu = blockMenuFactory(this.getStaticParams());
267
-
268
258
  document.body.addEventListener("drop", this.onDrop, true);
269
259
  document.body.addEventListener("dragover", this.onDragOver);
270
- this.ttEditor.view.dom.addEventListener("dragstart", this.onDragStart);
260
+ this.pmView.dom.addEventListener("dragstart", this.onDragStart);
271
261
 
272
262
  // Shows or updates menu position whenever the cursor moves, if the menu isn't frozen.
273
263
  document.body.addEventListener("mousemove", this.onMouseMove, true);
@@ -275,9 +265,7 @@ export class BlockMenuView<BSchema extends BlockSchema> {
275
265
  // Makes menu scroll with the page.
276
266
  document.addEventListener("scroll", this.onScroll);
277
267
 
278
- // Hides and unfreezes the menu whenever the user selects the editor with the mouse or presses a key.
279
- // TODO: Better integration with suggestions menu and only editor scope?
280
- document.body.addEventListener("mousedown", this.onMouseDown, true);
268
+ // Hides and unfreezes the menu whenever the user presses a key.
281
269
  document.body.addEventListener("keydown", this.onKeyDown, true);
282
270
  }
283
271
 
@@ -294,10 +282,13 @@ export class BlockMenuView<BSchema extends BlockSchema> {
294
282
  * when dragging / dropping to the side of the editor
295
283
  */
296
284
  onDrop = (event: DragEvent) => {
285
+ this.editor._tiptapEditor.commands.blur();
286
+
297
287
  if ((event as any).synthetic || !this.isDragging) {
298
288
  return;
299
289
  }
300
- let pos = this.ttEditor.view.posAtCoords({
290
+
291
+ let pos = this.pmView.posAtCoords({
301
292
  left: event.clientX,
302
293
  top: event.clientY,
303
294
  });
@@ -307,7 +298,7 @@ export class BlockMenuView<BSchema extends BlockSchema> {
307
298
  if (!pos || pos.inside === -1) {
308
299
  const evt = new Event("drop", event) as any;
309
300
  const editorBoundingBox = (
310
- this.ttEditor.view.dom.firstChild! as HTMLElement
301
+ this.pmView.dom.firstChild! as HTMLElement
311
302
  ).getBoundingClientRect();
312
303
  evt.clientX = editorBoundingBox.left + editorBoundingBox.width / 2;
313
304
  evt.clientY = event.clientY;
@@ -315,7 +306,7 @@ export class BlockMenuView<BSchema extends BlockSchema> {
315
306
  evt.preventDefault = () => event.preventDefault();
316
307
  evt.synthetic = true; // prevent recursion
317
308
  // console.log("dispatch fake drop");
318
- this.ttEditor.view.dom.dispatchEvent(evt);
309
+ this.pmView.dom.dispatchEvent(evt);
319
310
  }
320
311
  };
321
312
 
@@ -328,7 +319,7 @@ export class BlockMenuView<BSchema extends BlockSchema> {
328
319
  if ((event as any).synthetic || !this.isDragging) {
329
320
  return;
330
321
  }
331
- let pos = this.ttEditor.view.posAtCoords({
322
+ let pos = this.pmView.posAtCoords({
332
323
  left: event.clientX,
333
324
  top: event.clientY,
334
325
  });
@@ -336,7 +327,7 @@ export class BlockMenuView<BSchema extends BlockSchema> {
336
327
  if (!pos || pos.inside === -1) {
337
328
  const evt = new Event("dragover", event) as any;
338
329
  const editorBoundingBox = (
339
- this.ttEditor.view.dom.firstChild! as HTMLElement
330
+ this.pmView.dom.firstChild! as HTMLElement
340
331
  ).getBoundingClientRect();
341
332
  evt.clientX = editorBoundingBox.left + editorBoundingBox.width / 2;
342
333
  evt.clientY = event.clientY;
@@ -344,29 +335,15 @@ export class BlockMenuView<BSchema extends BlockSchema> {
344
335
  evt.preventDefault = () => event.preventDefault();
345
336
  evt.synthetic = true; // prevent recursion
346
337
  // console.log("dispatch fake dragover");
347
- this.ttEditor.view.dom.dispatchEvent(evt);
338
+ this.pmView.dom.dispatchEvent(evt);
348
339
  }
349
340
  };
350
341
 
351
342
  onKeyDown = (_event: KeyboardEvent) => {
352
- if (this.menuOpen) {
353
- this.menuOpen = false;
354
- this.blockMenu.hide();
343
+ if (this.sideMenuState?.show) {
344
+ this.sideMenuState.show = false;
345
+ this.updateSideMenu(this.sideMenuState);
355
346
  }
356
-
357
- this.menuFrozen = false;
358
- };
359
-
360
- onMouseDown = (event: MouseEvent) => {
361
- if (this.blockMenu.element?.contains(event.target as HTMLElement)) {
362
- return;
363
- }
364
-
365
- if (this.menuOpen) {
366
- this.menuOpen = false;
367
- this.blockMenu.hide();
368
- }
369
-
370
347
  this.menuFrozen = false;
371
348
  };
372
349
 
@@ -380,18 +357,19 @@ export class BlockMenuView<BSchema extends BlockSchema> {
380
357
  // blockGroup that wraps all blocks in the editor) for more accurate side
381
358
  // menu placement.
382
359
  const editorBoundingBox = (
383
- this.ttEditor.view.dom.firstChild! as HTMLElement
360
+ this.pmView.dom.firstChild! as HTMLElement
384
361
  ).getBoundingClientRect();
385
362
  // We want the full area of the editor to check if the cursor is hovering
386
363
  // above it though.
387
- const editorOuterBoundingBox =
388
- this.ttEditor.view.dom.getBoundingClientRect();
364
+ const editorOuterBoundingBox = this.pmView.dom.getBoundingClientRect();
389
365
  const cursorWithinEditor =
390
366
  event.clientX >= editorOuterBoundingBox.left &&
391
367
  event.clientX <= editorOuterBoundingBox.right &&
392
368
  event.clientY >= editorOuterBoundingBox.top &&
393
369
  event.clientY <= editorOuterBoundingBox.bottom;
394
370
 
371
+ const editorWrapper = this.pmView.dom.parentElement!;
372
+
395
373
  // Doesn't update if the mouse hovers an element that's over the editor but
396
374
  // isn't a part of it or the side menu.
397
375
  if (
@@ -401,15 +379,14 @@ export class BlockMenuView<BSchema extends BlockSchema> {
401
379
  event &&
402
380
  event.target &&
403
381
  // Element is outside the editor
404
- this.ttEditor.view.dom !== event.target &&
405
- !this.ttEditor.view.dom.contains(event.target as HTMLElement) &&
406
- // Element is outside the side menu
407
- this.blockMenu.element !== event.target &&
408
- !this.blockMenu.element?.contains(event.target as HTMLElement)
382
+ !(
383
+ editorWrapper === event.target ||
384
+ editorWrapper.contains(event.target as HTMLElement)
385
+ )
409
386
  ) {
410
- if (this.menuOpen) {
411
- this.menuOpen = false;
412
- this.blockMenu.hide();
387
+ if (this.sideMenuState?.show) {
388
+ this.sideMenuState.show = false;
389
+ this.updateSideMenu(this.sideMenuState);
413
390
  }
414
391
 
415
392
  return;
@@ -422,13 +399,13 @@ export class BlockMenuView<BSchema extends BlockSchema> {
422
399
  left: editorBoundingBox.left + editorBoundingBox.width / 2, // take middle of editor
423
400
  top: event.clientY,
424
401
  };
425
- const block = getDraggableBlockFromCoords(coords, this.ttEditor.view);
402
+ const block = getDraggableBlockFromCoords(coords, this.pmView);
426
403
 
427
404
  // Closes the menu if the mouse cursor is beyond the editor vertically.
428
405
  if (!block || !this.editor.isEditable) {
429
- if (this.menuOpen) {
430
- this.menuOpen = false;
431
- this.blockMenu.hide();
406
+ if (this.sideMenuState?.show) {
407
+ this.sideMenuState.show = false;
408
+ this.updateSideMenu(this.sideMenuState);
432
409
  }
433
410
 
434
411
  return;
@@ -436,7 +413,7 @@ export class BlockMenuView<BSchema extends BlockSchema> {
436
413
 
437
414
  // Doesn't update if the menu is already open and the mouse cursor is still hovering the same block.
438
415
  if (
439
- this.menuOpen &&
416
+ this.sideMenuState?.show &&
440
417
  this.hoveredBlock?.hasAttribute("data-id") &&
441
418
  this.hoveredBlock?.getAttribute("data-id") === block.id
442
419
  ) {
@@ -454,52 +431,69 @@ export class BlockMenuView<BSchema extends BlockSchema> {
454
431
 
455
432
  // Shows or updates elements.
456
433
  if (this.editor.isEditable) {
457
- if (!this.menuOpen) {
458
- this.menuOpen = true;
459
- this.blockMenu.render(this.getDynamicParams(), true);
460
- } else {
461
- this.blockMenu.render(this.getDynamicParams(), false);
462
- }
434
+ const blockContentBoundingBox = blockContent.getBoundingClientRect();
435
+
436
+ this.sideMenuState = {
437
+ show: true,
438
+ referencePos: new DOMRect(
439
+ this.horizontalPosAnchoredAtRoot
440
+ ? this.horizontalPosAnchor
441
+ : blockContentBoundingBox.x,
442
+ blockContentBoundingBox.y,
443
+ blockContentBoundingBox.width,
444
+ blockContentBoundingBox.height
445
+ ),
446
+ block: this.editor.getBlock(
447
+ this.hoveredBlock!.getAttribute("data-id")!
448
+ )!,
449
+ };
450
+
451
+ this.updateSideMenu(this.sideMenuState);
463
452
  }
464
453
  };
465
454
 
466
455
  onScroll = () => {
467
- // Editor itself may have padding or other styling which affects size/position, so we get the boundingRect of
468
- // the first child (i.e. the blockGroup that wraps all blocks in the editor) for a more accurate bounding box.
469
- const editorBoundingBox = (
470
- this.ttEditor.view.dom.firstChild! as HTMLElement
471
- ).getBoundingClientRect();
456
+ if (this.sideMenuState?.show) {
457
+ const blockContent = this.hoveredBlock!.firstChild as HTMLElement;
458
+ const blockContentBoundingBox = blockContent.getBoundingClientRect();
472
459
 
473
- this.horizontalPosAnchor = editorBoundingBox.x;
474
-
475
- if (this.menuOpen) {
476
- this.blockMenu.render(this.getDynamicParams(), false);
460
+ this.sideMenuState.referencePos = new DOMRect(
461
+ this.horizontalPosAnchoredAtRoot
462
+ ? this.horizontalPosAnchor
463
+ : blockContentBoundingBox.x,
464
+ blockContentBoundingBox.y,
465
+ blockContentBoundingBox.width,
466
+ blockContentBoundingBox.height
467
+ );
468
+ this.updateSideMenu(this.sideMenuState);
477
469
  }
478
470
  };
479
471
 
480
472
  destroy() {
481
- if (this.menuOpen) {
482
- this.menuOpen = false;
483
- this.blockMenu.hide();
473
+ if (this.sideMenuState?.show) {
474
+ this.sideMenuState.show = false;
475
+ this.updateSideMenu(this.sideMenuState);
484
476
  }
485
477
  document.body.removeEventListener("mousemove", this.onMouseMove);
486
478
  document.body.removeEventListener("dragover", this.onDragOver);
487
- this.ttEditor.view.dom.removeEventListener("dragstart", this.onDragStart);
488
- document.body.removeEventListener("drop", this.onDrop);
489
- document.body.removeEventListener("mousedown", this.onMouseDown);
479
+ this.pmView.dom.removeEventListener("dragstart", this.onDragStart);
480
+ document.body.removeEventListener("drop", this.onDrop, true);
490
481
  document.removeEventListener("scroll", this.onScroll);
491
- document.body.removeEventListener("keydown", this.onKeyDown);
482
+ document.body.removeEventListener("keydown", this.onKeyDown, true);
492
483
  }
493
484
 
494
485
  addBlock() {
495
- this.menuOpen = false;
486
+ if (this.sideMenuState?.show) {
487
+ this.sideMenuState.show = false;
488
+ this.updateSideMenu(this.sideMenuState);
489
+ }
490
+
496
491
  this.menuFrozen = true;
497
- this.blockMenu.hide();
498
492
 
499
493
  const blockContent = this.hoveredBlock!.firstChild! as HTMLElement;
500
494
  const blockContentBoundingBox = blockContent.getBoundingClientRect();
501
495
 
502
- const pos = this.ttEditor.view.posAtCoords({
496
+ const pos = this.pmView.posAtCoords({
503
497
  left: blockContentBoundingBox.left + blockContentBoundingBox.width / 2,
504
498
  top: blockContentBoundingBox.top + blockContentBoundingBox.height / 2,
505
499
  });
@@ -507,7 +501,10 @@ export class BlockMenuView<BSchema extends BlockSchema> {
507
501
  return;
508
502
  }
509
503
 
510
- const blockInfo = getBlockInfoFromPos(this.ttEditor.state.doc, pos.pos);
504
+ const blockInfo = getBlockInfoFromPos(
505
+ this.editor._tiptapEditor.state.doc,
506
+ pos.pos
507
+ );
511
508
  if (blockInfo === undefined) {
512
509
  return;
513
510
  }
@@ -519,75 +516,88 @@ export class BlockMenuView<BSchema extends BlockSchema> {
519
516
  const newBlockInsertionPos = endPos + 1;
520
517
  const newBlockContentPos = newBlockInsertionPos + 2;
521
518
 
522
- this.ttEditor
519
+ this.editor._tiptapEditor
523
520
  .chain()
524
521
  .BNCreateBlock(newBlockInsertionPos)
525
522
  .BNUpdateBlock(newBlockContentPos, { type: "paragraph", props: {} })
526
523
  .setTextSelection(newBlockContentPos)
527
524
  .run();
528
525
  } else {
529
- this.ttEditor.commands.setTextSelection(endPos);
526
+ this.editor._tiptapEditor.commands.setTextSelection(endPos);
530
527
  }
531
528
 
532
529
  // Focuses and activates the suggestion menu.
533
- this.ttEditor.view.focus();
534
- this.ttEditor.view.dispatch(
535
- this.ttEditor.view.state.tr.scrollIntoView().setMeta(SlashMenuPluginKey, {
530
+ this.pmView.focus();
531
+ this.pmView.dispatch(
532
+ this.pmView.state.tr.scrollIntoView().setMeta(slashMenuPluginKey, {
536
533
  // TODO import suggestion plugin key
537
534
  activate: true,
538
535
  type: "drag",
539
536
  })
540
537
  );
541
538
  }
539
+ }
542
540
 
543
- getStaticParams(): BlockSideMenuStaticParams<BSchema> {
544
- return {
545
- editor: this.editor,
546
- addBlock: () => this.addBlock(),
547
- blockDragStart: (event: DragEvent) => {
548
- // Sets isDragging when dragging blocks.
549
- this.isDragging = true;
550
- dragStart(event, this.ttEditor.view);
541
+ export const sideMenuPluginKey = new PluginKey("SideMenuPlugin");
542
+
543
+ export class SideMenuProsemirrorPlugin<
544
+ BSchema extends BlockSchema
545
+ > extends EventEmitter<any> {
546
+ private sideMenuView: SideMenuView<BSchema> | undefined;
547
+ public readonly plugin: Plugin;
548
+
549
+ constructor(private readonly editor: BlockNoteEditor<BSchema>) {
550
+ super();
551
+ this.plugin = new Plugin({
552
+ key: sideMenuPluginKey,
553
+ view: (editorView) => {
554
+ this.sideMenuView = new SideMenuView(
555
+ editor,
556
+ editorView,
557
+ (sideMenuState) => {
558
+ this.emit("update", sideMenuState);
559
+ }
560
+ );
561
+ return this.sideMenuView;
551
562
  },
552
- blockDragEnd: () => unsetDragImage(),
553
- freezeMenu: () => {
554
- this.menuFrozen = true;
555
- },
556
- unfreezeMenu: () => {
557
- this.menuFrozen = false;
558
- },
559
- };
563
+ });
560
564
  }
561
565
 
562
- getDynamicParams(): BlockSideMenuDynamicParams<BSchema> {
563
- const blockContent = this.hoveredBlock!.firstChild! as HTMLElement;
564
- const blockContentBoundingBox = blockContent.getBoundingClientRect();
565
-
566
- return {
567
- block: this.editor.getBlock(this.hoveredBlock!.getAttribute("data-id")!)!,
568
- referenceRect: new DOMRect(
569
- this.horizontalPosAnchoredAtRoot
570
- ? this.horizontalPosAnchor
571
- : blockContentBoundingBox.x,
572
- blockContentBoundingBox.y,
573
- blockContentBoundingBox.width,
574
- blockContentBoundingBox.height
575
- ),
576
- };
566
+ public onUpdate(callback: (state: SideMenuState<BSchema>) => void) {
567
+ return this.on("update", callback);
577
568
  }
578
- }
579
569
 
580
- export const createDraggableBlocksPlugin = <BSchema extends BlockSchema>(
581
- options: DraggableBlocksOptions<BSchema>
582
- ) => {
583
- return new Plugin({
584
- key: new PluginKey("DraggableBlocksPlugin"),
585
- view: () =>
586
- new BlockMenuView({
587
- tiptapEditor: options.tiptapEditor,
588
- editor: options.editor,
589
- blockMenuFactory: options.blockSideMenuFactory,
590
- horizontalPosAnchoredAtRoot: true,
591
- }),
592
- });
593
- };
570
+ /**
571
+ * If the block is empty, opens the slash menu. If the block has content,
572
+ * creates a new block below and opens the slash menu in it.
573
+ */
574
+ addBlock = () => this.sideMenuView!.addBlock();
575
+
576
+ /**
577
+ * Handles drag & drop events for blocks.
578
+ */
579
+ blockDragStart = (event: {
580
+ dataTransfer: DataTransfer | null;
581
+ clientY: number;
582
+ }) => {
583
+ this.sideMenuView!.isDragging = true;
584
+ dragStart(event, this.editor.prosemirrorView);
585
+ };
586
+
587
+ /**
588
+ * Handles drag & drop events for blocks.
589
+ */
590
+ blockDragEnd = () => unsetDragImage();
591
+ /**
592
+ * Freezes the side menu. When frozen, the side menu will stay
593
+ * attached to the same block regardless of which block is hovered by the
594
+ * mouse cursor.
595
+ */
596
+ freezeMenu = () => (this.sideMenuView!.menuFrozen = true);
597
+ /**
598
+ * Unfreezes the side menu. When frozen, the side menu will stay
599
+ * attached to the same block regardless of which block is hovered by the
600
+ * mouse cursor.
601
+ */
602
+ unfreezeMenu = () => (this.sideMenuView!.menuFrozen = false);
603
+ }
@@ -1,34 +1,11 @@
1
1
  import { SuggestionItem } from "../../shared/plugins/suggestion/SuggestionItem";
2
2
  import { BlockNoteEditor } from "../../BlockNoteEditor";
3
3
  import { BlockSchema } from "../Blocks/api/blockTypes";
4
+ import { DefaultBlockSchema } from "../Blocks/api/defaultBlocks";
4
5
 
5
- /**
6
- * A class that defines a slash command (/<command>).
7
- *
8
- * (Not to be confused with ProseMirror commands nor TipTap commands.)
9
- */
10
- export class BaseSlashMenuItem<
11
- BSchema extends BlockSchema
12
- > extends SuggestionItem {
13
- /**
14
- * Constructs a new slash-command.
15
- *
16
- * @param name The name of the command
17
- * @param execute The callback for creating a new node
18
- * @param aliases Aliases for this command
19
- */
20
- constructor(
21
- public readonly name: string,
22
- public readonly execute: (editor: BlockNoteEditor<BSchema>) => void,
23
- public readonly aliases: string[] = []
24
- ) {
25
- super(name, (query: string): boolean => {
26
- return (
27
- this.name.toLowerCase().startsWith(query.toLowerCase()) ||
28
- this.aliases.filter((alias) =>
29
- alias.toLowerCase().startsWith(query.toLowerCase())
30
- ).length !== 0
31
- );
32
- });
33
- }
34
- }
6
+ export type BaseSlashMenuItem<
7
+ BSchema extends BlockSchema = DefaultBlockSchema
8
+ > = SuggestionItem & {
9
+ execute: (editor: BlockNoteEditor<BSchema>) => void;
10
+ aliases?: string[];
11
+ };
@@ -0,0 +1,51 @@
1
+ import { Plugin, PluginKey } from "prosemirror-state";
2
+
3
+ import { BlockNoteEditor } from "../../BlockNoteEditor";
4
+ import { EventEmitter } from "../../shared/EventEmitter";
5
+ import {
6
+ SuggestionsMenuState,
7
+ setupSuggestionsMenu,
8
+ } from "../../shared/plugins/suggestion/SuggestionPlugin";
9
+ import { BlockSchema } from "../Blocks/api/blockTypes";
10
+ import { BaseSlashMenuItem } from "./BaseSlashMenuItem";
11
+
12
+ export const slashMenuPluginKey = new PluginKey("SlashMenuPlugin");
13
+
14
+ export class SlashMenuProsemirrorPlugin<
15
+ BSchema extends BlockSchema,
16
+ SlashMenuItem extends BaseSlashMenuItem<BSchema>
17
+ > extends EventEmitter<any> {
18
+ public readonly plugin: Plugin;
19
+ public readonly itemCallback: (item: SlashMenuItem) => void;
20
+
21
+ constructor(editor: BlockNoteEditor<BSchema>, items: SlashMenuItem[]) {
22
+ super();
23
+ const suggestions = setupSuggestionsMenu<SlashMenuItem, BSchema>(
24
+ editor,
25
+ (state) => {
26
+ this.emit("update", state);
27
+ },
28
+ slashMenuPluginKey,
29
+ "/",
30
+ (query) =>
31
+ items.filter(
32
+ ({ name, aliases }: SlashMenuItem) =>
33
+ name.toLowerCase().startsWith(query.toLowerCase()) ||
34
+ (aliases &&
35
+ aliases.filter((alias) =>
36
+ alias.toLowerCase().startsWith(query.toLowerCase())
37
+ ).length !== 0)
38
+ ),
39
+ ({ item, editor }) => item.execute(editor)
40
+ );
41
+
42
+ this.plugin = suggestions.plugin;
43
+ this.itemCallback = suggestions.itemCallback;
44
+ }
45
+
46
+ public onUpdate(
47
+ callback: (state: SuggestionsMenuState<SlashMenuItem>) => void
48
+ ) {
49
+ return this.on("update", callback);
50
+ }
51
+ }