@blocknote/core 0.30.1 → 0.31.1

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 (120) hide show
  1. package/dist/blocknote.cjs +9 -9
  2. package/dist/blocknote.cjs.map +1 -1
  3. package/dist/blocknote.js +2793 -2213
  4. package/dist/blocknote.js.map +1 -1
  5. package/dist/{en-D4taoCs4.cjs → en-BXVKCwYt.cjs} +2 -2
  6. package/dist/en-BXVKCwYt.cjs.map +1 -0
  7. package/dist/{en-B7ycW7c8.js → en-qGo6sk9V.js} +2 -3
  8. package/dist/en-qGo6sk9V.js.map +1 -0
  9. package/dist/locales.cjs +1 -1
  10. package/dist/locales.cjs.map +1 -1
  11. package/dist/locales.js +20 -39
  12. package/dist/locales.js.map +1 -1
  13. package/dist/style.css +1 -1
  14. package/dist/tsconfig.tsbuildinfo +1 -1
  15. package/dist/webpack-stats.json +1 -1
  16. package/package.json +5 -6
  17. package/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts +2 -3
  18. package/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts +1 -1
  19. package/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap +2816 -0
  20. package/src/api/blockManipulation/commands/updateBlock/updateBlock.test.ts +158 -0
  21. package/src/api/blockManipulation/commands/updateBlock/updateBlock.ts +87 -17
  22. package/src/api/blockManipulation/selections/selection.ts +48 -1
  23. package/src/api/blockManipulation/selections/{textCursorPosition/textCursorPosition.ts → textCursorPosition.ts} +7 -7
  24. package/src/api/getBlockInfoFromPos.ts +1 -1
  25. package/src/api/nodeConversions/blockToNode.ts +5 -2
  26. package/src/api/nodeConversions/nodeToBlock.ts +203 -8
  27. package/src/api/pmUtil.ts +3 -3
  28. package/src/blocks/CodeBlockContent/CodeBlockContent.ts +6 -6
  29. package/src/blocks/FileBlockContent/helpers/render/createAddFileButton.ts +1 -1
  30. package/src/blocks/TableBlockContent/TableBlockContent.ts +32 -2
  31. package/src/editor/Block.css +27 -1
  32. package/src/editor/BlockNoteEditor.test.ts +7 -0
  33. package/src/editor/BlockNoteEditor.ts +124 -39
  34. package/src/editor/BlockNoteExtension.ts +26 -0
  35. package/src/editor/BlockNoteExtensions.ts +28 -12
  36. package/src/editor/BlockNoteTipTapEditor.ts +23 -2
  37. package/src/extensions/Collaboration/CursorPlugin.ts +13 -7
  38. package/src/extensions/Collaboration/ForkYDocPlugin.test.ts +166 -0
  39. package/src/extensions/Collaboration/ForkYDocPlugin.ts +174 -0
  40. package/src/extensions/Collaboration/SyncPlugin.ts +7 -4
  41. package/src/extensions/Collaboration/UndoPlugin.ts +7 -4
  42. package/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-editor-forked.json +30 -0
  43. package/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-editor.json +30 -0
  44. package/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-forked.html +1 -0
  45. package/src/extensions/Collaboration/__snapshots__/fork-yjs-snap.html +1 -0
  46. package/src/extensions/Comments/CommentsPlugin.ts +79 -70
  47. package/src/extensions/FilePanel/FilePanelPlugin.ts +54 -49
  48. package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +60 -26
  49. package/src/extensions/LinkToolbar/LinkToolbarPlugin.ts +26 -21
  50. package/src/extensions/NodeSelectionKeyboard/NodeSelectionKeyboardPlugin.ts +49 -42
  51. package/src/extensions/Placeholder/PlaceholderPlugin.ts +115 -108
  52. package/src/extensions/PreviousBlockType/PreviousBlockTypePlugin.ts +183 -170
  53. package/src/extensions/ShowSelection/ShowSelectionPlugin.ts +26 -19
  54. package/src/extensions/SideMenu/SideMenuPlugin.ts +23 -18
  55. package/src/extensions/SuggestionMenu/SuggestionPlugin.ts +172 -168
  56. package/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +4 -4
  57. package/src/extensions/Suggestions/SuggestionMarks.ts +175 -0
  58. package/src/extensions/TableHandles/TableHandlesPlugin.ts +157 -150
  59. package/src/i18n/locales/ar.ts +0 -1
  60. package/src/i18n/locales/de.ts +0 -1
  61. package/src/i18n/locales/en.ts +0 -1
  62. package/src/i18n/locales/es.ts +0 -1
  63. package/src/i18n/locales/fr.ts +0 -1
  64. package/src/i18n/locales/hr.ts +0 -1
  65. package/src/i18n/locales/is.ts +0 -1
  66. package/src/i18n/locales/it.ts +0 -1
  67. package/src/i18n/locales/ja.ts +0 -1
  68. package/src/i18n/locales/ko.ts +0 -1
  69. package/src/i18n/locales/nl.ts +0 -1
  70. package/src/i18n/locales/no.ts +0 -1
  71. package/src/i18n/locales/pl.ts +0 -1
  72. package/src/i18n/locales/pt.ts +0 -1
  73. package/src/i18n/locales/ru.ts +0 -1
  74. package/src/i18n/locales/sk.ts +0 -1
  75. package/src/i18n/locales/uk.ts +0 -1
  76. package/src/i18n/locales/vi.ts +0 -1
  77. package/src/i18n/locales/zh-tw.ts +0 -1
  78. package/src/i18n/locales/zh.ts +0 -1
  79. package/src/index.ts +18 -8
  80. package/src/pm-nodes/BlockContainer.ts +1 -1
  81. package/src/pm-nodes/BlockGroup.ts +1 -1
  82. package/src/pm-nodes/Doc.ts +1 -0
  83. package/types/src/api/blockManipulation/commands/insertBlocks/insertBlocks.d.ts +1 -1
  84. package/types/src/api/blockManipulation/commands/updateBlock/updateBlock.d.ts +3 -1
  85. package/types/src/api/blockManipulation/selections/selection.d.ts +10 -0
  86. package/types/src/api/blockManipulation/selections/{textCursorPosition/textCursorPosition.d.ts → textCursorPosition.d.ts} +2 -2
  87. package/types/src/api/nodeConversions/nodeToBlock.d.ts +39 -2
  88. package/types/src/api/pmUtil.d.ts +3 -3
  89. package/types/src/blocks/TableBlockContent/TableBlockContent.d.ts +9 -1
  90. package/types/src/editor/BlockNoteEditor.d.ts +62 -10
  91. package/types/src/editor/BlockNoteExtension.d.ts +9 -0
  92. package/types/src/editor/BlockNoteExtensions.d.ts +2 -2
  93. package/types/src/editor/BlockNoteTipTapEditor.d.ts +2 -2
  94. package/types/src/extensions/Collaboration/CursorPlugin.d.ts +3 -3
  95. package/types/src/extensions/Collaboration/ForkYDocPlugin.d.ts +41 -0
  96. package/types/src/extensions/Collaboration/SyncPlugin.d.ts +3 -3
  97. package/types/src/extensions/Collaboration/UndoPlugin.d.ts +3 -3
  98. package/types/src/extensions/Comments/CommentsPlugin.d.ts +3 -4
  99. package/types/src/extensions/FilePanel/FilePanelPlugin.d.ts +4 -4
  100. package/types/src/extensions/FormattingToolbar/FormattingToolbarPlugin.d.ts +6 -5
  101. package/types/src/extensions/LinkToolbar/LinkToolbarPlugin.d.ts +4 -4
  102. package/types/src/extensions/NodeSelectionKeyboard/NodeSelectionKeyboardPlugin.d.ts +3 -3
  103. package/types/src/extensions/Placeholder/PlaceholderPlugin.d.ts +3 -3
  104. package/types/src/extensions/PreviousBlockType/PreviousBlockTypePlugin.d.ts +3 -3
  105. package/types/src/extensions/ShowSelection/ShowSelectionPlugin.d.ts +3 -3
  106. package/types/src/extensions/SideMenu/SideMenuPlugin.d.ts +4 -4
  107. package/types/src/extensions/SuggestionMenu/SuggestionPlugin.d.ts +3 -4
  108. package/types/src/extensions/Suggestions/SuggestionMarks.d.ts +4 -0
  109. package/types/src/extensions/TableHandles/TableHandlesPlugin.d.ts +6 -6
  110. package/types/src/i18n/locales/en.d.ts +0 -1
  111. package/types/src/i18n/locales/sk.d.ts +0 -1
  112. package/types/src/index.d.ts +15 -8
  113. package/dist/en-B7ycW7c8.js.map +0 -1
  114. package/dist/en-D4taoCs4.cjs.map +0 -1
  115. package/src/api/blockManipulation/selections/__snapshots__/selection.test.ts.snap +0 -844
  116. package/src/api/blockManipulation/selections/selection.test.ts +0 -72
  117. package/src/api/blockManipulation/selections/textCursorPosition/__snapshots__/textCursorPosition.test.ts.snap +0 -316
  118. package/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts +0 -74
  119. package/types/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.d.ts +0 -1
  120. /package/types/src/{api/blockManipulation/selections/selection.test.d.ts → extensions/Collaboration/ForkYDocPlugin.test.d.ts} +0 -0
@@ -33,12 +33,13 @@ import {
33
33
  import { insertContentAt } from "../api/blockManipulation/insertContentAt.js";
34
34
  import {
35
35
  getSelection,
36
+ getSelectionCutBlocks,
36
37
  setSelection,
37
38
  } from "../api/blockManipulation/selections/selection.js";
38
39
  import {
39
40
  getTextCursorPosition,
40
41
  setTextCursorPosition,
41
- } from "../api/blockManipulation/selections/textCursorPosition/textCursorPosition.js";
42
+ } from "../api/blockManipulation/selections/textCursorPosition.js";
42
43
  import { createExternalHTMLExporter } from "../api/exporters/html/externalHTMLExporter.js";
43
44
  import { blocksToMarkdown } from "../api/exporters/markdown/markdownExporter.js";
44
45
  import { HTMLToBlocks } from "../api/parsers/html/parseHTML.js";
@@ -93,6 +94,7 @@ import {
93
94
  import { Dictionary } from "../i18n/dictionary.js";
94
95
  import { en } from "../i18n/locales/index.js";
95
96
 
97
+ import { redo, undo } from "@tiptap/pm/history";
96
98
  import {
97
99
  TextSelection,
98
100
  type Command,
@@ -101,11 +103,10 @@ import {
101
103
  } from "@tiptap/pm/state";
102
104
  import { dropCursor } from "prosemirror-dropcursor";
103
105
  import { EditorView } from "prosemirror-view";
104
- import { undoCommand, redoCommand, ySyncPluginKey } from "y-prosemirror";
105
- import { undo, redo } from "@tiptap/pm/history";
106
+ import { redoCommand, undoCommand, ySyncPluginKey } from "y-prosemirror";
106
107
  import { createInternalHTMLSerializer } from "../api/exporters/html/internalHTMLSerializer.js";
107
108
  import { inlineContentToNodes } from "../api/nodeConversions/blockToNode.js";
108
- import { nodeToBlock } from "../api/nodeConversions/nodeToBlock.js";
109
+ import { docToBlocks } from "../api/nodeConversions/nodeToBlock.js";
109
110
  import {
110
111
  BlocksChanged,
111
112
  getBlocksChangedByTransaction,
@@ -113,20 +114,26 @@ import {
113
114
  import { nestedListsToBlockNoteStructure } from "../api/parsers/html/util/nestedLists.js";
114
115
  import { CodeBlockOptions } from "../blocks/CodeBlockContent/CodeBlockContent.js";
115
116
  import type { ThreadStore, User } from "../comments/index.js";
116
- import "../style.css";
117
+ import type { CursorPlugin } from "../extensions/Collaboration/CursorPlugin.js";
118
+ import type { ForkYDocPlugin } from "../extensions/Collaboration/ForkYDocPlugin.js";
117
119
  import { EventEmitter } from "../util/EventEmitter.js";
118
- import { CursorPlugin } from "../extensions/Collaboration/CursorPlugin.js";
120
+ import { BlockNoteExtension } from "./BlockNoteExtension.js";
121
+
122
+ import "../style.css";
119
123
 
124
+ /**
125
+ * A factory function that returns a BlockNoteExtension
126
+ * This is useful so we can create extensions that require an editor instance
127
+ * in the constructor
128
+ */
120
129
  export type BlockNoteExtensionFactory = (
121
130
  editor: BlockNoteEditor<any, any, any>,
122
131
  ) => BlockNoteExtension;
123
132
 
124
- export type BlockNoteExtension =
125
- | AnyExtension
126
- | {
127
- plugin: Plugin;
128
- priority?: number;
129
- };
133
+ /**
134
+ * We support Tiptap extensions and BlockNoteExtension based extensions
135
+ */
136
+ export type SupportedExtension = AnyExtension | BlockNoteExtension;
130
137
 
131
138
  export type BlockCache<
132
139
  BSchema extends BlockSchema = any,
@@ -369,9 +376,23 @@ export type BlockNoteEditorOptions<
369
376
  _tiptapOptions: Partial<EditorOptions>;
370
377
 
371
378
  /**
372
- * (experimental) add extra prosemirror plugins or tiptap extensions to the editor
379
+ * (experimental) add extra extensions to the editor
380
+ *
381
+ * @deprecated, should use `extensions` instead
382
+ */
383
+ _extensions: Record<
384
+ string,
385
+ | { plugin: Plugin; priority?: number }
386
+ | ((editor: BlockNoteEditor<any, any, any>) => {
387
+ plugin: Plugin;
388
+ priority?: number;
389
+ })
390
+ >;
391
+
392
+ /**
393
+ * Register
373
394
  */
374
- _extensions: Record<string, BlockNoteExtension | BlockNoteExtensionFactory>;
395
+ extensions: Array<BlockNoteExtension | BlockNoteExtensionFactory>;
375
396
 
376
397
  /**
377
398
  * Boolean indicating whether the editor is in headless mode.
@@ -404,7 +425,7 @@ export class BlockNoteEditor<
404
425
  /**
405
426
  * extensions that are added to the editor, can be tiptap extensions or prosemirror plugins
406
427
  */
407
- public readonly extensions: Record<string, BlockNoteExtension> = {};
428
+ public extensions: Record<string, SupportedExtension> = {};
408
429
 
409
430
  /**
410
431
  * Boolean indicating whether the editor is in headless mode.
@@ -473,8 +494,10 @@ export class BlockNoteEditor<
473
494
 
474
495
  private readonly showSelectionPlugin: ShowSelectionPlugin;
475
496
 
476
- private readonly cursorPlugin: CursorPlugin;
477
-
497
+ /**
498
+ * The plugin for forking a document, only defined if in collaboration mode
499
+ */
500
+ public readonly forkYDocPlugin?: ForkYDocPlugin;
478
501
  /**
479
502
  * The `uploadFile` method is what the editor uses when files need to be uploaded (for example when selecting an image to upload).
480
503
  * This method should set when creating the editor as this is application-specific.
@@ -609,12 +632,49 @@ export class BlockNoteEditor<
609
632
  });
610
633
 
611
634
  // add extensions from options
612
- Object.entries(newOptions._extensions || {}).forEach(([key, ext]) => {
635
+ for (let ext of newOptions.extensions || []) {
613
636
  if (typeof ext === "function") {
614
637
  // factory
615
638
  ext = ext(this);
616
639
  }
640
+ const key = (ext.constructor as any).key();
641
+ if (!key) {
642
+ throw new Error(
643
+ `Extension ${ext.constructor.name} does not have a key method`,
644
+ );
645
+ }
646
+ if (this.extensions[key]) {
647
+ throw new Error(
648
+ `Extension ${ext.constructor.name} already exists with key ${key}`,
649
+ );
650
+ }
617
651
  this.extensions[key] = ext;
652
+ }
653
+
654
+ // (when passed in via the deprecated `_extensions` option)
655
+ Object.entries(newOptions._extensions || {}).forEach(([key, ext]) => {
656
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
657
+ const editor = this;
658
+
659
+ const instance = typeof ext === "function" ? ext(editor) : ext;
660
+ if (!("plugin" in instance)) {
661
+ // Assume it is an Extension/Mark/Node
662
+ this.extensions[key] = instance;
663
+ return;
664
+ }
665
+
666
+ this.extensions[key] = new (class extends BlockNoteExtension {
667
+ public static key() {
668
+ return key;
669
+ }
670
+ constructor() {
671
+ super();
672
+ this.addProsemirrorPlugin(instance.plugin);
673
+ }
674
+ public get priority() {
675
+ return instance.priority;
676
+ }
677
+ })();
618
678
  });
619
679
 
620
680
  this.formattingToolbar = this.extensions["formattingToolbar"] as any;
@@ -625,7 +685,7 @@ export class BlockNoteEditor<
625
685
  this.tableHandles = this.extensions["tableHandles"] as any;
626
686
  this.comments = this.extensions["comments"] as any;
627
687
  this.showSelectionPlugin = this.extensions["showSelection"] as any;
628
- this.cursorPlugin = this.extensions["yCursorPlugin"] as any;
688
+ this.forkYDocPlugin = this.extensions["forkYDocPlugin"] as any;
629
689
 
630
690
  if (newOptions.uploadFile) {
631
691
  const uploadFile = newOptions.uploadFile;
@@ -691,20 +751,19 @@ export class BlockNoteEditor<
691
751
  return ext;
692
752
  }
693
753
 
694
- if (!ext.plugin) {
695
- throw new Error(
696
- "Extension should either be a TipTap extension or a ProseMirror plugin in a plugin property",
697
- );
754
+ if (ext instanceof BlockNoteExtension && !ext.plugins.length) {
755
+ return undefined;
698
756
  }
699
757
 
700
758
  // "blocknote" extensions (prosemirror plugins)
701
759
  return Extension.create({
702
760
  name: key,
703
761
  priority: ext.priority,
704
- addProseMirrorPlugins: () => [ext.plugin],
762
+ addProseMirrorPlugins: () => ext.plugins,
705
763
  });
706
764
  }),
707
- ];
765
+ ].filter((ext): ext is Extension => ext !== undefined);
766
+
708
767
  const tiptapOptions: BlockNoteTipTapEditorOptions = {
709
768
  ...blockNoteTipTapOptions,
710
769
  ...newOptions._tiptapOptions,
@@ -865,6 +924,26 @@ export class BlockNoteEditor<
865
924
  }
866
925
  }
867
926
 
927
+ // TO DISCUSS
928
+ /**
929
+ * Shorthand to get a typed extension from the editor, by
930
+ * just passing in the extension class.
931
+ *
932
+ * @param ext - The extension class to get
933
+ * @param key - optional, the key of the extension in the extensions object (defaults to the extension name)
934
+ * @returns The extension instance
935
+ */
936
+ public extension<T extends BlockNoteExtension>(
937
+ ext: { new (...args: any[]): T } & typeof BlockNoteExtension,
938
+ key = ext.key(),
939
+ ): T {
940
+ const extension = this.extensions[key] as T;
941
+ if (!extension) {
942
+ throw new Error(`Extension ${key} not found`);
943
+ }
944
+ return extension;
945
+ }
946
+
868
947
  /**
869
948
  * Mount the editor to a parent DOM element. Call mount(undefined) to clean up
870
949
  *
@@ -946,15 +1025,7 @@ export class BlockNoteEditor<
946
1025
  */
947
1026
  public get document(): Block<BSchema, ISchema, SSchema>[] {
948
1027
  return this.transact((tr) => {
949
- const blocks: Block<BSchema, ISchema, SSchema>[] = [];
950
-
951
- tr.doc.firstChild!.descendants((node) => {
952
- blocks.push(nodeToBlock(node, this.pmSchema));
953
-
954
- return false;
955
- });
956
-
957
- return blocks;
1028
+ return docToBlocks(tr.doc, this.pmSchema);
958
1029
  });
959
1030
  }
960
1031
 
@@ -1099,12 +1170,26 @@ export class BlockNoteEditor<
1099
1170
  }
1100
1171
 
1101
1172
  /**
1102
- * Gets a snapshot of the current selection.
1173
+ * Gets a snapshot of the current selection. This contains all blocks (included nested blocks)
1174
+ * that the selection spans across.
1175
+ *
1176
+ * If the selection starts / ends halfway through a block, the returned data will contain the entire block.
1103
1177
  */
1104
1178
  public getSelection(): Selection<BSchema, ISchema, SSchema> | undefined {
1105
1179
  return this.transact((tr) => getSelection(tr));
1106
1180
  }
1107
1181
 
1182
+ /**
1183
+ * Gets a snapshot of the current selection. This contains all blocks (included nested blocks)
1184
+ * that the selection spans across.
1185
+ *
1186
+ * If the selection starts / ends halfway through a block, the returned block will be
1187
+ * only the part of the block that is included in the selection.
1188
+ */
1189
+ public getSelectionCutBlocks() {
1190
+ return this.transact((tr) => getSelectionCutBlocks(tr));
1191
+ }
1192
+
1108
1193
  /**
1109
1194
  * Sets the selection to a range of blocks.
1110
1195
  * @param startBlock The identifier of the block that should be the start of the selection.
@@ -1500,7 +1585,7 @@ export class BlockNoteEditor<
1500
1585
  );
1501
1586
  }
1502
1587
 
1503
- this.cursorPlugin.updateUser(user);
1588
+ (this.extensions["yCursorPlugin"] as CursorPlugin).updateUser(user);
1504
1589
  }
1505
1590
 
1506
1591
  /**
@@ -1595,8 +1680,8 @@ export class BlockNoteEditor<
1595
1680
  if (!this.prosemirrorView) {
1596
1681
  return undefined;
1597
1682
  }
1598
- const state = this.prosemirrorView?.state;
1599
- const { selection } = state;
1683
+
1684
+ const { selection } = this.prosemirrorState;
1600
1685
 
1601
1686
  // support for CellSelections
1602
1687
  const { ranges } = selection;
@@ -1641,7 +1726,7 @@ export class BlockNoteEditor<
1641
1726
  if (pluginState?.deleteTriggerCharacter) {
1642
1727
  tr.insertText(triggerCharacter);
1643
1728
  }
1644
- tr.scrollIntoView().setMeta(this.suggestionMenus.plugin, {
1729
+ tr.scrollIntoView().setMeta(this.suggestionMenus.plugins[0], {
1645
1730
  triggerCharacter: triggerCharacter,
1646
1731
  deleteTriggerCharacter: pluginState?.deleteTriggerCharacter || false,
1647
1732
  ignoreQueryLength: pluginState?.ignoreQueryLength || false,
@@ -0,0 +1,26 @@
1
+ import { Plugin } from "prosemirror-state";
2
+ import { EventEmitter } from "../util/EventEmitter.js";
3
+
4
+ export abstract class BlockNoteExtension<
5
+ TEvent extends Record<string, any> = any,
6
+ > extends EventEmitter<TEvent> {
7
+ public static key(): string {
8
+ throw new Error("You must implement the key method in your extension");
9
+ }
10
+
11
+ protected addProsemirrorPlugin(plugin: Plugin) {
12
+ this.plugins.push(plugin);
13
+ }
14
+
15
+ public readonly plugins: Plugin[] = [];
16
+ public get priority(): number | undefined {
17
+ return undefined;
18
+ }
19
+
20
+ // eslint-disable-next-line
21
+ constructor(..._args: any[]) {
22
+ super();
23
+ // Allow subclasses to have constructors with parameters
24
+ // without this, we can't easily implement BlockNoteEditor.extension(MyExtension) pattern
25
+ }
26
+ }
@@ -9,13 +9,13 @@ import * as Y from "yjs";
9
9
  import { createDropFileExtension } from "../api/clipboard/fromClipboard/fileDropExtension.js";
10
10
  import { createPasteFromClipboardExtension } from "../api/clipboard/fromClipboard/pasteExtension.js";
11
11
  import { createCopyToClipboardExtension } from "../api/clipboard/toClipboard/copyExtension.js";
12
+ import type { ThreadStore } from "../comments/index.js";
12
13
  import { BackgroundColorExtension } from "../extensions/BackgroundColor/BackgroundColorExtension.js";
13
14
  import { CursorPlugin } from "../extensions/Collaboration/CursorPlugin.js";
14
- import { UndoPlugin } from "../extensions/Collaboration/UndoPlugin.js";
15
15
  import { SyncPlugin } from "../extensions/Collaboration/SyncPlugin.js";
16
+ import { UndoPlugin } from "../extensions/Collaboration/UndoPlugin.js";
16
17
  import { CommentMark } from "../extensions/Comments/CommentMark.js";
17
18
  import { CommentsPlugin } from "../extensions/Comments/CommentsPlugin.js";
18
- import type { ThreadStore } from "../comments/index.js";
19
19
  import { FilePanelProsemirrorPlugin } from "../extensions/FilePanel/FilePanelPlugin.js";
20
20
  import { FormattingToolbarProsemirrorPlugin } from "../extensions/FormattingToolbar/FormattingToolbarPlugin.js";
21
21
  import { HardBreak } from "../extensions/HardBreak/HardBreak.js";
@@ -31,6 +31,11 @@ import { PreviousBlockTypePlugin } from "../extensions/PreviousBlockType/Previou
31
31
  import { ShowSelectionPlugin } from "../extensions/ShowSelection/ShowSelectionPlugin.js";
32
32
  import { SideMenuProsemirrorPlugin } from "../extensions/SideMenu/SideMenuPlugin.js";
33
33
  import { SuggestionMenuProseMirrorPlugin } from "../extensions/SuggestionMenu/SuggestionPlugin.js";
34
+ import {
35
+ SuggestionAddMark,
36
+ SuggestionDeleteMark,
37
+ SuggestionModificationMark,
38
+ } from "../extensions/Suggestions/SuggestionMarks.js";
34
39
  import { TableHandlesProsemirrorPlugin } from "../extensions/TableHandles/TableHandlesPlugin.js";
35
40
  import { TextAlignmentExtension } from "../extensions/TextAlignment/TextAlignmentExtension.js";
36
41
  import { TextColorExtension } from "../extensions/TextColor/TextColorExtension.js";
@@ -49,8 +54,9 @@ import {
49
54
  import type {
50
55
  BlockNoteEditor,
51
56
  BlockNoteEditorOptions,
52
- BlockNoteExtension,
57
+ SupportedExtension,
53
58
  } from "./BlockNoteEditor.js";
59
+ import { ForkYDocPlugin } from "../extensions/Collaboration/ForkYDocPlugin.js";
54
60
 
55
61
  type ExtensionOptions<
56
62
  BSchema extends BlockSchema,
@@ -101,7 +107,7 @@ export const getBlockNoteExtensions = <
101
107
  >(
102
108
  opts: ExtensionOptions<BSchema, I, S>,
103
109
  ) => {
104
- const ret: Record<string, BlockNoteExtension> = {};
110
+ const ret: Record<string, SupportedExtension> = {};
105
111
  const tiptapExtensions = getTipTapExtensions(opts);
106
112
 
107
113
  for (const ext of tiptapExtensions) {
@@ -115,6 +121,10 @@ export const getBlockNoteExtensions = <
115
121
  if (opts.collaboration.provider?.awareness) {
116
122
  ret["yCursorPlugin"] = new CursorPlugin(opts.collaboration);
117
123
  }
124
+ ret["forkYDocPlugin"] = new ForkYDocPlugin({
125
+ editor: opts.editor,
126
+ collaboration: opts.collaboration,
127
+ });
118
128
  }
119
129
 
120
130
  // Note: this is pretty hardcoded and will break when user provides plugins with same keys.
@@ -139,14 +149,6 @@ export const getBlockNoteExtensions = <
139
149
  ret["tableHandles"] = new TableHandlesProsemirrorPlugin(opts.editor as any);
140
150
  }
141
151
 
142
- ret["dropCursor"] = {
143
- plugin: opts.dropCursor({
144
- width: 5,
145
- color: "#ddeeff",
146
- editor: opts.editor,
147
- }),
148
- };
149
-
150
152
  ret["nodeSelectionKeyboard"] = new NodeSelectionKeyboardPlugin();
151
153
 
152
154
  ret["showSelection"] = new ShowSelectionPlugin(opts.editor);
@@ -190,6 +192,17 @@ const getTipTapExtensions = <
190
192
  Gapcursor,
191
193
 
192
194
  // DropCursor,
195
+ Extension.create({
196
+ name: "dropCursor",
197
+ addProseMirrorPlugins: () => [
198
+ opts.dropCursor({
199
+ width: 5,
200
+ color: "#ddeeff",
201
+ editor: opts.editor,
202
+ }),
203
+ ],
204
+ }),
205
+
193
206
  UniqueID.configure({
194
207
  // everything from bnBlock group (nodes that represent a BlockNote block should have an id)
195
208
  types: ["blockContainer", "columnList", "column"],
@@ -202,6 +215,9 @@ const getTipTapExtensions = <
202
215
  Text,
203
216
 
204
217
  // marks:
218
+ SuggestionAddMark,
219
+ SuggestionDeleteMark,
220
+ SuggestionModificationMark,
205
221
  Link.extend({
206
222
  inclusive: false,
207
223
  }).configure({
@@ -1,5 +1,9 @@
1
- import { Editor, EditorOptions, createDocument } from "@tiptap/core";
2
- import { Editor as TiptapEditor } from "@tiptap/core";
1
+ import {
2
+ Editor,
3
+ EditorOptions,
4
+ Editor as TiptapEditor,
5
+ createDocument,
6
+ } from "@tiptap/core";
3
7
 
4
8
  import { Node } from "@tiptap/pm/model";
5
9
 
@@ -141,6 +145,10 @@ export class BlockNoteTipTapEditor extends TiptapEditor {
141
145
  if (!this.view) {
142
146
  // before view has been initialized
143
147
  this._state = this.state.apply(transaction);
148
+ this.emit("transaction", {
149
+ editor: this,
150
+ transaction,
151
+ });
144
152
  return;
145
153
  }
146
154
  // This is a verbatim copy of the default dispatch method, but with the following changes:
@@ -216,6 +224,19 @@ export class BlockNoteTipTapEditor extends TiptapEditor {
216
224
  });
217
225
  }
218
226
 
227
+ // a helper method that can enable plugins before the view has been initialized
228
+ // currently only used for testing
229
+ forceEnablePlugins() {
230
+ if (this.view) {
231
+ throw new Error(
232
+ "forcePluginsEnabled called after view has been initialized",
233
+ );
234
+ }
235
+ this._state = this.state.reconfigure({
236
+ plugins: this.extensionManager.plugins,
237
+ });
238
+ }
239
+
219
240
  /**
220
241
  * Replace the default `createView` method with a custom one - which we call on mount
221
242
  */
@@ -1,7 +1,7 @@
1
- import { Plugin } from "prosemirror-state";
2
1
  import { defaultSelectionBuilder, yCursorPlugin } from "y-prosemirror";
3
2
  import { Awareness } from "y-protocols/awareness.js";
4
3
  import * as Y from "yjs";
4
+ import { BlockNoteExtension } from "../../editor/BlockNoteExtension.js";
5
5
 
6
6
  export type CollaborationUser = {
7
7
  name: string;
@@ -9,8 +9,11 @@ export type CollaborationUser = {
9
9
  [key: string]: string;
10
10
  };
11
11
 
12
- export class CursorPlugin {
13
- public plugin: Plugin;
12
+ export class CursorPlugin extends BlockNoteExtension {
13
+ public static key() {
14
+ return "yCursorPlugin";
15
+ }
16
+
14
17
  private provider: { awareness: Awareness };
15
18
  private recentlyUpdatedCursors: Map<
16
19
  number,
@@ -25,6 +28,7 @@ export class CursorPlugin {
25
28
  showCursorLabels?: "always" | "activity";
26
29
  },
27
30
  ) {
31
+ super();
28
32
  this.provider = collaboration.provider;
29
33
  this.recentlyUpdatedCursors = new Map();
30
34
 
@@ -62,10 +66,12 @@ export class CursorPlugin {
62
66
  );
63
67
  }
64
68
 
65
- this.plugin = yCursorPlugin(this.provider.awareness, {
66
- selectionBuilder: defaultSelectionBuilder,
67
- cursorBuilder: this.renderCursor,
68
- });
69
+ this.addProsemirrorPlugin(
70
+ yCursorPlugin(this.provider.awareness, {
71
+ selectionBuilder: defaultSelectionBuilder,
72
+ cursorBuilder: this.renderCursor,
73
+ }),
74
+ );
69
75
  }
70
76
 
71
77
  public get priority() {
@@ -0,0 +1,166 @@
1
+ import { expect, it } from "vitest";
2
+ import * as Y from "yjs";
3
+ import { Awareness } from "y-protocols/awareness";
4
+ import { BlockNoteEditor } from "../../index.js";
5
+
6
+ /**
7
+ * @vitest-environment jsdom
8
+ */
9
+ it("can fork a document", async () => {
10
+ const doc = new Y.Doc();
11
+ const fragment = doc.getXmlFragment("doc");
12
+ const editor = BlockNoteEditor.create({
13
+ collaboration: {
14
+ fragment,
15
+ user: { name: "Hello", color: "#FFFFFF" },
16
+ provider: {
17
+ awareness: new Awareness(doc),
18
+ },
19
+ },
20
+ });
21
+
22
+ const div = document.createElement("div");
23
+ editor.mount(div);
24
+
25
+ editor.replaceBlocks(editor.document, [
26
+ {
27
+ type: "paragraph",
28
+ content: [{ text: "Hello", styles: {}, type: "text" }],
29
+ },
30
+ ]);
31
+
32
+ await expect(fragment.toJSON()).toMatchFileSnapshot(
33
+ "__snapshots__/fork-yjs-snap.html",
34
+ );
35
+ await expect(editor.document).toMatchFileSnapshot(
36
+ "__snapshots__/fork-yjs-snap-editor.json",
37
+ );
38
+
39
+ editor.forkYDocPlugin!.fork();
40
+
41
+ editor.replaceBlocks(editor.document, [
42
+ {
43
+ type: "paragraph",
44
+ content: [{ text: "Hello World", styles: {}, type: "text" }],
45
+ },
46
+ ]);
47
+
48
+ await expect(fragment.toJSON()).toMatchFileSnapshot(
49
+ "__snapshots__/fork-yjs-snap.html",
50
+ );
51
+ await expect(editor.document).toMatchFileSnapshot(
52
+ "__snapshots__/fork-yjs-snap-editor-forked.json",
53
+ );
54
+ });
55
+
56
+ it("can merge a document", async () => {
57
+ const doc = new Y.Doc();
58
+ const fragment = doc.getXmlFragment("doc");
59
+ const editor = BlockNoteEditor.create({
60
+ collaboration: {
61
+ fragment,
62
+ user: { name: "Hello", color: "#FFFFFF" },
63
+ provider: {
64
+ awareness: new Awareness(doc),
65
+ },
66
+ },
67
+ });
68
+
69
+ const div = document.createElement("div");
70
+ editor.mount(div);
71
+
72
+ editor.replaceBlocks(editor.document, [
73
+ {
74
+ type: "paragraph",
75
+ content: [{ text: "Hello", styles: {}, type: "text" }],
76
+ },
77
+ ]);
78
+
79
+ await expect(fragment.toJSON()).toMatchFileSnapshot(
80
+ "__snapshots__/fork-yjs-snap.html",
81
+ );
82
+ await expect(editor.document).toMatchFileSnapshot(
83
+ "__snapshots__/fork-yjs-snap-editor.json",
84
+ );
85
+
86
+ editor.forkYDocPlugin!.fork();
87
+
88
+ editor.replaceBlocks(editor.document, [
89
+ {
90
+ type: "paragraph",
91
+ content: [{ text: "Hello World", styles: {}, type: "text" }],
92
+ },
93
+ ]);
94
+
95
+ await expect(fragment.toJSON()).toMatchFileSnapshot(
96
+ "__snapshots__/fork-yjs-snap.html",
97
+ );
98
+ await expect(editor.document).toMatchFileSnapshot(
99
+ "__snapshots__/fork-yjs-snap-editor-forked.json",
100
+ );
101
+
102
+ editor.forkYDocPlugin!.merge({ keepChanges: false });
103
+
104
+ await expect(fragment.toJSON()).toMatchFileSnapshot(
105
+ "__snapshots__/fork-yjs-snap.html",
106
+ );
107
+ await expect(editor.document).toMatchFileSnapshot(
108
+ "__snapshots__/fork-yjs-snap-editor.json",
109
+ );
110
+ });
111
+
112
+ it("can fork an keep the changes to the original document", async () => {
113
+ const doc = new Y.Doc();
114
+ const fragment = doc.getXmlFragment("doc");
115
+ const editor = BlockNoteEditor.create({
116
+ collaboration: {
117
+ fragment,
118
+ user: { name: "Hello", color: "#FFFFFF" },
119
+ provider: {
120
+ awareness: new Awareness(doc),
121
+ },
122
+ },
123
+ });
124
+
125
+ const div = document.createElement("div");
126
+ editor.mount(div);
127
+
128
+ editor.replaceBlocks(editor.document, [
129
+ {
130
+ type: "paragraph",
131
+ content: [{ text: "Hello", styles: {}, type: "text" }],
132
+ },
133
+ ]);
134
+
135
+ await expect(fragment.toJSON()).toMatchFileSnapshot(
136
+ "__snapshots__/fork-yjs-snap.html",
137
+ );
138
+ await expect(editor.document).toMatchFileSnapshot(
139
+ "__snapshots__/fork-yjs-snap-editor.json",
140
+ );
141
+
142
+ editor.forkYDocPlugin!.fork();
143
+
144
+ editor.replaceBlocks(editor.document, [
145
+ {
146
+ type: "paragraph",
147
+ content: [{ text: "Hello World", styles: {}, type: "text" }],
148
+ },
149
+ ]);
150
+
151
+ await expect(fragment.toJSON()).toMatchFileSnapshot(
152
+ "__snapshots__/fork-yjs-snap.html",
153
+ );
154
+ await expect(editor.document).toMatchFileSnapshot(
155
+ "__snapshots__/fork-yjs-snap-editor-forked.json",
156
+ );
157
+
158
+ editor.forkYDocPlugin!.merge({ keepChanges: true });
159
+
160
+ await expect(fragment.toJSON()).toMatchFileSnapshot(
161
+ "__snapshots__/fork-yjs-snap-forked.html",
162
+ );
163
+ await expect(editor.document).toMatchFileSnapshot(
164
+ "__snapshots__/fork-yjs-snap-editor-forked.json",
165
+ );
166
+ });