UNPKG

@blocknote/core

Version:

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

1 lines 34.8 kB
{"version":3,"file":"comments.cjs","names":[],"sources":["../src/comments/mark.ts","../src/comments/extension.ts","../src/comments/threadstore/ThreadStoreAuth.ts","../src/comments/threadstore/DefaultThreadStoreAuth.ts","../src/comments/threadstore/TipTapThreadStore.ts"],"sourcesContent":["import { Mark, mergeAttributes } from \"@tiptap/core\";\n\nimport { NON_FORMATTING_MARK_GROUP } from \"../schema/markGroups.js\";\n\nexport const CommentMark = Mark.create({\n name: \"comment\",\n excludes: \"\",\n inclusive: false,\n keepOnSplit: true,\n // Allowed on \"plain\" blocks (e.g. code blocks) via this group.\n group: NON_FORMATTING_MARK_GROUP,\n\n addAttributes() {\n // Return an object with attribute configuration\n return {\n // orphans are marks that currently don't have an active thread. It could be\n // that users have resolved the thread. Resolved threads by default are not shown in the document,\n // but we need to keep the mark (positioning) data so we can still \"revive\" it when the thread is unresolved\n // or we enter a \"comments\" view that includes resolved threads.\n orphan: {\n parseHTML: (element) => !!element.getAttribute(\"data-orphan\"),\n renderHTML: (attributes) => {\n return (attributes as { orphan: boolean }).orphan\n ? {\n \"data-orphan\": \"true\",\n }\n : {};\n },\n default: false,\n },\n threadId: {\n parseHTML: (element) => element.getAttribute(\"data-bn-thread-id\"),\n renderHTML: (attributes) => {\n return {\n \"data-bn-thread-id\": (attributes as { threadId: string }).threadId,\n };\n },\n default: \"\",\n },\n };\n },\n\n renderHTML({ HTMLAttributes }: { HTMLAttributes: Record<string, any> }) {\n return [\n \"span\",\n mergeAttributes(HTMLAttributes, {\n class: \"bn-thread-mark\",\n }),\n ];\n },\n\n parseHTML() {\n return [{ tag: \"span.bn-thread-mark\" }];\n },\n\n extendMarkSchema(extension) {\n if (extension.name === \"comment\") {\n return {\n blocknoteIgnore: true,\n };\n }\n return {};\n },\n});\n","import { Node } from \"prosemirror-model\";\nimport { Plugin, PluginKey } from \"prosemirror-state\";\nimport { Decoration, DecorationSet } from \"prosemirror-view\";\nimport {\n createExtension,\n createStore,\n ExtensionOptions,\n} from \"../editor/BlockNoteExtension.js\";\nimport { ShowSelectionExtension } from \"../extensions/ShowSelection/ShowSelection.js\";\nimport { normalizeToUserStore, UserStoreOrResolver } from \"../user/index.js\";\nimport { CustomBlockNoteSchema } from \"../schema/schema.js\";\nimport { CommentMark } from \"./mark.js\";\nimport type { ThreadStore } from \"./threadstore/ThreadStore.js\";\nimport type { CommentBody, ThreadData } from \"./types.js\";\n\nconst PLUGIN_KEY = new PluginKey(\"blocknote-comments\");\n\ntype CommentsPluginState = {\n /**\n * Decorations to be rendered, specifically to indicate the selected thread\n */\n decorations: DecorationSet;\n};\n\n/**\n * Calculate the thread positions from the current document state\n */\nfunction getUpdatedThreadPositions(doc: Node, markType: string) {\n const threadPositions = new Map<string, { from: number; to: number }>();\n\n // find all thread marks and store their position + create decoration for selected thread\n doc.descendants((node, pos) => {\n node.marks.forEach((mark) => {\n if (mark.type.name === markType) {\n const thisThreadId = (mark.attrs as { threadId: string | undefined })\n .threadId;\n if (!thisThreadId) {\n return;\n }\n const from = pos;\n const to = from + node.nodeSize;\n\n // FloatingThreads component uses \"to\" as the position, so always store the largest \"to\" found\n // AnchoredThreads component uses \"from\" as the position, so always store the smallest \"from\" found\n const currentPosition = threadPositions.get(thisThreadId) ?? {\n from: Infinity,\n to: 0,\n };\n threadPositions.set(thisThreadId, {\n from: Math.min(from, currentPosition.from),\n to: Math.max(to, currentPosition.to),\n });\n }\n });\n });\n return threadPositions;\n}\n\nexport const CommentsExtension = createExtension(\n ({\n editor,\n options: {\n schema: commentEditorSchema,\n threadStore,\n resolveUsers,\n confirmBeforeDiscard = true,\n },\n }: ExtensionOptions<{\n /**\n * The thread store implementation to use for storing and retrieving comment threads\n */\n threadStore: ThreadStore;\n /**\n * Resolve user information (names, avatars) for comment authors.\n *\n * Either a resolver function (called with the ids of users that are not yet\n * cached, returning their information) or a pre-built user store (see\n * `createUserStore`). Pass the same store to the collaboration options so a\n * single de-duped user cache is shared across comments and collaboration.\n *\n * See [Comments](https://www.blocknotejs.org/docs/features/collaboration/comments) for more info.\n */\n resolveUsers: UserStoreOrResolver;\n /**\n * A schema to use for the comment editor (which allows you to customize the blocks and styles that are available in the comment editor)\n */\n schema?: CustomBlockNoteSchema<any, any, any>;\n /**\n * Whether to ask the user for confirmation before discarding unsaved text\n * in a comment composer (a new comment, a reply, or an in-progress edit)\n * when it's dismissed (e.g. by clicking outside or pressing Escape).\n *\n * @default true\n */\n confirmBeforeDiscard?: boolean;\n }>) => {\n if (!resolveUsers) {\n throw new Error(\n \"resolveUsers is required to be defined when using comments\",\n );\n }\n if (!threadStore) {\n throw new Error(\n \"threadStore is required to be defined when using comments\",\n );\n }\n // Resolve users through this store, exposed on the extension instance so the\n // comments UI can read from it directly. Accepts a resolver callback or a\n // shared store (see the option docs above).\n const userStore = normalizeToUserStore(resolveUsers);\n const markType = CommentMark.name;\n\n const store = createStore(\n {\n pendingComment: false,\n selectedThreadId: undefined as string | undefined,\n threadPositions: new Map<string, { from: number; to: number }>(),\n },\n {\n onUpdate() {\n // If the selected thread id changed, we need to update the decorations\n if (\n store.state.selectedThreadId !== store.prevState.selectedThreadId\n ) {\n // So, we issue a transaction to update the decorations\n editor.transact((tr) => tr.setMeta(PLUGIN_KEY, true));\n }\n },\n },\n );\n\n const updateMarksFromThreads = (threads: Map<string, ThreadData>) => {\n editor.transact((tr) => {\n tr.doc.descendants((node, pos) => {\n node.marks.forEach((mark) => {\n if (mark.type.name === markType) {\n const markTypeInstance = mark.type;\n const markThreadId = mark.attrs.threadId as string;\n const thread = threads.get(markThreadId);\n const isOrphan = !!(\n !thread ||\n thread.resolved ||\n thread.deletedAt\n );\n\n if (isOrphan !== mark.attrs.orphan) {\n const trimmedFrom = Math.max(pos, 0);\n const trimmedTo = Math.min(\n pos + node.nodeSize,\n tr.doc.content.size - 1,\n tr.doc.content.size - 1,\n );\n tr.removeMark(trimmedFrom, trimmedTo, mark);\n tr.addMark(\n trimmedFrom,\n trimmedTo,\n markTypeInstance.create({\n ...mark.attrs,\n orphan: isOrphan,\n }),\n );\n\n if (isOrphan && store.state.selectedThreadId === markThreadId) {\n // unselect\n store.setState((prev) => ({\n ...prev,\n selectedThreadId: undefined,\n }));\n }\n }\n }\n });\n });\n });\n };\n\n return {\n key: \"comments\",\n store,\n userStore,\n runsBefore: [\"link\"],\n tiptapExtensions: [CommentMark],\n prosemirrorPlugins: [\n new Plugin<CommentsPluginState>({\n key: PLUGIN_KEY,\n state: {\n init() {\n return {\n decorations: DecorationSet.empty,\n };\n },\n apply(tr, state) {\n const action = tr.getMeta(PLUGIN_KEY);\n\n if (!tr.docChanged && !action) {\n return state;\n }\n\n // only update threadPositions if the doc changed\n const newThreadPositions = tr.docChanged\n ? getUpdatedThreadPositions(tr.doc, markType)\n : store.state.threadPositions;\n\n if (\n newThreadPositions.size > 0 ||\n store.state.threadPositions.size > 0\n ) {\n // small optimization; don't emit event if threadPositions before / after were both empty\n store.setState((prev) => ({\n ...prev,\n threadPositions: newThreadPositions,\n }));\n }\n\n // update decorations if doc or selected thread changed\n const decorations = [] as any[];\n\n if (store.state.selectedThreadId) {\n const selectedThreadPosition = newThreadPositions.get(\n store.state.selectedThreadId,\n );\n\n if (selectedThreadPosition) {\n decorations.push(\n Decoration.inline(\n selectedThreadPosition.from,\n selectedThreadPosition.to,\n {\n class: \"bn-thread-mark-selected\",\n },\n ),\n );\n }\n }\n\n return {\n decorations: DecorationSet.create(tr.doc, decorations),\n };\n },\n },\n props: {\n decorations(state) {\n return (\n PLUGIN_KEY.getState(state)?.decorations ?? DecorationSet.empty\n );\n },\n handleClick: (view, pos, event) => {\n if (event.button !== 0) {\n return false;\n }\n\n const node = view.state.doc.nodeAt(pos);\n\n if (!node) {\n // unselect\n store.setState((prev) => ({\n ...prev,\n selectedThreadId: undefined,\n }));\n return false;\n }\n\n const commentMark = node.marks.find(\n (mark) =>\n mark.type.name === markType && mark.attrs.orphan !== true,\n );\n\n if (!commentMark) {\n // Clicked outside any comment thread. Deselect if needed but\n // don't consume the event so other handlers (e.g. link\n // navigation) can process it.\n if (store.state.selectedThreadId !== undefined) {\n store.setState((prev) => ({\n ...prev,\n selectedThreadId: undefined,\n }));\n }\n return false;\n }\n\n const threadId = commentMark.attrs.threadId as string;\n\n // If the clicked thread is already selected, do nothing and let\n // other handlers process the event (e.g. navigating a link).\n if (threadId === store.state.selectedThreadId) {\n return false;\n }\n\n store.setState((prev) => ({\n ...prev,\n selectedThreadId: threadId,\n }));\n\n return true;\n },\n },\n }),\n ],\n threadStore: threadStore,\n mount() {\n const unsubscribe = threadStore.subscribe(updateMarksFromThreads);\n updateMarksFromThreads(threadStore.getThreads());\n\n const unsubscribeOnSelectionChange = editor.onSelectionChange(() => {\n if (store.state.pendingComment) {\n store.setState((prev) => ({\n ...prev,\n pendingComment: false,\n }));\n }\n });\n\n return () => {\n unsubscribe();\n unsubscribeOnSelectionChange();\n };\n },\n selectThread(threadId: string | undefined, scrollToThread = true) {\n if (store.state.selectedThreadId === threadId) {\n return;\n }\n store.setState((prev) => ({\n ...prev,\n pendingComment: false,\n selectedThreadId: threadId,\n }));\n\n if (threadId && scrollToThread) {\n const selectedThreadPosition =\n store.state.threadPositions.get(threadId);\n if (!selectedThreadPosition) {\n return;\n }\n (\n editor.prosemirrorView?.domAtPos(selectedThreadPosition.from)\n .node as Element | undefined\n )?.scrollIntoView({\n behavior: \"smooth\",\n block: \"center\",\n });\n }\n },\n startPendingComment() {\n store.setState((prev) => ({\n ...prev,\n selectedThreadId: undefined,\n pendingComment: true,\n }));\n // Use `editor.domElement` as `editor.focus()` doesn't do anything if\n // the editor is non-editable. Editor needs to be focused as\n // `showSelection` will otherwise trigger a selection update which\n // triggers `stopPendingComment`.\n editor.domElement?.focus();\n editor\n .getExtension(ShowSelectionExtension)\n ?.showSelection(true, \"comments\");\n },\n stopPendingComment() {\n store.setState((prev) => ({\n ...prev,\n selectedThreadId: undefined,\n pendingComment: false,\n }));\n editor\n .getExtension(ShowSelectionExtension)\n ?.showSelection(false, \"comments\");\n },\n async createThread(options: {\n initialComment: { body: CommentBody; metadata?: any };\n metadata?: any;\n }) {\n const thread = await threadStore.createThread(options);\n if (threadStore.addThreadToDocument) {\n await threadStore.addThreadToDocument({\n threadId: thread.id,\n selection: editor.transact((tr) => tr.selection),\n editor,\n });\n } else {\n (editor as any)._tiptapEditor.commands.setMark(markType, {\n orphan: false,\n threadId: thread.id,\n });\n }\n },\n commentEditorSchema,\n confirmBeforeDiscard,\n } as const;\n },\n);\n","import { CommentData, ThreadData } from \"../types.js\";\n\nexport abstract class ThreadStoreAuth {\n abstract canCreateThread(): boolean;\n abstract canAddComment(thread: ThreadData): boolean;\n abstract canUpdateComment(comment: CommentData): boolean;\n abstract canDeleteComment(comment: CommentData): boolean;\n abstract canDeleteThread(thread: ThreadData): boolean;\n abstract canResolveThread(thread: ThreadData): boolean;\n abstract canUnresolveThread(thread: ThreadData): boolean;\n abstract canAddReaction(comment: CommentData, emoji?: string): boolean;\n abstract canDeleteReaction(comment: CommentData, emoji?: string): boolean;\n}\n","import { CommentData, ThreadData } from \"../types.js\";\nimport { ThreadStoreAuth } from \"./ThreadStoreAuth.js\";\n\n/*\n * The DefaultThreadStoreAuth class defines the authorization rules for interacting with comments.\n * We take a role (\"comment\" or \"editor\") and implement the rules.\n *\n * This class is then used in the UI to show / hide specific interactions.\n *\n * Rules:\n * - View-only users should not be able to see any comments\n * - Comment-only users and editors can:\n * - - create new comments / replies / reactions\n * - - edit / delete their own comments / reactions\n * - - resolve / unresolve threads\n * - Editors can also delete any comment or thread\n */\nexport class DefaultThreadStoreAuth extends ThreadStoreAuth {\n constructor(\n private readonly userId: string,\n private readonly role: \"comment\" | \"editor\",\n ) {\n super();\n }\n\n /**\n * Auth: should be possible by anyone with comment access\n */\n canCreateThread(): boolean {\n return true;\n }\n\n /**\n * Auth: should be possible by anyone with comment access\n */\n canAddComment(_thread: ThreadData): boolean {\n return true;\n }\n\n /**\n * Auth: should only be possible by the comment author\n */\n canUpdateComment(comment: CommentData): boolean {\n return comment.userId === this.userId;\n }\n\n /**\n * Auth: should be possible by the comment author OR an editor of the document\n */\n canDeleteComment(comment: CommentData): boolean {\n return comment.userId === this.userId || this.role === \"editor\";\n }\n\n /**\n * Auth: should only be possible by an editor of the document\n */\n canDeleteThread(_thread: ThreadData): boolean {\n return this.role === \"editor\";\n }\n\n /**\n * Auth: should be possible by anyone with comment access\n */\n canResolveThread(_thread: ThreadData): boolean {\n return true;\n }\n\n /**\n * Auth: should be possible by anyone with comment access\n */\n canUnresolveThread(_thread: ThreadData): boolean {\n return true;\n }\n\n /**\n * Auth: should be possible by anyone with comment access\n *\n * Note: will also check if the user has already reacted with the same emoji. TBD: is that a nice design or should this responsibility be outside of auth?\n */\n canAddReaction(comment: CommentData, emoji?: string): boolean {\n if (!emoji) {\n return true;\n }\n\n return !comment.reactions.some(\n (reaction) =>\n reaction.emoji === emoji && reaction.userIds.includes(this.userId),\n );\n }\n\n /**\n * Auth: should be possible by anyone with comment access\n *\n * Note: will also check if the user has already reacted with the same emoji. TBD: is that a nice design or should this responsibility be outside of auth?\n */\n canDeleteReaction(comment: CommentData, emoji?: string): boolean {\n if (!emoji) {\n return true;\n }\n\n return comment.reactions.some(\n (reaction) =>\n reaction.emoji === emoji && reaction.userIds.includes(this.userId),\n );\n }\n}\n","import {\n CommentBody,\n CommentData,\n CommentReactionData,\n ThreadData,\n} from \"../types.js\";\nimport { ThreadStore } from \"./ThreadStore.js\";\nimport { ThreadStoreAuth } from \"./ThreadStoreAuth.js\";\nimport type {\n TCollabComment,\n TCollabThread,\n TiptapCollabProvider,\n} from \"./tiptap/types.js\";\n\ntype ReactionAsTiptapData = {\n emoji: string;\n createdAt: number;\n userId: string;\n};\n\n/**\n * The `TiptapThreadStore` integrates with Tiptap's collaboration provider for comment management.\n * You can pass a `TiptapCollabProvider` to the constructor which takes care of storing the comments.\n *\n * Under the hood, this actually works similarly to the `YjsThreadStore` implementation. (comments are stored in the Yjs document)\n */\nexport class TiptapThreadStore extends ThreadStore {\n constructor(\n private readonly userId: string,\n private readonly provider: TiptapCollabProvider,\n auth: ThreadStoreAuth, // TODO: use?\n ) {\n super(auth);\n }\n\n /**\n * Creates a new thread with an initial comment.\n */\n public async createThread(options: {\n initialComment: {\n body: CommentBody;\n metadata?: any;\n };\n metadata?: any;\n }): Promise<ThreadData> {\n let thread = this.provider.createThread({\n data: options.metadata,\n });\n\n thread = this.provider.addComment(thread.id, {\n content: options.initialComment.body,\n data: {\n metadata: options.initialComment.metadata,\n userId: this.userId,\n },\n });\n\n return this.tiptapThreadToThreadData(thread);\n }\n\n // TipTapThreadStore does not support addThreadToDocument\n public addThreadToDocument = undefined;\n\n /**\n * Adds a comment to a thread.\n */\n public async addComment(options: {\n comment: {\n body: CommentBody;\n metadata?: any;\n };\n threadId: string;\n }): Promise<CommentBody> {\n const thread = this.provider.addComment(options.threadId, {\n content: options.comment.body,\n data: {\n metadata: options.comment.metadata,\n userId: this.userId,\n },\n });\n\n return this.tiptapCommentToCommentData(\n thread.comments[thread.comments.length - 1],\n );\n }\n\n /**\n * Updates a comment in a thread.\n */\n public async updateComment(options: {\n comment: {\n body: CommentBody;\n metadata?: any;\n };\n threadId: string;\n commentId: string;\n }) {\n const comment = this.provider.getThreadComment(\n options.threadId,\n options.commentId,\n true,\n );\n\n if (!comment) {\n throw new Error(\"Comment not found\");\n }\n\n this.provider.updateComment(options.threadId, options.commentId, {\n content: options.comment.body,\n data: {\n ...comment.data,\n metadata: options.comment.metadata,\n },\n });\n }\n\n private tiptapCommentToCommentData(comment: TCollabComment): CommentData {\n const reactions: CommentReactionData[] = [];\n\n for (const reaction of (comment.data?.reactions ||\n []) as ReactionAsTiptapData[]) {\n const existingReaction = reactions.find(\n (r) => r.emoji === reaction.emoji,\n );\n if (existingReaction) {\n existingReaction.userIds.push(reaction.userId);\n existingReaction.createdAt = new Date(\n Math.min(existingReaction.createdAt.getTime(), reaction.createdAt),\n );\n } else {\n reactions.push({\n emoji: reaction.emoji,\n createdAt: new Date(reaction.createdAt),\n userIds: [reaction.userId],\n });\n }\n }\n\n return {\n type: \"comment\",\n id: comment.id,\n body: comment.content,\n metadata: comment.data?.metadata,\n userId: comment.data?.userId,\n createdAt: new Date(comment.createdAt),\n updatedAt: new Date(comment.updatedAt),\n reactions,\n };\n }\n\n private tiptapThreadToThreadData(thread: TCollabThread): ThreadData {\n return {\n type: \"thread\",\n id: thread.id,\n comments: thread.comments.map((comment) =>\n this.tiptapCommentToCommentData(comment),\n ),\n resolved: !!thread.resolvedAt,\n metadata: thread.data?.metadata,\n createdAt: new Date(thread.createdAt),\n updatedAt: new Date(thread.updatedAt),\n };\n }\n\n /**\n * Deletes a comment from a thread.\n */\n public async deleteComment(options: { threadId: string; commentId: string }) {\n this.provider.deleteComment(options.threadId, options.commentId);\n }\n\n /**\n * Deletes a thread.\n */\n public async deleteThread(options: { threadId: string }) {\n this.provider.deleteThread(options.threadId);\n }\n\n /**\n * Marks a thread as resolved.\n */\n public async resolveThread(options: { threadId: string }) {\n this.provider.updateThread(options.threadId, {\n resolvedAt: new Date().toISOString(),\n });\n }\n\n /**\n * Marks a thread as unresolved.\n */\n public async unresolveThread(options: { threadId: string }) {\n this.provider.updateThread(options.threadId, {\n resolvedAt: null,\n });\n }\n\n /**\n * Adds a reaction to a comment.\n *\n * Auth: should be possible by anyone with comment access\n */\n public async addReaction(options: {\n threadId: string;\n commentId: string;\n emoji: string;\n }) {\n const comment = this.provider.getThreadComment(\n options.threadId,\n options.commentId,\n true,\n );\n\n if (!comment) {\n throw new Error(\"Comment not found\");\n }\n\n this.provider.updateComment(options.threadId, options.commentId, {\n data: {\n ...comment.data,\n reactions: [\n ...((comment.data?.reactions || []) as ReactionAsTiptapData[]),\n {\n emoji: options.emoji,\n createdAt: Date.now(),\n userId: this.userId,\n },\n ],\n },\n });\n }\n\n /**\n * Deletes a reaction from a comment.\n *\n * Auth: should be possible by the reaction author\n */\n public async deleteReaction(options: {\n threadId: string;\n commentId: string;\n emoji: string;\n }) {\n const comment = this.provider.getThreadComment(\n options.threadId,\n options.commentId,\n true,\n );\n\n if (!comment) {\n throw new Error(\"Comment not found\");\n }\n\n this.provider.updateComment(options.threadId, options.commentId, {\n data: {\n ...comment.data,\n reactions: (\n (comment.data?.reactions || []) as ReactionAsTiptapData[]\n ).filter(\n (reaction) =>\n reaction.emoji !== options.emoji && reaction.userId !== this.userId,\n ),\n },\n });\n }\n\n public getThread(threadId: string): ThreadData {\n const thread = this.provider.getThread(threadId);\n\n if (!thread) {\n throw new Error(\"Thread not found\");\n }\n\n return this.tiptapThreadToThreadData(thread);\n }\n\n public getThreads(): Map<string, ThreadData> {\n return new Map(\n this.provider\n .getThreads()\n .map((thread) => [thread.id, this.tiptapThreadToThreadData(thread)]),\n );\n }\n\n public subscribe(cb: (threads: Map<string, ThreadData>) => void): () => void {\n const newCb = () => {\n cb(this.getThreads());\n };\n this.provider.watchThreads(newCb);\n return () => {\n this.provider.unwatchThreads(newCb);\n };\n }\n}\n"],"mappings":"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"}