UNPKG

@convex-dev/prosemirror-sync

Version:

Sync ProseMirror documents for Tiptap using this Convex component.

134 lines (129 loc) 4.18 kB
import { useMemo } from "react"; import type { SyncApi } from "../client"; import { type UseSyncOptions, useTiptapSync } from "../tiptap"; import { type Block, BlockNoteEditor, type BlockNoteEditorOptions, nodeToBlock, } from "@blocknote/core"; import { JSONContent } from "@tiptap/core"; export type BlockNoteSyncOptions<Editor = BlockNoteEditor> = UseSyncOptions & { /** * If you pass options into the editor, you should pass them here, to ensure * the initialContent is parsed with the correct schema. */ editorOptions?: Partial< // eslint-disable-next-line @typescript-eslint/no-explicit-any Omit<BlockNoteEditorOptions<any, any, any>, "initialContent"> >; /** * @deprecated Do `useBlockNoteSync<BlockNoteEditor>` instead. * */ BlockNoteEditor?: Editor; }; /** * A hook to sync a BlockNote editor with a Convex document. * * Usually used like: * * ```tsx * const sync = useBlockNoteSync(api.example, "some-id"); * ``` * * If you see an error like: * ``` * Property 'options' is protected but type 'BlockNoteEditor<BSchema, ISchema, SSchema>' is not a class derived from 'BlockNoteEditor<BSchema, ISchema, SSchema>'. * ``` * You can pass your own BlockNoteEditor like: * ```tsx * import { BlockNoteEditor } from "@blocknote/core"; * //... * const sync = useBlockNoteSync<BlockNoteEditor>(api.example, "some-id"); * ``` * This is a workaround for the types of your editor not matching the editor * version used by prosemirror-sync. * * @param syncApi Wherever you exposed the sync api, e.g. `api.example`. * @param id The document ID. * @param opts Options to pass to the underlying BlockNoteEditor and sync opts. * @returns The editor, loading state, and fn to create the initial document. */ export function useBlockNoteSync<Editor = BlockNoteEditor>( syncApi: SyncApi, id: string, opts?: BlockNoteSyncOptions<Editor> ): | { editor: null; isLoading: true; create?: (content: JSONContent) => Promise<void>; } | { editor: null; isLoading: false; create: (content: JSONContent) => Promise<void>; } | { editor: Editor; isLoading: false; } { const sync = useTiptapSync(syncApi, id, opts); const editor = useMemo(() => { if (sync.initialContent === null) return null; const editor = BlockNoteEditor.create({ ...opts?.editorOptions, _headless: true, }); // eslint-disable-next-line @typescript-eslint/no-explicit-any const blocks: Block<any, any, any>[] = []; // Convert the prosemirror document to BlockNote blocks. // inspired by https://github.com/TypeCellOS/BlockNote/blob/main/packages/server-util/src/context/ServerBlockNoteEditor.ts#L42 const pmNode = editor.pmSchema.nodeFromJSON(sync.initialContent); if (pmNode.firstChild) { pmNode.firstChild.descendants((node) => { blocks.push(nodeToBlock(node, editor.pmSchema)); return false; }); } return BlockNoteEditor.create({ ...opts?.editorOptions, _tiptapOptions: { ...opts?.editorOptions?._tiptapOptions, extensions: [ ...(opts?.editorOptions?._tiptapOptions?.extensions ?? []), sync.extension, ], }, initialContent: blocks.length > 0 ? blocks : undefined, }); }, [sync.initialContent]); if (sync.isLoading) { return { editor: null, isLoading: true, /** * Create the document without waiting to hear from the server. * Warning: Only call this if you just created the document id. * It's safer to wait until loading is false. * It's also best practice to pass in the same initial content everywhere, * so if two clients create the same document id, they'll both end up * with the same initial content. Otherwise the second client will * throw an exception on the snapshot creation. */ create: sync.create, } as const; } if (!editor) { return { editor: null, isLoading: false, create: sync.create!, } as const; } return { editor: editor as unknown as Editor, isLoading: false, } as const; }