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)],