@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
@@ -0,0 +1,340 @@
1
+ import { v4 } from "uuid";
2
+ import * as Y from "yjs";
3
+ import { CommentBody, CommentData, ThreadData } from "../../types.js";
4
+ import { ThreadStoreAuth } from "../ThreadStoreAuth.js";
5
+ import { YjsThreadStoreBase } from "./YjsThreadStoreBase.js";
6
+ import {
7
+ commentToYMap,
8
+ threadToYMap,
9
+ yMapToComment,
10
+ yMapToThread,
11
+ } from "./yjsHelpers.js";
12
+
13
+ /**
14
+ * This is a Yjs-based implementation of the ThreadStore interface.
15
+ *
16
+ * It reads and writes thread / comments information directly to the underlying Yjs Document.
17
+ *
18
+ * @important While this is the easiest to add to your app, there are two challenges:
19
+ * - The user needs to be able to write to the Yjs document to store the information.
20
+ * So a user without write access to the Yjs document cannot leave any comments.
21
+ * - Even with write access, the operations are not secure. Unless your Yjs server
22
+ * guards against malicious operations, it's technically possible for one user to make changes to another user's comments, etc.
23
+ * (even though these options are not visible in the UI, a malicious user can make unauthorized changes to the underlying Yjs document)
24
+ */
25
+ export class YjsThreadStore extends YjsThreadStoreBase {
26
+ constructor(
27
+ private readonly userId: string,
28
+ threadsYMap: Y.Map<any>,
29
+ auth: ThreadStoreAuth
30
+ ) {
31
+ super(threadsYMap, auth);
32
+ }
33
+
34
+ private transact = <T, R>(
35
+ fn: (options: T) => R
36
+ ): ((options: T) => Promise<R>) => {
37
+ return async (options: T) => {
38
+ return this.threadsYMap.doc!.transact(() => {
39
+ return fn(options);
40
+ });
41
+ };
42
+ };
43
+
44
+ public createThread = this.transact(
45
+ (options: {
46
+ initialComment: {
47
+ body: CommentBody;
48
+ metadata?: any;
49
+ };
50
+ metadata?: any;
51
+ }) => {
52
+ if (!this.auth.canCreateThread()) {
53
+ throw new Error("Not authorized");
54
+ }
55
+
56
+ const date = new Date();
57
+
58
+ const comment: CommentData = {
59
+ type: "comment",
60
+ id: v4(),
61
+ userId: this.userId,
62
+ createdAt: date,
63
+ updatedAt: date,
64
+ reactions: [],
65
+ metadata: options.initialComment.metadata,
66
+ body: options.initialComment.body,
67
+ };
68
+
69
+ const thread: ThreadData = {
70
+ type: "thread",
71
+ id: v4(),
72
+ createdAt: date,
73
+ updatedAt: date,
74
+ comments: [comment],
75
+ resolved: false,
76
+ metadata: options.metadata,
77
+ };
78
+
79
+ this.threadsYMap.set(thread.id, threadToYMap(thread));
80
+
81
+ return thread;
82
+ }
83
+ );
84
+
85
+ // YjsThreadStore does not support addThreadToDocument
86
+ public addThreadToDocument = undefined;
87
+
88
+ public addComment = this.transact(
89
+ (options: {
90
+ comment: {
91
+ body: CommentBody;
92
+ metadata?: any;
93
+ };
94
+ threadId: string;
95
+ }) => {
96
+ const yThread = this.threadsYMap.get(options.threadId);
97
+ if (!yThread) {
98
+ throw new Error("Thread not found");
99
+ }
100
+
101
+ if (!this.auth.canAddComment(yMapToThread(yThread))) {
102
+ throw new Error("Not authorized");
103
+ }
104
+
105
+ const date = new Date();
106
+ const comment: CommentData = {
107
+ type: "comment",
108
+ id: v4(),
109
+ userId: this.userId,
110
+ createdAt: date,
111
+ updatedAt: date,
112
+ deletedAt: undefined,
113
+ reactions: [],
114
+ metadata: options.comment.metadata,
115
+ body: options.comment.body,
116
+ };
117
+
118
+ (yThread.get("comments") as Y.Array<Y.Map<any>>).push([
119
+ commentToYMap(comment),
120
+ ]);
121
+
122
+ yThread.set("updatedAt", new Date().getTime());
123
+ return comment;
124
+ }
125
+ );
126
+
127
+ public updateComment = this.transact(
128
+ (options: {
129
+ comment: {
130
+ body: CommentBody;
131
+ metadata?: any;
132
+ };
133
+ threadId: string;
134
+ commentId: string;
135
+ }) => {
136
+ const yThread = this.threadsYMap.get(options.threadId);
137
+ if (!yThread) {
138
+ throw new Error("Thread not found");
139
+ }
140
+
141
+ const yCommentIndex = yArrayFindIndex(
142
+ yThread.get("comments"),
143
+ (comment) => comment.get("id") === options.commentId
144
+ );
145
+
146
+ if (yCommentIndex === -1) {
147
+ throw new Error("Comment not found");
148
+ }
149
+
150
+ const yComment = yThread.get("comments").get(yCommentIndex);
151
+
152
+ if (!this.auth.canUpdateComment(yMapToComment(yComment))) {
153
+ throw new Error("Not authorized");
154
+ }
155
+
156
+ yComment.set("body", options.comment.body);
157
+ yComment.set("updatedAt", new Date().getTime());
158
+ yComment.set("metadata", options.comment.metadata);
159
+ }
160
+ );
161
+
162
+ public deleteComment = this.transact(
163
+ (options: {
164
+ threadId: string;
165
+ commentId: string;
166
+ softDelete?: boolean;
167
+ }) => {
168
+ const yThread = this.threadsYMap.get(options.threadId);
169
+ if (!yThread) {
170
+ throw new Error("Thread not found");
171
+ }
172
+
173
+ const yCommentIndex = yArrayFindIndex(
174
+ yThread.get("comments"),
175
+ (comment) => comment.get("id") === options.commentId
176
+ );
177
+
178
+ if (yCommentIndex === -1) {
179
+ throw new Error("Comment not found");
180
+ }
181
+
182
+ const yComment = yThread.get("comments").get(yCommentIndex);
183
+
184
+ if (!this.auth.canDeleteComment(yMapToComment(yComment))) {
185
+ throw new Error("Not authorized");
186
+ }
187
+
188
+ if (yComment.get("deletedAt")) {
189
+ throw new Error("Comment already deleted");
190
+ }
191
+
192
+ if (options.softDelete) {
193
+ yComment.set("deletedAt", new Date().getTime());
194
+ yComment.set("body", undefined);
195
+ } else {
196
+ yThread.get("comments").delete(yCommentIndex);
197
+ }
198
+
199
+ if (
200
+ (yThread.get("comments") as Y.Array<any>)
201
+ .toArray()
202
+ .every((comment) => comment.get("deletedAt"))
203
+ ) {
204
+ // all comments deleted
205
+ if (options.softDelete) {
206
+ yThread.set("deletedAt", new Date().getTime());
207
+ } else {
208
+ this.threadsYMap.delete(options.threadId);
209
+ }
210
+ }
211
+
212
+ yThread.set("updatedAt", new Date().getTime());
213
+ }
214
+ );
215
+
216
+ public deleteThread = this.transact((options: { threadId: string }) => {
217
+ if (
218
+ !this.auth.canDeleteThread(
219
+ yMapToThread(this.threadsYMap.get(options.threadId))
220
+ )
221
+ ) {
222
+ throw new Error("Not authorized");
223
+ }
224
+
225
+ this.threadsYMap.delete(options.threadId);
226
+ });
227
+
228
+ public resolveThread = this.transact((options: { threadId: string }) => {
229
+ const yThread = this.threadsYMap.get(options.threadId);
230
+ if (!yThread) {
231
+ throw new Error("Thread not found");
232
+ }
233
+
234
+ if (!this.auth.canResolveThread(yMapToThread(yThread))) {
235
+ throw new Error("Not authorized");
236
+ }
237
+
238
+ yThread.set("resolved", true);
239
+ yThread.set("resolvedUpdatedAt", new Date().getTime());
240
+ });
241
+
242
+ public unresolveThread = this.transact((options: { threadId: string }) => {
243
+ const yThread = this.threadsYMap.get(options.threadId);
244
+ if (!yThread) {
245
+ throw new Error("Thread not found");
246
+ }
247
+
248
+ if (!this.auth.canUnresolveThread(yMapToThread(yThread))) {
249
+ throw new Error("Not authorized");
250
+ }
251
+
252
+ yThread.set("resolved", false);
253
+ yThread.set("resolvedUpdatedAt", new Date().getTime());
254
+ });
255
+
256
+ public addReaction = this.transact(
257
+ (options: { threadId: string; commentId: string; emoji: string }) => {
258
+ const yThread = this.threadsYMap.get(options.threadId);
259
+ if (!yThread) {
260
+ throw new Error("Thread not found");
261
+ }
262
+
263
+ const yCommentIndex = yArrayFindIndex(
264
+ yThread.get("comments"),
265
+ (comment) => comment.get("id") === options.commentId
266
+ );
267
+
268
+ if (yCommentIndex === -1) {
269
+ throw new Error("Comment not found");
270
+ }
271
+
272
+ const yComment = yThread.get("comments").get(yCommentIndex);
273
+
274
+ if (!this.auth.canAddReaction(yMapToComment(yComment), options.emoji)) {
275
+ throw new Error("Not authorized");
276
+ }
277
+
278
+ const date = new Date();
279
+
280
+ const key = `${this.userId}-${options.emoji}`;
281
+
282
+ const reactionsByUser = yComment.get("reactionsByUser");
283
+
284
+ if (reactionsByUser.has(key)) {
285
+ // already exists
286
+ return;
287
+ } else {
288
+ const reaction = new Y.Map();
289
+ reaction.set("emoji", options.emoji);
290
+ reaction.set("createdAt", date.getTime());
291
+ reaction.set("userId", this.userId);
292
+ reactionsByUser.set(key, reaction);
293
+ }
294
+ }
295
+ );
296
+
297
+ public deleteReaction = this.transact(
298
+ (options: { threadId: string; commentId: string; emoji: string }) => {
299
+ const yThread = this.threadsYMap.get(options.threadId);
300
+ if (!yThread) {
301
+ throw new Error("Thread not found");
302
+ }
303
+
304
+ const yCommentIndex = yArrayFindIndex(
305
+ yThread.get("comments"),
306
+ (comment) => comment.get("id") === options.commentId
307
+ );
308
+
309
+ if (yCommentIndex === -1) {
310
+ throw new Error("Comment not found");
311
+ }
312
+
313
+ const yComment = yThread.get("comments").get(yCommentIndex);
314
+
315
+ if (
316
+ !this.auth.canDeleteReaction(yMapToComment(yComment), options.emoji)
317
+ ) {
318
+ throw new Error("Not authorized");
319
+ }
320
+
321
+ const key = `${this.userId}-${options.emoji}`;
322
+
323
+ const reactionsByUser = yComment.get("reactionsByUser");
324
+
325
+ reactionsByUser.delete(key);
326
+ }
327
+ );
328
+ }
329
+
330
+ function yArrayFindIndex(
331
+ yArray: Y.Array<any>,
332
+ predicate: (item: any) => boolean
333
+ ) {
334
+ for (let i = 0; i < yArray.length; i++) {
335
+ if (predicate(yArray.get(i))) {
336
+ return i;
337
+ }
338
+ }
339
+ return -1;
340
+ }
@@ -0,0 +1,48 @@
1
+ import * as Y from "yjs";
2
+ import { ThreadData } from "../../types.js";
3
+ import { ThreadStore } from "../ThreadStore.js";
4
+ import { ThreadStoreAuth } from "../ThreadStoreAuth.js";
5
+ import { yMapToThread } from "./yjsHelpers.js";
6
+
7
+ /**
8
+ * This is an abstract class that only implements the READ methods required by the ThreadStore interface.
9
+ * The data is read from a Yjs Map.
10
+ */
11
+ export abstract class YjsThreadStoreBase extends ThreadStore {
12
+ constructor(
13
+ protected readonly threadsYMap: Y.Map<any>,
14
+ auth: ThreadStoreAuth
15
+ ) {
16
+ super(auth);
17
+ }
18
+
19
+ // TODO: async / reactive interface?
20
+ public getThread(threadId: string) {
21
+ const yThread = this.threadsYMap.get(threadId);
22
+ if (!yThread) {
23
+ throw new Error("Thread not found");
24
+ }
25
+ const thread = yMapToThread(yThread);
26
+ return thread;
27
+ }
28
+
29
+ public getThreads(): Map<string, ThreadData> {
30
+ const threadMap = new Map<string, ThreadData>();
31
+ this.threadsYMap.forEach((yThread, id) => {
32
+ threadMap.set(id, yMapToThread(yThread));
33
+ });
34
+ return threadMap;
35
+ }
36
+
37
+ public subscribe(cb: (threads: Map<string, ThreadData>) => void) {
38
+ const observer = () => {
39
+ cb(this.getThreads());
40
+ };
41
+
42
+ this.threadsYMap.observeDeep(observer);
43
+
44
+ return () => {
45
+ this.threadsYMap.unobserveDeep(observer);
46
+ };
47
+ }
48
+ }
@@ -0,0 +1,121 @@
1
+ import * as Y from "yjs";
2
+ import { CommentData, CommentReactionData, ThreadData } from "../../types.js";
3
+
4
+ export function commentToYMap(comment: CommentData) {
5
+ const yMap = new Y.Map<any>();
6
+ yMap.set("id", comment.id);
7
+ yMap.set("userId", comment.userId);
8
+ yMap.set("createdAt", comment.createdAt.getTime());
9
+ yMap.set("updatedAt", comment.updatedAt.getTime());
10
+ if (comment.deletedAt) {
11
+ yMap.set("deletedAt", comment.deletedAt.getTime());
12
+ yMap.set("body", undefined);
13
+ } else {
14
+ yMap.set("body", comment.body);
15
+ }
16
+ if (comment.reactions.length > 0) {
17
+ throw new Error("Reactions should be empty in commentToYMap");
18
+ }
19
+
20
+ /**
21
+ * Reactions are stored in a map keyed by {userId-emoji},
22
+ * this makes it easy to add / remove reactions and in a way that works local-first.
23
+ * The cost is that "reading" the reactions is a bit more complex (see yMapToReactions).
24
+ */
25
+ yMap.set("reactionsByUser", new Y.Map());
26
+ yMap.set("metadata", comment.metadata);
27
+
28
+ return yMap;
29
+ }
30
+
31
+ export function threadToYMap(thread: ThreadData) {
32
+ const yMap = new Y.Map();
33
+ yMap.set("id", thread.id);
34
+ yMap.set("createdAt", thread.createdAt.getTime());
35
+ yMap.set("updatedAt", thread.updatedAt.getTime());
36
+ const commentsArray = new Y.Array<Y.Map<any>>();
37
+
38
+ commentsArray.push(thread.comments.map((comment) => commentToYMap(comment)));
39
+
40
+ yMap.set("comments", commentsArray);
41
+ yMap.set("resolved", thread.resolved);
42
+ yMap.set("resolvedUpdatedAt", thread.resolvedUpdatedAt?.getTime());
43
+ yMap.set("metadata", thread.metadata);
44
+ return yMap;
45
+ }
46
+
47
+ type SingleUserCommentReactionData = {
48
+ emoji: string;
49
+ createdAt: Date;
50
+ userId: string;
51
+ };
52
+
53
+ export function yMapToReaction(
54
+ yMap: Y.Map<any>
55
+ ): SingleUserCommentReactionData {
56
+ return {
57
+ emoji: yMap.get("emoji"),
58
+ createdAt: new Date(yMap.get("createdAt")),
59
+ userId: yMap.get("userId"),
60
+ };
61
+ }
62
+
63
+ function yMapToReactions(yMap: Y.Map<any>): CommentReactionData[] {
64
+ const flatReactions = [...yMap.values()].map((reaction: Y.Map<any>) =>
65
+ yMapToReaction(reaction)
66
+ );
67
+ // combine reactions by the same emoji
68
+ return flatReactions.reduce(
69
+ (acc: CommentReactionData[], reaction: SingleUserCommentReactionData) => {
70
+ const existingReaction = acc.find((r) => r.emoji === reaction.emoji);
71
+ if (existingReaction) {
72
+ existingReaction.userIds.push(reaction.userId);
73
+ existingReaction.createdAt = new Date(
74
+ Math.min(
75
+ existingReaction.createdAt.getTime(),
76
+ reaction.createdAt.getTime()
77
+ )
78
+ );
79
+ } else {
80
+ acc.push({
81
+ emoji: reaction.emoji,
82
+ createdAt: reaction.createdAt,
83
+ userIds: [reaction.userId],
84
+ });
85
+ }
86
+ return acc;
87
+ },
88
+ [] as CommentReactionData[]
89
+ );
90
+ }
91
+
92
+ export function yMapToComment(yMap: Y.Map<any>): CommentData {
93
+ return {
94
+ type: "comment",
95
+ id: yMap.get("id"),
96
+ userId: yMap.get("userId"),
97
+ createdAt: new Date(yMap.get("createdAt")),
98
+ updatedAt: new Date(yMap.get("updatedAt")),
99
+ deletedAt: yMap.get("deletedAt")
100
+ ? new Date(yMap.get("deletedAt"))
101
+ : undefined,
102
+ reactions: yMapToReactions(yMap.get("reactionsByUser")),
103
+ metadata: yMap.get("metadata"),
104
+ body: yMap.get("body"),
105
+ };
106
+ }
107
+
108
+ export function yMapToThread(yMap: Y.Map<any>): ThreadData {
109
+ return {
110
+ type: "thread",
111
+ id: yMap.get("id"),
112
+ createdAt: new Date(yMap.get("createdAt")),
113
+ updatedAt: new Date(yMap.get("updatedAt")),
114
+ comments: ((yMap.get("comments") as Y.Array<Y.Map<any>>) || []).map(
115
+ (comment) => yMapToComment(comment)
116
+ ),
117
+ resolved: yMap.get("resolved"),
118
+ resolvedUpdatedAt: yMap.get("resolvedUpdatedAt"),
119
+ metadata: yMap.get("metadata"),
120
+ };
121
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * The body of a comment. This actually is a BlockNote document (array of blocks)
3
+ */
4
+ export type CommentBody = any;
5
+
6
+ /**
7
+ * A reaction to a comment.
8
+ */
9
+ export type CommentReactionData = {
10
+ /**
11
+ * The emoji that was reacted to the comment.
12
+ */
13
+ emoji: string;
14
+ /**
15
+ * The date the first user reacted to the comment with this emoji.
16
+ */
17
+ createdAt: Date;
18
+ /**
19
+ * The user ids of the users that have reacted to the comment with this emoji
20
+ */
21
+ userIds: string[];
22
+ };
23
+
24
+ /**
25
+ * Information about a comment.
26
+ */
27
+ export type CommentData = {
28
+ type: "comment";
29
+ /**
30
+ * The unique identifier for the comment.
31
+ */
32
+ id: string;
33
+ /**
34
+ * The user id of the author of the comment.
35
+ */
36
+ userId: string;
37
+ /**
38
+ * The date when the comment was created.
39
+ */
40
+ createdAt: Date;
41
+ /**
42
+ * The date when the comment was last updated.
43
+ */
44
+ updatedAt: Date;
45
+
46
+ /**
47
+ * The reactions (emoji reactions) to the comment.
48
+ */
49
+ reactions: CommentReactionData[];
50
+
51
+ /**
52
+ * You can use this store any additional information about the comment.
53
+ */
54
+ metadata: any;
55
+ } & (
56
+ | {
57
+ /**
58
+ * The date when the comment was deleted. This applies only for "soft deletes",
59
+ * otherwise the comment is removed entirely.
60
+ */
61
+ deletedAt: Date;
62
+ /**
63
+ * The body of the comment is undefined if the comment is deleted.
64
+ */
65
+ body: undefined;
66
+ }
67
+ | {
68
+ /**
69
+ * In case of a non-deleted comment, this is not set
70
+ */
71
+ deletedAt?: never;
72
+ /**
73
+ * The body of the comment.
74
+ */
75
+ body: CommentBody;
76
+ }
77
+ );
78
+
79
+ /**
80
+ * Information about a thread. A thread holds a list of comments.
81
+ */
82
+ export type ThreadData = {
83
+ type: "thread";
84
+ /**
85
+ * The unique identifier for the thread.
86
+ */
87
+ id: string;
88
+ /**
89
+ * The date when the thread was created.
90
+ */
91
+ createdAt: Date;
92
+ /**
93
+ * The date when the thread was last updated.
94
+ */
95
+ updatedAt: Date;
96
+ /**
97
+ * The comments in the thread.
98
+ */
99
+ comments: CommentData[];
100
+ /**
101
+ * Whether the thread has been marked as resolved.
102
+ */
103
+ resolved: boolean;
104
+ /**
105
+ * The date when the thread was marked as resolved.
106
+ */
107
+ resolvedUpdatedAt?: Date;
108
+ /**
109
+ * You can use this store any additional information about the thread.
110
+ */
111
+ metadata: any;
112
+ /**
113
+ * The date when the thread was deleted. (or undefined if it is not deleted)
114
+ * This only applies for "soft deletes", otherwise the thread is removed entirely.
115
+ */
116
+ deletedAt?: Date;
117
+ };
@@ -499,23 +499,23 @@ NESTED BLOCKS
499
499
 
500
500
  /* TEXT ALIGNMENT */
501
501
  [data-text-alignment="left"] {
502
- justify-content: flex-start;
503
- text-align: left;
502
+ justify-content: flex-start !important;
503
+ text-align: left !important;
504
504
  }
505
505
 
506
506
  [data-text-alignment="center"] {
507
- justify-content: center;
508
- text-align: center;
507
+ justify-content: center !important;
508
+ text-align: center !important;
509
509
  }
510
510
 
511
511
  [data-text-alignment="right"] {
512
- justify-content: flex-end;
513
- text-align: right;
512
+ justify-content: flex-end !important;
513
+ text-align: right !important;
514
514
  }
515
515
 
516
516
  [data-text-alignment="justify"] {
517
- justify-content: flex-start;
518
- text-align: justify;
517
+ justify-content: flex-start !important;
518
+ text-align: justify !important;
519
519
  }
520
520
 
521
521
  .bn-block-column-list {
@@ -537,3 +537,11 @@ NESTED BLOCKS
537
537
  .bn-block-column:last-child {
538
538
  padding-right: 0;
539
539
  }
540
+
541
+ .bn-thread-mark:not([data-orphan="true"]) {
542
+ background: rgba(255, 200, 0, 0.15);
543
+ }
544
+
545
+ .bn-thread-mark:not([data-orphan="true"]) .bn-thread-mark-selected {
546
+ background: rgba(255, 200, 0, 0.25);
547
+ }