@blocknote/core 0.24.2 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/dist/blocknote.cjs +12 -0
  2. package/dist/blocknote.cjs.map +1 -0
  3. package/dist/blocknote.js +4754 -3514
  4. package/dist/blocknote.js.map +1 -1
  5. package/dist/comments.cjs +2 -0
  6. package/dist/comments.cjs.map +1 -0
  7. package/dist/comments.js +593 -0
  8. package/dist/comments.js.map +1 -0
  9. package/dist/style.css +1 -1
  10. package/dist/tsconfig.tsbuildinfo +1 -1
  11. package/dist/webpack-stats.json +1 -1
  12. package/package.json +39 -26
  13. package/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap +1022 -378
  14. package/src/api/blockManipulation/commands/mergeBlocks/__snapshots__/mergeBlocks.test.ts.snap +730 -270
  15. package/src/api/blockManipulation/commands/moveBlocks/__snapshots__/moveBlocks.test.ts.snap +3100 -1260
  16. package/src/api/blockManipulation/commands/removeBlocks/__snapshots__/removeBlocks.test.ts.snap +438 -162
  17. package/src/api/blockManipulation/commands/replaceBlocks/__snapshots__/replaceBlocks.test.ts.snap +1168 -432
  18. package/src/api/blockManipulation/commands/splitBlock/__snapshots__/splitBlock.test.ts.snap +930 -378
  19. package/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap +2485 -1015
  20. package/src/api/blockManipulation/commands/updateBlock/updateBlock.test.ts +28 -1
  21. package/src/api/blockManipulation/commands/updateBlock/updateBlock.ts +1 -1
  22. package/src/api/blockManipulation/selections/__snapshots__/selection.test.ts.snap +292 -108
  23. package/src/api/blockManipulation/setupTestEnv.ts +14 -1
  24. package/src/api/blockManipulation/tables/tables.test.ts +1987 -0
  25. package/src/api/blockManipulation/tables/tables.ts +887 -0
  26. package/src/api/clipboard/__snapshots__/external/pasteEndOfParagraph.html +66 -24
  27. package/src/api/clipboard/__snapshots__/external/pasteEndOfParagraphText.html +66 -24
  28. package/src/api/clipboard/__snapshots__/external/pasteImage.html +66 -24
  29. package/src/api/clipboard/__snapshots__/external/pasteParagraphInCustomBlock.html +66 -24
  30. package/src/api/clipboard/__snapshots__/external/pasteTable.html +132 -48
  31. package/src/api/clipboard/__snapshots__/external/pasteTableInExistingTable.html +136 -44
  32. package/src/api/clipboard/toClipboard/copyExtension.ts +2 -3
  33. package/src/api/exporters/html/__snapshots__/table/headerCols/external.html +1 -0
  34. package/src/api/exporters/html/__snapshots__/table/headerCols/internal.html +1 -0
  35. package/src/api/exporters/html/__snapshots__/table/headerRows/external.html +1 -0
  36. package/src/api/exporters/html/__snapshots__/table/headerRows/internal.html +1 -0
  37. package/src/api/exporters/html/__snapshots__/table/headersRows/external.html +1 -0
  38. package/src/api/exporters/html/__snapshots__/table/headersRows/internal.html +1 -0
  39. package/src/api/exporters/html/__snapshots__/table/mixedCellColors/external.html +1 -0
  40. package/src/api/exporters/html/__snapshots__/table/mixedCellColors/internal.html +1 -0
  41. package/src/api/exporters/html/__snapshots__/table/mixedRowspansAndColspans/external.html +1 -0
  42. package/src/api/exporters/html/__snapshots__/table/mixedRowspansAndColspans/internal.html +1 -0
  43. package/src/api/exporters/markdown/__snapshots__/table/headerCols/markdown.md +4 -0
  44. package/src/api/exporters/markdown/__snapshots__/table/headerRows/markdown.md +4 -0
  45. package/src/api/exporters/markdown/__snapshots__/table/mixedCellColors/markdown.md +5 -0
  46. package/src/api/exporters/markdown/__snapshots__/table/mixedRowspansAndColspans/markdown.md +5 -0
  47. package/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +985 -20
  48. package/src/api/nodeConversions/blockToNode.ts +63 -20
  49. package/src/api/nodeConversions/nodeToBlock.ts +75 -13
  50. package/src/api/parsers/html/__snapshots__/parse-notion-html.json +145 -54
  51. package/src/api/testUtil/cases/defaultSchema.ts +782 -9
  52. package/src/api/testUtil/partialBlockTestUtil.ts +39 -4
  53. package/src/blocks/TableBlockContent/TableBlockContent.ts +11 -5
  54. package/src/blocks/defaultBlockTypeGuards.ts +8 -0
  55. package/src/comments/index.ts +9 -0
  56. package/src/comments/models/User.ts +8 -0
  57. package/src/comments/threadstore/DefaultThreadStoreAuth.ts +106 -0
  58. package/src/comments/threadstore/ThreadStore.ts +134 -0
  59. package/src/comments/threadstore/ThreadStoreAuth.ts +13 -0
  60. package/src/comments/threadstore/TipTapThreadStore.ts +292 -0
  61. package/src/comments/threadstore/yjs/RESTYjsThreadStore.ts +144 -0
  62. package/src/comments/threadstore/yjs/YjsThreadStore.test.ts +294 -0
  63. package/src/comments/threadstore/yjs/YjsThreadStore.ts +340 -0
  64. package/src/comments/threadstore/yjs/YjsThreadStoreBase.ts +48 -0
  65. package/src/comments/threadstore/yjs/yjsHelpers.ts +121 -0
  66. package/src/comments/types.ts +117 -0
  67. package/src/editor/Block.css +16 -8
  68. package/src/editor/BlockNoteEditor.ts +269 -92
  69. package/src/editor/BlockNoteExtensions.ts +24 -1
  70. package/src/editor/BlockNoteTipTapEditor.ts +5 -1
  71. package/src/editor/editor.css +17 -0
  72. package/src/extensions/BackgroundColor/BackgroundColorExtension.ts +1 -1
  73. package/src/extensions/Comments/CommentMark.ts +61 -0
  74. package/src/extensions/Comments/CommentsPlugin.ts +301 -0
  75. package/src/extensions/Comments/userstore/UserStore.ts +72 -0
  76. package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +9 -5
  77. package/src/extensions/LinkToolbar/LinkToolbarPlugin.ts +3 -3
  78. package/src/extensions/ShowSelection/ShowSelectionPlugin.ts +52 -0
  79. package/src/extensions/TableHandles/TableHandlesPlugin.ts +409 -57
  80. package/src/extensions/TextAlignment/TextAlignmentExtension.ts +2 -0
  81. package/src/extensions/TextColor/TextColorExtension.ts +1 -1
  82. package/src/i18n/locales/ar.ts +23 -0
  83. package/src/i18n/locales/de.ts +15 -0
  84. package/src/i18n/locales/en.ts +25 -1
  85. package/src/i18n/locales/es.ts +16 -1
  86. package/src/i18n/locales/fr.ts +23 -0
  87. package/src/i18n/locales/hr.ts +18 -0
  88. package/src/i18n/locales/is.ts +24 -1
  89. package/src/i18n/locales/it.ts +15 -0
  90. package/src/i18n/locales/ja.ts +23 -0
  91. package/src/i18n/locales/ko.ts +23 -0
  92. package/src/i18n/locales/nl.ts +23 -0
  93. package/src/i18n/locales/no.ts +23 -0
  94. package/src/i18n/locales/pl.ts +23 -0
  95. package/src/i18n/locales/pt.ts +23 -0
  96. package/src/i18n/locales/ru.ts +23 -0
  97. package/src/i18n/locales/uk.ts +23 -0
  98. package/src/i18n/locales/vi.ts +23 -0
  99. package/src/i18n/locales/zh.ts +23 -0
  100. package/src/index.ts +6 -4
  101. package/src/schema/blocks/types.ts +32 -2
  102. package/src/util/browser.ts +1 -1
  103. package/src/util/table.ts +107 -0
  104. package/types/src/api/blockManipulation/tables/tables.d.ts +343 -0
  105. package/types/src/api/blockManipulation/tables/tables.test.d.ts +1 -0
  106. package/types/src/api/clipboard/toClipboard/copyExtension.d.ts +1 -1
  107. package/types/src/blocks/TableBlockContent/TableBlockContent.d.ts +1 -2
  108. package/types/src/blocks/defaultBlockTypeGuards.d.ts +3 -0
  109. package/types/src/comments/index.d.ts +9 -0
  110. package/types/src/comments/models/User.d.ts +8 -0
  111. package/types/src/comments/threadstore/DefaultThreadStoreAuth.d.ts +47 -0
  112. package/types/src/comments/threadstore/ThreadStore.d.ts +121 -0
  113. package/types/src/comments/threadstore/ThreadStoreAuth.d.ts +12 -0
  114. package/types/src/comments/threadstore/TipTapThreadStore.d.ts +97 -0
  115. package/types/src/comments/threadstore/yjs/RESTYjsThreadStore.d.ts +83 -0
  116. package/types/src/comments/threadstore/yjs/YjsThreadStore.d.ts +79 -0
  117. package/types/src/comments/threadstore/yjs/YjsThreadStore.test.d.ts +1 -0
  118. package/types/src/comments/threadstore/yjs/YjsThreadStoreBase.d.ts +15 -0
  119. package/types/src/comments/threadstore/yjs/yjsHelpers.d.ts +13 -0
  120. package/types/src/comments/types.d.ts +109 -0
  121. package/types/src/editor/BlockNoteEditor.d.ts +146 -66
  122. package/types/src/editor/BlockNoteExtensions.d.ts +4 -0
  123. package/types/src/extensions/Collaboration/createCollaborationExtensions.d.ts +1 -1
  124. package/types/src/extensions/Comments/CommentMark.d.ts +2 -0
  125. package/types/src/extensions/Comments/CommentsPlugin.d.ts +49 -0
  126. package/types/src/extensions/Comments/userstore/UserStore.d.ts +31 -0
  127. package/types/src/extensions/ShowSelection/ShowSelectionPlugin.d.ts +15 -0
  128. package/types/src/extensions/TableHandles/TableHandlesPlugin.d.ts +66 -1
  129. package/types/src/i18n/locales/de.d.ts +15 -0
  130. package/types/src/i18n/locales/en.d.ts +20 -0
  131. package/types/src/i18n/locales/es.d.ts +15 -0
  132. package/types/src/i18n/locales/hr.d.ts +18 -0
  133. package/types/src/i18n/locales/it.d.ts +15 -0
  134. package/types/src/index.d.ts +5 -4
  135. package/types/src/pm-nodes/BlockContainer.d.ts +2 -2
  136. package/types/src/pm-nodes/BlockGroup.d.ts +2 -2
  137. package/types/src/schema/blocks/types.d.ts +23 -2
  138. package/types/src/util/browser.d.ts +1 -1
  139. package/types/src/util/table.d.ts +12 -0
  140. package/dist/blocknote.umd.cjs +0 -11
  141. package/dist/blocknote.umd.cjs.map +0 -1
