@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.
- package/dist/blocknote.cjs +12 -0
- package/dist/blocknote.cjs.map +1 -0
- package/dist/blocknote.js +4754 -3514
- 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 +39 -26
- 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 +17 -0
- package/src/extensions/BackgroundColor/BackgroundColorExtension.ts +1 -1
- 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/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
|
@@ -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
|
+
}
|