UNPKG

@liveblocks/node-prosemirror

Version:

A server-side utility that lets you modify prosemirror and tiptap documents hosted in Liveblocks.

249 lines (243 loc) 6.82 kB
// src/index.ts import { detectDupes } from "@liveblocks/core"; // src/version.ts var PKG_NAME = "@liveblocks/node-prosemirror"; var PKG_VERSION = "3.4.0"; var PKG_FORMAT = "esm"; // src/document.ts import { getSchema, getText } from "@tiptap/core"; import { EditorState } from "@tiptap/pm/state"; import StarterKit from "@tiptap/starter-kit"; import { defaultMarkdownSerializer } from "prosemirror-markdown"; import { initProseMirrorDoc, updateYFragment } from "y-prosemirror"; import { applyUpdate, Doc, encodeStateAsUpdate, encodeStateVector } from "yjs"; // src/comment.ts import { Mark, mergeAttributes } from "@tiptap/core"; var LIVEBLOCKS_COMMENT_MARK_TYPE = "liveblocksCommentMark"; var CommentExtension = Mark.create({ name: LIVEBLOCKS_COMMENT_MARK_TYPE, excludes: "", inclusive: false, keepOnSplit: true, addAttributes() { return { orphan: { parseHTML: (element) => !!element.getAttribute("data-orphan"), renderHTML: (attributes) => { return attributes.orphan ? { "data-orphan": "true" } : {}; }, default: false }, threadId: { parseHTML: (element) => element.getAttribute("data-lb-thread-id"), renderHTML: (attributes) => { return { "data-lb-thread-id": attributes.threadId }; }, default: "" } }; }, renderHTML({ HTMLAttributes }) { return [ "span", mergeAttributes(HTMLAttributes, { class: "lb-root lb-tiptap-thread-mark" }) ]; } }); // src/mention.ts import { mergeAttributes as mergeAttributes2, Node } from "@tiptap/core"; var LIVEBLOCKS_MENTION_TYPE = "liveblocksMention"; var MentionExtension = Node.create({ name: LIVEBLOCKS_MENTION_TYPE, group: "inline", inline: true, selectable: true, atom: true, priority: 101, parseHTML() { return [ { tag: "liveblocks-mention" } ]; }, renderHTML({ HTMLAttributes }) { return ["liveblocks-mention", mergeAttributes2(HTMLAttributes)]; }, addAttributes() { return { id: { default: null, parseHTML: (element) => element.getAttribute("data-id"), renderHTML: (attributes) => { if (!attributes.id) { return {}; } return { "data-id": attributes.id // "as" typing because TipTap doesn't have a way to type attributes }; } }, notificationId: { default: null, parseHTML: (element) => element.getAttribute("data-notification-id"), renderHTML: (attributes) => { if (!attributes.notificationId) { return {}; } return { "data-notification-id": attributes.notificationId // "as" typing because TipTap doesn't have a way to type attributes }; } } }; } }); // src/document.ts var DEFAULT_SCHEMA = getSchema([ StarterKit, CommentExtension, MentionExtension ]); var getLiveblocksDocumentState = async (roomId, client, schema, field) => { const update = new Uint8Array( await client.getYjsDocumentAsBinaryUpdate(roomId) ); const ydoc = new Doc(); applyUpdate(ydoc, update); const fragment = ydoc.getXmlFragment(field ?? "default"); const { mapping, doc } = initProseMirrorDoc(fragment, schema); const state = EditorState.create({ schema, doc }); return { fragment, state, ydoc, mapping }; }; var createDocumentFromContent = (content, schema) => { try { return schema.nodeFromJSON(content); } catch (error) { console.warn( "[warn]: Invalid content.", "Passed value:", content, "Error:", error ); return false; } }; async function withProsemirrorDocument({ roomId, schema: maybeSchema, client, field }, callback) { const schema = maybeSchema ?? DEFAULT_SCHEMA; let liveblocksState = await getLiveblocksDocumentState( roomId, client, schema, field ?? "default" ); const val = await callback({ /** * Fetches and resyncs the latest document with Liveblocks */ async refresh() { liveblocksState = await getLiveblocksDocumentState( roomId, client, schema, field ?? "default" ); }, /** * Provide a callback to modify documetns with Lexical's standard api. All calls are discrete. */ async update(modifyFn) { const { ydoc, fragment, state, mapping } = liveblocksState; const beforeVector = encodeStateVector(ydoc); const afterState = state.apply(modifyFn(state.doc, state.tr)); ydoc.transact(() => { updateYFragment(ydoc, fragment, afterState.doc, mapping); }); const diffUpdate = encodeStateAsUpdate(ydoc, beforeVector); await client.sendYjsBinaryUpdate(roomId, diffUpdate); await this.refresh(); }, /** * allows you to set content similar to TipTap's setcontent. Only accepts nulls, objects or strings. * Unlike TipTap, strings won't be parsed with DOMParser * */ async setContent(content) { if (typeof content === "string") { return this.update((doc, tr) => { tr.delete(0, doc.content.size); tr.insertText(content); return tr; }); } if (content === null) { return this.clearContent(); } const node = createDocumentFromContent(content, schema); if (!node) { throw "Invalid content"; } return this.update((doc, tr) => { tr.delete(0, doc.content.size); tr.insert(0, node); return tr; }); }, async clearContent() { await this.update((doc, tr) => { tr.delete(0, doc.content.size); return tr; }); }, /** * Uses TipTap's getText function, which allows passing a custom text serializer */ getText(options) { const { state } = liveblocksState; return getText(state.doc, options); }, /** * Helper function to return prosemirror document in JSON form */ toJSON() { return liveblocksState.state.doc.toJSON(); }, /** * Helper function to return editor state as Markdown. By default it uses the defaultMarkdownSerializer from prosemirror-markdown, but you may pass your own */ toMarkdown(serializer) { return (serializer ?? defaultMarkdownSerializer).serialize( liveblocksState.state.doc ); }, /** * Helper function to return the editor's current prosemirror state */ getEditorState() { return liveblocksState.state; } }); return val; } // src/index.ts detectDupes(PKG_NAME, PKG_VERSION, PKG_FORMAT); export { withProsemirrorDocument }; //# sourceMappingURL=index.js.map