UNPKG

@blocknote/core

Version:

A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.

173 lines (156 loc) 4.92 kB
import * as Y from "yjs"; import { absolutePositionToRelativePosition, ySyncPluginKey, } from "y-prosemirror"; import type { CommentBody } from "../../comments/types.js"; import type { ThreadStoreAuth } from "../../comments/threadstore/ThreadStoreAuth.js"; import { YjsThreadStoreBase } from "./YjsThreadStoreBase.js"; import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; /** * This is a REST-based implementation of the YjsThreadStoreBase. * It Reads data directly from the underlying document (same as YjsThreadStore), * but for Writes, it sends data to a REST API that should: * - check the user has the correct permissions to make the desired changes * - apply the updates to the underlying Yjs document * * (see https://github.com/TypeCellOS/BlockNote-demo-nextjs-hocuspocus) * * The reason we still use the Yjs document as underlying storage is that it makes it easy to * sync updates in real-time to other collaborators. * (but technically, you could also implement a different storage altogether * and not store the thread related data in the Yjs document) */ export class RESTYjsThreadStore extends YjsThreadStoreBase { constructor( private readonly BASE_URL: string, private readonly headers: Record<string, string>, threadsYMap: Y.Map<any>, auth: ThreadStoreAuth, ) { super(threadsYMap, auth); } private doRequest = async (path: string, method: string, body?: any) => { const response = await fetch(`${this.BASE_URL}${path}`, { method, body: JSON.stringify(body), headers: { "Content-Type": "application/json", ...this.headers, }, }); if (!response.ok) { throw new Error(`Failed to ${method} ${path}: ${response.statusText}`); } return response.json(); }; public addThreadToDocument = async (options: { threadId: string; selection: { head: number; anchor: number; }; editor: BlockNoteEditor<any, any, any>; }) => { const { threadId, selection } = options; // Note: the positions have to be resolved against the *binding's* type and // mapping. The plugin state has a `type` of its own, but no `mapping`, and // its `type` can go stale (e.g. while the doc is forked, see // `ForkYDocExtension`). const binding = ySyncPluginKey.getState( options.editor.prosemirrorState, )?.binding; const yjsSelection = binding ? { head: absolutePositionToRelativePosition( selection.head, binding.type, binding.mapping, ), anchor: absolutePositionToRelativePosition( selection.anchor, binding.type, binding.mapping, ), } : undefined; return this.doRequest(`/${threadId}/addToDocument`, "POST", { selection: { prosemirror: selection, yjs: yjsSelection, }, }); }; public createThread = async (options: { initialComment: { body: CommentBody; metadata?: any; }; metadata?: any; }) => { return this.doRequest("", "POST", options); }; public addComment = (options: { comment: { body: CommentBody; metadata?: any; }; threadId: string; }) => { const { threadId, ...rest } = options; return this.doRequest(`/${threadId}/comments`, "POST", rest); }; public updateComment = (options: { comment: { body: CommentBody; metadata?: any; }; threadId: string; commentId: string; }) => { const { threadId, commentId, ...rest } = options; return this.doRequest(`/${threadId}/comments/${commentId}`, "PUT", rest); }; public deleteComment = (options: { threadId: string; commentId: string; softDelete?: boolean; }) => { const { threadId, commentId, ...rest } = options; return this.doRequest( `/${threadId}/comments/${commentId}?soft=${!!rest.softDelete}`, "DELETE", ); }; public deleteThread = (options: { threadId: string }) => { return this.doRequest(`/${options.threadId}`, "DELETE"); }; public resolveThread = (options: { threadId: string }) => { return this.doRequest(`/${options.threadId}/resolve`, "POST"); }; public unresolveThread = (options: { threadId: string }) => { return this.doRequest(`/${options.threadId}/unresolve`, "POST"); }; public addReaction = (options: { threadId: string; commentId: string; emoji: string; }) => { const { threadId, commentId, ...rest } = options; return this.doRequest( `/${threadId}/comments/${commentId}/reactions`, "POST", rest, ); }; public deleteReaction = (options: { threadId: string; commentId: string; emoji: string; }) => { return this.doRequest( `/${options.threadId}/comments/${options.commentId}/reactions/${options.emoji}`, "DELETE", ); }; }