@@ -1,7 +1,12 @@
1
1
  import { Block, PartialBlock } from "../../blocks/defaultBlocks.js";
2
2
  import { BlockNoteSchema } from "../../editor/BlockNoteSchema.js";
3
3
  import UniqueID from "../../extensions/UniqueID/UniqueID.js";
4
- import { BlockSchema, TableContent } from "../../schema/blocks/types.js";
4
+ import {
5
+ BlockSchema,
6
+ PartialTableCell,
7
+ TableCell,
8
+ TableContent,
9
+ } from "../../schema/blocks/types.js";
5
10
  import {
6
11
  InlineContent,
7
12
  InlineContentSchema,
@@ -28,8 +33,16 @@ function textShorthandToStyledText(
28
33
  }
29
34
 
30
35
  function partialContentToInlineContent(
31
- content: PartialInlineContent<any, any> | TableContent<any> | undefined
32
- ): InlineContent<any, any>[] | TableContent<any> | undefined {
36
+ content:
37
+ | PartialInlineContent<any, any>
38
+ | PartialTableCell<any, any>
39
+ | TableContent<any>
40
+ | undefined
41
+ ):
42
+ | InlineContent<any, any>[]
43
+ | TableContent<any>
44
+ | TableCell<any, any>
45
+ | undefined {
33
46
  if (typeof content === "string") {
34
47
  return textShorthandToStyledText(content);
35
48
  }
@@ -59,6 +72,8 @@ function partialContentToInlineContent(
59
72
  return {
60
73
  type: "tableContent",
61
74
  columnWidths: content.columnWidths,
75
+ headerRows: content.headerRows,
76
+ headerCols: content.headerCols,
62
77
  rows: content.rows.map((row) => ({
63
78
  ...row,
64
79
  cells: row.cells.map(
@@ -66,6 +81,18 @@ function partialContentToInlineContent(
66
81
  ),
67
82
  })),
68
83
  };
84
+ } else if (content?.type === "tableCell") {
85
+ return {
86
+ type: "tableCell",
87
+ content: partialContentToInlineContent(content.content) as any[],
88
+ props: {
89
+ backgroundColor: content.props?.backgroundColor ?? "default",
90
+ textColor: content.props?.textColor ?? "default",
91
+ textAlignment: content.props?.textAlignment ?? "left",
92
+ colspan: content.props?.colspan ?? 1,
93
+ rowspan: content.props?.rowspan ?? 1,
94
+ },
95
+ } satisfies TableCell<any, any>;
69
96
  }
70
97
 
71
98
  return content;
@@ -103,7 +130,13 @@ export function partialBlockToBlockForTesting<
103
130
  contentType === "inline"
104
131
  ? []
105
132
  : contentType === "table"
106
- ? { type: "tableContent", columnWidths: [], rows: [] }
133
+ ? {
134
+ type: "tableContent",
135
+ columnWidths: undefined,
136
+ headerRows: undefined,
137
+ headerCols: undefined,
138
+ rows: [],
139
+ }
107
140
  : (undefined as any),
108
141
  children: [] as any,
109
142
  ...partialBlock,
@@ -131,6 +164,8 @@ export function partialBlockToBlockForTesting<
131
164
  content?.columnWidths ||
132
165
  content?.rows[0]?.cells.map(() => undefined) ||
133
166
  [],
167
+ headerRows: content?.headerRows || undefined,
168
+ headerCols: content?.headerCols || undefined,
134
169
  rows:
135
170
  content?.rows.map((row) => ({
136
171
  cells: row.cells.map((cell) => partialContentToInlineContent(cell)),
@@ -1,4 +1,3 @@
1
- import { Node } from "@tiptap/core";
2
1
  import { TableCell } from "@tiptap/extension-table-cell";
3
2
  import { TableHeader } from "@tiptap/extension-table-header";
4
3
  import { TableRow } from "@tiptap/extension-table-row";
@@ -102,12 +101,12 @@ export const TableBlockContent = createStronglyTypedTiptapNode({
102
101
  return new BlockNoteTableView(node, EMPTY_CELL_WIDTH, {
103
102
  ...(this.options.domAttributes?.blockContent || {}),
104
103
  ...HTMLAttributes,
105
- }) as NodeView;
104
+ }) as NodeView; // needs cast, tiptap types (wrongly) doesn't support return tableview here
106
105
  };
107
106
  },
108
107
  });
109
108
 
110
- const TableParagraph = Node.create({
109
+ const TableParagraph = createStronglyTypedTiptapNode({
111
110
  name: "tableParagraph",
112
111
  group: "tableContent",
113
112
  content: "inline*",
@@ -160,10 +159,17 @@ export const Table = createBlockSpecFromStronglyTypedTiptapNode(
160
159
  TableExtension,
161
160
  TableParagraph,
162
161
  TableHeader.extend({
163
- content: "tableContent",
162
+ /**
163
+ * We allow table headers and cells to have multiple tableContent nodes because
164
+ * when merging cells, prosemirror-tables will concat the contents of the cells naively.
165
+ * This would cause that content to overflow into other cells when prosemirror tries to enforce the cell structure.
166
+ *
167
+ * So, we manually fix this up when reading back in the `nodeToBlock` and only ever place a single tableContent back into the cell.
168
+ */
169
+ content: "tableContent+",
164
170
  }),
165
171
  TableCell.extend({
166
- content: "tableContent",
172
+ content: "tableContent+",
167
173
  }),
168
174
  TableRow,
169
175
  ]
@@ -1,3 +1,4 @@
1
+ import { CellSelection } from "prosemirror-tables";
1
2
  import type { BlockNoteEditor } from "../editor/BlockNoteEditor.js";
2
3
  import {
3
4
  BlockFromConfig,
@@ -14,6 +15,7 @@ import {
14
15
  defaultInlineContentSchema,
15
16
  } from "./defaultBlocks.js";
16
17
  import { defaultProps } from "./defaultProps.js";
18
+ import { Selection } from "prosemirror-state";
17
19
 
18
20
  export function checkDefaultBlockTypeInSchema<
19
21
  BlockType extends keyof DefaultBlockSchema,
@@ -159,3 +161,9 @@ export function checkBlockHasDefaultProp<
159
161
  > {
160
162
  return checkBlockTypeHasDefaultProp(prop, block.type, editor);
161
163
  }
164
+
165
+ export function isTableCellSelection(
166
+ selection: Selection
167
+ ): selection is CellSelection {
168
+ return selection instanceof CellSelection;
169
+ }
@@ -0,0 +1,9 @@
1
+ export * from "./models/User.js";
2
+ export * from "./threadstore/DefaultThreadStoreAuth.js";
3
+ export * from "./threadstore/ThreadStore.js";
4
+ export * from "./threadstore/ThreadStoreAuth.js";
5
+ export * from "./threadstore/TipTapThreadStore.js";
6
+ export * from "./threadstore/yjs/RESTYjsThreadStore.js";
7
+ export * from "./threadstore/yjs/YjsThreadStore.js";
8
+ export * from "./threadstore/yjs/YjsThreadStoreBase.js";
9
+ export * from "./types.js";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * A collaborator of the document.
3
+ */
4
+ export type User = {
5
+ id: string;
6
+ username: string;
7
+ avatarUrl: string;
8
+ };
@@ -0,0 +1,106 @@
1
+ import { CommentData, ThreadData } from "../types.js";
2
+ import { ThreadStoreAuth } from "./ThreadStoreAuth.js";
3
+
4
+ /*
5
+ * The DefaultThreadStoreAuth class defines the authorization rules for interacting with comments.
6
+ * We take a role ("comment" or "editor") and implement the rules.
7
+ *
8
+ * This class is then used in the UI to show / hide specific interactions.
9
+ *
10
+ * Rules:
11
+ * - View-only users should not be able to see any comments
12
+ * - Comment-only users and editors can:
13
+ * - - create new comments / replies / reactions
14
+ * - - edit / delete their own comments / reactions
15
+ * - - resolve / unresolve threads
16
+ * - Editors can also delete any comment or thread
17
+ */
18
+ export class DefaultThreadStoreAuth extends ThreadStoreAuth {
19
+ constructor(
20
+ private readonly userId: string,
21
+ private readonly role: "comment" | "editor"
22
+ ) {
23
+ super();
24
+ }
25
+
26
+ /**
27
+ * Auth: should be possible by anyone with comment access
28
+ */
29
+ canCreateThread(): boolean {
30
+ return true;
31
+ }
32
+
33
+ /**
34
+ * Auth: should be possible by anyone with comment access
35
+ */
36
+ canAddComment(_thread: ThreadData): boolean {
37
+ return true;
38
+ }
39
+
40
+ /**
41
+ * Auth: should only be possible by the comment author
42
+ */
43
+ canUpdateComment(comment: CommentData): boolean {
44
+ return comment.userId === this.userId;
45
+ }
46
+
47
+ /**
48
+ * Auth: should be possible by the comment author OR an editor of the document
49
+ */
50
+ canDeleteComment(comment: CommentData): boolean {
51
+ return comment.userId === this.userId || this.role === "editor";
52
+ }
53
+
54
+ /**
55
+ * Auth: should only be possible by an editor of the document
56
+ */
57
+ canDeleteThread(_thread: ThreadData): boolean {
58
+ return this.role === "editor";
59
+ }
60
+
61
+ /**
62
+ * Auth: should be possible by anyone with comment access
63
+ */
64
+ canResolveThread(_thread: ThreadData): boolean {
65
+ return true;
66
+ }
67
+
68
+ /**
69
+ * Auth: should be possible by anyone with comment access
70
+ */
71
+ canUnresolveThread(_thread: ThreadData): boolean {
72
+ return true;
73
+ }
74
+
75
+ /**
76
+ * Auth: should be possible by anyone with comment access
77
+ *
78
+ * 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?
79
+ */
80
+ canAddReaction(comment: CommentData, emoji?: string): boolean {
81
+ if (!emoji) {
82
+ return true;
83
+ }
84
+
85
+ return !comment.reactions.some(
86
+ (reaction) =>
87
+ reaction.emoji === emoji && reaction.userIds.includes(this.userId)
88
+ );
89
+ }
90
+
91
+ /**
92
+ * Auth: should be possible by anyone with comment access
93
+ *
94
+ * 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?
95
+ */
96
+ canDeleteReaction(comment: CommentData, emoji?: string): boolean {
97
+ if (!emoji) {
98
+ return true;
99
+ }
100
+
101
+ return comment.reactions.some(
102
+ (reaction) =>
103
+ reaction.emoji === emoji && reaction.userIds.includes(this.userId)
104
+ );
105
+ }
106
+ }
@@ -0,0 +1,134 @@
1
+ import { CommentBody, CommentData, ThreadData } from "../types.js";
2
+ import { ThreadStoreAuth } from "./ThreadStoreAuth.js";
3
+
4
+ /**
5
+ * ThreadStore is an abstract class that defines the interface
6
+ * to read / add / update / delete threads and comments.
7
+ */
8
+ export abstract class ThreadStore {
9
+ public readonly auth: ThreadStoreAuth;
10
+
11
+ constructor(auth: ThreadStoreAuth) {
12
+ this.auth = auth;
13
+ }
14
+
15
+ /**
16
+ * A "thread" in the ThreadStore only contains information about the content
17
+ * of the thread / comments. It does not contain information about the position.
18
+ *
19
+ * This function can be implemented to store the thread in the document (by creating a mark)
20
+ * If not implemented, default behavior will apply (creating the mark via TipTap)
21
+ * See CommentsPlugin.ts for more details.
22
+ */
23
+ abstract addThreadToDocument?(options: {
24
+ threadId: string;
25
+ selection: {
26
+ prosemirror: {
27
+ head: number;
28
+ anchor: number;
29
+ };
30
+ yjs: {
31
+ head: any;
32
+ anchor: any;
33
+ };
34
+ };
35
+ }): Promise<void>;
36
+
37
+ /**
38
+ * Creates a new thread with an initial comment.
39
+ */
40
+ abstract createThread(options: {
41
+ initialComment: {
42
+ body: CommentBody;
43
+ metadata?: any;
44
+ };
45
+ metadata?: any;
46
+ }): Promise<ThreadData>;
47
+
48
+ /**
49
+ * Adds a comment to a thread.
50
+ */
51
+ abstract addComment(options: {
52
+ comment: {
53
+ body: CommentBody;
54
+ metadata?: any;
55
+ };
56
+ threadId: string;
57
+ }): Promise<CommentData>;
58
+
59
+ /**
60
+ * Updates a comment in a thread.
61
+ */
62
+ abstract updateComment(options: {
63
+ comment: {
64
+ body: CommentBody;
65
+ metadata?: any;
66
+ };
67
+ threadId: string;
68
+ commentId: string;
69
+ }): Promise<void>;
70
+
71
+ /**
72
+ * Deletes a comment from a thread.
73
+ */
74
+ abstract deleteComment(options: {
75
+ threadId: string;
76
+ commentId: string;
77
+ }): Promise<void>;
78
+
79
+ /**
80
+ * Deletes a thread.
81
+ */
82
+ abstract deleteThread(options: { threadId: string }): Promise<void>;
83
+
84
+ /**
85
+ * Marks a thread as resolved.
86
+ */
87
+ abstract resolveThread(options: { threadId: string }): Promise<void>;
88
+
89
+ /**
90
+ * Marks a thread as unresolved.
91
+ */
92
+ abstract unresolveThread(options: { threadId: string }): Promise<void>;
93
+
94
+ /**
95
+ * Adds a reaction to a comment.
96
+ *
97
+ * Auth: should be possible by anyone with comment access
98
+ */
99
+ abstract addReaction(options: {
100
+ threadId: string;
101
+ commentId: string;
102
+ emoji: string;
103
+ }): Promise<void>;
104
+
105
+ /**
106
+ * Deletes a reaction from a comment.
107
+ *
108
+ * Auth: should be possible by the reaction author
109
+ */
110
+ abstract deleteReaction(options: {
111
+ threadId: string;
112
+ commentId: string;
113
+ emoji: string;
114
+ }): Promise<void>;
115
+
116
+ /**
117
+ * Retrieve data for a specific thread.
118
+ */
119
+ abstract getThread(threadId: string): ThreadData;
120
+
121
+ /**
122
+ * Retrieve all threads.
123
+ */
124
+ abstract getThreads(): Map<string, ThreadData>;
125
+
126
+ /**
127
+ * Subscribe to changes in the thread store.
128
+ *
129
+ * @returns a function to unsubscribe from the thread store
130
+ */
131
+ abstract subscribe(
132
+ cb: (threads: Map<string, ThreadData>) => void
133
+ ): () => void;
134
+ }
@@ -0,0 +1,13 @@
1
+ import { CommentData, ThreadData } from "../types.js";
2
+
3
+ export abstract class ThreadStoreAuth {
4
+ abstract canCreateThread(): boolean;
5
+ abstract canAddComment(thread: ThreadData): boolean;
6
+ abstract canUpdateComment(comment: CommentData): boolean;
7
+ abstract canDeleteComment(comment: CommentData): boolean;
8
+ abstract canDeleteThread(thread: ThreadData): boolean;
9
+ abstract canResolveThread(thread: ThreadData): boolean;
10
+ abstract canUnresolveThread(thread: ThreadData): boolean;
11
+ abstract canAddReaction(comment: CommentData, emoji?: string): boolean;
12
+ abstract canDeleteReaction(comment: CommentData, emoji?: string): boolean;
13
+ }
@@ -0,0 +1,292 @@
1
+ import type {
2
+ TCollabComment,
3
+ TCollabThread,
4
+ TiptapCollabProvider,
5
+ } from "@hocuspocus/provider";
6
+ import {
7
+ CommentBody,
8
+ CommentData,
9
+ CommentReactionData,
10
+ ThreadData,
11
+ } from "../types.js";
12
+ import { ThreadStore } from "./ThreadStore.js";
13
+ import { ThreadStoreAuth } from "./ThreadStoreAuth.js";
14
+
15
+ type ReactionAsTiptapData = {
16
+ emoji: string;
17
+ createdAt: number;
18
+ userId: string;
19
+ };
20
+
21
+ /**
22
+ * The `TiptapThreadStore` integrates with Tiptap's collaboration provider for comment management.
23
+ * You can pass a `TiptapCollabProvider` to the constructor which takes care of storing the comments.
24
+ *
25
+ * Under the hood, this actually works similarly to the `YjsThreadStore` implementation. (comments are stored in the Yjs document)
26
+ */
27
+ export class TiptapThreadStore extends ThreadStore {
28
+ constructor(
29
+ private readonly userId: string,
30
+ private readonly provider: TiptapCollabProvider,
31
+ auth: ThreadStoreAuth // TODO: use?
32
+ ) {
33
+ super(auth);
34
+ }
35
+
36
+ /**
37
+ * Creates a new thread with an initial comment.
38
+ */
39
+ public async createThread(options: {
40
+ initialComment: {
41
+ body: CommentBody;
42
+ metadata?: any;
43
+ };
44
+ metadata?: any;
45
+ }): Promise<ThreadData> {
46
+ let thread = this.provider.createThread({
47
+ data: options.metadata,
48
+ });
49
+
50
+ thread = this.provider.addComment(thread.id, {
51
+ content: options.initialComment.body,
52
+ data: {
53
+ metadata: options.initialComment.metadata,
54
+ userId: this.userId,
55
+ },
56
+ });
57
+
58
+ return this.tiptapThreadToThreadData(thread);
59
+ }
60
+
61
+ // TipTapThreadStore does not support addThreadToDocument
62
+ public addThreadToDocument = undefined;
63
+
64
+ /**
65
+ * Adds a comment to a thread.
66
+ */
67
+ public async addComment(options: {
68
+ comment: {
69
+ body: CommentBody;
70
+ metadata?: any;
71
+ };
72
+ threadId: string;
73
+ }): Promise<CommentBody> {
74
+ const thread = this.provider.addComment(options.threadId, {
75
+ content: options.comment.body,
76
+ data: {
77
+ metadata: options.comment.metadata,
78
+ userId: this.userId,
79
+ },
80
+ });
81
+
82
+ return this.tiptapCommentToCommentData(
83
+ thread.comments[thread.comments.length - 1]
84
+ );
85
+ }
86
+
87
+ /**
88
+ * Updates a comment in a thread.
89
+ */
90
+ public async updateComment(options: {
91
+ comment: {
92
+ body: CommentBody;
93
+ metadata?: any;
94
+ };
95
+ threadId: string;
96
+ commentId: string;
97
+ }) {
98
+ const comment = this.provider.getThreadComment(
99
+ options.threadId,
100
+ options.commentId,
101
+ true
102
+ );
103
+
104
+ if (!comment) {
105
+ throw new Error("Comment not found");
106
+ }
107
+
108
+ this.provider.updateComment(options.threadId, options.commentId, {
109
+ content: options.comment.body,
110
+ data: {
111
+ ...comment.data,
112
+ metadata: options.comment.metadata,
113
+ },
114
+ });
115
+ }
116
+
117
+ private tiptapCommentToCommentData(comment: TCollabComment): CommentData {
118
+ const reactions: CommentReactionData[] = [];
119
+
120
+ for (const reaction of (comment.data?.reactions ||
121
+ []) as ReactionAsTiptapData[]) {
122
+ const existingReaction = reactions.find(
123
+ (r) => r.emoji === reaction.emoji
124
+ );
125
+ if (existingReaction) {
126
+ existingReaction.userIds.push(reaction.userId);
127
+ existingReaction.createdAt = new Date(
128
+ Math.min(existingReaction.createdAt.getTime(), reaction.createdAt)
129
+ );
130
+ } else {
131
+ reactions.push({
132
+ emoji: reaction.emoji,
133
+ createdAt: new Date(reaction.createdAt),
134
+ userIds: [reaction.userId],
135
+ });
136
+ }
137
+ }
138
+
139
+ return {
140
+ type: "comment",
141
+ id: comment.id,
142
+ body: comment.content,
143
+ metadata: comment.data?.metadata,
144
+ userId: comment.data?.userId,
145
+ createdAt: new Date(comment.createdAt),
146
+ updatedAt: new Date(comment.updatedAt),
147
+ reactions,
148
+ };
149
+ }
150
+
151
+ private tiptapThreadToThreadData(thread: TCollabThread): ThreadData {
152
+ return {
153
+ type: "thread",
154
+ id: thread.id,
155
+ comments: thread.comments.map((comment) =>
156
+ this.tiptapCommentToCommentData(comment)
157
+ ),
158
+ resolved: !!thread.resolvedAt,
159
+ metadata: thread.data?.metadata,
160
+ createdAt: new Date(thread.createdAt),
161
+ updatedAt: new Date(thread.updatedAt),
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Deletes a comment from a thread.
167
+ */
168
+ public async deleteComment(options: { threadId: string; commentId: string }) {
169
+ this.provider.deleteComment(options.threadId, options.commentId);
170
+ }
171
+
172
+ /**
173
+ * Deletes a thread.
174
+ */
175
+ public async deleteThread(options: { threadId: string }) {
176
+ this.provider.deleteThread(options.threadId);
177
+ }
178
+
179
+ /**
180
+ * Marks a thread as resolved.
181
+ */
182
+ public async resolveThread(options: { threadId: string }) {
183
+ this.provider.updateThread(options.threadId, {
184
+ resolvedAt: new Date().toISOString(),
185
+ });
186
+ }
187
+
188
+ /**
189
+ * Marks a thread as unresolved.
190
+ */
191
+ public async unresolveThread(options: { threadId: string }) {
192
+ this.provider.updateThread(options.threadId, {
193
+ resolvedAt: null,
194
+ });
195
+ }
196
+
197
+ /**
198
+ * Adds a reaction to a comment.
199
+ *
200
+ * Auth: should be possible by anyone with comment access
201
+ */
202
+ public async addReaction(options: {
203
+ threadId: string;
204
+ commentId: string;
205
+ emoji: string;
206
+ }) {
207
+ const comment = this.provider.getThreadComment(
208
+ options.threadId,
209
+ options.commentId,
210
+ true
211
+ );
212
+
213
+ if (!comment) {
214
+ throw new Error("Comment not found");
215
+ }
216
+
217
+ this.provider.updateComment(options.threadId, options.commentId, {
218
+ data: {
219
+ ...comment.data,
220
+ reactions: [
221
+ ...((comment.data?.reactions || []) as ReactionAsTiptapData[]),
222
+ {
223
+ emoji: options.emoji,
224
+ createdAt: Date.now(),
225
+ userId: this.userId,
226
+ },
227
+ ],
228
+ },
229
+ });
230
+ }
231
+
232
+ /**
233
+ * Deletes a reaction from a comment.
234
+ *
235
+ * Auth: should be possible by the reaction author
236
+ */
237
+ public async deleteReaction(options: {
238
+ threadId: string;
239
+ commentId: string;
240
+ emoji: string;
241
+ }) {
242
+ const comment = this.provider.getThreadComment(
243
+ options.threadId,
244
+ options.commentId,
245
+ true
246
+ );
247
+
248
+ if (!comment) {
249
+ throw new Error("Comment not found");
250
+ }
251
+
252
+ this.provider.updateComment(options.threadId, options.commentId, {
253
+ data: {
254
+ ...comment.data,
255
+ reactions: (
256
+ (comment.data?.reactions || []) as ReactionAsTiptapData[]
257
+ ).filter(
258
+ (reaction) =>
259
+ reaction.emoji !== options.emoji && reaction.userId !== this.userId
260
+ ),
261
+ },
262
+ });
263
+ }
264
+
265
+ public getThread(threadId: string): ThreadData {
266
+ const thread = this.provider.getThread(threadId);
267
+
268
+ if (!thread) {
269
+ throw new Error("Thread not found");
270
+ }
271
+
272
+ return this.tiptapThreadToThreadData(thread);
273
+ }
274
+
275
+ public getThreads(): Map<string, ThreadData> {
276
+ return new Map(
277
+ this.provider
278
+ .getThreads()
279
+ .map((thread) => [thread.id, this.tiptapThreadToThreadData(thread)])
280
+ );
281
+ }
282
+
283
+ public subscribe(cb: (threads: Map<string, ThreadData>) => void): () => void {
284
+ const newCb = () => {
285
+ cb(this.getThreads());
286
+ };
287
+ this.provider.watchThreads(newCb);
288
+ return () => {
289
+ this.provider.unwatchThreads(newCb);
290
+ };
291
+ }
292
+ }