@blocknote/core 0.24.1 → 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 +5028 -3444
- 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/fromClipboard/handleFileInsertion.ts +36 -14
- 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 +16 -10
- package/src/extensions/LinkToolbar/LinkToolbarPlugin.ts +3 -3
- package/src/extensions/ShowSelection/ShowSelectionPlugin.ts +52 -0
- package/src/extensions/SideMenu/SideMenuPlugin.ts +22 -9
- 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/extensions/UniqueID/UniqueID.ts +8 -3
- 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/index.ts +1 -0
- package/src/i18n/locales/is.ts +24 -1
- package/src/i18n/locales/it.ts +21 -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 +346 -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/FormattingToolbar/FormattingToolbarPlugin.d.ts +1 -1
- package/types/src/extensions/ShowSelection/ShowSelectionPlugin.d.ts +15 -0
- package/types/src/extensions/SideMenu/SideMenuPlugin.d.ts +1 -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/index.d.ts +1 -0
- package/types/src/i18n/locales/it.d.ts +21 -0
- package/types/src/i18n/locales/no.d.ts +2 -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(
|
|
@@ -67,7 +66,7 @@ export class FormattingToolbarView implements PluginView {
|
|
|
67
66
|
};
|
|
68
67
|
|
|
69
68
|
pmView.dom.addEventListener("mousedown", this.viewMousedownHandler);
|
|
70
|
-
pmView.
|
|
69
|
+
pmView.root.addEventListener("mouseup", this.mouseupHandler);
|
|
71
70
|
pmView.dom.addEventListener("dragstart", this.dragHandler);
|
|
72
71
|
pmView.dom.addEventListener("dragover", this.dragHandler);
|
|
73
72
|
pmView.dom.addEventListener("blur", this.blurHandler);
|
|
@@ -113,9 +112,11 @@ export class FormattingToolbarView implements PluginView {
|
|
|
113
112
|
this.preventShow = true;
|
|
114
113
|
};
|
|
115
114
|
|
|
116
|
-
|
|
117
|
-
this.preventShow
|
|
118
|
-
|
|
115
|
+
mouseupHandler = () => {
|
|
116
|
+
if (this.preventShow) {
|
|
117
|
+
this.preventShow = false;
|
|
118
|
+
setTimeout(() => this.update(this.pmView));
|
|
119
|
+
}
|
|
119
120
|
};
|
|
120
121
|
|
|
121
122
|
// For dragging the whole editor.
|
|
@@ -154,15 +155,20 @@ export class FormattingToolbarView implements PluginView {
|
|
|
154
155
|
const from = Math.min(...ranges.map((range) => range.$from.pos));
|
|
155
156
|
const to = Math.max(...ranges.map((range) => range.$to.pos));
|
|
156
157
|
|
|
157
|
-
const shouldShow = this.shouldShow
|
|
158
|
+
const shouldShow = this.shouldShow({
|
|
158
159
|
view,
|
|
159
160
|
state,
|
|
160
161
|
from,
|
|
161
162
|
to,
|
|
162
163
|
});
|
|
163
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
|
+
|
|
164
170
|
// Checks if menu should be shown/updated.
|
|
165
|
-
if (!this.preventShow && (shouldShow || this.preventHide)) {
|
|
171
|
+
if (!this.preventShow && (shouldShow || this.preventHide) && !jsdom) {
|
|
166
172
|
// Unlike other UI elements, we don't prevent the formatting toolbar from
|
|
167
173
|
// showing when the editor is not editable. This is because some buttons,
|
|
168
174
|
// e.g. the download file button, should still be accessible. Therefore,
|
|
@@ -193,7 +199,7 @@ export class FormattingToolbarView implements PluginView {
|
|
|
193
199
|
|
|
194
200
|
destroy() {
|
|
195
201
|
this.pmView.dom.removeEventListener("mousedown", this.viewMousedownHandler);
|
|
196
|
-
this.pmView.
|
|
202
|
+
this.pmView.root.removeEventListener("mouseup", this.mouseupHandler);
|
|
197
203
|
this.pmView.dom.removeEventListener("dragstart", this.dragHandler);
|
|
198
204
|
this.pmView.dom.removeEventListener("dragover", this.dragHandler);
|
|
199
205
|
this.pmView.dom.removeEventListener("blur", this.blurHandler);
|
|
@@ -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
|
+
}
|
|
@@ -178,6 +178,11 @@ export class SideMenuView<
|
|
|
178
178
|
this.onDrop as EventListener,
|
|
179
179
|
true
|
|
180
180
|
);
|
|
181
|
+
this.pmView.root.addEventListener(
|
|
182
|
+
"dragend",
|
|
183
|
+
this.onDragEnd as EventListener,
|
|
184
|
+
true
|
|
185
|
+
);
|
|
181
186
|
initializeESMDependencies();
|
|
182
187
|
|
|
183
188
|
// Shows or updates menu position whenever the cursor moves, if the menu isn't frozen.
|
|
@@ -300,8 +305,8 @@ export class SideMenuView<
|
|
|
300
305
|
// a block from a different editor is being dropped, this causes some
|
|
301
306
|
// issues that the code below fixes:
|
|
302
307
|
if (!this.isDragOrigin && this.pmView.dom === parentEditorElement) {
|
|
303
|
-
//
|
|
304
|
-
//
|
|
308
|
+
// Because the editor selection is unrelated to the dragged content, we
|
|
309
|
+
// don't want PM to delete its content. Therefore, we collapse the
|
|
305
310
|
// selection.
|
|
306
311
|
this.pmView.dispatch(
|
|
307
312
|
this.pmView.state.tr.setSelection(
|
|
@@ -312,8 +317,8 @@ export class SideMenuView<
|
|
|
312
317
|
)
|
|
313
318
|
);
|
|
314
319
|
} else if (this.isDragOrigin && this.pmView.dom !== parentEditorElement) {
|
|
315
|
-
//
|
|
316
|
-
//
|
|
320
|
+
// Because the editor from which the block originates doesn't get a drop
|
|
321
|
+
// event on it, PM doesn't delete its selected content. Therefore, we
|
|
317
322
|
// need to do so manually.
|
|
318
323
|
//
|
|
319
324
|
// Note: Deleting the selected content from the editor from which the
|
|
@@ -328,11 +333,6 @@ export class SideMenuView<
|
|
|
328
333
|
0
|
|
329
334
|
);
|
|
330
335
|
}
|
|
331
|
-
// 3. PM only clears `view.dragging` on the editor that the block was
|
|
332
|
-
// dropped, so we manually have to clear it on all the others. However,
|
|
333
|
-
// PM also needs to read `view.dragging` while handling the event, so we
|
|
334
|
-
// use a `setTimeout` to ensure it's only cleared after that.
|
|
335
|
-
setTimeout(() => (this.pmView.dragging = null), 0);
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
if (
|
|
@@ -360,6 +360,14 @@ export class SideMenuView<
|
|
|
360
360
|
}
|
|
361
361
|
};
|
|
362
362
|
|
|
363
|
+
onDragEnd = () => {
|
|
364
|
+
// When the user starts dragging a block, `view.dragging` is set on all
|
|
365
|
+
// BlockNote editors. However, when the drag ends, only the editor that the
|
|
366
|
+
// drag originated in automatically clears `view.dragging`. Therefore, we
|
|
367
|
+
// have to manually clear it on all editors.
|
|
368
|
+
this.pmView.dragging = null;
|
|
369
|
+
};
|
|
370
|
+
|
|
363
371
|
/**
|
|
364
372
|
* If a block is being dragged, ProseMirror usually gets the context of what's
|
|
365
373
|
* being dragged from `view.dragging`, which is automatically set when a
|
|
@@ -580,6 +588,11 @@ export class SideMenuView<
|
|
|
580
588
|
this.onDrop as EventListener,
|
|
581
589
|
true
|
|
582
590
|
);
|
|
591
|
+
this.pmView.root.removeEventListener(
|
|
592
|
+
"dragend",
|
|
593
|
+
this.onDragEnd as EventListener,
|
|
594
|
+
true
|
|
595
|
+
);
|
|
583
596
|
this.pmView.root.removeEventListener(
|
|
584
597
|
"keydown",
|
|
585
598
|
this.onKeyDown as EventListener,
|