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, { DocEventType } from '@yorkie-js/sdk';3import type { EditOpInfo, OperationInfo } from '@yorkie-js/sdk';4import { basicSetup, EditorView } from 'codemirror';5import { keymap } from '@codemirror/view';6import {7 markdown,8 markdownKeymap,9 markdownLanguage,10} from '@codemirror/lang-markdown';11import { Transaction, TransactionSpec } from '@codemirror/state';12import { network } from './network';13import { displayLog, displayPeers } from './utils';14import { YorkieDoc, YorkiePresence } from './type';15import './style.css';1617const editorParentElem = document.getElementById('editor')!;18const peersElem = document.getElementById('peers')!;19const documentElem = document.getElementById('document')!;20const documentTextElem = document.getElementById('document-text')!;21const networkStatusElem = document.getElementById('network-status')!;2223async function main() {24 // 01. create client with RPCAddr then activate it.25 const client = new yorkie.Client(import.meta.env.VITE_YORKIE_API_ADDR, {26 apiKey: import.meta.env.VITE_YORKIE_API_KEY,27 });28 await client.activate();2930 window.addEventListener('beforeunload', () => {31 client.deactivate({ keepalive: true });32 });3334 // 02-1. create a document then attach it into the client.35 const doc = new yorkie.Document<YorkieDoc, YorkiePresence>(36 `codemirror6-${new Date()37 .toISOString()38 .substring(0, 10)39 .replace(/-/g, '')}`,40 {41 enableDevtools: true,42 },43 );44 doc.subscribe('connection', (event) => {45 network.statusListener(networkStatusElem)(event);46 });47 doc.subscribe('presence', (event) => {48 if (event.type !== DocEventType.PresenceChanged) {49 displayPeers(peersElem, doc.getPresences(), client.getID()!);50 }51 });52 await client.attach(doc);53 doc.update((root) => {54 if (!root.content) {55 root.content = new yorkie.Text();56 }57 }, 'create content if not exists');5859 // 02-2. subscribe document event.60 const syncText = () => {61 const text = doc.getRoot().content;62 const selection = doc.getMyPresence().selection;63 const transactionSpec: TransactionSpec = {64 changes: { from: 0, to: view.state.doc.length, insert: text.toString() },65 annotations: [Transaction.remote.of(true)],66 };6768 if (selection) {69 // Restore the cursor position when the text is replaced.70 const cursor = text.posRangeToIndexRange(selection);71 transactionSpec['selection'] = {72 anchor: cursor[0],73 head: cursor[1],74 };75 }76 view.dispatch(transactionSpec);77 };78 doc.subscribe((event) => {79 if (event.type === 'snapshot') {80 // The text is replaced to snapshot and must be re-synced.81 syncText();82 }83 displayLog(documentElem, documentTextElem, doc);84 });8586 doc.subscribe('$.content', (event) => {87 if (event.type === 'remote-change') {88 const { operations } = event.value;89 handleOperations(operations);90 }91 });9293 await client.sync();9495 // 03-1. define function that bind the document with the codemirror(broadcast local changes to peers)96 const updateListener = EditorView.updateListener.of((viewUpdate) => {97 if (viewUpdate.docChanged) {98 for (const tr of viewUpdate.transactions) {99 const events = ['select', 'input', 'delete', 'move', 'undo', 'redo'];100 if (!events.map((event) => tr.isUserEvent(event)).some(Boolean)) {101 continue;102 }103 if (tr.annotation(Transaction.remote)) {104 continue;105 }106 let adj = 0;107 tr.changes.iterChanges((fromA, toA, _, __, inserted) => {108 const insertText = inserted.toJSON().join('\n');109 doc.update((root) => {110 root.content.edit(fromA + adj, toA + adj, insertText);111 }, `update content byA ${client.getID()}`);112 adj += insertText.length - (toA - fromA);113 });114 }115 }116117 const hasFocus =118 viewUpdate.view.hasFocus && viewUpdate.view.dom.ownerDocument.hasFocus();119 const sel = hasFocus ? viewUpdate.state.selection.main : null;120121 doc.update((root, presence) => {122 if (sel && root.content) {123 const selection = root.content.indexRangeToPosRange([124 sel.anchor,125 sel.head,126 ]);127128 if (129 JSON.stringify(selection) !==130 JSON.stringify(presence.get('selection'))131 ) {132 presence.set({133 selection,134 });135 }136 } else if (presence.get('selection')) {137 presence.set({138 selection: undefined,139 });140 }141 });142 });143144 // 03-2. create codemirror instance145 const view = new EditorView({146 doc: '',147 extensions: [148 basicSetup,149 markdown({ base: markdownLanguage }),150 keymap.of(markdownKeymap),151 updateListener,152 ],153 parent: editorParentElem,154 });155156 // 03-3. define event handler that apply remote changes to local157 function handleOperations(operations: Array<OperationInfo>) {158 for (const op of operations) {159 if (op.type === 'edit') {160 handleEditOp(op);161 }162 }163 }164 function handleEditOp(op: EditOpInfo) {165 const changes = [166 {167 from: Math.max(0, op.from),168 to: Math.max(0, op.to),169 insert: op.value!.content,170 },171 ];172173 view.dispatch({174 changes,175 annotations: [Transaction.remote.of(true)],176 });177 }178179 syncText();180 displayLog(documentElem, documentTextElem, doc);181}182183main();
- User 1
- User 2
- User 1
- User 2
Event Log