@blocknote/core 0.24.2 → 0.25.1
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.
- package/dist/blocknote.cjs +12 -0
- package/dist/blocknote.cjs.map +1 -0
- package/dist/blocknote.js +4784 -3545
- package/dist/blocknote.js.map +1 -1
- package/dist/comments.cjs +2 -0
- package/dist/comments.cjs.map +1 -0
- package/dist/comments.js +593 -0
- package/dist/comments.js.map +1 -0
- package/dist/style.css +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/webpack-stats.json +1 -1
- package/package.json +40 -27
- package/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap +1022 -378
- package/src/api/blockManipulation/commands/mergeBlocks/__snapshots__/mergeBlocks.test.ts.snap +730 -270
- package/src/api/blockManipulation/commands/moveBlocks/__snapshots__/moveBlocks.test.ts.snap +3100 -1260
- package/src/api/blockManipulation/commands/removeBlocks/__snapshots__/removeBlocks.test.ts.snap +438 -162
- package/src/api/blockManipulation/commands/replaceBlocks/__snapshots__/replaceBlocks.test.ts.snap +1168 -432
- package/src/api/blockManipulation/commands/splitBlock/__snapshots__/splitBlock.test.ts.snap +930 -378
- package/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap +2485 -1015
- package/src/api/blockManipulation/commands/updateBlock/updateBlock.test.ts +28 -1
- package/src/api/blockManipulation/commands/updateBlock/updateBlock.ts +1 -1
- package/src/api/blockManipulation/selections/__snapshots__/selection.test.ts.snap +292 -108
- package/src/api/blockManipulation/setupTestEnv.ts +14 -1
- package/src/api/blockManipulation/tables/tables.test.ts +1987 -0
- package/src/api/blockManipulation/tables/tables.ts +887 -0
- package/src/api/clipboard/__snapshots__/external/pasteEndOfParagraph.html +66 -24
- package/src/api/clipboard/__snapshots__/external/pasteEndOfParagraphText.html +66 -24
- package/src/api/clipboard/__snapshots__/external/pasteImage.html +66 -24
- package/src/api/clipboard/__snapshots__/external/pasteParagraphInCustomBlock.html +66 -24
- package/src/api/clipboard/__snapshots__/external/pasteTable.html +132 -48
- package/src/api/clipboard/__snapshots__/external/pasteTableInExistingTable.html +136 -44
- package/src/api/clipboard/toClipboard/copyExtension.ts +2 -3
- package/src/api/exporters/html/__snapshots__/table/headerCols/external.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/headerCols/internal.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/headerRows/external.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/headerRows/internal.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/headersRows/external.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/headersRows/internal.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/mixedCellColors/external.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/mixedCellColors/internal.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/mixedRowspansAndColspans/external.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/mixedRowspansAndColspans/internal.html +1 -0
- package/src/api/exporters/markdown/__snapshots__/table/headerCols/markdown.md +4 -0
- package/src/api/exporters/markdown/__snapshots__/table/headerRows/markdown.md +4 -0
- package/src/api/exporters/markdown/__snapshots__/table/mixedCellColors/markdown.md +5 -0
- package/src/api/exporters/markdown/__snapshots__/table/mixedRowspansAndColspans/markdown.md +5 -0
- package/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +985 -20
- package/src/api/nodeConversions/blockToNode.ts +63 -20
- package/src/api/nodeConversions/nodeToBlock.ts +75 -13
- package/src/api/parsers/html/__snapshots__/parse-notion-html.json +145 -54
- package/src/api/testUtil/cases/defaultSchema.ts +782 -9
- package/src/api/testUtil/partialBlockTestUtil.ts +39 -4
- package/src/blocks/TableBlockContent/TableBlockContent.ts +11 -5
- package/src/blocks/defaultBlockTypeGuards.ts +8 -0
- package/src/comments/index.ts +9 -0
- package/src/comments/models/User.ts +8 -0
- package/src/comments/threadstore/DefaultThreadStoreAuth.ts +106 -0
- package/src/comments/threadstore/ThreadStore.ts +134 -0
- package/src/comments/threadstore/ThreadStoreAuth.ts +13 -0
- package/src/comments/threadstore/TipTapThreadStore.ts +292 -0
- package/src/comments/threadstore/yjs/RESTYjsThreadStore.ts +144 -0
- package/src/comments/threadstore/yjs/YjsThreadStore.test.ts +294 -0
- package/src/comments/threadstore/yjs/YjsThreadStore.ts +340 -0
- package/src/comments/threadstore/yjs/YjsThreadStoreBase.ts +48 -0
- package/src/comments/threadstore/yjs/yjsHelpers.ts +121 -0
- package/src/comments/types.ts +117 -0
- package/src/editor/Block.css +16 -8
- package/src/editor/BlockNoteEditor.ts +269 -92
- package/src/editor/BlockNoteExtensions.ts +24 -1
- package/src/editor/BlockNoteTipTapEditor.ts +5 -1
- package/src/editor/editor.css +23 -4
- package/src/extensions/BackgroundColor/BackgroundColorExtension.ts +1 -1
- package/src/extensions/Collaboration/createCollaborationExtensions.ts +8 -17
- package/src/extensions/Comments/CommentMark.ts +61 -0
- package/src/extensions/Comments/CommentsPlugin.ts +301 -0
- package/src/extensions/Comments/userstore/UserStore.ts +72 -0
- package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +9 -5
- package/src/extensions/LinkToolbar/LinkToolbarPlugin.ts +3 -3
- package/src/extensions/ShowSelection/ShowSelectionPlugin.ts +52 -0
- package/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts +34 -17
- package/src/extensions/TableHandles/TableHandlesPlugin.ts +409 -57
- package/src/extensions/TextAlignment/TextAlignmentExtension.ts +2 -0
- package/src/extensions/TextColor/TextColorExtension.ts +1 -1
- package/src/i18n/locales/ar.ts +23 -0
- package/src/i18n/locales/de.ts +15 -0
- package/src/i18n/locales/en.ts +25 -1
- package/src/i18n/locales/es.ts +16 -1
- package/src/i18n/locales/fr.ts +23 -0
- package/src/i18n/locales/hr.ts +18 -0
- package/src/i18n/locales/is.ts +24 -1
- package/src/i18n/locales/it.ts +15 -0
- package/src/i18n/locales/ja.ts +23 -0
- package/src/i18n/locales/ko.ts +23 -0
- package/src/i18n/locales/nl.ts +23 -0
- package/src/i18n/locales/no.ts +23 -0
- package/src/i18n/locales/pl.ts +23 -0
- package/src/i18n/locales/pt.ts +23 -0
- package/src/i18n/locales/ru.ts +23 -0
- package/src/i18n/locales/uk.ts +23 -0
- package/src/i18n/locales/vi.ts +23 -0
- package/src/i18n/locales/zh.ts +23 -0
- package/src/index.ts +6 -4
- package/src/schema/blocks/types.ts +32 -2
- package/src/util/browser.ts +1 -1
- package/src/util/table.ts +107 -0
- package/types/src/api/blockManipulation/tables/tables.d.ts +343 -0
- package/types/src/api/blockManipulation/tables/tables.test.d.ts +1 -0
- package/types/src/api/clipboard/toClipboard/copyExtension.d.ts +1 -1
- package/types/src/blocks/TableBlockContent/TableBlockContent.d.ts +1 -2
- package/types/src/blocks/defaultBlockTypeGuards.d.ts +3 -0
- package/types/src/comments/index.d.ts +9 -0
- package/types/src/comments/models/User.d.ts +8 -0
- package/types/src/comments/threadstore/DefaultThreadStoreAuth.d.ts +47 -0
- package/types/src/comments/threadstore/ThreadStore.d.ts +121 -0
- package/types/src/comments/threadstore/ThreadStoreAuth.d.ts +12 -0
- package/types/src/comments/threadstore/TipTapThreadStore.d.ts +97 -0
- package/types/src/comments/threadstore/yjs/RESTYjsThreadStore.d.ts +83 -0
- package/types/src/comments/threadstore/yjs/YjsThreadStore.d.ts +79 -0
- package/types/src/comments/threadstore/yjs/YjsThreadStore.test.d.ts +1 -0
- package/types/src/comments/threadstore/yjs/YjsThreadStoreBase.d.ts +15 -0
- package/types/src/comments/threadstore/yjs/yjsHelpers.d.ts +13 -0
- package/types/src/comments/types.d.ts +109 -0
- package/types/src/editor/BlockNoteEditor.d.ts +146 -66
- package/types/src/editor/BlockNoteExtensions.d.ts +4 -0
- package/types/src/extensions/Collaboration/createCollaborationExtensions.d.ts +1 -1
- package/types/src/extensions/Comments/CommentMark.d.ts +2 -0
- package/types/src/extensions/Comments/CommentsPlugin.d.ts +49 -0
- package/types/src/extensions/Comments/userstore/UserStore.d.ts +31 -0
- package/types/src/extensions/ShowSelection/ShowSelectionPlugin.d.ts +15 -0
- package/types/src/extensions/TableHandles/TableHandlesPlugin.d.ts +66 -1
- package/types/src/i18n/locales/de.d.ts +15 -0
- package/types/src/i18n/locales/en.d.ts +20 -0
- package/types/src/i18n/locales/es.d.ts +15 -0
- package/types/src/i18n/locales/hr.d.ts +18 -0
- package/types/src/i18n/locales/it.d.ts +15 -0
- package/types/src/index.d.ts +5 -4
- package/types/src/pm-nodes/BlockContainer.d.ts +2 -2
- package/types/src/pm-nodes/BlockGroup.d.ts +2 -2
- package/types/src/schema/blocks/types.d.ts +23 -2
- package/types/src/util/browser.d.ts +1 -1
- package/types/src/util/table.d.ts +12 -0
- package/dist/blocknote.umd.cjs +0 -11
- package/dist/blocknote.umd.cjs.map +0 -1
package/src/editor/editor.css
CHANGED
|
@@ -10,6 +10,15 @@
|
|
|
10
10
|
--N40: #dfe1e6; /* Light neutral used for subtle borders and text on dark background */
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
.bn-comment-editor {
|
|
14
|
+
width: 100%;
|
|
15
|
+
padding: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.bn-comment-editor .bn-editor {
|
|
19
|
+
padding: 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
/*
|
|
14
23
|
bn-root should be applied to all top-level elements
|
|
15
24
|
|
|
@@ -77,11 +86,11 @@ Tippy popups that are appended to document.body directly
|
|
|
77
86
|
opacity: 0.001;
|
|
78
87
|
}
|
|
79
88
|
|
|
80
|
-
.collaboration-cursor__base {
|
|
89
|
+
.bn-editor .bn-collaboration-cursor__base {
|
|
81
90
|
position: relative;
|
|
82
91
|
}
|
|
83
92
|
|
|
84
|
-
.collaboration-cursor__caret {
|
|
93
|
+
.bn-editor .bn-collaboration-cursor__base .bn-collaboration-cursor__caret {
|
|
85
94
|
position: absolute;
|
|
86
95
|
width: 2px;
|
|
87
96
|
top: 1px;
|
|
@@ -89,7 +98,7 @@ Tippy popups that are appended to document.body directly
|
|
|
89
98
|
left: -1px;
|
|
90
99
|
}
|
|
91
100
|
|
|
92
|
-
.collaboration-cursor__label {
|
|
101
|
+
.bn-editor .bn-collaboration-cursor__base .bn-collaboration-cursor__label {
|
|
93
102
|
pointer-events: none;
|
|
94
103
|
border-radius: 0 1.5px 1.5px 0;
|
|
95
104
|
font-size: 12px;
|
|
@@ -110,7 +119,9 @@ Tippy popups that are appended to document.body directly
|
|
|
110
119
|
transition: all 0.2s;
|
|
111
120
|
}
|
|
112
121
|
|
|
113
|
-
.
|
|
122
|
+
.bn-editor
|
|
123
|
+
.bn-collaboration-cursor__base[data-active]
|
|
124
|
+
.bn-collaboration-cursor__label {
|
|
114
125
|
color: #0d0d0d;
|
|
115
126
|
max-height: 1.1rem;
|
|
116
127
|
max-width: 20rem;
|
|
@@ -179,3 +190,11 @@ Tippy popups that are appended to document.body directly
|
|
|
179
190
|
.prosemirror-dropcursor-vertical {
|
|
180
191
|
transition-property: left, right;
|
|
181
192
|
}
|
|
193
|
+
|
|
194
|
+
/*
|
|
195
|
+
For the ShowSelectionPlugin
|
|
196
|
+
*/
|
|
197
|
+
[data-show-selection] {
|
|
198
|
+
background-color: highlight;
|
|
199
|
+
padding: 2px 0;
|
|
200
|
+
}
|
|
@@ -7,7 +7,7 @@ export const BackgroundColorExtension = Extension.create({
|
|
|
7
7
|
addGlobalAttributes() {
|
|
8
8
|
return [
|
|
9
9
|
{
|
|
10
|
-
types: ["blockContainer"],
|
|
10
|
+
types: ["blockContainer", "tableCell", "tableHeader"],
|
|
11
11
|
attributes: {
|
|
12
12
|
backgroundColor: {
|
|
13
13
|
default: defaultProps.backgroundColor.default,
|
|
@@ -65,16 +65,16 @@ export const createCollaborationExtensions = (collaboration: {
|
|
|
65
65
|
const renderCursor = (user: { name: string; color: string }) => {
|
|
66
66
|
const cursorElement = document.createElement("span");
|
|
67
67
|
|
|
68
|
-
cursorElement.classList.add("collaboration-cursor__base");
|
|
68
|
+
cursorElement.classList.add("bn-collaboration-cursor__base");
|
|
69
69
|
|
|
70
70
|
const caretElement = document.createElement("span");
|
|
71
71
|
caretElement.setAttribute("contentedEditable", "false");
|
|
72
|
-
caretElement.classList.add("collaboration-cursor__caret");
|
|
72
|
+
caretElement.classList.add("bn-collaboration-cursor__caret");
|
|
73
73
|
caretElement.setAttribute("style", `background-color: ${user.color}`);
|
|
74
74
|
|
|
75
75
|
const labelElement = document.createElement("span");
|
|
76
76
|
|
|
77
|
-
labelElement.classList.add("collaboration-cursor__label");
|
|
77
|
+
labelElement.classList.add("bn-collaboration-cursor__label");
|
|
78
78
|
labelElement.setAttribute("style", `background-color: ${user.color}`);
|
|
79
79
|
labelElement.insertBefore(document.createTextNode(user.name), null);
|
|
80
80
|
|
|
@@ -87,19 +87,10 @@ export const createCollaborationExtensions = (collaboration: {
|
|
|
87
87
|
return cursorElement;
|
|
88
88
|
};
|
|
89
89
|
|
|
90
|
-
const render = (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (!clientState) {
|
|
96
|
-
throw new Error(
|
|
97
|
-
"Could not find client state for user, " + JSON.stringify(user)
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const clientID = clientState[0];
|
|
102
|
-
|
|
90
|
+
const render = (
|
|
91
|
+
user: { color: string; name: string },
|
|
92
|
+
clientID: number
|
|
93
|
+
) => {
|
|
103
94
|
let cursorData = cursors.get(clientID);
|
|
104
95
|
|
|
105
96
|
if (!cursorData) {
|
|
@@ -146,7 +137,7 @@ export const createCollaborationExtensions = (collaboration: {
|
|
|
146
137
|
tiptapExtensions.push(
|
|
147
138
|
CollaborationCursor.configure({
|
|
148
139
|
user: collaboration.user,
|
|
149
|
-
render,
|
|
140
|
+
render: render as any, // tiptap type not compatible with latest y-prosemirror
|
|
150
141
|
provider: collaboration.provider,
|
|
151
142
|
})
|
|
152
143
|
);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Mark, mergeAttributes } from "@tiptap/core";
|
|
2
|
+
|
|
3
|
+
export const CommentMark = Mark.create({
|
|
4
|
+
name: "comment",
|
|
5
|
+
excludes: "",
|
|
6
|
+
inclusive: false,
|
|
7
|
+
keepOnSplit: true,
|
|
8
|
+
group: "blocknoteIgnore", // ignore in blocknote json
|
|
9
|
+
|
|
10
|
+
addAttributes() {
|
|
11
|
+
// Return an object with attribute configuration
|
|
12
|
+
return {
|
|
13
|
+
// orphans are marks that currently don't have an active thread. It could be
|
|
14
|
+
// that users have resolved the thread. Resolved threads by default are not shown in the document,
|
|
15
|
+
// but we need to keep the mark (positioning) data so we can still "revive" it when the thread is unresolved
|
|
16
|
+
// or we enter a "comments" view that includes resolved threads.
|
|
17
|
+
orphan: {
|
|
18
|
+
parseHTML: (element) => !!element.getAttribute("data-orphan"),
|
|
19
|
+
renderHTML: (attributes) => {
|
|
20
|
+
return (attributes as { orphan: boolean }).orphan
|
|
21
|
+
? {
|
|
22
|
+
"data-orphan": "true",
|
|
23
|
+
}
|
|
24
|
+
: {};
|
|
25
|
+
},
|
|
26
|
+
default: false,
|
|
27
|
+
},
|
|
28
|
+
threadId: {
|
|
29
|
+
parseHTML: (element) => element.getAttribute("data-bn-thread-id"),
|
|
30
|
+
renderHTML: (attributes) => {
|
|
31
|
+
return {
|
|
32
|
+
"data-bn-thread-id": (attributes as { threadId: string }).threadId,
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
default: "",
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
renderHTML({ HTMLAttributes }: { HTMLAttributes: Record<string, any> }) {
|
|
41
|
+
return [
|
|
42
|
+
"span",
|
|
43
|
+
mergeAttributes(HTMLAttributes, {
|
|
44
|
+
class: "bn-thread-mark",
|
|
45
|
+
}),
|
|
46
|
+
];
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
parseHTML() {
|
|
50
|
+
return [{ tag: "span.bn-thread-mark" }];
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
extendMarkSchema(extension) {
|
|
54
|
+
if (extension.name === "comment") {
|
|
55
|
+
return {
|
|
56
|
+
blocknoteIgnore: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return {};
|
|
60
|
+
},
|
|
61
|
+
});
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { Node } from "prosemirror-model";
|
|
2
|
+
import { Plugin, PluginKey } from "prosemirror-state";
|
|
3
|
+
import { Decoration, DecorationSet } from "prosemirror-view";
|
|
4
|
+
import { getRelativeSelection, ySyncPluginKey } from "y-prosemirror";
|
|
5
|
+
import type {
|
|
6
|
+
CommentBody,
|
|
7
|
+
ThreadData,
|
|
8
|
+
ThreadStore,
|
|
9
|
+
User,
|
|
10
|
+
} from "../../comments/index.js";
|
|
11
|
+
import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
|
|
12
|
+
import { EventEmitter } from "../../util/EventEmitter.js";
|
|
13
|
+
import { UserStore } from "./userstore/UserStore.js";
|
|
14
|
+
|
|
15
|
+
const PLUGIN_KEY = new PluginKey(`blocknote-comments`);
|
|
16
|
+
const SET_SELECTED_THREAD_ID = "SET_SELECTED_THREAD_ID";
|
|
17
|
+
|
|
18
|
+
type CommentsPluginState = {
|
|
19
|
+
/**
|
|
20
|
+
* Store the positions of all threads in the document.
|
|
21
|
+
* this can be used later to implement a floating sidebar
|
|
22
|
+
*/
|
|
23
|
+
threadPositions: Map<string, { from: number; to: number }>;
|
|
24
|
+
/**
|
|
25
|
+
* Decorations to be rendered, specifically to indicate the selected thread
|
|
26
|
+
*/
|
|
27
|
+
decorations: DecorationSet;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get a new state (theadPositions and decorations) from the current document state
|
|
32
|
+
*/
|
|
33
|
+
function updateState(
|
|
34
|
+
doc: Node,
|
|
35
|
+
selectedThreadId: string | undefined,
|
|
36
|
+
markType: string
|
|
37
|
+
): CommentsPluginState {
|
|
38
|
+
const threadPositions = new Map<string, { from: number; to: number }>();
|
|
39
|
+
const decorations: Decoration[] = [];
|
|
40
|
+
// find all thread marks and store their position + create decoration for selected thread
|
|
41
|
+
doc.descendants((node, pos) => {
|
|
42
|
+
node.marks.forEach((mark) => {
|
|
43
|
+
if (mark.type.name === markType) {
|
|
44
|
+
const thisThreadId = (mark.attrs as { threadId: string | undefined })
|
|
45
|
+
.threadId;
|
|
46
|
+
if (!thisThreadId) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const from = pos;
|
|
50
|
+
const to = from + node.nodeSize;
|
|
51
|
+
|
|
52
|
+
// FloatingThreads component uses "to" as the position, so always store the largest "to" found
|
|
53
|
+
// AnchoredThreads component uses "from" as the position, so always store the smallest "from" found
|
|
54
|
+
const currentPosition = threadPositions.get(thisThreadId) ?? {
|
|
55
|
+
from: Infinity,
|
|
56
|
+
to: 0,
|
|
57
|
+
};
|
|
58
|
+
threadPositions.set(thisThreadId, {
|
|
59
|
+
from: Math.min(from, currentPosition.from),
|
|
60
|
+
to: Math.max(to, currentPosition.to),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (selectedThreadId === thisThreadId) {
|
|
64
|
+
decorations.push(
|
|
65
|
+
Decoration.inline(from, to, {
|
|
66
|
+
class: "bn-thread-mark-selected",
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
decorations: DecorationSet.create(doc, decorations),
|
|
75
|
+
threadPositions,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class CommentsPlugin extends EventEmitter<any> {
|
|
80
|
+
public readonly plugin: Plugin;
|
|
81
|
+
public readonly userStore: UserStore<User>;
|
|
82
|
+
|
|
83
|
+
private pendingComment = false;
|
|
84
|
+
private selectedThreadId: string | undefined;
|
|
85
|
+
|
|
86
|
+
private emitStateUpdate() {
|
|
87
|
+
this.emit("update", {
|
|
88
|
+
selectedThreadId: this.selectedThreadId,
|
|
89
|
+
pendingComment: this.pendingComment,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* when a thread is resolved or deleted, we need to update the marks to reflect the new state
|
|
95
|
+
*/
|
|
96
|
+
private updateMarksFromThreads = (threads: Map<string, ThreadData>) => {
|
|
97
|
+
const ttEditor = this.editor._tiptapEditor;
|
|
98
|
+
|
|
99
|
+
ttEditor.state.doc.descendants((node, pos) => {
|
|
100
|
+
node.marks.forEach((mark) => {
|
|
101
|
+
if (mark.type.name === this.markType) {
|
|
102
|
+
const markType = mark.type;
|
|
103
|
+
const markThreadId = mark.attrs.threadId;
|
|
104
|
+
const thread = threads.get(markThreadId);
|
|
105
|
+
const isOrphan = !!(!thread || thread.resolved || thread.deletedAt);
|
|
106
|
+
|
|
107
|
+
if (isOrphan !== mark.attrs.orphan) {
|
|
108
|
+
const { tr } = ttEditor.state;
|
|
109
|
+
const trimmedFrom = Math.max(pos, 0);
|
|
110
|
+
const trimmedTo = Math.min(
|
|
111
|
+
pos + node.nodeSize,
|
|
112
|
+
ttEditor.state.doc.content.size - 1
|
|
113
|
+
);
|
|
114
|
+
tr.removeMark(trimmedFrom, trimmedTo, markType);
|
|
115
|
+
tr.addMark(
|
|
116
|
+
trimmedFrom,
|
|
117
|
+
trimmedTo,
|
|
118
|
+
markType.create({
|
|
119
|
+
...mark.attrs,
|
|
120
|
+
orphan: isOrphan,
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
ttEditor.dispatch(tr);
|
|
124
|
+
|
|
125
|
+
if (isOrphan && this.selectedThreadId === markThreadId) {
|
|
126
|
+
// unselect
|
|
127
|
+
this.selectedThreadId = undefined;
|
|
128
|
+
this.emitStateUpdate();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
constructor(
|
|
137
|
+
private readonly editor: BlockNoteEditor<any, any, any>,
|
|
138
|
+
public readonly threadStore: ThreadStore,
|
|
139
|
+
private readonly markType: string
|
|
140
|
+
) {
|
|
141
|
+
super();
|
|
142
|
+
|
|
143
|
+
if (!editor.resolveUsers) {
|
|
144
|
+
throw new Error("resolveUsers is required for comments");
|
|
145
|
+
}
|
|
146
|
+
this.userStore = new UserStore<User>(editor.resolveUsers);
|
|
147
|
+
|
|
148
|
+
// Note: Plugins are currently not destroyed when the editor is destroyed.
|
|
149
|
+
// We should unsubscribe from the threadStore when the editor is destroyed.
|
|
150
|
+
this.threadStore.subscribe(this.updateMarksFromThreads);
|
|
151
|
+
|
|
152
|
+
editor.onCreate(() => {
|
|
153
|
+
// Need to wait for TipTap editor state to be initialized
|
|
154
|
+
this.updateMarksFromThreads(this.threadStore.getThreads());
|
|
155
|
+
editor.onSelectionChange(() => {
|
|
156
|
+
if (this.pendingComment) {
|
|
157
|
+
this.pendingComment = false;
|
|
158
|
+
this.emitStateUpdate();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
164
|
+
const self = this;
|
|
165
|
+
|
|
166
|
+
this.plugin = new Plugin<CommentsPluginState>({
|
|
167
|
+
key: PLUGIN_KEY,
|
|
168
|
+
state: {
|
|
169
|
+
init() {
|
|
170
|
+
return {
|
|
171
|
+
threadPositions: new Map<string, { from: number; to: number }>(),
|
|
172
|
+
decorations: DecorationSet.empty,
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
apply(tr, state) {
|
|
176
|
+
const action = tr.getMeta(PLUGIN_KEY);
|
|
177
|
+
if (!tr.docChanged && !action) {
|
|
178
|
+
return state;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// The doc changed or the selected thread changed
|
|
182
|
+
return updateState(tr.doc, self.selectedThreadId, markType);
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
props: {
|
|
186
|
+
decorations(state) {
|
|
187
|
+
return PLUGIN_KEY.getState(state)?.decorations ?? DecorationSet.empty;
|
|
188
|
+
},
|
|
189
|
+
/**
|
|
190
|
+
* Handle click on a thread mark and mark it as selected
|
|
191
|
+
*/
|
|
192
|
+
handleClick: (view, pos, event) => {
|
|
193
|
+
if (event.button !== 0) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const node = view.state.doc.nodeAt(pos);
|
|
198
|
+
|
|
199
|
+
if (!node) {
|
|
200
|
+
self.selectThread(undefined);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const commentMark = node.marks.find(
|
|
205
|
+
(mark) => mark.type.name === markType && mark.attrs.orphan !== true
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const threadId = commentMark?.attrs.threadId as string | undefined;
|
|
209
|
+
self.selectThread(threadId);
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Subscribe to state updates
|
|
217
|
+
*/
|
|
218
|
+
public onUpdate(
|
|
219
|
+
callback: (state: {
|
|
220
|
+
pendingComment: boolean;
|
|
221
|
+
selectedThreadId: string | undefined;
|
|
222
|
+
}) => void
|
|
223
|
+
) {
|
|
224
|
+
return this.on("update", callback);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Set the selected thread
|
|
229
|
+
*/
|
|
230
|
+
public selectThread(threadId: string | undefined) {
|
|
231
|
+
if (this.selectedThreadId === threadId) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
this.selectedThreadId = threadId;
|
|
235
|
+
this.emitStateUpdate();
|
|
236
|
+
this.editor.dispatch(
|
|
237
|
+
this.editor.prosemirrorView!.state.tr.setMeta(PLUGIN_KEY, {
|
|
238
|
+
name: SET_SELECTED_THREAD_ID,
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Start a pending comment (e.g.: when clicking the "Add comment" button)
|
|
245
|
+
*/
|
|
246
|
+
public startPendingComment() {
|
|
247
|
+
this.pendingComment = true;
|
|
248
|
+
this.emitStateUpdate();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Stop a pending comment (e.g.: user closes the comment composer)
|
|
253
|
+
*/
|
|
254
|
+
public stopPendingComment() {
|
|
255
|
+
this.pendingComment = false;
|
|
256
|
+
this.emitStateUpdate();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Create a thread at the current selection
|
|
261
|
+
*/
|
|
262
|
+
public async createThread(options: {
|
|
263
|
+
initialComment: {
|
|
264
|
+
body: CommentBody;
|
|
265
|
+
metadata?: any;
|
|
266
|
+
};
|
|
267
|
+
metadata?: any;
|
|
268
|
+
}) {
|
|
269
|
+
const thread = await this.threadStore.createThread(options);
|
|
270
|
+
|
|
271
|
+
if (this.threadStore.addThreadToDocument) {
|
|
272
|
+
// creating the mark is handled by the store
|
|
273
|
+
// this is useful if we don't have write-access to the document.
|
|
274
|
+
// We can then offload the responsibility of creating the mark to the server.
|
|
275
|
+
// (e.g.: RESTYjsThreadStore)
|
|
276
|
+
const view = this.editor.prosemirrorView!;
|
|
277
|
+
const pmSelection = view.state.selection;
|
|
278
|
+
|
|
279
|
+
const ystate = ySyncPluginKey.getState(view.state);
|
|
280
|
+
|
|
281
|
+
const selection = {
|
|
282
|
+
prosemirror: {
|
|
283
|
+
head: pmSelection.head,
|
|
284
|
+
anchor: pmSelection.anchor,
|
|
285
|
+
},
|
|
286
|
+
yjs: getRelativeSelection(ystate.binding, view.state),
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
await this.threadStore.addThreadToDocument({
|
|
290
|
+
threadId: thread.id,
|
|
291
|
+
selection,
|
|
292
|
+
});
|
|
293
|
+
} else {
|
|
294
|
+
// we create the mark directly in the document
|
|
295
|
+
this.editor._tiptapEditor.commands.setMark(this.markType, {
|
|
296
|
+
orphan: false,
|
|
297
|
+
threadId: thread.id,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { User } from "../../../comments/index.js";
|
|
2
|
+
import { EventEmitter } from "../../../util/EventEmitter.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The `UserStore` is used to retrieve and cache information about users.
|
|
6
|
+
*
|
|
7
|
+
* It does this by calling `resolveUsers` (which is user-defined in the Editor Options)
|
|
8
|
+
* for users that are not yet cached.
|
|
9
|
+
*/
|
|
10
|
+
export class UserStore<U extends User> extends EventEmitter<any> {
|
|
11
|
+
private userCache: Map<string, U> = new Map();
|
|
12
|
+
|
|
13
|
+
// avoid duplicate loads
|
|
14
|
+
private loadingUsers = new Set<string>();
|
|
15
|
+
|
|
16
|
+
public constructor(
|
|
17
|
+
private readonly resolveUsers: (userIds: string[]) => Promise<U[]>
|
|
18
|
+
) {
|
|
19
|
+
super();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load information about users based on an array of user ids.
|
|
24
|
+
*/
|
|
25
|
+
public async loadUsers(userIds: string[]) {
|
|
26
|
+
const missingUsers = userIds.filter(
|
|
27
|
+
(id) => !this.userCache.has(id) && !this.loadingUsers.has(id)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
if (missingUsers.length === 0) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const id of missingUsers) {
|
|
35
|
+
this.loadingUsers.add(id);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const users = await this.resolveUsers(missingUsers);
|
|
40
|
+
for (const user of users) {
|
|
41
|
+
this.userCache.set(user.id, user);
|
|
42
|
+
}
|
|
43
|
+
this.emit("update", this.userCache);
|
|
44
|
+
} finally {
|
|
45
|
+
for (const id of missingUsers) {
|
|
46
|
+
// delete the users from the loading set
|
|
47
|
+
// on a next call to `loadUsers` we will either
|
|
48
|
+
// return the cached user or retry loading the user if the request failed failed
|
|
49
|
+
this.loadingUsers.delete(id);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Retrieve information about a user based on their id, if cached.
|
|
56
|
+
*
|
|
57
|
+
* The user will have to be loaded via `loadUsers` first
|
|
58
|
+
*/
|
|
59
|
+
public getUser(userId: string): U | undefined {
|
|
60
|
+
return this.userCache.get(userId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Subscribe to changes in the user store.
|
|
65
|
+
*
|
|
66
|
+
* @param cb - The callback to call when the user store changes.
|
|
67
|
+
* @returns A function to unsubscribe from the user store.
|
|
68
|
+
*/
|
|
69
|
+
public subscribe(cb: (users: Map<string, U>) => void): () => void {
|
|
70
|
+
return this.on("update", cb);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -25,7 +25,7 @@ export class FormattingToolbarView implements PluginView {
|
|
|
25
25
|
state: EditorState;
|
|
26
26
|
from: number;
|
|
27
27
|
to: number;
|
|
28
|
-
}) => boolean = ({ state, from, to
|
|
28
|
+
}) => boolean = ({ state, from, to }) => {
|
|
29
29
|
const { doc, selection } = state;
|
|
30
30
|
const { empty } = selection;
|
|
31
31
|
|
|
@@ -43,8 +43,7 @@ export class FormattingToolbarView implements PluginView {
|
|
|
43
43
|
return false;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
return !(!view.hasFocus() || empty || isEmptyTextBlock);
|
|
46
|
+
return !(empty || isEmptyTextBlock);
|
|
48
47
|
};
|
|
49
48
|
|
|
50
49
|
constructor(
|
|
@@ -156,15 +155,20 @@ export class FormattingToolbarView implements PluginView {
|
|
|
156
155
|
const from = Math.min(...ranges.map((range) => range.$from.pos));
|
|
157
156
|
const to = Math.max(...ranges.map((range) => range.$to.pos));
|
|
158
157
|
|
|
159
|
-
const shouldShow = this.shouldShow
|
|
158
|
+
const shouldShow = this.shouldShow({
|
|
160
159
|
view,
|
|
161
160
|
state,
|
|
162
161
|
from,
|
|
163
162
|
to,
|
|
164
163
|
});
|
|
165
164
|
|
|
165
|
+
// in jsdom, Range.prototype.getClientRects is not implemented,
|
|
166
|
+
// this would cause `getSelectionBoundingBox` to fail
|
|
167
|
+
// we can just ignore jsdom for now and not show the toolbar
|
|
168
|
+
const jsdom = typeof Range.prototype.getClientRects === "undefined";
|
|
169
|
+
|
|
166
170
|
// Checks if menu should be shown/updated.
|
|
167
|
-
if (!this.preventShow && (shouldShow || this.preventHide)) {
|
|
171
|
+
if (!this.preventShow && (shouldShow || this.preventHide) && !jsdom) {
|
|
168
172
|
// Unlike other UI elements, we don't prevent the formatting toolbar from
|
|
169
173
|
// showing when the editor is not editable. This is because some buttons,
|
|
170
174
|
// e.g. the download file button, should still be accessible. Therefore,
|
|
@@ -52,7 +52,7 @@ class LinkToolbarView implements PluginView {
|
|
|
52
52
|
|
|
53
53
|
this.startMenuUpdateTimer = () => {
|
|
54
54
|
this.menuUpdateTimer = setTimeout(() => {
|
|
55
|
-
this.update(this.pmView);
|
|
55
|
+
this.update(this.pmView, undefined, true);
|
|
56
56
|
}, 250);
|
|
57
57
|
};
|
|
58
58
|
|
|
@@ -190,7 +190,7 @@ class LinkToolbarView implements PluginView {
|
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
update(view: EditorView, oldState?: EditorState) {
|
|
193
|
+
update(view: EditorView, oldState?: EditorState, fromMouseOver = false) {
|
|
194
194
|
const { state } = view;
|
|
195
195
|
|
|
196
196
|
const isSame =
|
|
@@ -235,7 +235,7 @@ class LinkToolbarView implements PluginView {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
if (this.mouseHoveredLinkMark) {
|
|
238
|
+
if (this.mouseHoveredLinkMark && fromMouseOver) {
|
|
239
239
|
this.linkMark = this.mouseHoveredLinkMark;
|
|
240
240
|
this.linkMarkRange = this.mouseHoveredLinkMarkRange;
|
|
241
241
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Plugin, PluginKey } from "prosemirror-state";
|
|
2
|
+
import { Decoration, DecorationSet } from "prosemirror-view";
|
|
3
|
+
import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
|
|
4
|
+
|
|
5
|
+
const PLUGIN_KEY = new PluginKey(`blocknote-show-selection`);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Plugin that shows adds a decoration around the current selection
|
|
9
|
+
* This can be used to highlight the current selection in the UI even when the
|
|
10
|
+
* text editor is not focused.
|
|
11
|
+
*/
|
|
12
|
+
export class ShowSelectionPlugin {
|
|
13
|
+
public readonly plugin: Plugin;
|
|
14
|
+
private enabled = false;
|
|
15
|
+
|
|
16
|
+
public constructor(private readonly editor: BlockNoteEditor<any, any, any>) {
|
|
17
|
+
this.plugin = new Plugin({
|
|
18
|
+
key: PLUGIN_KEY,
|
|
19
|
+
props: {
|
|
20
|
+
decorations: (state) => {
|
|
21
|
+
const { doc, selection } = state;
|
|
22
|
+
|
|
23
|
+
if (!this.enabled) {
|
|
24
|
+
return DecorationSet.empty;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const dec = Decoration.inline(selection.from, selection.to, {
|
|
28
|
+
"data-show-selection": "true",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return DecorationSet.create(doc, [dec]);
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public setEnabled(enabled: boolean) {
|
|
38
|
+
if (this.enabled === enabled) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.enabled = enabled;
|
|
43
|
+
|
|
44
|
+
this.editor.prosemirrorView?.dispatch(
|
|
45
|
+
this.editor.prosemirrorView?.state.tr.setMeta(PLUGIN_KEY, {})
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public getEnabled() {
|
|
50
|
+
return this.enabled;
|
|
51
|
+
}
|
|
52
|
+
}
|