Skip to content

Commit 63e6de7

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 63e6de7

38 files changed

+2084
-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: 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)