diff --git a/examples/07-collaboration/06-ghost-writer/.bnexample.json b/examples/07-collaboration/06-ghost-writer/.bnexample.json
new file mode 100644
index 0000000000..2d28ad5ec5
--- /dev/null
+++ b/examples/07-collaboration/06-ghost-writer/.bnexample.json
@@ -0,0 +1,10 @@
+{
+ "playground": true,
+ "docs": false,
+ "author": "nperez0111",
+ "tags": ["Advanced", "Development", "Collaboration"],
+ "dependencies": {
+ "y-partykit": "^0.0.25",
+ "yjs": "^13.6.15"
+ }
+}
diff --git a/examples/07-collaboration/06-ghost-writer/App.tsx b/examples/07-collaboration/06-ghost-writer/App.tsx
new file mode 100644
index 0000000000..4344c5c11a
--- /dev/null
+++ b/examples/07-collaboration/06-ghost-writer/App.tsx
@@ -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 ? (
+
+ ) : (
+ <>
+
+
+
+ >
+ )}
+
+
+ {!isGhostWriting && (
+
+ {Array.from({ length: numGhostWriters }).map((_, index) => (
+
+ ))}
+
+ )}
+ >
+ );
+}
diff --git a/examples/07-collaboration/06-ghost-writer/README.md b/examples/07-collaboration/06-ghost-writer/README.md
new file mode 100644
index 0000000000..49cc44c869
--- /dev/null
+++ b/examples/07-collaboration/06-ghost-writer/README.md
@@ -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)
diff --git a/examples/07-collaboration/06-ghost-writer/index.html b/examples/07-collaboration/06-ghost-writer/index.html
new file mode 100644
index 0000000000..bcca4b1a5c
--- /dev/null
+++ b/examples/07-collaboration/06-ghost-writer/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Ghost Writer
+
+
+
+
+
+
diff --git a/examples/07-collaboration/06-ghost-writer/main.tsx b/examples/07-collaboration/06-ghost-writer/main.tsx
new file mode 100644
index 0000000000..6284417d60
--- /dev/null
+++ b/examples/07-collaboration/06-ghost-writer/main.tsx
@@ -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(
+
+
+
+);
diff --git a/examples/07-collaboration/06-ghost-writer/package.json b/examples/07-collaboration/06-ghost-writer/package.json
new file mode 100644
index 0000000000..44b1e1c3ac
--- /dev/null
+++ b/examples/07-collaboration/06-ghost-writer/package.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/examples/07-collaboration/06-ghost-writer/styles.css b/examples/07-collaboration/06-ghost-writer/styles.css
new file mode 100644
index 0000000000..588b4f01fa
--- /dev/null
+++ b/examples/07-collaboration/06-ghost-writer/styles.css
@@ -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;
+}
diff --git a/examples/07-collaboration/06-ghost-writer/tsconfig.json b/examples/07-collaboration/06-ghost-writer/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/07-collaboration/06-ghost-writer/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": "bundler",
+ "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/06-ghost-writer/vite.config.ts b/examples/07-collaboration/06-ghost-writer/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/07-collaboration/06-ghost-writer/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/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts
index f3d4c108b0..18be707664 100644
--- a/packages/core/src/editor/BlockNoteEditor.ts
+++ b/packages/core/src/editor/BlockNoteEditor.ts
@@ -1231,7 +1231,10 @@ export class BlockNoteEditor<
*
* @param content can be a string, or array of partial inline content elements
*/
- public insertInlineContent(content: PartialInlineContent) {
+ public insertInlineContent(
+ content: PartialInlineContent,
+ { updateSelection = false }: { updateSelection?: boolean } = {},
+ ) {
const nodes = inlineContentToNodes(content, this.pmSchema);
this.transact((tr) => {
@@ -1242,6 +1245,9 @@ export class BlockNoteEditor<
to: tr.selection.to,
},
nodes,
+ {
+ updateSelection,
+ },
);
});
}
diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx
index 7c9e1a5d8a..23615a61ce 100644
--- a/playground/src/examples.gen.tsx
+++ b/playground/src/examples.gen.tsx
@@ -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"
+ }
}
]
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4e39030a3f..d88f2a99c4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2645,6 +2645,49 @@ importers:
specifier: ^5.3.4
version: 5.4.15(@types/node@22.14.1)(terser@5.39.0)
+ examples/07-collaboration/06-ghost-writer:
+ dependencies:
+ '@blocknote/ariakit':
+ specifier: latest
+ version: link:../../../packages/ariakit
+ '@blocknote/core':
+ specifier: latest
+ version: link:../../../packages/core
+ '@blocknote/mantine':
+ specifier: latest
+ version: link:../../../packages/mantine
+ '@blocknote/react':
+ specifier: latest
+ version: link:../../../packages/react
+ '@blocknote/shadcn':
+ specifier: latest
+ version: link:../../../packages/shadcn
+ react:
+ specifier: ^18.3.1
+ version: 18.3.1
+ react-dom:
+ specifier: ^18.3.1
+ version: 18.3.1(react@18.3.1)
+ y-partykit:
+ specifier: ^0.0.25
+ version: 0.0.25
+ yjs:
+ specifier: ^13.6.15
+ version: 13.6.24
+ devDependencies:
+ '@types/react':
+ specifier: ^18.0.25
+ version: 18.3.20
+ '@types/react-dom':
+ specifier: ^18.0.9
+ version: 18.3.5(@types/react@18.3.20)
+ '@vitejs/plugin-react':
+ specifier: ^4.3.1
+ version: 4.3.4(vite@5.4.15(@types/node@22.14.1)(terser@5.39.0))
+ vite:
+ specifier: ^5.3.4
+ version: 5.4.15(@types/node@22.14.1)(terser@5.39.0)
+
examples/08-extensions/01-tiptap-arrow-conversion:
dependencies:
'@blocknote/ariakit':