UNPKG

collaborative-editor

Version:

JSON CRDT str node bindings to any generic plain text editor.

225 lines 9.52 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.StrBinding = void 0; const util_1 = require("./util"); const Selection_1 = require("./Selection"); const util_2 = require("./util"); const str_1 = require("json-joy/lib/util/diff/str"); class StrBinding { constructor(str, editor) { this.str = str; this.editor = editor; this.race = (0, util_1.invokeFirstOnly)(); this.onModelChange = () => { this.race(() => { this.syncFromModel(); this.saveSelection(); }); }; this.onchange = (changes, verify) => { this.race(() => { // console.time('onchange'); if (changes instanceof Array && changes.length > 0) { const str = this.str(); if (!str) return; let applyChanges = true; if (verify) { let view = this.view; for (let i = 0; i < length; i++) { const change = changes[i]; const [position, remove, insert] = change; view = (0, util_2.applyChange)(view, position, remove, insert); } const editor = this.editor; if ((editor.getLength && view.length !== editor.getLength()) || view !== editor.get()) applyChanges = false; else this.view = view; } if (applyChanges) { const length = changes.length; try { str.api.transaction(() => { let view = this.view; for (let i = 0; i < length; i++) { const change = changes[i]; const [position, remove, insert] = change; view = (0, util_2.applyChange)(view, position, remove, insert); if (remove) str.del(position, remove); if (insert) str.ins(position, insert); } this.view = view; }); this.saveSelection(); // console.timeEnd('onchange'); return; } catch (_a) { } } } this.syncFromEditor(); // console.timeEnd('onchange'); }); }; // ------------------------------------------------------------------ Polling this.pollingInterval = 1000; this._p = null; this.pollChanges = () => { this._p = setTimeout(() => { this.race(() => { try { const view = this.view; const editor = this.editor; const needsSync = (editor.getLength ? view.length !== editor.getLength() : false) || view !== editor.get(); if (needsSync) this.syncFromEditor(); } catch (_a) { } if (this._p) this.pollChanges(); }); }, this.pollingInterval); }; // ------------------------------------------------------------------ Binding this._s = null; this.bind = (polling) => { this.syncFromModel(); const editor = this.editor; editor.onchange = this.onchange; editor.onselection = () => this.saveSelection(); if (polling) this.pollChanges(); this._s = this.str().api.onChange.listen(this.onModelChange); }; this.unbind = () => { var _a, _b, _c; this.stopPolling(); (_a = this._s) === null || _a === void 0 ? void 0 : _a.call(this); (_c = (_b = this.editor).dispose) === null || _c === void 0 ? void 0 : _c.call(_b); }; this.view = str().view(); editor.selection = this.selection = new Selection_1.Selection(); } // ---------------------------------------------------------------- Selection saveSelection() { var _a, _b, _c; const { editor, selection } = this; const str = this.str(); if (!str) return; const [selectionStart, selectionEnd, selectionDirection] = ((_a = editor.getSelection) === null || _a === void 0 ? void 0 : _a.call(editor)) || [-1, -1, 0]; const { start, end } = selection; const now = Date.now(); const tick = str.api.model.tick; // Return early to avoid excessive RGA queries. if (start === selectionStart && end === selectionEnd && (tick === selection.tick || now - selection.ts < 3000)) return; selection.start = selectionStart; selection.end = selectionEnd; selection.dir = selectionDirection; selection.ts = now; selection.tick = tick; selection.startId = typeof selectionStart === 'number' ? ((_b = str.findId((selectionStart !== null && selectionStart !== void 0 ? selectionStart : 0) - 1)) !== null && _b !== void 0 ? _b : null) : null; selection.endId = typeof selectionEnd === 'number' ? ((_c = str.findId((selectionEnd !== null && selectionEnd !== void 0 ? selectionEnd : 0) - 1)) !== null && _c !== void 0 ? _c : null) : null; } // ----------------------------------------------------- Model-to-Editor sync syncFromModel() { const editor = this.editor; const str = this.str(); if (!str) return; const view = (this.view = str.view()) || ''; if (editor.ins && editor.del) { const editorText = editor.get(); if (view === editorText) return; // TODO: PERF: Construct `changes` from JSON CRDT Patches. const changes = (0, str_1.diff)(editorText, view); const changeLen = changes.length; let pos = 0; for (let i = 0; i < changeLen; i++) { const change = changes[i]; const [type, text] = change; const len = text.length; switch (type) { case -1 /* PATCH_OP_TYPE.DEL */: editor.del(pos, len); break; case 0 /* PATCH_OP_TYPE.EQL */: pos += len; break; case 1 /* PATCH_OP_TYPE.INS */: editor.ins(pos, text); pos += len; break; } } } else { editor.set(view); const { selection } = this; if (editor.setSelection) { const start = selection.startId ? str.findPos(selection.startId) + 1 : -1; const end = selection.endId ? str.findPos(selection.endId) + 1 : -1; editor.setSelection(start, end, selection.dir); } } } // ----------------------------------------------------- Editor-to-Model sync syncFromEditor() { var _a; let view = this.view; const value = this.editor.get(); if (value === view) return; const selection = this.selection; const caretPos = selection.start === selection.end ? ((_a = selection.start) !== null && _a !== void 0 ? _a : undefined) : undefined; const changes = (0, str_1.diffEdit)(view, value, caretPos || 0); const changeLen = changes.length; const str = this.str(); if (!str) return; str.api.transaction(() => { let pos = 0; for (let i = 0; i < changeLen; i++) { const change = changes[i]; const [type, text] = change; switch (type) { case -1 /* PATCH_OP_TYPE.DEL */: { view = (0, util_2.applyChange)(view, pos, text.length, ''); str.del(pos, text.length); break; } case 0 /* PATCH_OP_TYPE.EQL */: { pos += text.length; break; } case 1 /* PATCH_OP_TYPE.INS */: { view = (0, util_2.applyChange)(view, pos, 0, text); str.ins(pos, text); pos += text.length; break; } } } }); this.view = view; this.saveSelection(); } stopPolling() { if (this._p) clearTimeout(this._p); this._p = null; } } exports.StrBinding = StrBinding; StrBinding.bind = (str, editor, polling) => { const binding = new StrBinding(str, editor); binding.syncFromModel(); binding.bind(polling); return binding.unbind; }; //# sourceMappingURL=StrBinding.js.map