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 // 02-1. create a document then attach it into the client.31 const doc = new yorkie.Document<YorkieDoc, YorkiePresence>(32 `codemirror6-${new Date()33 .toISOString()34 .substring(0, 10)35 .replace(/-/g, '')}`,36 {37 enableDevtools: true,38 },39 );40 doc.subscribe('connection', (event) => {41 network.statusListener(networkStatusElem)(event);42 });43 doc.subscribe('presence', (event) => {44 if (event.type !== DocEventType.PresenceChanged) {45 displayPeers(peersElem, doc.getPresences(), client.getID()!);46 }47 });48 await client.attach(doc);49 doc.update((root) => {50 if (!root.content) {51 root.content = new yorkie.Text();52 }53 }, 'create content if not exists');5455 // 02-2. subscribe document event.56 const syncText = () => {57 const text = doc.getRoot().content;58 const selection = doc.getMyPresence().selection;59 const transactionSpec: TransactionSpec = {60 changes: { from: 0, to: view.state.doc.length, insert: text.toString() },61 annotations: [Transaction.remote.of(true)],62 };6364 if (selection) {65 // Restore the cursor position when the text is replaced.66 const cursor = text.posRangeToIndexRange(selection);67 transactionSpec['selection'] = {68 anchor: cursor[0],69 head: cursor[1],70 };71 }72 view.dispatch(transactionSpec);73 };74 doc.subscribe((event) => {75 if (event.type === 'snapshot') {76 // The text is replaced to snapshot and must be re-synced.77 syncText();78 }79 displayLog(documentElem, documentTextElem, doc);80 });8182 doc.subscribe('$.content', (event) => {83 if (event.type === 'remote-change') {84 const { operations } = event.value;85 handleOperations(operations);86 }87 });8889 await client.sync();9091 // 03-1. define function that bind the document with the codemirror(broadcast local changes to peers)92 const updateListener = EditorView.updateListener.of((viewUpdate) => {93 if (viewUpdate.docChanged) {94 for (const tr of viewUpdate.transactions) {95 const events = ['select', 'input', 'delete', 'move', 'undo', 'redo'];96 if (!events.map((event) => tr.isUserEvent(event)).some(Boolean)) {97 continue;98 }99 if (tr.annotation(Transaction.remote)) {100 continue;101 }102 let adj = 0;103 tr.changes.iterChanges((fromA, toA, _, __, inserted) => {104 const insertText = inserted.toJSON().join('\n');105 doc.update((root) => {106 root.content.edit(fromA + adj, toA + adj, insertText);107 }, `update content byA ${client.getID()}`);108 adj += insertText.length - (toA - fromA);109 });110 }111 }112113 const hasFocus =114 viewUpdate.view.hasFocus && viewUpdate.view.dom.ownerDocument.hasFocus();115 const sel = hasFocus ? viewUpdate.state.selection.main : null;116117 doc.update((root, presence) => {118 if (sel && root.content) {119 const selection = root.content.indexRangeToPosRange([120 sel.anchor,121 sel.head,122 ]);123124 if (125 JSON.stringify(selection) !==126 JSON.stringify(presence.get('selection'))127 ) {128 presence.set({129 selection,130 });131 }132 } else if (presence.get('selection')) {133 presence.set({134 selection: undefined,135 });136 }137 });138 });139140 // 03-2. create codemirror instance141 const view = new EditorView({142 doc: '',143 extensions: [144 basicSetup,145 markdown({ base: markdownLanguage }),146 keymap.of(markdownKeymap),147 updateListener,148 ],149 parent: editorParentElem,150 });151152 // 03-3. define event handler that apply remote changes to local153 function handleOperations(operations: Array<OperationInfo>) {154 for (const op of operations) {155 if (op.type === 'edit') {156 handleEditOp(op);157 }158 }159 }160 function handleEditOp(op: EditOpInfo) {161 const changes = [162 {163 from: Math.max(0, op.from),164 to: Math.max(0, op.to),165 insert: op.value!.content,166 },167 ];168169 view.dispatch({170 changes,171 annotations: [Transaction.remote.of(true)],172 });173 }174175 syncText();176 displayLog(documentElem, documentTextElem, doc);177}178179main();
- User 1
- User 2
- User 1
- User 2
Event Log