diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts
index c048a7eed9..13f5bfeaba 100644
--- a/packages/core/src/editor/BlockNoteEditor.ts
+++ b/packages/core/src/editor/BlockNoteEditor.ts
@@ -7,7 +7,6 @@ import {
} from "@tiptap/core";
import { type Command, type Plugin, type Transaction } from "@tiptap/pm/state";
import { Node, Schema } from "prosemirror-model";
-import * as Y from "yjs";
import type { BlocksChanged } from "../api/getBlocksChangedByTransaction.js";
import { blockToNode } from "../api/nodeConversions/blockToNode.js";
@@ -53,6 +52,7 @@ import {
import type { Selection } from "./selectionTypes.js";
import { transformPasted } from "./transformPasted.js";
import { BlockChangeExtension } from "../extensions/index.js";
+import type { CollaborationOptions } from "../extensions/Collaboration/Collaboration.js";
export type BlockCache<
BSchema extends BlockSchema = any,
@@ -82,37 +82,8 @@ export interface BlockNoteEditorOptions<
/**
* When enabled, allows for collaboration between multiple users.
* See [Real-time Collaboration](https://www.blocknotejs.org/docs/advanced/real-time-collaboration) for more info.
- *
- * @remarks `CollaborationOptions`
*/
- collaboration?: {
- /**
- * The Yjs XML fragment that's used for collaboration.
- */
- fragment: Y.XmlFragment;
- /**
- * The user info for the current user that's shown to other collaborators.
- */
- user: {
- name: string;
- color: string;
- };
- /**
- * A Yjs provider (used for awareness / cursor information)
- */
- provider: any;
- /**
- * Optional function to customize how cursors of users are rendered
- */
- renderCursor?: (user: any) => HTMLElement;
- /**
- * Optional flag to set when the user label should be shown with the default
- * collaboration cursor. Setting to "always" will always show the label,
- * while "activity" will only show the label when the user moves the cursor
- * or types. Defaults to "activity".
- */
- showCursorLabels?: "always" | "activity";
- };
+ collaboration?: CollaborationOptions;
/**
* Use default BlockNote font and reset the styles of
elements etc., that are used in BlockNote.
diff --git a/packages/core/src/editor/BlockNoteExtension.ts b/packages/core/src/editor/BlockNoteExtension.ts
index 22a59e41d3..7346333990 100644
--- a/packages/core/src/editor/BlockNoteExtension.ts
+++ b/packages/core/src/editor/BlockNoteExtension.ts
@@ -89,6 +89,11 @@ export interface Extension {
* Add additional tiptap extensions to the editor.
*/
readonly tiptapExtensions?: ReadonlyArray;
+
+ /**
+ * Add additional BlockNote extensions to the editor.
+ */
+ readonly blockNoteExtensions?: ReadonlyArray;
}
/**
diff --git a/packages/core/src/editor/managers/ExtensionManager/extensions.ts b/packages/core/src/editor/managers/ExtensionManager/extensions.ts
index 5af4f625fb..fb86c4a332 100644
--- a/packages/core/src/editor/managers/ExtensionManager/extensions.ts
+++ b/packages/core/src/editor/managers/ExtensionManager/extensions.ts
@@ -14,22 +14,17 @@ import {
BlockChangeExtension,
DropCursorExtension,
FilePanelExtension,
- ForkYDocExtension,
FormattingToolbarExtension,
HistoryExtension,
LinkToolbarExtension,
NodeSelectionKeyboardExtension,
PlaceholderExtension,
PreviousBlockTypeExtension,
- SchemaMigration,
ShowSelectionExtension,
SideMenuExtension,
SuggestionMenu,
TableHandlesExtension,
TrailingNodeExtension,
- YCursorExtension,
- YSyncExtension,
- YUndoExtension,
} from "../../../extensions/index.js";
import {
DEFAULT_LINK_PROTOCOL,
@@ -52,6 +47,7 @@ import {
BlockNoteEditorOptions,
} from "../../BlockNoteEditor.js";
import { ExtensionFactoryInstance } from "../../BlockNoteExtension.js";
+import { CollaborationExtension } from "../../../extensions/Collaboration/Collaboration.js";
// TODO remove linkify completely by vendoring the link extension & dropping linkifyjs as a dependency
let LINKIFY_INITIALIZED = false;
@@ -190,11 +186,7 @@ export function getDefaultExtensions(
] as ExtensionFactoryInstance[];
if (options.collaboration) {
- extensions.push(ForkYDocExtension(options.collaboration));
- extensions.push(YCursorExtension(options.collaboration));
- extensions.push(YSyncExtension(options.collaboration));
- extensions.push(YUndoExtension());
- extensions.push(SchemaMigration(options.collaboration));
+ extensions.push(CollaborationExtension(options.collaboration));
} else {
// YUndo is not compatible with ProseMirror's history plugin
extensions.push(HistoryExtension());
diff --git a/packages/core/src/editor/managers/ExtensionManager/index.ts b/packages/core/src/editor/managers/ExtensionManager/index.ts
index d09820a150..67b50871ed 100644
--- a/packages/core/src/editor/managers/ExtensionManager/index.ts
+++ b/packages/core/src/editor/managers/ExtensionManager/index.ts
@@ -203,6 +203,12 @@ export class ExtensionManager {
this.extensions.push(instance);
+ if (instance.blockNoteExtensions) {
+ for (const extension of instance.blockNoteExtensions) {
+ this.addExtension(extension);
+ }
+ }
+
return instance as any;
}
diff --git a/packages/core/src/extensions/Collaboration/Collaboration.ts b/packages/core/src/extensions/Collaboration/Collaboration.ts
new file mode 100644
index 0000000000..719a7bdc8d
--- /dev/null
+++ b/packages/core/src/extensions/Collaboration/Collaboration.ts
@@ -0,0 +1,55 @@
+import type * as Y from "yjs";
+import type { Awareness } from "y-protocols/awareness";
+import {
+ createExtension,
+ ExtensionOptions,
+} from "../../editor/BlockNoteExtension.js";
+import { ForkYDocExtension } from "./ForkYDoc.js";
+import { SchemaMigration } from "./schemaMigration/SchemaMigration.js";
+import { YCursorExtension } from "./YCursorPlugin.js";
+import { YSyncExtension } from "./YSync.js";
+import { YUndoExtension } from "./YUndo.js";
+
+export type CollaborationOptions = {
+ /**
+ * The Yjs XML fragment that's used for collaboration.
+ */
+ fragment: Y.XmlFragment;
+ /**
+ * The user info for the current user that's shown to other collaborators.
+ */
+ user: {
+ name: string;
+ color: string;
+ };
+ /**
+ * A Yjs provider (used for awareness / cursor information)
+ */
+ provider?: { awareness?: Awareness };
+ /**
+ * Optional function to customize how cursors of users are rendered
+ */
+ renderCursor?: (user: any) => HTMLElement;
+ /**
+ * Optional flag to set when the user label should be shown with the default
+ * collaboration cursor. Setting to "always" will always show the label,
+ * while "activity" will only show the label when the user moves the cursor
+ * or types. Defaults to "activity".
+ */
+ showCursorLabels?: "always" | "activity";
+};
+
+export const CollaborationExtension = createExtension(
+ ({ options }: ExtensionOptions) => {
+ return {
+ key: "collaboration",
+ blockNoteExtensions: [
+ ForkYDocExtension(options),
+ YCursorExtension(options),
+ YSyncExtension(options),
+ YUndoExtension(),
+ SchemaMigration(options),
+ ],
+ } as const;
+ },
+);
diff --git a/packages/core/src/extensions/Collaboration/ForkYDoc.ts b/packages/core/src/extensions/Collaboration/ForkYDoc.ts
index b2958c56f1..84c714f1d3 100644
--- a/packages/core/src/extensions/Collaboration/ForkYDoc.ts
+++ b/packages/core/src/extensions/Collaboration/ForkYDoc.ts
@@ -5,10 +5,10 @@ import {
createStore,
ExtensionOptions,
} from "../../editor/BlockNoteExtension.js";
+import { CollaborationOptions } from "./Collaboration.js";
import { YCursorExtension } from "./YCursorPlugin.js";
import { YSyncExtension } from "./YSync.js";
import { YUndoExtension } from "./YUndo.js";
-import { BlockNoteEditorOptions } from "../../editor/BlockNoteEditor.js";
/**
* To find a fragment in another ydoc, we need to search for it.
@@ -44,12 +44,7 @@ function findTypeInOtherYdoc>(
}
export const ForkYDocExtension = createExtension(
- ({
- editor,
- options,
- }: ExtensionOptions<
- NonNullable["collaboration"]>
- >) => {
+ ({ editor, options }: ExtensionOptions) => {
let forkedState:
| {
originalFragment: Y.XmlFragment;
diff --git a/packages/core/src/extensions/Collaboration/YCursorPlugin.ts b/packages/core/src/extensions/Collaboration/YCursorPlugin.ts
index 3bf17f574e..784c4a17b7 100644
--- a/packages/core/src/extensions/Collaboration/YCursorPlugin.ts
+++ b/packages/core/src/extensions/Collaboration/YCursorPlugin.ts
@@ -3,7 +3,7 @@ import {
createExtension,
ExtensionOptions,
} from "../../editor/BlockNoteExtension.js";
-import { BlockNoteEditorOptions } from "../../editor/BlockNoteEditor.js";
+import { CollaborationOptions } from "./Collaboration.js";
export type CollaborationUser = {
name: string;
@@ -67,29 +67,24 @@ function defaultCursorRender(user: CollaborationUser) {
}
export const YCursorExtension = createExtension(
- ({
- options,
- }: ExtensionOptions<
- NonNullable["collaboration"]>
- >) => {
+ ({ options }: ExtensionOptions) => {
const recentlyUpdatedCursors = new Map();
- const hasAwareness =
+ const awareness =
options.provider &&
"awareness" in options.provider &&
- typeof options.provider.awareness === "object";
- if (hasAwareness) {
+ typeof options.provider.awareness === "object"
+ ? options.provider.awareness
+ : undefined;
+ if (awareness) {
if (
- "setLocalStateField" in options.provider.awareness &&
- typeof options.provider.awareness.setLocalStateField === "function"
+ "setLocalStateField" in awareness &&
+ typeof awareness.setLocalStateField === "function"
) {
- options.provider.awareness.setLocalStateField("user", options.user);
+ awareness.setLocalStateField("user", options.user);
}
- if (
- "on" in options.provider.awareness &&
- typeof options.provider.awareness.on === "function"
- ) {
+ if ("on" in awareness && typeof awareness.on === "function") {
if (options.showCursorLabels !== "always") {
- options.provider.awareness.on(
+ awareness.on(
"change",
({
updated,
@@ -125,8 +120,8 @@ export const YCursorExtension = createExtension(
return {
key: "yCursor",
prosemirrorPlugins: [
- hasAwareness
- ? yCursorPlugin(options.provider.awareness, {
+ awareness
+ ? yCursorPlugin(awareness, {
selectionBuilder: defaultSelectionBuilder,
cursorBuilder(user: CollaborationUser, clientID: number) {
let cursorData = recentlyUpdatedCursors.get(clientID);
@@ -177,7 +172,7 @@ export const YCursorExtension = createExtension(
].filter(Boolean),
dependsOn: ["ySync"],
updateUser(user: { name: string; color: string; [key: string]: string }) {
- options.provider.awareness.setLocalStateField("user", user);
+ awareness?.setLocalStateField("user", user);
},
} as const;
},
diff --git a/packages/core/src/extensions/Collaboration/YSync.ts b/packages/core/src/extensions/Collaboration/YSync.ts
index 3c1898f523..f4641cb41d 100644
--- a/packages/core/src/extensions/Collaboration/YSync.ts
+++ b/packages/core/src/extensions/Collaboration/YSync.ts
@@ -1,12 +1,12 @@
import { ySyncPlugin } from "y-prosemirror";
-import { XmlFragment } from "yjs";
import {
ExtensionOptions,
createExtension,
} from "../../editor/BlockNoteExtension.js";
+import { CollaborationOptions } from "./Collaboration.js";
export const YSyncExtension = createExtension(
- ({ options }: ExtensionOptions<{ fragment: XmlFragment }>) => {
+ ({ options }: ExtensionOptions>) => {
return {
key: "ySync",
prosemirrorPlugins: [ySyncPlugin(options.fragment)],