Skip to content

Commit 513441a

Browse files
committed
feat: working @y/prosemirror demo
1 parent 85f3acc commit 513441a

49 files changed

Lines changed: 7289 additions & 14 deletions

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: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,12 @@
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-rc.2",
103+
"@y/y": "^14.0.0-rc.16",
104+
"@y/prosemirror": "^2.0.0-2",
105+
"@floating-ui/react": "^0.27.18"
101106
},
102107
"devDependencies": {
103108
"@blocknote/code-block": "workspace:*",
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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+
}
13+
}
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: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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": "vite",
9+
"dev": "vite",
10+
"build:prod": "tsc && vite build",
11+
"preview": "vite 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+
},
29+
"devDependencies": {
30+
"@types/react": "^19.2.3",
31+
"@types/react-dom": "^19.2.3",
32+
"@vitejs/plugin-react": "^6.0.1",
33+
"vite": "^8.0.8"
34+
}
35+
}
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import "@blocknote/core/fonts/inter.css";
2+
import { SuggestionsExtension, VersioningExtension } from "@blocknote/core/y";
3+
import {
4+
BlockNoteViewEditor,
5+
FloatingComposerController,
6+
useCreateBlockNote,
7+
useEditorState,
8+
useExtension,
9+
useExtensionState,
10+
} from "@blocknote/react";
11+
import { BlockNoteView } from "@blocknote/mantine";
12+
import "@blocknote/mantine/style.css";
13+
import { useEffect, useMemo, useState } from "react";
14+
import { RiChat3Line, RiHistoryLine } from "react-icons/ri";
15+
import * as Y from "@y/y";
16+
import { WebsocketProvider } from "@y/websocket";
17+
18+
import { getRandomColor, HARDCODED_USERS, MyUserType } from "./userdata";
19+
import { SettingsSelect } from "./SettingsSelect";
20+
import "./style.css";
21+
import {
22+
DefaultThreadStoreAuth,
23+
CommentsExtension,
24+
} from "@blocknote/core/comments";
25+
import { YjsThreadStore } from "@blocknote/core/yjs";
26+
27+
import { CommentsSidebar } from "./CommentsSidebar";
28+
import { VersionHistorySidebar } from "./VersionHistorySidebar";
29+
import { SuggestionActions } from "./SuggestionActions";
30+
import { SuggestionActionsPopup } from "./SuggestionActionsPopup";
31+
32+
const roomName = "blocknote-versioning-example";
33+
const doc = new Y.Doc();
34+
const provider = new WebsocketProvider(
35+
"wss://demos.yjs.dev/ws",
36+
roomName,
37+
doc,
38+
{ connect: false },
39+
);
40+
provider.connectBc();
41+
doc.on("update", () => {
42+
console.log("doc-update", doc.get().toJSON());
43+
});
44+
45+
const suggestionModeDoc = new Y.Doc({ isSuggestionDoc: true });
46+
suggestionModeDoc.on("update", () => {
47+
console.log("suggestion-update", suggestionModeDoc.get().toJSON());
48+
});
49+
const suggestionModeProvider = new WebsocketProvider(
50+
"wss://demos.yjs.dev/ws",
51+
roomName + "-suggestions",
52+
suggestionModeDoc,
53+
{ connect: false },
54+
);
55+
const suggestionModeAttributionManager = Y.createAttributionManagerFromDiff(
56+
doc,
57+
suggestionModeDoc,
58+
// {
59+
// attrs: [
60+
// // Y.createAttributionItem("insert", ["John Doe"]),
61+
// // Y.createAttributionItem("delete", ["John Doe"]),
62+
// ],
63+
// },
64+
);
65+
suggestionModeProvider.connectBc();
66+
67+
async function resolveUsers(userIds: string[]) {
68+
// fake a (slow) network request
69+
await new Promise((resolve) => setTimeout(resolve, 1000));
70+
71+
return HARDCODED_USERS.filter((user) => userIds.includes(user.id));
72+
}
73+
74+
export default function App() {
75+
const [activeUser, setActiveUser] = useState<MyUserType>(HARDCODED_USERS[0]);
76+
77+
const threadStore = useMemo(() => {
78+
return new YjsThreadStore(
79+
activeUser.id,
80+
doc.get("threads") as any,
81+
new DefaultThreadStoreAuth(activeUser.id, activeUser.role),
82+
);
83+
}, [doc, activeUser]);
84+
85+
const editor = useCreateBlockNote({
86+
collaboration: {
87+
provider,
88+
suggestionDoc: suggestionModeDoc,
89+
attributionManager: suggestionModeAttributionManager,
90+
fragment: doc.get(),
91+
user: { color: getRandomColor(), name: activeUser.username },
92+
},
93+
extensions: [
94+
CommentsExtension({ threadStore, resolveUsers }),
95+
SuggestionsExtension(),
96+
VersioningExtension({
97+
endpoints: {} as any,
98+
fragment: doc.get(),
99+
}),
100+
],
101+
});
102+
103+
const {
104+
enableSuggestions,
105+
disableSuggestions,
106+
showSuggestions,
107+
checkUnresolvedSuggestions,
108+
} = useExtension(SuggestionsExtension, { editor });
109+
const hasUnresolvedSuggestions = useEditorState({
110+
selector: () => checkUnresolvedSuggestions(),
111+
editor,
112+
});
113+
114+
const { selectSnapshot } = useExtension(VersioningExtension, { editor });
115+
const { selectedSnapshotId } = useExtensionState(VersioningExtension, {
116+
editor,
117+
});
118+
119+
const [editingMode, setEditingMode] = useState<
120+
"editing" | "suggestions" | "view-suggestions"
121+
>("editing");
122+
useEffect(() => {
123+
if (editingMode !== "editing") {
124+
disableSuggestions();
125+
setEditingMode("editing");
126+
}
127+
}, [selectedSnapshotId]);
128+
const [sidebar, setSidebar] = useState<
129+
"comments" | "versionHistory" | "none"
130+
>("none");
131+
132+
return (
133+
<BlockNoteView
134+
className={"full-collaboration"}
135+
editor={editor}
136+
editable={
137+
(sidebar !== "versionHistory" || selectedSnapshotId === undefined) &&
138+
activeUser.role === "editor"
139+
}
140+
// In other examples, `BlockNoteView` renders both editor element itself,
141+
// and the container element which contains the necessary context for
142+
// BlockNote UI components. However, in this example, we want more control
143+
// over the rendering of the editor, so we set `renderEditor` to `false`.
144+
// Now, `BlockNoteView` will only render the container element, and we can
145+
// render the editor element anywhere we want using `BlockNoteEditorView`.
146+
renderEditor={false}
147+
// We also disable the default rendering of comments in the editor, as we
148+
// want to render them in the `ThreadsSidebar` component instead.
149+
comments={sidebar !== "comments"}
150+
>
151+
<div className="full-collaboration-main-container">
152+
{/* We place the editor, the sidebar, and any settings selects within
153+
`BlockNoteView` as they use BlockNote UI components and need the context
154+
for them. */}
155+
<div className={"editor-layout-wrapper"}>
156+
<div className="sidebar-selectors">
157+
<div
158+
className={`sidebar-selector ${sidebar === "versionHistory" ? "selected" : ""}`}
159+
onClick={() => {
160+
setSidebar((sidebar) =>
161+
sidebar !== "versionHistory" ? "versionHistory" : "none",
162+
);
163+
selectSnapshot(undefined);
164+
}}
165+
>
166+
<RiHistoryLine />
167+
<span>Version History</span>
168+
</div>
169+
<div
170+
className={`sidebar-selector ${sidebar === "comments" ? "selected" : ""}`}
171+
onClick={() =>
172+
setSidebar((sidebar) =>
173+
sidebar !== "comments" ? "comments" : "none",
174+
)
175+
}
176+
>
177+
<RiChat3Line />
178+
<span>Comments</span>
179+
</div>
180+
</div>
181+
<div className={"editor-section"}>
182+
{/* <h1>Editor</h1> */}
183+
{selectedSnapshotId === undefined && (
184+
<div className={"settings"}>
185+
<SettingsSelect
186+
label={"User"}
187+
items={HARDCODED_USERS.map((user) => ({
188+
text: `${user.username} (${
189+
user.role === "editor" ? "Editor" : "Commenter"
190+
})`,
191+
icon: null,
192+
onClick: () => {
193+
setActiveUser(user);
194+
},
195+
isSelected: user.id === activeUser.id,
196+
}))}
197+
/>
198+
{activeUser.role === "editor" && (
199+
<SettingsSelect
200+
label={"Mode"}
201+
items={[
202+
{
203+
text: "Editing",
204+
icon: null,
205+
onClick: () => {
206+
disableSuggestions();
207+
setEditingMode("editing");
208+
},
209+
isSelected: editingMode === "editing",
210+
},
211+
{
212+
text: "Editing + Viewing Suggestions",
213+
icon: null,
214+
onClick: () => {
215+
showSuggestions();
216+
setEditingMode("view-suggestions");
217+
},
218+
isSelected: editingMode === "view-suggestions",
219+
},
220+
{
221+
text: "Suggesting",
222+
icon: null,
223+
onClick: () => {
224+
enableSuggestions();
225+
setEditingMode("suggestions");
226+
},
227+
isSelected: editingMode === "suggestions",
228+
},
229+
]}
230+
/>
231+
)}
232+
{activeUser.role === "editor" &&
233+
editingMode === "suggestions" &&
234+
hasUnresolvedSuggestions && <SuggestionActions />}
235+
</div>
236+
)}
237+
{/* Because we set `renderEditor` to false, we can now manually place
238+
`BlockNoteViewEditor` (the actual editor component) in its own
239+
section below the user settings select. */}
240+
<BlockNoteViewEditor />
241+
<SuggestionActionsPopup />
242+
{/* Since we disabled rendering of comments with `comments={false}`,
243+
we need to re-add the floating composer, which is the UI element that
244+
appears when creating new threads. */}
245+
{sidebar === "comments" && <FloatingComposerController />}
246+
</div>
247+
</div>
248+
{sidebar === "comments" && <CommentsSidebar />}
249+
{sidebar === "versionHistory" && <VersionHistorySidebar />}
250+
</div>
251+
</BlockNoteView>
252+
);
253+
}

0 commit comments

Comments
 (0)