From 74560b45fd408a9789de57d9644e10f73f77e124 Mon Sep 17 00:00:00 2001 From: Martin Anselmo Date: Wed, 7 Aug 2024 14:33:41 -0400 Subject: [PATCH] feat: Add collaborative initial content example --- .../.bnexample.json | 6 ++ .../README.md | 7 ++ .../client/App.tsx | 64 ++++++++++++++++ .../client/README.md | 11 +++ .../client/index.html | 14 ++++ .../client/main.tsx | 16 ++++ .../client/package.json | 41 ++++++++++ .../client/styles.css | 74 +++++++++++++++++++ .../client/tsconfig.json | 36 +++++++++ .../client/vite.config.ts | 32 ++++++++ .../package.json | 17 +++++ .../server/index.js | 54 ++++++++++++++ .../server/package.json | 23 ++++++ 13 files changed, 395 insertions(+) create mode 100644 examples/07-collaboration/03-collaborative-initial-content/.bnexample.json create mode 100644 examples/07-collaboration/03-collaborative-initial-content/README.md create mode 100644 examples/07-collaboration/03-collaborative-initial-content/client/App.tsx create mode 100644 examples/07-collaboration/03-collaborative-initial-content/client/README.md create mode 100644 examples/07-collaboration/03-collaborative-initial-content/client/index.html create mode 100644 examples/07-collaboration/03-collaborative-initial-content/client/main.tsx create mode 100644 examples/07-collaboration/03-collaborative-initial-content/client/package.json create mode 100644 examples/07-collaboration/03-collaborative-initial-content/client/styles.css create mode 100644 examples/07-collaboration/03-collaborative-initial-content/client/tsconfig.json create mode 100644 examples/07-collaboration/03-collaborative-initial-content/client/vite.config.ts create mode 100644 examples/07-collaboration/03-collaborative-initial-content/package.json create mode 100644 examples/07-collaboration/03-collaborative-initial-content/server/index.js create mode 100644 examples/07-collaboration/03-collaborative-initial-content/server/package.json diff --git a/examples/07-collaboration/03-collaborative-initial-content/.bnexample.json b/examples/07-collaboration/03-collaborative-initial-content/.bnexample.json new file mode 100644 index 000000000..b1515d3fa --- /dev/null +++ b/examples/07-collaboration/03-collaborative-initial-content/.bnexample.json @@ -0,0 +1,6 @@ +{ + "playground": true, + "docs": true, + "author": "mfanselmo", + "tags": ["Advanced", "Blocks", "yjs", "Collaboration"] +} diff --git a/examples/07-collaboration/03-collaborative-initial-content/README.md b/examples/07-collaboration/03-collaborative-initial-content/README.md new file mode 100644 index 000000000..122be57e4 --- /dev/null +++ b/examples/07-collaboration/03-collaborative-initial-content/README.md @@ -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 \ No newline at end of file diff --git a/examples/07-collaboration/03-collaborative-initial-content/client/App.tsx b/examples/07-collaboration/03-collaborative-initial-content/client/App.tsx new file mode 100644 index 000000000..19e4d90eb --- /dev/null +++ b/examples/07-collaboration/03-collaborative-initial-content/client/App.tsx @@ -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 ( + <> + + + + ); +} diff --git a/examples/07-collaboration/03-collaborative-initial-content/client/README.md b/examples/07-collaboration/03-collaborative-initial-content/client/README.md new file mode 100644 index 000000000..8c064c8b1 --- /dev/null +++ b/examples/07-collaboration/03-collaborative-initial-content/client/README.md @@ -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) \ No newline at end of file diff --git a/examples/07-collaboration/03-collaborative-initial-content/client/index.html b/examples/07-collaboration/03-collaborative-initial-content/client/index.html new file mode 100644 index 000000000..84532134d --- /dev/null +++ b/examples/07-collaboration/03-collaborative-initial-content/client/index.html @@ -0,0 +1,14 @@ + + + + + + Alert Block + + +
+ + + diff --git a/examples/07-collaboration/03-collaborative-initial-content/client/main.tsx b/examples/07-collaboration/03-collaborative-initial-content/client/main.tsx new file mode 100644 index 000000000..7e5156fed --- /dev/null +++ b/examples/07-collaboration/03-collaborative-initial-content/client/main.tsx @@ -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( + + + + + +); diff --git a/examples/07-collaboration/03-collaborative-initial-content/client/package.json b/examples/07-collaboration/03-collaborative-initial-content/client/package.json new file mode 100644 index 000000000..387278be7 --- /dev/null +++ b/examples/07-collaboration/03-collaborative-initial-content/client/package.json @@ -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" + ] +} diff --git a/examples/07-collaboration/03-collaborative-initial-content/client/styles.css b/examples/07-collaboration/03-collaborative-initial-content/client/styles.css new file mode 100644 index 000000000..7296b28dc --- /dev/null +++ b/examples/07-collaboration/03-collaborative-initial-content/client/styles.css @@ -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; +} \ No newline at end of file diff --git a/examples/07-collaboration/03-collaborative-initial-content/client/tsconfig.json b/examples/07-collaboration/03-collaborative-initial-content/client/tsconfig.json new file mode 100644 index 000000000..1bd8ab3c5 --- /dev/null +++ b/examples/07-collaboration/03-collaborative-initial-content/client/tsconfig.json @@ -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/" + } + ] +} \ No newline at end of file diff --git a/examples/07-collaboration/03-collaborative-initial-content/client/vite.config.ts b/examples/07-collaboration/03-collaborative-initial-content/client/vite.config.ts new file mode 100644 index 000000000..f62ab20bc --- /dev/null +++ b/examples/07-collaboration/03-collaborative-initial-content/client/vite.config.ts @@ -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), + }, +})); diff --git a/examples/07-collaboration/03-collaborative-initial-content/package.json b/examples/07-collaboration/03-collaborative-initial-content/package.json new file mode 100644 index 000000000..03eec516b --- /dev/null +++ b/examples/07-collaboration/03-collaborative-initial-content/package.json @@ -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" + } +} diff --git a/examples/07-collaboration/03-collaborative-initial-content/server/index.js b/examples/07-collaboration/03-collaborative-initial-content/server/index.js new file mode 100644 index 000000000..e472df952 --- /dev/null +++ b/examples/07-collaboration/03-collaborative-initial-content/server/index.js @@ -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}`); +}); diff --git a/examples/07-collaboration/03-collaborative-initial-content/server/package.json b/examples/07-collaboration/03-collaborative-initial-content/server/package.json new file mode 100644 index 000000000..34c60a50c --- /dev/null +++ b/examples/07-collaboration/03-collaborative-initial-content/server/package.json @@ -0,0 +1,23 @@ +{ + "name": "server", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "start": "nodemon index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@blocknote/server-util": "^0.15.3", + "express": "^4.19.2", + "js-base64": "^3.7.7", + "lorem-ipsum": "^2.0.8", + "yjs": "^13.6.18" + }, + "devDependencies": { + "nodemon": "^3.1.4" + } +}