@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
350 lines (349 loc) • 9.75 kB
JavaScript
import { i as e, n as t, t as n } from "./BlockNoteExtension-B_IEJHS8.js";
import { n as r, r as i } from "./UserStore-QbbPFj0z.js";
import { t as a } from "./ThreadStore-et9wB5Xt.js";
import { Mark as o, mergeAttributes as s } from "@tiptap/core";
import { Plugin as c, PluginKey as l } from "prosemirror-state";
import { Decoration as u, DecorationSet as d } from "prosemirror-view";
//#region src/comments/mark.ts
var f = o.create({
name: "comment",
excludes: "",
inclusive: !1,
keepOnSplit: !0,
group: e,
addAttributes() {
return {
orphan: {
parseHTML: (e) => !!e.getAttribute("data-orphan"),
renderHTML: (e) => e.orphan ? { "data-orphan": "true" } : {},
default: !1
},
threadId: {
parseHTML: (e) => e.getAttribute("data-bn-thread-id"),
renderHTML: (e) => ({ "data-bn-thread-id": e.threadId }),
default: ""
}
};
},
renderHTML({ HTMLAttributes: e }) {
return ["span", s(e, { class: "bn-thread-mark" })];
},
parseHTML() {
return [{ tag: "span.bn-thread-mark" }];
},
extendMarkSchema(e) {
return e.name === "comment" ? { blocknoteIgnore: !0 } : {};
}
}), p = new l("blocknote-comments");
function m(e, t) {
let n = /* @__PURE__ */ new Map();
return e.descendants((e, r) => {
e.marks.forEach((i) => {
if (i.type.name === t) {
let t = i.attrs.threadId;
if (!t) return;
let a = r, o = a + e.nodeSize, s = n.get(t) ?? {
from: Infinity,
to: 0
};
n.set(t, {
from: Math.min(a, s.from),
to: Math.max(o, s.to)
});
}
});
}), n;
}
var h = n(({ editor: e, options: { schema: n, threadStore: a, resolveUsers: o, confirmBeforeDiscard: s = !0 } }) => {
if (!o) throw Error("resolveUsers is required to be defined when using comments");
if (!a) throw Error("threadStore is required to be defined when using comments");
let l = r(o), h = f.name, g = t({
pendingComment: !1,
selectedThreadId: void 0,
threadPositions: /* @__PURE__ */ new Map()
}, { onUpdate() {
g.state.selectedThreadId !== g.prevState.selectedThreadId && e.transact((e) => e.setMeta(p, !0));
} }), _ = (t) => {
e.transact((e) => {
e.doc.descendants((n, r) => {
n.marks.forEach((i) => {
if (i.type.name === h) {
let a = i.type, o = i.attrs.threadId, s = t.get(o), c = !!(!s || s.resolved || s.deletedAt);
if (c !== i.attrs.orphan) {
let t = Math.max(r, 0), s = Math.min(r + n.nodeSize, e.doc.content.size - 1, e.doc.content.size - 1);
e.removeMark(t, s, i), e.addMark(t, s, a.create({
...i.attrs,
orphan: c
})), c && g.state.selectedThreadId === o && g.setState((e) => ({
...e,
selectedThreadId: void 0
}));
}
}
});
});
});
};
return {
key: "comments",
store: g,
userStore: l,
runsBefore: ["link"],
tiptapExtensions: [f],
prosemirrorPlugins: [new c({
key: p,
state: {
init() {
return { decorations: d.empty };
},
apply(e, t) {
let n = e.getMeta(p);
if (!e.docChanged && !n) return t;
let r = e.docChanged ? m(e.doc, h) : g.state.threadPositions;
(r.size > 0 || g.state.threadPositions.size > 0) && g.setState((e) => ({
...e,
threadPositions: r
}));
let i = [];
if (g.state.selectedThreadId) {
let e = r.get(g.state.selectedThreadId);
e && i.push(u.inline(e.from, e.to, { class: "bn-thread-mark-selected" }));
}
return { decorations: d.create(e.doc, i) };
}
},
props: {
decorations(e) {
return p.getState(e)?.decorations ?? d.empty;
},
handleClick: (e, t, n) => {
if (n.button !== 0) return !1;
let r = e.state.doc.nodeAt(t);
if (!r) return g.setState((e) => ({
...e,
selectedThreadId: void 0
})), !1;
let i = r.marks.find((e) => e.type.name === h && e.attrs.orphan !== !0);
if (!i) return g.state.selectedThreadId !== void 0 && g.setState((e) => ({
...e,
selectedThreadId: void 0
})), !1;
let a = i.attrs.threadId;
return a === g.state.selectedThreadId ? !1 : (g.setState((e) => ({
...e,
selectedThreadId: a
})), !0);
}
}
})],
threadStore: a,
mount() {
let t = a.subscribe(_);
_(a.getThreads());
let n = e.onSelectionChange(() => {
g.state.pendingComment && g.setState((e) => ({
...e,
pendingComment: !1
}));
});
return () => {
t(), n();
};
},
selectThread(t, n = !0) {
if (g.state.selectedThreadId !== t && (g.setState((e) => ({
...e,
pendingComment: !1,
selectedThreadId: t
})), t && n)) {
let n = g.state.threadPositions.get(t);
if (!n) return;
(e.prosemirrorView?.domAtPos(n.from).node)?.scrollIntoView({
behavior: "smooth",
block: "center"
});
}
},
startPendingComment() {
g.setState((e) => ({
...e,
selectedThreadId: void 0,
pendingComment: !0
})), e.domElement?.focus(), e.getExtension(i)?.showSelection(!0, "comments");
},
stopPendingComment() {
g.setState((e) => ({
...e,
selectedThreadId: void 0,
pendingComment: !1
})), e.getExtension(i)?.showSelection(!1, "comments");
},
async createThread(t) {
let n = await a.createThread(t);
a.addThreadToDocument ? await a.addThreadToDocument({
threadId: n.id,
selection: e.transact((e) => e.selection),
editor: e
}) : e._tiptapEditor.commands.setMark(h, {
orphan: !1,
threadId: n.id
});
},
commentEditorSchema: n,
confirmBeforeDiscard: s
};
}), g = class {}, _ = class extends g {
userId;
role;
constructor(e, t) {
super(), this.userId = e, this.role = t;
}
canCreateThread() {
return !0;
}
canAddComment(e) {
return !0;
}
canUpdateComment(e) {
return e.userId === this.userId;
}
canDeleteComment(e) {
return e.userId === this.userId || this.role === "editor";
}
canDeleteThread(e) {
return this.role === "editor";
}
canResolveThread(e) {
return !0;
}
canUnresolveThread(e) {
return !0;
}
canAddReaction(e, t) {
return t ? !e.reactions.some((e) => e.emoji === t && e.userIds.includes(this.userId)) : !0;
}
canDeleteReaction(e, t) {
return t ? e.reactions.some((e) => e.emoji === t && e.userIds.includes(this.userId)) : !0;
}
}, v = class extends a {
userId;
provider;
constructor(e, t, n) {
super(n), this.userId = e, this.provider = t;
}
async createThread(e) {
let t = this.provider.createThread({ data: e.metadata });
return t = this.provider.addComment(t.id, {
content: e.initialComment.body,
data: {
metadata: e.initialComment.metadata,
userId: this.userId
}
}), this.tiptapThreadToThreadData(t);
}
addThreadToDocument = void 0;
async addComment(e) {
let t = this.provider.addComment(e.threadId, {
content: e.comment.body,
data: {
metadata: e.comment.metadata,
userId: this.userId
}
});
return this.tiptapCommentToCommentData(t.comments[t.comments.length - 1]);
}
async updateComment(e) {
let t = this.provider.getThreadComment(e.threadId, e.commentId, !0);
if (!t) throw Error("Comment not found");
this.provider.updateComment(e.threadId, e.commentId, {
content: e.comment.body,
data: {
...t.data,
metadata: e.comment.metadata
}
});
}
tiptapCommentToCommentData(e) {
let t = [];
for (let n of e.data?.reactions || []) {
let e = t.find((e) => e.emoji === n.emoji);
e ? (e.userIds.push(n.userId), e.createdAt = new Date(Math.min(e.createdAt.getTime(), n.createdAt))) : t.push({
emoji: n.emoji,
createdAt: new Date(n.createdAt),
userIds: [n.userId]
});
}
return {
type: "comment",
id: e.id,
body: e.content,
metadata: e.data?.metadata,
userId: e.data?.userId,
createdAt: new Date(e.createdAt),
updatedAt: new Date(e.updatedAt),
reactions: t
};
}
tiptapThreadToThreadData(e) {
return {
type: "thread",
id: e.id,
comments: e.comments.map((e) => this.tiptapCommentToCommentData(e)),
resolved: !!e.resolvedAt,
metadata: e.data?.metadata,
createdAt: new Date(e.createdAt),
updatedAt: new Date(e.updatedAt)
};
}
async deleteComment(e) {
this.provider.deleteComment(e.threadId, e.commentId);
}
async deleteThread(e) {
this.provider.deleteThread(e.threadId);
}
async resolveThread(e) {
this.provider.updateThread(e.threadId, { resolvedAt: (/* @__PURE__ */ new Date()).toISOString() });
}
async unresolveThread(e) {
this.provider.updateThread(e.threadId, { resolvedAt: null });
}
async addReaction(e) {
let t = this.provider.getThreadComment(e.threadId, e.commentId, !0);
if (!t) throw Error("Comment not found");
this.provider.updateComment(e.threadId, e.commentId, { data: {
...t.data,
reactions: [...t.data?.reactions || [], {
emoji: e.emoji,
createdAt: Date.now(),
userId: this.userId
}]
} });
}
async deleteReaction(e) {
let t = this.provider.getThreadComment(e.threadId, e.commentId, !0);
if (!t) throw Error("Comment not found");
this.provider.updateComment(e.threadId, e.commentId, { data: {
...t.data,
reactions: (t.data?.reactions || []).filter((t) => t.emoji !== e.emoji && t.userId !== this.userId)
} });
}
getThread(e) {
let t = this.provider.getThread(e);
if (!t) throw Error("Thread not found");
return this.tiptapThreadToThreadData(t);
}
getThreads() {
return new Map(this.provider.getThreads().map((e) => [e.id, this.tiptapThreadToThreadData(e)]));
}
subscribe(e) {
let t = () => {
e(this.getThreads());
};
return this.provider.watchThreads(t), () => {
this.provider.unwatchThreads(t);
};
}
};
//#endregion
export { f as CommentMark, h as CommentsExtension, _ as DefaultThreadStoreAuth, a as ThreadStore, g as ThreadStoreAuth, v as TiptapThreadStore };
//# sourceMappingURL=comments.js.map