CodeMirror
This is a real-time collaborative example of the CodeMirror 6 editor. It uses the Text, a custom CRDT type from Yorkie.
main.ts
1/* eslint-disable jsdoc/require-jsdoc */2import yorkie, {3 type TextChange,4 type Text as YorkieText,5} from 'yorkie-js-sdk';6import { basicSetup, EditorView } from 'codemirror';7import { keymap } from '@codemirror/view';8import {9 markdown,10 markdownKeymap,11 markdownLanguage,12} from '@codemirror/lang-markdown';13import { Transaction, type ChangeSpec } from '@codemirror/state';14import { network } from './network';15import { displayLog, displayPeers } from './utils';16import './style.css';1718type YorkieDoc = {19 content: YorkieText;20};2122const editorParentElem = document.getElementById('editor')!;23const peersElem = document.getElementById('peers')!;24const documentElem = document.getElementById('document')!;25const documentTextElem = document.getElementById('document-text')!;26const networkStatusElem = document.getElementById('network-status')!;2728async function main() {29 // 01. create client with RPCAddr(envoy) then activate it.30 const client = new yorkie.Client(import.meta.env.VITE_YORKIE_API_ADDR, {31 apiKey: import.meta.env.VITE_YORKIE_API_KEY,32 });33 await client.activate();3435 // subscribe peer change event36 client.subscribe((event) => {37 network.statusListener(networkStatusElem)(event);38 if (event.type === 'peers-changed') {39 displayPeers(peersElem, event.value[doc.getKey()], client.getID() ?? '');40 }41 });4243 // 02-1. create a document then attach it into the client.44 const doc = new yorkie.Document<YorkieDoc>('vanilla-codemirror6');45 await client.attach(doc);46 doc.update((root) => {47 if (!root.content) {48 root.content = new yorkie.Text();49 }50 }, 'create content if not exists');5152 // 02-2. subscribe document event.53 const syncText = () => {54 const text = doc.getRoot().content;55 view.dispatch({56 changes: { from: 0, to: view.state.doc.length, insert: text.toString() },57 annotations: [Transaction.remote.of(true)],58 });59 };60 doc.subscribe((event) => {61 if (event.type === 'snapshot') {62 // The text is replaced to snapshot and must be re-synced.63 syncText();64 }65 displayLog(documentElem, documentTextElem, doc);66 });67 await client.sync();6869 // 03-1. define function that bind the document with the codemirror(broadcast local changes to peers)70 const updateListener = EditorView.updateListener.of((viewUpdate) => {71 if (viewUpdate.docChanged) {72 for (const tr of viewUpdate.transactions) {73 const events = ['select', 'input', 'delete', 'move', 'undo', 'redo'];74 if (!events.map((event) => tr.isUserEvent(event)).some(Boolean)) {75 continue;76 }77 if (tr.annotation(Transaction.remote)) {78 continue;79 }80 tr.changes.iterChanges((fromA, toA, _, __, inserted) => {81 doc.update((root) => {82 root.content.edit(fromA, toA, inserted.toJSON().join('\n'));83 }, `update content byA ${client.getID()}`);84 });85 }86 }87 });8889 // 03-2. create codemirror instance90 const view = new EditorView({91 doc: '',92 extensions: [93 basicSetup,94 markdown({ base: markdownLanguage }),95 keymap.of(markdownKeymap),96 updateListener,97 ],98 parent: editorParentElem,99 });100101 // 03-3. define event handler that apply remote changes to local102 const changeEventHandler = (changes: Array<TextChange>) => {103 const clientId = client.getID();104 const changeSpecs: Array<ChangeSpec> = changes105 .filter(106 (change) => change.type === 'content' && change.actor !== clientId,107 )108 .map((change) => ({109 from: Math.max(0, change.from),110 to: Math.max(0, change.to),111 insert: change.content,112 }));113114 view.dispatch({115 changes: changeSpecs,116 annotations: [Transaction.remote.of(true)],117 });118 };119 const text = doc.getRoot().content;120 text.onChanges(changeEventHandler);121 syncText();122 displayLog(documentElem, documentTextElem, doc);123}124125main();
- User 1
- User 2
- User 1
- User 2
Event Log