Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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