Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,14 @@
"tailwind-merge": "^3.4.0",
"y-partykit": "^0.0.25",
"yjs": "^13.6.27",
"zod": "^4.3.5"
"zod": "^4.3.5",
"@y/protocols": "^1.0.6-rc.1",
"@y/websocket": "^4.0.0-3",
"@y/y": "^14.0.0-rc.16",
"@y/prosemirror": "^2.0.0-2",
"@floating-ui/react": "^0.27.18",
"lib0": "1.0.0-rc.13",
"y-websocket": "^2.1.0"
},
"devDependencies": {
"@blocknote/code-block": "workspace:*",
Expand Down
14 changes: 14 additions & 0 deletions examples/07-collaboration/10-versioning/.bnexample.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"playground": true,
"docs": true,
"author": "matthewlipski",
"tags": ["Advanced", "Development", "Collaboration"],
"dependencies": {
"@y/protocols": "^1.0.6-rc.1",
"@y/websocket": "^4.0.0-3",
"@y/y": "^14.0.0-rc.16",
"react-icons": "5.6.0",
"@floating-ui/react": "^0.27.18",
"lib0": "1.0.0-rc.13"
}
}
15 changes: 15 additions & 0 deletions examples/07-collaboration/10-versioning/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Collaborative Editing Features Showcase

In this example, you can play with all of the collaboration features BlockNote has to offer:

**Comments**: Add comments to parts of the document - other users can then view, reply to, react to, and resolve them.

**Versioning**: Save snapshots of the document - later preview saved snapshots and restore them to ensure work is never lost.

**Suggestions**: Suggest changes directly in the editor - users can choose to then apply or reject those changes.

**Relevant Docs:**

- [Editor Setup](/docs/getting-started/editor-setup)
- [Comments](/docs/features/collaboration/comments)
- [Real-time collaboration](/docs/features/collaboration)
14 changes: 14 additions & 0 deletions examples/07-collaboration/10-versioning/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Collaborative Editing Features Showcase</title>
<script>
<!-- AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY -->
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
11 changes: 11 additions & 0 deletions examples/07-collaboration/10-versioning/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./src/App.jsx";

const root = createRoot(document.getElementById("root")!);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
36 changes: 36 additions & 0 deletions examples/07-collaboration/10-versioning/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@blocknote/example-collaboration-versioning",
"description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
"type": "module",
"private": true,
"version": "0.12.4",
"scripts": {
"start": "vite",
"dev": "vite",
"build:prod": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@blocknote/ariakit": "latest",
"@blocknote/core": "latest",
"@blocknote/mantine": "latest",
"@blocknote/react": "latest",
"@blocknote/shadcn": "latest",
"@mantine/core": "^9.0.2",
"@mantine/hooks": "^9.0.2",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"@y/protocols": "^1.0.6-rc.1",
"@y/websocket": "^4.0.0-3",
"@y/y": "^14.0.0-rc.16",
"react-icons": "5.6.0",
"@floating-ui/react": "^0.27.18",
"lib0": "1.0.0-rc.13"
},
"devDependencies": {
"@types/react": "^19.2.3",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"vite": "^8.0.8"
}
}
225 changes: 225 additions & 0 deletions examples/07-collaboration/10-versioning/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import "@blocknote/core/fonts/inter.css";
import {
withCollaboration,
SuggestionsExtension,
} from "@blocknote/core/y";
import { localStorageEndpoints } from "./localStorageEndpoints.js";
import { VersioningExtension } from "@blocknote/core/extensions";
import {
BlockNoteViewEditor,
FloatingComposerController,
useCreateBlockNote,
useEditorState,
useExtension,
useExtensionState,
} from "@blocknote/react";
import { BlockNoteView } from "@blocknote/mantine";
import "@blocknote/mantine/style.css";
import { useEffect, useMemo, useState } from "react";
import * as Y from "@y/y";
import { WebsocketProvider } from "@y/websocket";

import { getRandomColor, HARDCODED_USERS, MyUserType } from "./userdata";
import { SettingsSelect } from "./SettingsSelect";
import "./style.css";
import {
DefaultThreadStoreAuth,
CommentsExtension,
} from "@blocknote/core/comments";
import { YjsThreadStore } from "@blocknote/core/y";

import { CommentsSidebar } from "./CommentsSidebar";
import { VersionHistorySidebar } from "./VersionHistorySidebar";
import { SuggestionActions } from "./SuggestionActions";
import { SuggestionActionsPopup } from "./SuggestionActionsPopup";

const roomName = "blocknote-versioning-example";
const doc = new Y.Doc();
const provider = new WebsocketProvider(
"wss://demos.yjs.dev/ws",
roomName,
doc,
{ connect: false },
);
provider.connectBc();
doc.on("update", () => {
console.log("doc-update", doc.get().toJSON());
});

