Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add collaborative initial content example #994

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"playground": true,
"docs": true,
"author": "mfanselmo",
"tags": ["Advanced", "Blocks", "yjs", "Collaboration"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Initial content with server-util and yjs

This example shows how to instantiate a collaborative editor with existing content, using the `@blocknote/server-util` API

## Relevant documentation
- https://docs.yjs.dev/api/document-updates
- https://www.blocknotejs.org/docs/editor-api/server-processing
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { BlockNoteSchema } from "@blocknote/core";
import "@blocknote/core/fonts/inter.css";
import { useCreateBlockNote } from "@blocknote/react";
import { BlockNoteView } from "@blocknote/ariakit";
import "@blocknote/ariakit/style.css";
import * as Y from "yjs";
import { useEffect, useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import { toUint8Array } from "js-base64";

const schema = BlockNoteSchema.create();

export default function App() {
// Just fetches the initial contents
const { data: initialContentsUpdateVector, refetch } = useQuery({
queryKey: [1],
queryFn: async () =>
fetch("http://localhost:8080/")
.then((res) => res.text())
.then((res) => toUint8Array(res)), // API returns initial doc as a yjs update encoded as base64
});

const ydoc = useMemo(() => {
const doc = new Y.Doc();
if (initialContentsUpdateVector) {
// https://docs.yjs.dev/api/document-updates
Y.applyUpdateV2(doc, initialContentsUpdateVector);
}

return doc;
}, [initialContentsUpdateVector]);

// Cleanup of the ydoc it is re-initialized
useEffect(() => {
return () => {
if (ydoc) {
ydoc.destroy();
}
};
}, [ydoc]);

// Creates a new editor instance.
const editor = useCreateBlockNote(
{
collaboration: {
fragment: ydoc.getXmlFragment("document-store"),
provider: null,
user: {
name: "me",
color: "white",
},
},
schema,
},
[ydoc] // Since we are changing the doc we need to re-initialize the editor
);

return (
<>
<button onClick={() => refetch()}>refetch initial contents</button>
<BlockNoteView editor={editor}></BlockNoteView>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Alert Block

In this example, we create a custom `Alert` block which is used to emphasize text. In addition, we create a Slash Menu item which inserts an `Alert` block.

**Try it out:** Press the "/" key to open the Slash Menu and insert an `Alert` block!

**Relevant Docs:**

- [Custom Blocks](/docs/custom-schemas/custom-blocks)
- [Changing Slash Menu Items](/docs/ui-components/suggestion-menus#changing-slash-menu-items)
- [Editor Setup](/docs/editor-basics/setup)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<html lang="en">
<head>
<script>
<!-- AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY -->
</script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Alert Block</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const root = createRoot(document.getElementById('root')!);
const queryClient = new QueryClient();

root.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "@blocknote/example-alert-block",
"description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
"private": true,
"version": "0.12.4",
"scripts": {
"start": "vite",
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --max-warnings 0"
},
"dependencies": {
"@blocknote/ariakit": "latest",
"@blocknote/core": "latest",
"@blocknote/mantine": "latest",
"@blocknote/react": "latest",
"@blocknote/shadcn": "latest",
"@mantine/core": "^7.10.1",
"@tanstack/react-query": "^5.51.21",
"js-base64": "^3.7.7",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.2.1"
},
"devDependencies": {
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"@vitejs/plugin-react": "^4.0.4",
"eslint": "^8.10.0",
"vite": "^4.4.8"
},
"eslintConfig": {
"extends": [
"../../../.eslintrc.js"
]
},
"eslintIgnore": [
"dist"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
.alert {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
border-radius: 4px;
min-height: 48px;
padding: 4px;
}

.alert[data-alert-type="warning"] {
background-color: #fff6e6;
}

.alert[data-alert-type="error"] {
background-color: #ffe6e6;
}

.alert[data-alert-type="info"] {
background-color: #e6ebff;
}

.alert[data-alert-type="success"] {
background-color: #e6ffe6;
}

[data-color-scheme="dark"] .alert[data-alert-type="warning"] {
background-color: #805d20;
}

[data-color-scheme="dark"] .alert[data-alert-type="error"] {
background-color: #802020;
}

[data-color-scheme="dark"] .alert[data-alert-type="info"] {
background-color: #203380;
}

[data-color-scheme="dark"] .alert[data-alert-type="success"] {
background-color: #208020;
}

.alert-icon-wrapper {
border-radius: 16px;
display: flex;
justify-content: center;
align-items: center;
margin-left: 12px;
margin-right: 12px;
height: 18px;
width: 18px;
user-select: none;
cursor: pointer;
}

.alert-icon[data-alert-icon-type="warning"] {
color: #e69819
}

.alert-icon[data-alert-icon-type="error"] {
color: #d80d0d
}

.alert-icon[data-alert-icon-type="info"] {
color: #507aff
}

.alert-icon[data-alert-icon-type="success"] {
color: #0bc10b
}

.inline-content {
flex-grow: 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": [
"DOM",
"DOM.Iterable",
"ESNext"
],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"composite": true
},
"include": [
"."
],
"__ADD_FOR_LOCAL_DEV_references": [
{
"path": "../../../packages/core/"
},
{
"path": "../../../packages/react/"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
import react from "@vitejs/plugin-react";
import * as fs from "fs";
import * as path from "path";
import { defineConfig } from "vite";
// import eslintPlugin from "vite-plugin-eslint";
// https://vitejs.dev/config/
export default defineConfig((conf) => ({
plugins: [react()],
optimizeDeps: {},
build: {
sourcemap: true,
},
resolve: {
alias:
conf.command === "build" ||
!fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
? {}
: ({
// Comment out the lines below to load a built version of blocknote
// or, keep as is to load live from sources with live reload working
"@blocknote/core": path.resolve(
__dirname,
"../../packages/core/src/"
),
"@blocknote/react": path.resolve(
__dirname,
"../../packages/react/src/"
),
} as any),
},
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "github-a6kavu-x9g2cp",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "npm-run-all --parallel start-server start-client",
"start-client": "cd client && npm install && npm run dev",
"start-server": "cd server && npm install && npm run start",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"npm-run-all": "^4.1.5"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import express from "express";
import { ServerBlockNoteEditor } from "@blocknote/server-util";
import { fromUint8Array } from "js-base64";
import * as Y from "yjs";
import { LoremIpsum } from "lorem-ipsum";

const lorem = new LoremIpsum({
sentencesPerParagraph: {
max: 8,
min: 4,
},
wordsPerSentence: {
max: 16,
min: 4,
},
});

const app = express();
const port = 8080;
const editor = ServerBlockNoteEditor.create();

app.get("/", async (req, res) => {
const doc = await editor.blocksToYDoc(
[
{
id: "7d181c92-fd43-405a-9760-d7feff142917",
type: "paragraph",
props: {
textColor: "default",
backgroundColor: "default",
textAlignment: "left",
},
content: [
{
type: "text",
text: lorem.generateSentences(2),
styles: {},
},
],
children: [],
},
],
"document-store"
);

// https://docs.yjs.dev/api/document-updates encode the state as a vector which we can later apply in the frontend
const state = Y.encodeStateAsUpdateV2(doc);
const initialContent = fromUint8Array(state);
res.send(initialContent);
});

app.listen(port, () => {
console.log(`App listening at http://localhost:${port}`);
});
Loading
Loading