@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
157 lines (150 loc) • 6.02 kB
text/typescript
import type { Awareness } from "@y/protocols/awareness";
import type * as Y from "@y/y";
import { BlockNoteEditorOptions } from "../../editor/BlockNoteEditor.js";
import {
createExtension,
ExtensionOptions,
} from "../../editor/BlockNoteExtension.js";
import {
VersioningEndpoints,
VersioningEndpointsFactory,
VersioningExtension,
} from "../../extensions/Versioning/index.js";
import { normalizeToUserStore, UserStoreOrResolver } from "../../user/index.js";
import { AttributionExtension } from "./AttributionExtension.js";
import { RelativePositionMappingExtension } from "./RelativePositionMapping.js";
import { SuggestionsExtension } from "./Suggestions.js";
import { createYjsVersioningAdapter } from "./Versioning.js";
import { CollaborationUser, YCursorExtension } from "./YCursorPlugin.js";
import type { GetAttributionMarkClassName } from "./YAttributionMarks.js";
import { YSyncExtension } from "./YSync.js";
export type CollaborationOptions = {
/**
* The Yjs Type that's used for collaboration.
*/
fragment: Y.Type;
/**
* The user info for the current user that's shown to other collaborators.
*/
user: CollaborationUser;
/**
* Resolve user information (usernames, colors) for suggestions and versions,
* used to drive suggestion-author tooltips, suggestion colors and
* version-history author labels.
*
* Either a resolver function (called with the ids of users that are not yet
* cached, returning their information — a user store is built from it
* internally) or a pre-built user store (see `createUserStore`). Pass the same
* store you give the comments extension so a single de-duped user cache is
* shared across comments, suggestions and versioning.
*/
resolveUsers?: UserStoreOrResolver;
/**
* A Yjs provider (used for awareness / cursor information)
*/
provider?: { awareness?: Awareness };
/**
* Optional function to customize how cursors of users are rendered
*/
renderCursor?: (user: CollaborationUser) => 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";
/**
* The renderer for the collaboration.
*/
renderer?: Y.DiffRenderer;
/**
* The suggestion doc for the collaboration. If using suggestion mode
*/
suggestionDoc?: Y.Doc;
/**
* Optional callback to override suggestion-mark styling by change type instead
* of by author. Given a mark's content/modification type, return a class name
* applied to the mark and its hover tooltip; the per-user color is then
* dropped for that mark. See {@link GetAttributionMarkClassName}.
*/
getAttributionMarkClassName?: GetAttributionMarkClassName;
/**
* The endpoints for the versioning functionality.
*/
versioningEndpoints?:
| VersioningEndpoints<Y.Type, Uint8Array>
| VersioningEndpointsFactory<Y.Type, Uint8Array>;
};
export const CollaborationExtension = createExtension(
({ editor, options }: ExtensionOptions<CollaborationOptions>) => {
// Build a single user store here (from a resolver callback or a store the
// consumer passed in) and hand that same store down to every sub-extension
// that needs it — suggestions, suggestion-mark tooltips/colors, versioning —
// so they share one de-duped cache. Passing the resolved store (rather than
// the raw resolver) is what guarantees the sharing: each child re-normalizes
// it to itself instead of building its own.
const userStore = normalizeToUserStore(options.resolveUsers);
const optionsWithUserStore = { ...options, resolveUsers: userStore };
return {
key: "collaboration",
userStore,
blockNoteExtensions: [
options.suggestionDoc
? SuggestionsExtension(optionsWithUserStore)
: null,
RelativePositionMappingExtension(),
YSyncExtension(optionsWithUserStore),
YCursorExtension(options),
options.versioningEndpoints
? VersioningExtension({
...createYjsVersioningAdapter(editor, options.fragment),
endpoints: options.versioningEndpoints,
resolveUsers: userStore,
})
: null,
AttributionExtension({
resolveUsers: userStore,
getAttributionMarkClassName: options.getAttributionMarkClassName,
}),
].filter((a) => a !== null),
} as const;
},
);
export function withCollaboration<
Options extends Partial<BlockNoteEditorOptions<any, any, any>>,
>(
options: Options & {
/**
* Options for configuring the collaboration functionality.
*/
collaboration: CollaborationOptions;
},
): Options {
if (options.initialContent) {
// eslint-disable-next-line no-console
console.warn(
"When using Collaboration, initialContent might cause conflicts, because changes should come from the collaboration provider",
);
}
return {
...options,
extensions: [
...(options.extensions ?? []),
CollaborationExtension(options.collaboration),
],
// We disable the default prosemirror history plugin, since it's not compatible with yjs
disableExtensions: ["history", ...(options.disableExtensions ?? [])],
// We don't want the default initial content, since it will generate a random id for the initial block on each client,
// leading to conflicts when syncing happens afterwards.
initialContent: [{ type: "paragraph", id: "initialBlockId" }],
};
}
export * from "./AttributionExtension.js";
export * from "./DiffVersioningExtension.js";
export * from "./RelativePositionMapping.js";
export * from "./Suggestions.js";
export * from "./Versioning.js";
export * from "./YAttributionMarks.js";
export * from "./YCursorPlugin.js";
export * from "./YSync.js";