const suggestionModeDoc = new Y.Doc({ isSuggestionDoc: true });
suggestionModeDoc.on("update", () => {
console.log("suggestion-update", suggestionModeDoc.get().toJSON());
});
const suggestionModeProvider = new WebsocketProvider(
"wss://demos.yjs.dev/ws",
roomName + "-suggestions",
suggestionModeDoc,
{ connect: false },
);
const suggestionModeAttributionManager = Y.createAttributionManagerFromDiff(
doc,
suggestionModeDoc,
// {
// attrs: [
// // Y.createAttributionItem("insert", ["John Doe"]),
// // Y.createAttributionItem("delete", ["John Doe"]),
// ],
// },
);
suggestionModeProvider.connectBc();

async function resolveUsers(userIds: string[]) {
// fake a (slow) network request
await new Promise((resolve) => setTimeout(resolve, 1000));

return HARDCODED_USERS.filter((user) => userIds.includes(user.id));
}

export default function App() {
const [activeUser, setActiveUser] = useState<MyUserType>(HARDCODED_USERS[0]);

const threadStore = useMemo(() => {
return new YjsThreadStore(
activeUser.id,
doc.get("threads"),
new DefaultThreadStoreAuth(activeUser.id, activeUser.role),
);
}, [doc, activeUser]);

const editor = useCreateBlockNote(
withCollaboration({
collaboration: {
provider,
suggestionDoc: suggestionModeDoc,
attributionManager: suggestionModeAttributionManager,
fragment: doc.get(),
user: { color: getRandomColor(), name: activeUser.username },
versioningEndpoints: localStorageEndpoints,
},
extensions: [CommentsExtension({ threadStore, resolveUsers })],
}),
);

const {
enableSuggestions,
disableSuggestions,
showSuggestions,
checkUnresolvedSuggestions,
} = useExtension(SuggestionsExtension, { editor });
const hasUnresolvedSuggestions = useEditorState({
selector: () => checkUnresolvedSuggestions(),
editor,
});

const { previewedSnapshotId } = useExtensionState(VersioningExtension, {
editor,
});

const [editingMode, setEditingMode] = useState<
"editing" | "suggestions" | "view-suggestions"
>("editing");
useEffect(() => {
if (editingMode !== "editing") {
disableSuggestions();
setEditingMode("editing");
}
}, [previewedSnapshotId]);
const [sidebar, setSidebar] = useState<"comments" | "versionHistory">(
"versionHistory",
);

return (
<div className="wrapper">
<BlockNoteView
className={"full-collaboration"}
editor={editor}
editable={
previewedSnapshotId === undefined && activeUser.role === "editor"
}
renderEditor={false}
comments={sidebar !== "comments"}
>
<div className="layout">
<div className="editor-panel">
{previewedSnapshotId === undefined && (
<div className={"settings"}>
<SettingsSelect
label={"User"}
items={HARDCODED_USERS.map((user) => ({
text: `${user.username} (${
user.role === "editor" ? "Editor" : "Commenter"
})`,
icon: null,
onClick: () => {
setActiveUser(user);
},
isSelected: user.id === activeUser.id,
}))}
/>
{activeUser.role === "editor" && (
<SettingsSelect
label={"Mode"}
items={[
{
text: "Editing",
icon: null,
onClick: () => {
disableSuggestions();
setEditingMode("editing");
},
isSelected: editingMode === "editing",
},
{
text: "Editing + Viewing Suggestions",
icon: null,
onClick: () => {
showSuggestions();
setEditingMode("view-suggestions");
},
isSelected: editingMode === "view-suggestions",
},
{
text: "Suggesting",
icon: null,
onClick: () => {
enableSuggestions();
setEditingMode("suggestions");
},
isSelected: editingMode === "suggestions",
},
]}
/>
)}
<SettingsSelect
label={"Sidebar"}
items={[
{
text: "Version History",
icon: null,
onClick: () => setSidebar("versionHistory"),
isSelected: sidebar === "versionHistory",
},
{
text: "Comments",
icon: null,
onClick: () => setSidebar("comments"),
isSelected: sidebar === "comments",
},
]}
/>
{activeUser.role === "editor" &&
editingMode === "suggestions" &&
hasUnresolvedSuggestions && <SuggestionActions />}
</div>
)}
<BlockNoteViewEditor />
<SuggestionActionsPopup />
{sidebar === "comments" && <FloatingComposerController />}
</div>
{sidebar === "comments" && <CommentsSidebar />}
{sidebar === "versionHistory" && <VersionHistorySidebar />}
</div>
</BlockNoteView>
</div>
);
}
Loading
Loading