Skip to content

Commit c14971c

Browse files
hackerwinsclaude
andcommitted
Add remote selection range display and vanilla-prosemirror example (#1166)
Add a ProseMirror plugin that renders inline decorations for remote selection ranges, so collaborators see colored highlights (not just a caret). Update the binding to convert both ends of the selection and dispatch decoration metadata. Also add a standalone vanilla-prosemirror example following the existing vanilla-quill pattern. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0abd7f8 commit c14971c

39 files changed

+2116
-1319
lines changed

.github/workflows/npm-publish.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,10 @@ jobs:
5757
run: pnpm publish --filter=@yorkie-js/schema --no-git-checks --provenance
5858
env:
5959
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
60+
61+
- name: ProseMirror Build
62+
run: pnpm prosemirror build
63+
- name: ProseMirror Publish
64+
run: pnpm publish --filter=@yorkie-js/prosemirror --no-git-checks --provenance
65+
env:
66+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

examples/vanilla-prosemirror/.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
VITE_YORKIE_API_ADDR='http://localhost:8080'
2+
VITE_YORKIE_API_KEY=''
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
VITE_YORKIE_API_ADDR='https://api.yorkie.dev'
2+
VITE_YORKIE_API_KEY='D6sd9E5ehtXBssmeRZ4QQP'
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Yorkie Vanilla ProseMirror Example
2+
3+
<p>
4+
<a href="https://yorkie.dev/yorkie-js-sdk/examples/vanilla-prosemirror/" target="_blank">
5+
<img src="https://img.shields.io/badge/preview-message?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMTUiIHZpZXdCb3g9IjAgMCAyNCAxNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYuODU3MTcgMi43ODE5OUwxMS4yNzUxIDkuMTI2NzhDMTEuNTU0NCA5LjUyODAxIDEyLjEwNjIgOS42MjY3NiAxMi41MDc0IDkuMzQ3NDRDMTIuNTkzNCA5LjI4NzUgMTIuNjY4MSA5LjIxMjggMTIuNzI4MSA5LjEyNjc4TDE3LjE0NiAyLjc4MTk5QzE3LjcwNDggMS45Nzk1NCAxNy41MDcyIDAuODc2MTMxIDE2LjcwNDggMC4zMTc0OTRDMTYuNDA4IDAuMTEwODM3IDE2LjA1NSAwIDE1LjY5MzIgMEg4LjMxMDAxQzcuMzMyMiAwIDYuNTM5NTUgMC43OTI2NTQgNi41Mzk1NSAxLjc3MDQ2QzYuNTM5NjggMi4xMzIxMSA2LjY1MDUxIDIuNDg1MTEgNi44NTcxNyAyLjc4MTk5WiIgZmlsbD0iIzUxNEM0OSIvPgo8cGF0aCBkPSJNMTMuODA4OSAxNC4yMzg4QzE0LjEyMzEgMTQuNDE4IDE0LjQ4NDcgMTQuNDk2NiAxNC44NDUgMTQuNDY0MkwyMi45MjYgMTMuNzM1QzIzLjU3NTMgMTMuNjc2NSAyNC4wNTQgMTMuMTAyNyAyMy45OTU1IDEyLjQ1MzVDMjMuOTkyNCAxMi40MTkyIDIzLjk4NzggMTIuMzg1MSAyMy45ODE3IDEyLjM1MTNDMjMuNzM4OSAxMC45OTY4IDIzLjI2MTEgOS42OTUyNyAyMi41Njk5IDguNTA1NDZDMjEuODc4NiA3LjMxNTY1IDIwLjk4NDggNi4yNTU3NyAxOS45Mjg2IDUuMzczOTFDMTkuNDI4MiA0Ljk1NjE0IDE4LjY4MzkgNS4wMjMwNyAxOC4yNjYyIDUuNTIzNTZDMTguMjQ0MiA1LjU0OTkgMTguMjIzMyA1LjU3NzI2IDE4LjIwMzYgNS42MDU1MUwxMy41NjcgMTIuMjY0MUMxMy4zNjAzIDEyLjU2MSAxMy4yNDk1IDEyLjkxNCAxMy4yNDk1IDEzLjI3NThWMTMuMjUzN0MxMy4yNDk1IDEzLjQ1NjIgMTMuMzAxNiAxMy42NTU0IDEzLjQwMDggMTMuODMxOUMxMy41MDUgMTQuMDA1NCAxMy42NTIxIDE0LjE0OTMgMTMuODI4MSAxNC4yNDk2IiBmaWxsPSIjRkRDNDMzIi8+CjxwYXRoIGQ9Ik0xMC42NDE2IDEzLjc0MzRDMTAuNTM3NSAxMy45NTU5IDEwLjM3MiAxNC4xMzIyIDEwLjE2NjUgMTQuMjQ5NEwxMC4xOTE1IDE0LjIzNTFDOS44NzczNCAxNC40MTQzIDkuNTE1NjkgMTQuNDkyOSA5LjE1NTQ0IDE0LjQ2MDVMMS4wNzQ0MSAxMy43MzEzQzEuMDQwMTggMTMuNzI4MyAxLjAwNjA3IDEzLjcyMzcgMC45NzIyMjUgMTMuNzE3NkMwLjMzMDYyIDEzLjYwMjUgLTAuMDk2MzExOSAxMi45ODkyIDAuMDE4NzI0MiAxMi4zNDc2QzAuMjYxNTIyIDEwLjk5MyAwLjczOTM1NCA5LjY5MTU2IDEuNDMwNDYgOC41MDE2M0MyLjEyMTU3IDcuMzExNjkgMy4wMTU1MSA2LjI1MjA2IDQuMDcxODQgNS4zNzAwOEM0LjA5ODE4IDUuMzQ4MDYgNC4xMjU1NCA1LjMyNzE5IDQuMTUzNzkgNS4zMDc0N0M0LjY4ODc2IDQuOTM1IDUuNDI0MjcgNS4wNjY3MSA1Ljc5Njg3IDUuNjAxNjhMMTAuNDMzNCAxMi4yNjA0QzEwLjY0MDEgMTIuNTU3MyAxMC43NTA5IDEyLjkxMDMgMTAuNzUwOSAxMy4yNzIxVjEzLjI0MzJDMTAuNzUwOSAxMy40Nzk3IDEwLjY3OTggMTMuNzExIDEwLjU0NjggMTMuOTA2NyIgZmlsbD0iI0ZEQzQzMyIvPgo8L3N2Zz4K&color=FEF3D7" alt="Live Preview" />
6+
</a>
7+
</p>
8+
9+
## How to run demo
10+
11+
At project root, run below command to start Yorkie server.
12+
13+
```bash
14+
$ docker compose -f docker/docker-compose.yml up --build -d
15+
```
16+
17+
Then install dependencies and run the demo.
18+
19+
```bash
20+
# In the root directory of the repository.
21+
$ pnpm install
22+
```
23+
24+
Now you can run the demo.
25+
26+
```bash
27+
# In the root directory of the repository.
28+
$ pnpm vanilla-prosemirror dev
29+
30+
# Or in the directory of the example.
31+
$ pnpm dev
32+
```
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Yorkie + ProseMirror Example</title>
7+
</head>
8+
<body>
9+
<div id="status" class="status">Connecting...</div>
10+
<div id="editor-wrapper" style="position: relative;">
11+
<div id="editor"></div>
12+
<div id="cursor-overlay"></div>
13+
</div>
14+
<script type="module" src="/src/main.ts"></script>
15+
</body>
16+
</html>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "vanilla-prosemirror",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "tsc && vite build",
9+
"preview": "vite preview"
10+
},
11+
"devDependencies": {
12+
"typescript": "^5.9.3",
13+
"vite": "^7.2.6"
14+
},
15+
"dependencies": {
16+
"@yorkie-js/sdk": "workspace:*",
17+
"@yorkie-js/prosemirror": "workspace:*",
18+
"prosemirror-commands": "^1.6.2",
19+
"prosemirror-example-setup": "^1.2.2",
20+
"prosemirror-history": "^1.4.1",
21+
"prosemirror-keymap": "^1.2.2",
22+
"prosemirror-model": "^1.23.0",
23+
"prosemirror-schema-basic": "^1.2.3",
24+
"prosemirror-schema-list": "^1.5.0",
25+
"prosemirror-state": "^1.4.3",
26+
"prosemirror-view": "^1.37.0"
27+
}
28+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import yorkie from '@yorkie-js/sdk';
2+
import { EditorState } from 'prosemirror-state';
3+
import { EditorView } from 'prosemirror-view';
4+
import { Schema } from 'prosemirror-model';
5+
import { schema as basicSchema } from 'prosemirror-schema-basic';
6+
import { addListNodes } from 'prosemirror-schema-list';
7+
import { exampleSetup } from 'prosemirror-example-setup';
8+
import {
9+
YorkieProseMirrorBinding,
10+
remoteSelectionPlugin,
11+
} from '@yorkie-js/prosemirror';
12+
import './style.css';
13+
14+
const statusEl = document.getElementById('status')!;
15+
const editorEl = document.getElementById('editor')!;
16+
const cursorOverlayEl = document.getElementById('cursor-overlay')!;
17+
const editorWrapperEl = document.getElementById('editor-wrapper')!;
18+
19+
// Extend basic schema with list nodes
20+
const mySchema = new Schema({
21+
nodes: addListNodes(basicSchema.spec.nodes, 'paragraph block*', 'block'),
22+
marks: basicSchema.spec.marks,
23+
});
24+
25+
// Document key from URL query param or date-based fallback
26+
const params = new URLSearchParams(window.location.search);
27+
const docKey =
28+
params.get('key') ||
29+
`pm-vanilla-${new Date().toISOString().substring(0, 10).replace(/-/g, '')}`;
30+
31+
function setStatus(text: string, type: 'connecting' | 'connected' | 'error') {
32+
statusEl.textContent = text;
33+
statusEl.className = `status ${type === 'connecting' ? '' : type}`;
34+
}
35+
36+
// Initial document
37+
const initialDoc = mySchema.node('doc', null, [
38+
mySchema.node('heading', { level: 2 }, [
39+
mySchema.text('Collaborative ProseMirror'),
40+
]),
41+
mySchema.node('paragraph', null, [
42+
mySchema.text('Start editing to collaborate in real time.'),
43+
]),
44+
]);
45+
46+
async function main() {
47+
setStatus(`Connecting to Yorkie server... (doc: ${docKey})`, 'connecting');
48+
49+
try {
50+
const client = new yorkie.Client({
51+
rpcAddr:
52+
(import.meta as any).env?.VITE_YORKIE_API_ADDR ||
53+
'http://localhost:8080',
54+
apiKey: (import.meta as any).env?.VITE_YORKIE_API_KEY,
55+
});
56+
await client.activate();
57+
58+
const doc = new yorkie.Document<Record<string, any>, Record<string, any>>(
59+
docKey,
60+
{ enableDevtools: true },
61+
);
62+
await client.attach(doc, { initialPresence: {} });
63+
64+
setStatus(`Connected — doc: ${docKey}`, 'connected');
65+
66+
const state = EditorState.create({
67+
doc: initialDoc,
68+
plugins: [
69+
...exampleSetup({ schema: mySchema }),
70+
remoteSelectionPlugin(),
71+
],
72+
});
73+
74+
const view = new EditorView(editorEl, { state });
75+
76+
const binding = new YorkieProseMirrorBinding(view, doc, 'tree', {
77+
cursors: {
78+
enabled: true,
79+
overlayElement: cursorOverlayEl,
80+
wrapperElement: editorWrapperEl,
81+
},
82+
});
83+
binding.initialize();
84+
85+
view.focus();
86+
} catch (e) {
87+
setStatus(`Connection failed: ${(e as Error).message}`, 'error');
88+
console.error(e);
89+
}
90+
}
91+
92+
main();

0 commit comments

Comments
 (0)