@blocknote/core 0.30.1 → 0.31.0
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.
- package/dist/blocknote.cjs +9 -9
- package/dist/blocknote.cjs.map +1 -1
- package/dist/blocknote.js +2754 -2230
- package/dist/blocknote.js.map +1 -1
- package/dist/{en-D4taoCs4.cjs → en-BXVKCwYt.cjs} +2 -2
- package/dist/en-BXVKCwYt.cjs.map +1 -0
- package/dist/{en-B7ycW7c8.js → en-qGo6sk9V.js} +2 -3
- package/dist/en-qGo6sk9V.js.map +1 -0
- package/dist/locales.cjs +1 -1
- package/dist/locales.cjs.map +1 -1
- package/dist/locales.js +20 -39
- package/dist/locales.js.map +1 -1
- package/dist/style.css +1 -1
- package/dist/webpack-stats.json +1 -1
- package/package.json +4 -5
- package/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts +2 -3
- package/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts +1 -1
- package/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap +2816 -0
- package/src/api/blockManipulation/commands/updateBlock/updateBlock.test.ts +158 -0
- package/src/api/blockManipulation/commands/updateBlock/updateBlock.ts +87 -17
- package/src/api/blockManipulation/selections/selection.ts +48 -1
- package/src/api/blockManipulation/selections/{textCursorPosition/textCursorPosition.ts → textCursorPosition.ts} +7 -7
- package/src/api/getBlockInfoFromPos.ts +1 -1
- package/src/api/nodeConversions/blockToNode.ts +5 -2
- package/src/api/nodeConversions/nodeToBlock.ts +203 -8
- package/src/api/pmUtil.ts +3 -3
- package/src/blocks/CodeBlockContent/CodeBlockContent.ts +6 -6
- package/src/blocks/FileBlockContent/helpers/render/createAddFileButton.ts +1 -1
- package/src/blocks/TableBlockContent/TableBlockContent.ts +32 -2
- package/src/editor/Block.css +27 -1
- package/src/editor/BlockNoteEditor.test.ts +7 -0
- package/src/editor/BlockNoteEditor.ts +88 -37
- package/src/editor/BlockNoteExtension.ts +26 -0
- package/src/editor/BlockNoteExtensions.ts +28 -12
- package/src/editor/BlockNoteTipTapEditor.ts +23 -2
- package/src/extensions/Collaboration/CursorPlugin.ts +13 -7
- package/src/extensions/Collaboration/ForkYDocPlugin.test.ts +166 -0
- package/src/extensions/Collaboration/ForkYDocPlugin.ts +174 -0
- package/src/extensions/Collaboration/SyncPlugin.ts +7 -4
- package/src/extensions/Collaboration/UndoPlugin.ts +7 -4
- package/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-editor-forked.json +30 -0
- package/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-editor.json +30 -0
- package/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-forked.html +1 -0
- package/src/extensions/Collaboration/__snapshots__/fork-yjs-snap.html +1 -0
- package/src/extensions/Comments/CommentsPlugin.ts +75 -70
- package/src/extensions/FilePanel/FilePanelPlugin.ts +50 -49
- package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +56 -26
- package/src/extensions/LinkToolbar/LinkToolbarPlugin.ts +22 -21
- package/src/extensions/NodeSelectionKeyboard/NodeSelectionKeyboardPlugin.ts +45 -42
- package/src/extensions/Placeholder/PlaceholderPlugin.ts +111 -108
- package/src/extensions/PreviousBlockType/PreviousBlockTypePlugin.ts +179 -170
- package/src/extensions/ShowSelection/ShowSelectionPlugin.ts +22 -19
- package/src/extensions/SideMenu/SideMenuPlugin.ts +19 -18
- package/src/extensions/SuggestionMenu/SuggestionPlugin.ts +168 -168
- package/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +4 -4
- package/src/extensions/Suggestions/SuggestionMarks.ts +175 -0
- package/src/extensions/TableHandles/TableHandlesPlugin.ts +153 -150
- package/src/i18n/locales/ar.ts +0 -1
- package/src/i18n/locales/de.ts +0 -1
- package/src/i18n/locales/en.ts +0 -1
- package/src/i18n/locales/es.ts +0 -1
- package/src/i18n/locales/fr.ts +0 -1
- package/src/i18n/locales/hr.ts +0 -1
- package/src/i18n/locales/is.ts +0 -1
- package/src/i18n/locales/it.ts +0 -1
- package/src/i18n/locales/ja.ts +0 -1
- package/src/i18n/locales/ko.ts +0 -1
- package/src/i18n/locales/nl.ts +0 -1
- package/src/i18n/locales/no.ts +0 -1
- package/src/i18n/locales/pl.ts +0 -1
- package/src/i18n/locales/pt.ts +0 -1
- package/src/i18n/locales/ru.ts +0 -1
- package/src/i18n/locales/sk.ts +0 -1
- package/src/i18n/locales/uk.ts +0 -1
- package/src/i18n/locales/vi.ts +0 -1
- package/src/i18n/locales/zh-tw.ts +0 -1
- package/src/i18n/locales/zh.ts +0 -1
- package/src/index.ts +18 -8
- package/src/pm-nodes/BlockContainer.ts +1 -1
- package/src/pm-nodes/BlockGroup.ts +1 -1
- package/src/pm-nodes/Doc.ts +1 -0
- package/types/src/api/blockManipulation/commands/insertBlocks/insertBlocks.d.ts +1 -1
- package/types/src/api/blockManipulation/commands/removeBlocks/removeBlocks.d.ts +4 -0
- package/types/src/api/blockManipulation/commands/removeBlocks/removeBlocks.test.d.ts +1 -0
- package/types/src/api/blockManipulation/commands/updateBlock/updateBlock.d.ts +3 -1
- package/types/src/api/blockManipulation/selections/selection.d.ts +10 -0
- package/types/src/api/blockManipulation/selections/textCursorPosition.d.ts +5 -0
- package/types/src/api/blockManipulation/transactions.test.d.ts +0 -0
- package/types/src/api/clipboard/clipboardExternal.test.d.ts +1 -0
- package/types/src/api/clipboard/clipboardInternal.test.d.ts +1 -0
- package/types/src/api/clipboard/testUtil.d.ts +541 -0
- package/types/src/api/exporters/html/htmlConversion.test.d.ts +1 -0
- package/types/src/api/exporters/markdown/markdownExporter.test.d.ts +1 -0
- package/types/src/api/nodeConversions/nodeConversions.test.d.ts +1 -0
- package/types/src/api/nodeConversions/nodeToBlock.d.ts +39 -2
- package/types/src/api/parsers/html/parseHTML.test.d.ts +1 -0
- package/types/src/api/parsers/markdown/parseMarkdown.test.d.ts +1 -0
- package/types/src/api/pmUtil.d.ts +3 -3
- package/types/src/api/testUtil/cases/customBlocks.d.ts +670 -0
- package/types/src/api/testUtil/cases/customInlineContent.d.ts +558 -0
- package/types/src/api/testUtil/cases/customStyles.d.ts +552 -0
- package/types/src/api/testUtil/cases/defaultSchema.d.ts +4 -0
- package/types/src/api/testUtil/index.d.ts +14 -0
- package/types/src/api/testUtil/partialBlockTestUtil.d.ts +9 -0
- package/types/src/api/testUtil/paste.d.ts +2 -0
- package/types/src/blocks/CodeBlockContent/defaultSupportedLanguages.d.ts +6 -0
- package/types/src/blocks/TableBlockContent/TableBlockContent.d.ts +9 -1
- package/types/src/editor/BlockNoteEditor.d.ts +55 -9
- package/types/src/editor/BlockNoteExtension.d.ts +9 -0
- package/types/src/editor/BlockNoteExtensions.d.ts +2 -2
- package/types/src/editor/BlockNoteTipTapEditor.d.ts +2 -2
- package/types/src/extensions/Collaboration/CursorPlugin.d.ts +3 -3
- package/types/src/extensions/Collaboration/ForkYDocPlugin.d.ts +41 -0
- package/types/src/extensions/Collaboration/ForkYDocPlugin.test.d.ts +1 -0
- package/types/src/extensions/Collaboration/SyncPlugin.d.ts +3 -3
- package/types/src/extensions/Collaboration/UndoPlugin.d.ts +3 -3
- package/types/src/extensions/Collaboration/createCollaborationExtensions.d.ts +17 -0
- package/types/src/extensions/Comments/CommentsPlugin.d.ts +2 -4
- package/types/src/extensions/FilePanel/FilePanelPlugin.d.ts +3 -4
- package/types/src/extensions/FormattingToolbar/FormattingToolbarPlugin.d.ts +5 -5
- package/types/src/extensions/LinkToolbar/LinkToolbarPlugin.d.ts +3 -4
- package/types/src/extensions/NodeSelectionKeyboard/NodeSelectionKeyboardPlugin.d.ts +2 -3
- package/types/src/extensions/Placeholder/PlaceholderPlugin.d.ts +2 -3
- package/types/src/extensions/PreviousBlockType/PreviousBlockTypePlugin.d.ts +2 -3
- package/types/src/extensions/ShowSelection/ShowSelectionPlugin.d.ts +2 -3
- package/types/src/extensions/SideMenu/SideMenuPlugin.d.ts +3 -4
- package/types/src/extensions/SuggestionMenu/SuggestionPlugin.d.ts +2 -4
- package/types/src/extensions/Suggestions/SuggestionMarks.d.ts +4 -0
- package/types/src/extensions/TableHandles/TableHandlesPlugin.d.ts +5 -6
- package/types/src/i18n/locales/en.d.ts +0 -1
- package/types/src/i18n/locales/sk.d.ts +0 -1
- package/types/src/index.d.ts +15 -8
- package/dist/en-B7ycW7c8.js.map +0 -1
- package/dist/en-D4taoCs4.cjs.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/api/blockManipulation/selections/__snapshots__/selection.test.ts.snap +0 -844
- package/src/api/blockManipulation/selections/selection.test.ts +0 -72
- package/src/api/blockManipulation/selections/textCursorPosition/__snapshots__/textCursorPosition.test.ts.snap +0 -316
- package/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts +0 -74
|
@@ -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 name(): string {
|
|
8
|
+
throw new Error("You must implement the name 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
|
-
|
|
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,
|
|
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 {
|
|
2
|
-
|
|
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
|
|
12
|
+
export class CursorPlugin extends BlockNoteExtension {
|
|
13
|
+
public static name() {
|
|
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.
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
yCursorPluginKey,
|
|
5
|
+
ySyncPluginKey,
|
|
6
|
+
yUndoPluginKey,
|
|
7
|
+
} from "y-prosemirror";
|
|
8
|
+
import { CursorPlugin } from "./CursorPlugin.js";
|
|
9
|
+
import { SyncPlugin } from "./SyncPlugin.js";
|
|
10
|
+
import { UndoPlugin } from "./UndoPlugin.js";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
BlockNoteEditor,
|
|
14
|
+
BlockNoteEditorOptions,
|
|
15
|
+
} from "../../editor/BlockNoteEditor.js";
|
|
16
|
+
import { BlockNoteExtension } from "../../editor/BlockNoteExtension.js";
|
|
17
|
+
|
|
18
|
+
export class ForkYDocPlugin extends BlockNoteExtension<{
|
|
19
|
+
forked: boolean;
|
|
20
|
+
}> {
|
|
21
|
+
public static name() {
|
|
22
|
+
return "ForkYDocPlugin";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private editor: BlockNoteEditor<any, any, any>;
|
|
26
|
+
private collaboration: BlockNoteEditorOptions<any, any, any>["collaboration"];
|
|
27
|
+
|
|
28
|
+
constructor({
|
|
29
|
+
editor,
|
|
30
|
+
collaboration,
|
|
31
|
+
}: {
|
|
32
|
+
editor: BlockNoteEditor<any, any, any>;
|
|
33
|
+
collaboration: BlockNoteEditorOptions<any, any, any>["collaboration"];
|
|
34
|
+
}) {
|
|
35
|
+
super(editor);
|
|
36
|
+
this.editor = editor;
|
|
37
|
+
this.collaboration = collaboration;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* To find a fragment in another ydoc, we need to search for it.
|
|
42
|
+
*/
|
|
43
|
+
private findTypeInOtherYdoc<T extends Y.AbstractType<any>>(
|
|
44
|
+
ytype: T,
|
|
45
|
+
otherYdoc: Y.Doc,
|
|
46
|
+
): T {
|
|
47
|
+
const ydoc = ytype.doc!;
|
|
48
|
+
if (ytype._item === null) {
|
|
49
|
+
/**
|
|
50
|
+
* If is a root type, we need to find the root key in the original ydoc
|
|
51
|
+
* and use it to get the type in the other ydoc.
|
|
52
|
+
*/
|
|
53
|
+
const rootKey = Array.from(ydoc.share.keys()).find(
|
|
54
|
+
(key) => ydoc.share.get(key) === ytype,
|
|
55
|
+
);
|
|
56
|
+
if (rootKey == null) {
|
|
57
|
+
throw new Error("type does not exist in other ydoc");
|
|
58
|
+
}
|
|
59
|
+
return otherYdoc.get(rootKey, ytype.constructor as new () => T) as T;
|
|
60
|
+
} else {
|
|
61
|
+
/**
|
|
62
|
+
* If it is a sub type, we use the item id to find the history type.
|
|
63
|
+
*/
|
|
64
|
+
const ytypeItem = ytype._item;
|
|
65
|
+
const otherStructs =
|
|
66
|
+
otherYdoc.store.clients.get(ytypeItem.id.client) ?? [];
|
|
67
|
+
const itemIndex = Y.findIndexSS(otherStructs, ytypeItem.id.clock);
|
|
68
|
+
const otherItem = otherStructs[itemIndex] as Y.Item;
|
|
69
|
+
const otherContent = otherItem.content as Y.ContentType;
|
|
70
|
+
return otherContent.type as T;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Whether the editor is editing a forked document,
|
|
76
|
+
* preserving a reference to the original document and the forked document.
|
|
77
|
+
*/
|
|
78
|
+
public get isForkedFromRemote() {
|
|
79
|
+
return this.forkedState !== undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Stores whether the editor is editing a forked document,
|
|
84
|
+
* preserving a reference to the original document and the forked document.
|
|
85
|
+
*/
|
|
86
|
+
private forkedState:
|
|
87
|
+
| {
|
|
88
|
+
originalFragment: Y.XmlFragment;
|
|
89
|
+
forkedFragment: Y.XmlFragment;
|
|
90
|
+
}
|
|
91
|
+
| undefined;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Fork the Y.js document from syncing to the remote,
|
|
95
|
+
* allowing modifications to the document without affecting the remote.
|
|
96
|
+
* These changes can later be rolled back or applied to the remote.
|
|
97
|
+
*/
|
|
98
|
+
public fork() {
|
|
99
|
+
if (this.isForkedFromRemote) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const originalFragment = this.collaboration.fragment;
|
|
104
|
+
|
|
105
|
+
if (!originalFragment) {
|
|
106
|
+
throw new Error("No fragment to fork from");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const doc = new Y.Doc();
|
|
110
|
+
// Copy the original document to a new Yjs document
|
|
111
|
+
Y.applyUpdate(doc, Y.encodeStateAsUpdate(originalFragment.doc!));
|
|
112
|
+
|
|
113
|
+
// Find the forked fragment in the new Yjs document
|
|
114
|
+
const forkedFragment = this.findTypeInOtherYdoc(originalFragment, doc);
|
|
115
|
+
|
|
116
|
+
this.forkedState = {
|
|
117
|
+
originalFragment,
|
|
118
|
+
forkedFragment,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Need to reset all the yjs plugins
|
|
122
|
+
this.editor._tiptapEditor.unregisterPlugin([
|
|
123
|
+
yCursorPluginKey,
|
|
124
|
+
yUndoPluginKey,
|
|
125
|
+
ySyncPluginKey,
|
|
126
|
+
]);
|
|
127
|
+
// Register them again, based on the new forked fragment
|
|
128
|
+
this.editor._tiptapEditor.registerPlugin(
|
|
129
|
+
new SyncPlugin(forkedFragment).plugins[0],
|
|
130
|
+
);
|
|
131
|
+
this.editor._tiptapEditor.registerPlugin(new UndoPlugin().plugins[0]);
|
|
132
|
+
// No need to register the cursor plugin again, it's a local fork
|
|
133
|
+
this.emit("forked", true);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Resume syncing the Y.js document to the remote
|
|
138
|
+
* If `keepChanges` is true, any changes that have been made to the forked document will be applied to the original document.
|
|
139
|
+
* Otherwise, the original document will be restored and the changes will be discarded.
|
|
140
|
+
*/
|
|
141
|
+
public merge({ keepChanges }: { keepChanges: boolean }) {
|
|
142
|
+
if (!this.forkedState) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Remove the forked fragment's plugins
|
|
146
|
+
this.editor._tiptapEditor.unregisterPlugin(ySyncPluginKey);
|
|
147
|
+
this.editor._tiptapEditor.unregisterPlugin(yUndoPluginKey);
|
|
148
|
+
|
|
149
|
+
const { originalFragment, forkedFragment } = this.forkedState;
|
|
150
|
+
if (keepChanges) {
|
|
151
|
+
// Apply any changes that have been made to the fork, onto the original doc
|
|
152
|
+
const update = Y.encodeStateAsUpdate(forkedFragment.doc!);
|
|
153
|
+
Y.applyUpdate(originalFragment.doc!, update);
|
|
154
|
+
}
|
|
155
|
+
this.editor.extensions["ySyncPlugin"] = new SyncPlugin(originalFragment);
|
|
156
|
+
this.editor.extensions["yCursorPlugin"] = new CursorPlugin(
|
|
157
|
+
this.collaboration!,
|
|
158
|
+
);
|
|
159
|
+
this.editor.extensions["yUndoPlugin"] = new UndoPlugin();
|
|
160
|
+
// Register the plugins again, based on the original fragment
|
|
161
|
+
this.editor._tiptapEditor.registerPlugin(
|
|
162
|
+
this.editor.extensions["ySyncPlugin"].plugins[0],
|
|
163
|
+
);
|
|
164
|
+
this.editor._tiptapEditor.registerPlugin(
|
|
165
|
+
this.editor.extensions["yCursorPlugin"].plugins[0],
|
|
166
|
+
);
|
|
167
|
+
this.editor._tiptapEditor.registerPlugin(
|
|
168
|
+
this.editor.extensions["yUndoPlugin"].plugins[0],
|
|
169
|
+
);
|
|
170
|
+
// Reset the forked state
|
|
171
|
+
this.forkedState = undefined;
|
|
172
|
+
this.emit("forked", false);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { Plugin } from "prosemirror-state";
|
|
2
1
|
import { ySyncPlugin } from "y-prosemirror";
|
|
3
2
|
import type * as Y from "yjs";
|
|
3
|
+
import { BlockNoteExtension } from "../../editor/BlockNoteExtension.js";
|
|
4
4
|
|
|
5
|
-
export class SyncPlugin {
|
|
6
|
-
public
|
|
5
|
+
export class SyncPlugin extends BlockNoteExtension {
|
|
6
|
+
public static name() {
|
|
7
|
+
return "ySyncPlugin";
|
|
8
|
+
}
|
|
7
9
|
|
|
8
10
|
constructor(fragment: Y.XmlFragment) {
|
|
9
|
-
|
|
11
|
+
super();
|
|
12
|
+
this.addProsemirrorPlugin(ySyncPlugin(fragment));
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
public get priority() {
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { Plugin } from "prosemirror-state";
|
|
2
1
|
import { yUndoPlugin } from "y-prosemirror";
|
|
2
|
+
import { BlockNoteExtension } from "../../editor/BlockNoteExtension.js";
|
|
3
3
|
|
|
4
|
-
export class UndoPlugin {
|
|
5
|
-
public
|
|
4
|
+
export class UndoPlugin extends BlockNoteExtension {
|
|
5
|
+
public static name() {
|
|
6
|
+
return "yUndoPlugin";
|
|
7
|
+
}
|
|
6
8
|
|
|
7
9
|
constructor() {
|
|
8
|
-
|
|
10
|
+
super();
|
|
11
|
+
this.addProsemirrorPlugin(yUndoPlugin());
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
public get priority() {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"children": [],
|
|
4
|
+
"content": [
|
|
5
|
+
{
|
|
6
|
+
"styles": {},
|
|
7
|
+
"text": "Hello World",
|
|
8
|
+
"type": "text",
|
|
9
|
+
},
|
|
10
|
+
],
|
|
11
|
+
"id": "2",
|
|
12
|
+
"props": {
|
|
13
|
+
"backgroundColor": "default",
|
|
14
|
+
"textAlignment": "left",
|
|
15
|
+
"textColor": "default",
|
|
16
|
+
},
|
|
17
|
+
"type": "paragraph",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"children": [],
|
|
21
|
+
"content": [],
|
|
22
|
+
"id": "3",
|
|
23
|
+
"props": {
|
|
24
|
+
"backgroundColor": "default",
|
|
25
|
+
"textAlignment": "left",
|
|
26
|
+
"textColor": "default",
|
|
27
|
+
},
|
|
28
|
+
"type": "paragraph",
|
|
29
|
+
},
|
|
30
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"children": [],
|
|
4
|
+
"content": [
|
|
5
|
+
{
|
|
6
|
+
"styles": {},
|
|
7
|
+
"text": "Hello",
|
|
8
|
+
"type": "text",
|
|
9
|
+
},
|
|
10
|
+
],
|
|
11
|
+
"id": "0",
|
|
12
|
+
"props": {
|
|
13
|
+
"backgroundColor": "default",
|
|
14
|
+
"textAlignment": "left",
|
|
15
|
+
"textColor": "default",
|
|
16
|
+
},
|
|
17
|
+
"type": "paragraph",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"children": [],
|
|
21
|
+
"content": [],
|
|
22
|
+
"id": "1",
|
|
23
|
+
"props": {
|
|
24
|
+
"backgroundColor": "default",
|
|
25
|
+
"textAlignment": "left",
|
|
26
|
+
"textColor": "default",
|
|
27
|
+
},
|
|
28
|
+
"type": "paragraph",
|
|
29
|
+
},
|
|
30
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<blockgroup><blockcontainer backgroundColor="default" id="2" textColor="default"><paragraph textAlignment="left">Hello World</paragraph></blockcontainer><blockcontainer backgroundColor="default" id="3" textColor="default"><paragraph textAlignment="left"></paragraph></blockcontainer></blockgroup>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<blockgroup><blockcontainer backgroundColor="default" id="0" textColor="default"><paragraph textAlignment="left">Hello</paragraph></blockcontainer><blockcontainer backgroundColor="default" id="1" textColor="default"><paragraph textAlignment="left"></paragraph></blockcontainer></blockgroup>
|