UNPKG

@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
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";