Skip to content
Merged
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
10 changes: 10 additions & 0 deletions examples/07-collaboration/06-ghost-writer/.bnexample.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"playground": true,
"docs": false,
"author": "nperez0111",
"tags": ["Advanced", "Development", "Collaboration"],
"dependencies": {
"y-partykit": "^0.0.25",
"yjs": "^13.6.15"
}
}
126 changes: 126 additions & 0 deletions examples/07-collaboration/06-ghost-writer/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import "@blocknote/core/fonts/inter.css";
import "@blocknote/mantine/style.css";
import { BlockNoteView } from "@blocknote/mantine";
import { useCreateBlockNote } from "@blocknote/react";

import YPartyKitProvider from "y-partykit/provider";
import * as Y from "yjs";
import "./styles.css";
import { useEffect, useState } from "react";
// eslint-disable-next-line import/no-extraneous-dependencies
import { EditorView } from "prosemirror-view";

const params = new URLSearchParams(window.location.search);
const ghostWritingRoom = params.get("room");
const ghostWriterIndex = parseInt(params.get("index") || "1");
const isGhostWriting = Boolean(ghostWritingRoom);
const roomName = ghostWritingRoom || `ghost-writer-${Date.now()}`;
// Sets up Yjs document and PartyKit Yjs provider.
const doc = new Y.Doc();
const provider = new YPartyKitProvider(
"blocknote-dev.yousefed.partykit.dev",
// Use a unique name as a "room" for your application.
roomName,
doc,
);

/**
* Y-prosemirror has an optimization, where it doesn't send awareness updates unless the editor is currently focused.
* So, for the ghost writers, we override the hasFocus method to always return true.
*/
if (isGhostWriting) {
EditorView.prototype.hasFocus = () => true;
}

const ghostContent =
"This demo shows a two-way sync of documents. It allows you to test collaboration features, and see how stable the editor is. ";

export default function App() {
const [numGhostWriters, setNumGhostWriters] = useState(1);
const [isPaused, setIsPaused] = useState(false);
const editor = useCreateBlockNote({
collaboration: {
// The Yjs Provider responsible for transporting updates:
provider,
// Where to store BlockNote data in the Y.Doc:
fragment: doc.getXmlFragment("document-store"),
// Information (name and color) for this user:
user: {
name: isGhostWriting
? `Ghost Writer #${ghostWriterIndex}`
: "My Username",
color: isGhostWriting ? "#CCCCCC" : "#00ff00",
},
},
});

useEffect(() => {
if (!isGhostWriting || isPaused) {
return;
}
let index = 0;
let timeout: NodeJS.Timeout;

const scheduleNextChar = () => {
const jitter = Math.random() * 200; // Random delay between 0-200ms
timeout = setTimeout(() => {
const firstBlock = editor.document?.[0];
if (firstBlock) {
editor.insertInlineContent(ghostContent[index], {
updateSelection: true,
});
index = (index + 1) % ghostContent.length;
}
scheduleNextChar();
}, 50 + jitter);
};

scheduleNextChar();

return () => clearTimeout(timeout);
}, [editor, isPaused]);

// Renders the editor instance.
return (
<>
{isGhostWriting ? (
<button onClick={() => setIsPaused((a) => !a)}>
{isPaused ? "Resume Ghost Writer" : "Pause Ghost Writer"}
</button>
) : (
<>
<button onClick={() => setNumGhostWriters((a) => a + 1)}>
Add a Ghost Writer
</button>
<button onClick={() => setNumGhostWriters((a) => a - 1)}>
Remove a Ghost Writer
</button>
<button
onClick={() => {
window.open(
`${window.location.origin}${window.location.pathname}?room=${roomName}&index=-1`,
"_blank",
);
}}>
Ghost Writer in a new window
</button>
</>
)}
<BlockNoteView editor={editor} />

{!isGhostWriting && (
<div className="two-way-sync">
{Array.from({ length: numGhostWriters }).map((_, index) => (
<iframe
src={`${window.location.origin}${
window.location.pathname
}?room=${roomName}&index=${index + 1}&hideMenu=true`}
title="ghost writer"
className="ghost-writer"
/>
))}
</div>
)}
</>
);
}
9 changes: 9 additions & 0 deletions examples/07-collaboration/06-ghost-writer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Ghost Writer

