collaborative-editor
Version:
JSON CRDT str node bindings to any generic plain text editor.
225 lines • 9.52 kB
JavaScript
"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