Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 2 additions & 31 deletions packages/core/src/editor/BlockNoteEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 <p> <li> <h1> elements etc., that are used in BlockNote.
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/editor/BlockNoteExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ export interface Extension<State = any, Key extends string = string> {
* Add additional tiptap extensions to the editor.
*/
readonly tiptapExtensions?: ReadonlyArray<AnyExtension>;

/**
* Add additional BlockNote extensions to the editor.
*/
readonly blockNoteExtensions?: ReadonlyArray<ExtensionFactoryInstance>;
}

/**
Expand Down
12 changes: 2 additions & 10 deletions packages/core/src/editor/managers/ExtensionManager/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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());
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/editor/managers/ExtensionManager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
55 changes: 55 additions & 0 deletions packages/core/src/extensions/Collaboration/Collaboration.ts
Original file line number Diff line number Diff line change
@@ -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<CollaborationOptions>) => {
return {
key: "collaboration",
blockNoteExtensions: [
ForkYDocExtension(options),
YCursorExtension(options),
YSyncExtension(options),
YUndoExtension(),
SchemaMigration(options),
],
} as const;
},
);
9 changes: 2 additions & 7 deletions packages/core/src/extensions/Collaboration/ForkYDoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -44,12 +44,7 @@ function findTypeInOtherYdoc<T extends Y.AbstractType<any>>(
}

export const ForkYDocExtension = createExtension(
({
editor,
options,
}: ExtensionOptions<
NonNullable<BlockNoteEditorOptions<any, any, any>["collaboration"]>
>) => {
({ editor, options }: ExtensionOptions<CollaborationOptions>) => {
let forkedState:
| {
originalFragment: Y.XmlFragment;
Expand Down
35 changes: 15 additions & 20 deletions packages/core/src/extensions/Collaboration/YCursorPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -67,29 +67,24 @@ function defaultCursorRender(user: CollaborationUser) {
}

export const YCursorExtension = createExtension(
({
options,
}: ExtensionOptions<
NonNullable<BlockNoteEditorOptions<any, any, any>["collaboration"]>
>) => {
({ options }: ExtensionOptions<CollaborationOptions>) => {
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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
},
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/extensions/Collaboration/YSync.ts
Original file line number Diff line number Diff line change
@@ -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<Pick<CollaborationOptions, "fragment">>) => {
return {
key: "ySync",
prosemirrorPlugins: [ySyncPlugin(options.fragment)],
Expand Down
Loading