Skip to content

Commit 5d2ce5c

Browse files
committed
feat: y-prosemirror v14 (squashed rebase onto origin/main)
1 parent ad9e456 commit 5d2ce5c

163 files changed

Lines changed: 14020 additions & 1008 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,14 @@
9797
"tailwind-merge": "^3.4.0",
9898
"y-partykit": "^0.0.25",
9999
"yjs": "^13.6.27",
100-
"zod": "^4.3.5"
100+
"zod": "^4.3.5",
101+
"@y/protocols": "^1.0.6-rc.1",
102+
"@y/websocket": "^4.0.0-3",
103+
"@y/y": "^14.0.0-rc.16",
104+
"@y/prosemirror": "^2.0.0-2",
105+
"@floating-ui/react": "^0.27.18",
106+
"lib0": "1.0.0-rc.13",
107+
"y-websocket": "^2.1.0"
101108
},
102109
"devDependencies": {
103110
"@blocknote/code-block": "workspace:*",
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"playground": true,
3+
"docs": true,
4+
"author": "matthewlipski",
5+
"tags": ["Advanced", "Development", "Collaboration"],
6+
"dependencies": {
7+
"@y/protocols": "^1.0.6-rc.1",
8+
"@y/websocket": "^4.0.0-3",
9+
"@y/y": "^14.0.0-rc.16",
10+
"react-icons": "5.6.0",
11+
"@floating-ui/react": "^0.27.18",
12+
"lib0": "1.0.0-rc.13"
13+
}
14+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Collaborative Editing Features Showcase
2+
3+
In this example, you can play with all of the collaboration features BlockNote has to offer:
4+
5+
**Comments**: Add comments to parts of the document - other users can then view, reply to, react to, and resolve them.
6+
7+
**Versioning**: Save snapshots of the document - later preview saved snapshots and restore them to ensure work is never lost.
8+
9+
**Suggestions**: Suggest changes directly in the editor - users can choose to then apply or reject those changes.
10+
11+
**Relevant Docs:**
12+
13+
- [Editor Setup](/docs/getting-started/editor-setup)
14+
- [Comments](/docs/features/collaboration/comments)
15+
- [Real-time collaboration](/docs/features/collaboration)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<html lang="en">
2+
<head>
3+
<meta charset="UTF-8" />
4+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
5+
<title>Collaborative Editing Features Showcase</title>
6+
<script>
7+
<!-- AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY -->
8+
</script>
9+
</head>
10+
<body>
11+
<div id="root"></div>
12+
<script type="module" src="./main.tsx"></script>
13+
</body>
14+
</html>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
2+
import React from "react";
3+
import { createRoot } from "react-dom/client";
4+
import App from "./src/App.jsx";
5+
6+
const root = createRoot(document.getElementById("root")!);
7+
root.render(
8+
<React.StrictMode>
9+
<App />
10+
</React.StrictMode>,
11+
);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "@blocknote/example-collaboration-versioning",
3+
"description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
4+
"type": "module",
5+
"private": true,
6+
"version": "0.12.4",
7+
"scripts": {
8+
"start": "vp dev",
9+
"dev": "vp dev",
10+
"build:prod": "tsc && vp build",
11+
"preview": "vp preview"
12+
},
13+
"dependencies": {
14+
"@blocknote/ariakit": "latest",
15+
"@blocknote/core": "latest",
16+
"@blocknote/mantine": "latest",
17+
"@blocknote/react": "latest",
18+
"@blocknote/shadcn": "latest",
19+
"@mantine/core": "^9.0.2",
20+
"@mantine/hooks": "^9.0.2",
21+
"react": "^19.2.3",
22+
"react-dom": "^19.2.3",
23+
"@y/protocols": "^1.0.6-rc.1",
24+
"@y/websocket": "^4.0.0-3",
25+
"@y/y": "^14.0.0-rc.16",
26+
"react-icons": "5.6.0",
27+
"@floating-ui/react": "^0.27.18",
28+
"lib0": "1.0.0-rc.13"
29+
},
30+
"devDependencies": {
31+
"@types/react": "^19.2.3",
32+
"@types/react-dom": "^19.2.3",
33+
"@vitejs/plugin-react": "^6.0.1",
34+
"vite-plus": "catalog:"
35+
}
36+
}
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import "@blocknote/core/fonts/inter.css";
2+
import { withCollaboration, SuggestionsExtension } from "@blocknote/core/y";
3+
import { localStorageEndpoints } from "./localStorageEndpoints.js";
4+
import { VersioningExtension } from "@blocknote/core/extensions";
5+
import {
6+
BlockNoteViewEditor,
7+
FloatingComposerController,
8+
useCreateBlockNote,
9+
useEditorState,
10+
useExtension,
11+
useExtensionState,
12+
} from "@blocknote/react";
13+
import { BlockNoteView } from "@blocknote/mantine";
14+
import "@blocknote/mantine/style.css";
15+
import { useEffect, useMemo, useState } from "react";
16+
import * as Y from "@y/y";
17+
import { WebsocketProvider } from "@y/websocket";
18+
19+
import { getRandomColor, HARDCODED_USERS, MyUserType } from "./userdata";
20+
import { SettingsSelect } from "./SettingsSelect";
21+
import "./style.css";
22+
import {
23+
DefaultThreadStoreAuth,
24+
CommentsExtension,
25+
} from "@blocknote/core/comments";
26+
import { YjsThreadStore } from "@blocknote/core/y";
27+
28+
import { CommentsSidebar } from "./CommentsSidebar";
29+
import { VersionHistorySidebar } from "./VersionHistorySidebar";
30+
import { SuggestionActions } from "./SuggestionActions";
31+
import { SuggestionActionsPopup } from "./SuggestionActionsPopup";
32+
33+
const roomName = "blocknote-versioning-example";
34+
const doc = new Y.Doc();
35+
const provider = new WebsocketProvider(
36+
"wss://demos.yjs.dev/ws",
37+
roomName,
38+
doc,
39+
{ connect: false },
40+
);
41+
provider.connectBc();
42+
doc.on("update", () => {
43+
console.log("doc-update", doc.get().toJSON());
44+
});
45+
46+
const suggestionModeDoc = new Y.Doc({ isSuggestionDoc: true });
47+
suggestionModeDoc.on("update", () => {
48+
console.log("suggestion-update", suggestionModeDoc.get().toJSON());
49+
});
50+
const suggestionModeProvider = new WebsocketProvider(
51+
"wss://demos.yjs.dev/ws",
52+
roomName + "-suggestions",
53+
suggestionModeDoc,
54+
{ connect: false },
55+
);
56+
const suggestionModeAttributionManager = Y.createAttributionManagerFromDiff(
57+
doc,
58+
suggestionModeDoc,
59+
// {
60+
// attrs: [
61+
// // Y.createAttributionItem("insert", ["John Doe"]),
62+
// // Y.createAttributionItem("delete", ["John Doe"]),
63+
// ],
64+
// },
65+
);
66+
suggestionModeProvider.connectBc();
67+
68+
async function resolveUsers(userIds: string[]) {
69+
// fake a (slow) network request
70+
await new Promise((resolve) => setTimeout(resolve, 1000));
71+
72+
return HARDCODED_USERS.filter((user) => userIds.includes(user.id));
73+
}
74+
75+
export default function App() {
76+
const [activeUser, setActiveUser] = useState<MyUserType>(HARDCODED_USERS[0]);
77+
78+
const threadStore = useMemo(() => {
79+
return new YjsThreadStore(
80+
activeUser.id,
81+
doc.get("threads"),
82+
new DefaultThreadStoreAuth(activeUser.id, activeUser.role),
83+
);
84+
}, [doc, activeUser]);
85+
86+
const editor = useCreateBlockNote(
87+
withCollaboration({
88+
collaboration: {
89+
provider,
90+
suggestionDoc: suggestionModeDoc,
91+
attributionManager: suggestionModeAttributionManager,
92+
fragment: doc.get(),
93+
user: { color: getRandomColor(), name: activeUser.username },
94+
versioningEndpoints: localStorageEndpoints,
95+
},
96+
extensions: [CommentsExtension({ threadStore, resolveUsers })],
97+
}),
98+
);
99+
100+
const {
101+
enableSuggestions,
102+
disableSuggestions,
103+
viewSuggestions,
104+
checkUnresolvedSuggestions,
105+
} = useExtension(SuggestionsExtension, { editor });
106+
const hasUnresolvedSuggestions = useEditorState({
107+
selector: () => checkUnresolvedSuggestions(),
108+
editor,
109+
});
110+
111+
const { previewedSnapshotId } = useExtensionState(VersioningExtension, {
112+
editor,
113+
});
114+
115+
const [editingMode, setEditingMode] = useState<
116+
"editing" | "suggestions" | "view-suggestions"
117+
>("editing");
118+
useEffect(() => {
119+
if (editingMode !== "editing") {
120+
disableSuggestions();
121+
setEditingMode("editing");
122+
}
123+
}, [previewedSnapshotId]);
124+
const [sidebar, setSidebar] = useState<"comments" | "versionHistory">(
125+
"versionHistory",
126+
);
127+
128+
return (
129+
<div className="wrapper">
130+
<BlockNoteView
131+
className={"full-collaboration"}
132+
editor={editor}
133+
editable={
134+
previewedSnapshotId === undefined && activeUser.role === "editor"
135+
}
136+
renderEditor={false}
137+
comments={sidebar !== "comments"}
138+
>
139+
<div className="layout">
140+
<div className="editor-panel">
141+
{previewedSnapshotId === undefined && (
142+
<div className={"settings"}>
143+
<SettingsSelect
144+
label={"User"}
145+
items={HARDCODED_USERS.map((user) => ({
146+
text: `${user.username} (${
147+
user.role === "editor" ? "Editor" : "Commenter"
148+
})`,
149+
icon: null,
150+
onClick: () => {
151+
setActiveUser(user);
152+
},
153+
isSelected: user.id === activeUser.id,
154+
}))}
155+
/>
156+
{activeUser.role === "editor" && (
157+
<SettingsSelect
158+
label={"Mode"}
159+
items={[
160+
{
161+
text: "Editing",
162+
icon: null,
163+
onClick: () => {
164+
disableSuggestions();
165+
setEditingMode("editing");
166+
},
167+
isSelected: editingMode === "editing",
168+
},
169+
{
170+
text: "Editing + Viewing Suggestions",
171+
icon: null,
172+
onClick: () => {
173+
viewSuggestions();
174+
setEditingMode("view-suggestions");
175+
},
176+
isSelected: editingMode === "view-suggestions",
177+
},
178+
{
179+
text: "Suggesting",
180+
icon: null,
181+
onClick: () => {
182+
enableSuggestions();
183+
setEditingMode("suggestions");
184+
},
185+
isSelected: editingMode === "suggestions",
186+
},
187+
]}
188+
/>
189+
)}
190+
<SettingsSelect
191+
label={"Sidebar"}
192+
items={[
193+
{
194+
text: "Version History",
195+
icon: null,
196+
onClick: () => setSidebar("versionHistory"),
197+
isSelected: sidebar === "versionHistory",
198+
},
199+
{
200+
text: "Comments",
201+
icon: null,
202+
onClick: () => setSidebar("comments"),
203+
isSelected: sidebar === "comments",
204+
},
205+
]}
206+
/>
207+
{activeUser.role === "editor" &&
208+
editingMode === "suggestions" &&
209+
hasUnresolvedSuggestions && <SuggestionActions />}
210+
</div>
211+
)}
212+
<BlockNoteViewEditor />
213+
<SuggestionActionsPopup />
214+
{sidebar === "comments" && <FloatingComposerController />}
215+
</div>
216+
{sidebar === "comments" && <CommentsSidebar />}
217+
{sidebar === "versionHistory" && <VersionHistorySidebar />}
218+
</div>
219+
</BlockNoteView>
220+
</div>
221+
);
222+
}

0 commit comments

Comments
 (0)