In this example, we use a local Yjs document to store the document state, and have a ghost writer that edits the document in real-time.

**Try it out:** Open this page in a new browser tab or window to see it in action!

**Relevant Docs:**

- [Editor Setup](/docs/editor-basics/setup)
14 changes: 14 additions & 0 deletions examples/07-collaboration/06-ghost-writer/index.html
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>Ghost Writer</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
11 changes: 11 additions & 0 deletions examples/07-collaboration/06-ghost-writer/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 "./App.jsx";

const root = createRoot(document.getElementById("root")!);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
29 changes: 29 additions & 0 deletions examples/07-collaboration/06-ghost-writer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@blocknote/example-ghost-writer",
"description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
"private": true,
"version": "0.12.4",
"scripts": {
"start": "vite",
"dev": "vite",
"build:prod": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@blocknote/core": "latest",
"@blocknote/react": "latest",
"@blocknote/ariakit": "latest",
"@blocknote/mantine": "latest",
"@blocknote/shadcn": "latest",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"y-partykit": "^0.0.25",
"yjs": "^13.6.15"
},
"devDependencies": {
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"@vitejs/plugin-react": "^4.3.1",
"vite": "^5.3.4"
}
}
12 changes: 12 additions & 0 deletions examples/07-collaboration/06-ghost-writer/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.two-way-sync {
display: flex;
flex-direction: row;
height: 100%;
margin-top: 10px;
gap: 8px;
}

.ghost-writer {
flex: 1;
border: 1px solid #ccc;
}
36 changes: 36 additions & 0 deletions examples/07-collaboration/06-ghost-writer/tsconfig.json
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": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"composite": true
},
"include": [
"."
],
"__ADD_FOR_LOCAL_DEV_references": [
{
"path": "../../../packages/core/"
},
{
"path": "../../../packages/react/"
}
]
}
32 changes: 32 additions & 0 deletions examples/07-collaboration/06-ghost-writer/vite.config.ts
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),
},
}));
8 changes: 7 additions & 1 deletion packages/core/src/editor/BlockNoteEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1231,7 +1231,10 @@ export class BlockNoteEditor<
*
* @param content can be a string, or array of partial inline content elements
*/
public insertInlineContent(content: PartialInlineContent<ISchema, SSchema>) {
public insertInlineContent(
content: PartialInlineContent<ISchema, SSchema>,
{ updateSelection = false }: { updateSelection?: boolean } = {},
) {
const nodes = inlineContentToNodes(content, this.pmSchema);

this.transact((tr) => {
Expand All @@ -1242,6 +1245,9 @@ export class BlockNoteEditor<
to: tr.selection.to,
},
nodes,
{
updateSelection,
},
);
});
}
Expand Down
24 changes: 24 additions & 0 deletions playground/src/examples.gen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1326,6 +1326,30 @@
"pathFromRoot": "examples/07-collaboration",
"slug": "collaboration"
}
},
{
"projectSlug": "ghost-writer",
"fullSlug": "collaboration/ghost-writer",
"pathFromRoot": "examples/07-collaboration/06-ghost-writer",
"config": {
"playground": true,
"docs": false,
"author": "nperez0111",
"tags": [
"Advanced",
"Development",
"Collaboration"
],
"dependencies": {
"y-partykit": "^0.0.25",
"yjs": "^13.6.15"
} as any
},
"title": "Ghost Writer",
"group": {
"pathFromRoot": "examples/07-collaboration",
"slug": "collaboration"
}
}
]
},
Expand Down
43 changes: 43 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading