@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
116 lines (110 loc) • 4.84 kB
text/typescript
import { configureYProsemirror, pauseSync } from "@y/prosemirror";
import * as Y from "@y/y";
import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
import type { PreviewController } from "../../extensions/Versioning/index.js";
import {
findTypeInOtherYdoc,
getProseMirrorTrFromYFragment,
} from "../utils.js";
/**
* Empties the document before a {@link configureYProsemirror} refill so
* ProseMirror rebuilds every node view instead of reusing a stale-positioned one
* (BlockNote node views resolve their block eagerly via `getPos()` and throw on
* a moved node). Sync is paused first so the clear never reaches the Y.Doc.
*
* TODO: remove once `configureYProsemirror` applies a minimal diff.
*/
function clearDocumentForConfigure(editor: BlockNoteEditor<any, any, any>) {
// Pause sync (ytype -> null) so the deletion below stays local.
editor.exec(pauseSync);
editor.removeBlocks(editor.document);
}
/**
* Creates a Yjs-specific adapter that provides the {@link PreviewController}
* and `getCurrentDocument` callback required by the base
* {@link VersioningExtension}.
*
* This is wired automatically by the {@link CollaborationExtension} when
* `versioningEndpoints` is provided. You only need to call this directly if
* you're using the `VersioningExtension` outside of the collaboration wrapper.
*/
export function createYjsVersioningAdapter(
editor: BlockNoteEditor<any, any, any>,
fragment: Y.Type,
): {
preview: PreviewController<Uint8Array, Y.ContentMap>;
getCurrentDocument: () => Y.Type;
serializeCurrentContent: () => Uint8Array;
} {
return {
getCurrentDocument: () => fragment,
// Serialise the live document as a V2 update — the same format that
// `getContent` returns (via `convertUpdateFormatV1ToV2`) and that
// `enterPreview` consumes (`applyUpdateV2`). Used to render a read-only
// diff of the live document against a snapshot.
serializeCurrentContent: () => Y.encodeStateAsUpdateV2(fragment.doc!),
preview: {
enterPreview: (
snapshotContent: Uint8Array,
compareToContent?: Uint8Array,
attributions?: Y.ContentMap,
) => {
let prevSnapshot: { fragment: Y.Type } | undefined;
if (compareToContent) {
const compareToDoc = new Y.Doc({ isSuggestionDoc: true });
Y.applyUpdateV2(compareToDoc, compareToContent);
prevSnapshot = {
fragment: findTypeInOtherYdoc(fragment, compareToDoc),
};
}
const doc = new Y.Doc();
Y.applyUpdateV2(doc, snapshotContent);
// Empty the document before reconfiguring so ProseMirror rebuilds node
// views from scratch instead of reusing stale-positioned ones. See
// clearDocumentForConfigure.
clearDocumentForConfigure(editor);
editor.exec((state, dispatch) => {
const tr = getProseMirrorTrFromYFragment({
tr: state.tr,
fragment: findTypeInOtherYdoc(fragment, doc),
// Pass the optional content map as `attrs` so the diff renderer
// knows who/when authored each change. Without it, the renderer
// only produces "what changed" (empty userIds, null timestamps) and
// downstream mark tooltips show "unknown / unknown time".
renderer: prevSnapshot
? Y.createDiffRenderer(
prevSnapshot.fragment.doc!,
doc,
attributions ? { attrs: attributions } : undefined,
)
: undefined,
});
if (dispatch) {
dispatch(tr);
}
return true;
});
},
exitPreview: () => {
// Empty the document before reconfiguring so ProseMirror rebuilds node
// views from scratch instead of reusing stale-positioned ones. See
// clearDocumentForConfigure.
clearDocumentForConfigure(editor);
editor.exec(configureYProsemirror({ ytype: fragment }));
},
applyRestore: (_snapshotContent: Uint8Array) => {
// For Yjs-backed versioning, restoration happens on the server (e.g.
// YHub's `/rollback` endpoint) which publishes a reverting update to
// the document's room. That update propagates back to this client over
// the live sync connection and updates `fragment` automatically, so
// there is nothing to apply locally — we only need to leave preview
// mode. `exitPreview` is already called by the base extension before
// this runs, so this is a no-op.
//
// Note: this assumes `endpoints.restore` performs the server-side
// restore. The default in-memory adapter has no server, which is why
// this is specific to the Yjs collaboration setup.
},
},
};
}