Skip to content

Commit 8d1916a

Browse files
authored
feat(extensions): extensions can now include other extensions for grouping into one extension (#2284)
1 parent f87fbc8 commit 8d1916a

File tree

11 files changed

+93
-75
lines changed

11 files changed

+93
-75
lines changed

packages/core/src/editor/BlockNoteEditor.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,6 @@ it("sets an initial block id when using Y.js", async () => {
136136
collaboration: {
137137
fragment,
138138
user: { name: "Hello", color: "#FFFFFF" },
139-
provider: null,
140139
},
141140
_tiptapOptions: {
142141
onTransaction: () => {

packages/core/src/editor/BlockNoteEditor.ts

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
} from "@tiptap/core";
88
import { type Command, type Plugin, type Transaction } from "@tiptap/pm/state";
99
import { Node, Schema } from "prosemirror-model";
10-
import * as Y from "yjs";
1110

1211
import type { BlocksChanged } from "../api/getBlocksChangedByTransaction.js";
1312
import { blockToNode } from "../api/nodeConversions/blockToNode.js";
@@ -53,6 +52,7 @@ import {
5352
import type { Selection } from "./selectionTypes.js";
5453
import { transformPasted } from "./transformPasted.js";
5554
import { BlockChangeExtension } from "../extensions/index.js";
55+
import type { CollaborationOptions } from "../extensions/Collaboration/Collaboration.js";
5656

5757
export type BlockCache<
5858
BSchema extends BlockSchema = any,
@@ -82,37 +82,8 @@ export interface BlockNoteEditorOptions<
8282
/**
8383
* When enabled, allows for collaboration between multiple users.
8484
* See [Real-time Collaboration](https://www.blocknotejs.org/docs/advanced/real-time-collaboration) for more info.
85-
*
86-
* @remarks `CollaborationOptions`
8785
*/
88-
collaboration?: {
89-
/**
90-
* The Yjs XML fragment that's used for collaboration.
91-
*/
92-
fragment: Y.XmlFragment;
93-
/**
94-
* The user info for the current user that's shown to other collaborators.
95-
*/
96-
user: {
97-
name: string;
98-
color: string;
99-
};
100-
/**
101-
* A Yjs provider (used for awareness / cursor information)
102-
*/
103-
provider: any;
104-
/**
105-
* Optional function to customize how cursors of users are rendered
106-
*/
107-
renderCursor?: (user: any) => HTMLElement;
108-
/**
109-
* Optional flag to set when the user label should be shown with the default
110-
* collaboration cursor. Setting to "always" will always show the label,
111-
* while "activity" will only show the label when the user moves the cursor
112-
* or types. Defaults to "activity".
113-
*/
114-
showCursorLabels?: "always" | "activity";
115-
};
86+
collaboration?: CollaborationOptions;
11687

11788
/**
11889
* Use default BlockNote font and reset the styles of <p> <li> <h1> elements etc., that are used in BlockNote.
@@ -912,7 +883,7 @@ export class BlockNoteEditor<
912883
*/
913884
public onBeforeChange(
914885
callback: (context: {
915-
getChanges: () => BlocksChanged<any, any, any>;
886+
getChanges: () => BlocksChanged<BSchema, ISchema, SSchema>;
916887
tr: Transaction;
917888
}) => boolean | void,
918889
): () => void {

packages/core/src/editor/BlockNoteExtension.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ export interface Extension<State = any, Key extends string = string> {
8989
* Add additional tiptap extensions to the editor.
9090
*/
9191
readonly tiptapExtensions?: ReadonlyArray<AnyExtension>;
92+
93+
/**
94+
* Add additional BlockNote extensions to the editor.
95+
*/
96+
readonly blockNoteExtensions?: ReadonlyArray<ExtensionFactoryInstance>;
9297
}
9398

9499
/**

packages/core/src/editor/managers/EventManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export class EventManager<
8989
}
9090
callback(this.editor, {
9191
getChanges() {
92-
return getBlocksChangedByTransaction(
92+
return getBlocksChangedByTransaction<BSchema, I, S>(
9393
transaction,
9494
appendedTransactions,
9595
);

packages/core/src/editor/managers/ExtensionManager/extensions.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,17 @@ import {
1414
BlockChangeExtension,
1515
DropCursorExtension,
1616
FilePanelExtension,
17-
ForkYDocExtension,
1817
FormattingToolbarExtension,
1918
HistoryExtension,
2019
LinkToolbarExtension,
2120
NodeSelectionKeyboardExtension,
2221
PlaceholderExtension,
2322
PreviousBlockTypeExtension,
24-
SchemaMigration,
2523
ShowSelectionExtension,
2624
SideMenuExtension,
2725
SuggestionMenu,
2826
TableHandlesExtension,
2927
TrailingNodeExtension,
30-
YCursorExtension,
31-
YSyncExtension,
32-
YUndoExtension,
3328
} from "../../../extensions/index.js";
3429
import {
3530
DEFAULT_LINK_PROTOCOL,
@@ -52,6 +47,7 @@ import {
5247
BlockNoteEditorOptions,
5348
} from "../../BlockNoteEditor.js";
5449
import { ExtensionFactoryInstance } from "../../BlockNoteExtension.js";
50+
import { CollaborationExtension } from "../../../extensions/Collaboration/Collaboration.js";
5551

5652
// TODO remove linkify completely by vendoring the link extension & dropping linkifyjs as a dependency
5753
let LINKIFY_INITIALIZED = false;
@@ -190,11 +186,7 @@ export function getDefaultExtensions(
190186
] as ExtensionFactoryInstance[];
191187

192188
if (options.collaboration) {
193-
extensions.push(ForkYDocExtension(options.collaboration));
194-
extensions.push(YCursorExtension(options.collaboration));
195-
extensions.push(YSyncExtension(options.collaboration));
196-
extensions.push(YUndoExtension());
197-
extensions.push(SchemaMigration(options.collaboration));
189+
extensions.push(CollaborationExtension(options.collaboration));
198190
} else {
199191
// YUndo is not compatible with ProseMirror's history plugin
200192
extensions.push(HistoryExtension());

packages/core/src/editor/managers/ExtensionManager/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,12 @@ export class ExtensionManager {
203203

204204
this.extensions.push(instance);
205205

206+
if (instance.blockNoteExtensions) {
207+
for (const extension of instance.blockNoteExtensions) {
208+
this.addExtension(extension);
209+
}
210+
}
211+
206212
return instance as any;
207213
}
208214

packages/core/src/extensions/BlockChange/BlockChange.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const BlockChangeExtension = createExtension(() => {
2020
key: new PluginKey("blockChange"),
2121
filterTransaction: (tr) => {
2222
let changes:
23-
| ReturnType<typeof getBlocksChangedByTransaction>
23+
| ReturnType<typeof getBlocksChangedByTransaction<any, any, any>>
2424
| undefined = undefined;
2525

2626
return beforeChangeCallbacks.reduce((acc, cb) => {
@@ -34,7 +34,7 @@ export const BlockChangeExtension = createExtension(() => {
3434
if (changes) {
3535
return changes;
3636
}
37-
changes = getBlocksChangedByTransaction(tr);
37+
changes = getBlocksChangedByTransaction<any, any, any>(tr);
3838
return changes;
3939
},
4040
tr,
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type * as Y from "yjs";
2+
import type { Awareness } from "y-protocols/awareness";
3+
import {
4+
createExtension,
5+
ExtensionOptions,
6+
} from "../../editor/BlockNoteExtension.js";
7+
import { ForkYDocExtension } from "./ForkYDoc.js";
8+
import { SchemaMigration } from "./schemaMigration/SchemaMigration.js";
9+
import { YCursorExtension } from "./YCursorPlugin.js";
10+
import { YSyncExtension } from "./YSync.js";
11+
import { YUndoExtension } from "./YUndo.js";
12+
13+
export type CollaborationOptions = {
14+
/**
15+
* The Yjs XML fragment that's used for collaboration.
16+
*/
17+
fragment: Y.XmlFragment;
18+
/**
19+
* The user info for the current user that's shown to other collaborators.
20+
*/
21+
user: {
22+
name: string;
23+
color: string;
24+
};
25+
/**
26+
* A Yjs provider (used for awareness / cursor information)
27+
*/
28+
provider?: { awareness?: Awareness };
29+
/**
30+
* Optional function to customize how cursors of users are rendered
31+
*/
32+
renderCursor?: (user: any) => HTMLElement;
33+
/**
34+
* Optional flag to set when the user label should be shown with the default
35+
* collaboration cursor. Setting to "always" will always show the label,
36+
* while "activity" will only show the label when the user moves the cursor
37+
* or types. Defaults to "activity".
38+
*/
39+
showCursorLabels?: "always" | "activity";
40+
};
41+
42+
export const CollaborationExtension = createExtension(
43+
({ options }: ExtensionOptions<CollaborationOptions>) => {
44+
return {
45+
key: "collaboration",
46+
blockNoteExtensions: [
47+
ForkYDocExtension(options),
48+
YCursorExtension(options),
49+
YSyncExtension(options),
50+
YUndoExtension(),
51+
SchemaMigration(options),
52+
],
53+
} as const;
54+
},
55+
);

packages/core/src/extensions/Collaboration/ForkYDoc.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import {
55
createStore,
66
ExtensionOptions,
77
} from "../../editor/BlockNoteExtension.js";
8+
import { CollaborationOptions } from "./Collaboration.js";
89
import { YCursorExtension } from "./YCursorPlugin.js";
910
import { YSyncExtension } from "./YSync.js";
1011
import { YUndoExtension } from "./YUndo.js";
11-
import { BlockNoteEditorOptions } from "../../editor/BlockNoteEditor.js";
1212

1313
/**
1414
* To find a fragment in another ydoc, we need to search for it.
@@ -44,12 +44,7 @@ function findTypeInOtherYdoc<T extends Y.AbstractType<any>>(
4444
}
4545

4646
export const ForkYDocExtension = createExtension(
47-
({
48-
editor,
49-
options,
50-
}: ExtensionOptions<
51-
NonNullable<BlockNoteEditorOptions<any, any, any>["collaboration"]>
52-
>) => {
47+
({ editor, options }: ExtensionOptions<CollaborationOptions>) => {
5348
let forkedState:
5449
| {
5550
originalFragment: Y.XmlFragment;

packages/core/src/extensions/Collaboration/YCursorPlugin.ts

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
createExtension,
44
ExtensionOptions,
55
} from "../../editor/BlockNoteExtension.js";
6-
import { BlockNoteEditorOptions } from "../../editor/BlockNoteEditor.js";
6+
import { CollaborationOptions } from "./Collaboration.js";
77

88
export type CollaborationUser = {
99
name: string;
@@ -67,29 +67,24 @@ function defaultCursorRender(user: CollaborationUser) {
6767
}
6868

6969
export const YCursorExtension = createExtension(
70-
({
71-
options,
72-
}: ExtensionOptions<
73-
NonNullable<BlockNoteEditorOptions<any, any, any>["collaboration"]>
74-
>) => {
70+
({ options }: ExtensionOptions<CollaborationOptions>) => {
7571
const recentlyUpdatedCursors = new Map();
76-
const hasAwareness =
72+
const awareness =
7773
options.provider &&
7874
"awareness" in options.provider &&
79-
typeof options.provider.awareness === "object";
80-
if (hasAwareness) {
75+
typeof options.provider.awareness === "object"
76+
? options.provider.awareness
77+
: undefined;
78+
if (awareness) {
8179
if (
82-
"setLocalStateField" in options.provider.awareness &&
83-
typeof options.provider.awareness.setLocalStateField === "function"
80+
"setLocalStateField" in awareness &&
81+
typeof awareness.setLocalStateField === "function"
8482
) {
85-
options.provider.awareness.setLocalStateField("user", options.user);
83+
awareness.setLocalStateField("user", options.user);
8684
}
87-
if (
88-
"on" in options.provider.awareness &&
89-
typeof options.provider.awareness.on === "function"
90-
) {
85+
if ("on" in awareness && typeof awareness.on === "function") {
9186
if (options.showCursorLabels !== "always") {
92-
options.provider.awareness.on(
87+
awareness.on(
9388
"change",
9489
({
9590
updated,
@@ -125,8 +120,8 @@ export const YCursorExtension = createExtension(
125120
return {
126121
key: "yCursor",
127122
prosemirrorPlugins: [
128-
hasAwareness
129-
? yCursorPlugin(options.provider.awareness, {
123+
awareness
124+
? yCursorPlugin(awareness, {
130125
selectionBuilder: defaultSelectionBuilder,
131126
cursorBuilder(user: CollaborationUser, clientID: number) {
132127
let cursorData = recentlyUpdatedCursors.get(clientID);
@@ -177,7 +172,7 @@ export const YCursorExtension = createExtension(
177172
].filter(Boolean),
178173
dependsOn: ["ySync"],
179174
updateUser(user: { name: string; color: string; [key: string]: string }) {
180-
options.provider.awareness.setLocalStateField("user", user);
175+
awareness?.setLocalStateField("user", user);
181176
},
182177
} as const;
183178
},

0 commit comments

Comments
 (0)