codimd/public/vendor/ot/codemirror-adapter.js

395 lines
14 KiB
JavaScript

/*global ot */
ot.CodeMirrorAdapter = (function (global) {
'use strict';
var TextOperation = ot.TextOperation;
var Selection = ot.Selection;
function CodeMirrorAdapter(cm) {
this.cm = cm;
this.ignoreNextChange = false;
this.changeInProgress = false;
this.selectionChanged = false;
bind(this, 'onChanges');
bind(this, 'onChange');
bind(this, 'onCursorActivity');
bind(this, 'onFocus');
bind(this, 'onBlur');
cm.on('changes', this.onChanges);
cm.on('change', this.onChange);
cm.on('cursorActivity', this.onCursorActivity);
cm.on('focus', this.onFocus);
cm.on('blur', this.onBlur);
}
// Removes all event listeners from the CodeMirror instance.
CodeMirrorAdapter.prototype.detach = function () {
this.cm.off('changes', this.onChanges);
this.cm.off('change', this.onChange);
this.cm.off('cursorActivity', this.onCursorActivity);
this.cm.off('focus', this.onFocus);
this.cm.off('blur', this.onBlur);
};
function cmpPos(a, b) {
if (a.line < b.line) {
return -1;
}
if (a.line > b.line) {
return 1;
}
if (a.ch < b.ch) {
return -1;
}
if (a.ch > b.ch) {
return 1;
}
return 0;
}
function posEq(a, b) {
return cmpPos(a, b) === 0;
}
function posLe(a, b) {
return cmpPos(a, b) <= 0;
}
function minPos(a, b) {
return posLe(a, b) ? a : b;
}
function maxPos(a, b) {
return posLe(a, b) ? b : a;
}
function codemirrorDocLength(doc) {
return doc.indexFromPos({
line: doc.lastLine(),
ch: 0
}) +
doc.getLine(doc.lastLine()).length;
}
// Converts a CodeMirror change array (as obtained from the 'changes' event
// in CodeMirror v4) or single change or linked list of changes (as returned
// by the 'change' event in CodeMirror prior to version 4) into a
// TextOperation and its inverse and returns them as a two-element array.
CodeMirrorAdapter.operationFromCodeMirrorChanges = function (changes, doc) {
// Approach: Replay the changes, beginning with the most recent one, and
// construct the operation and its inverse. We have to convert the position
// in the pre-change coordinate system to an index. We have a method to
// convert a position in the coordinate system after all changes to an index,
// namely CodeMirror's `indexFromPos` method. We can use the information of
// a single change object to convert a post-change coordinate system to a
// pre-change coordinate system. We can now proceed inductively to get a
// pre-change coordinate system for all changes in the linked list.
// A disadvantage of this approach is its complexity `O(n^2)` in the length
// of the linked list of changes.
var docEndLength = codemirrorDocLength(doc);
var operation = new TextOperation().retain(docEndLength);
var inverse = new TextOperation().retain(docEndLength);
var indexFromPos = function (pos) {
return doc.indexFromPos(pos);
};
function last(arr) {
return arr[arr.length - 1];
}
function sumLengths(strArr) {
if (strArr.length === 0) {
return 0;
}
var sum = 0;
for (var i = 0; i < strArr.length; i++) {
sum += strArr[i].length;
}
return sum + strArr.length - 1;
}
function updateIndexFromPos(indexFromPos, change) {
return function (pos) {
if (posLe(pos, change.from)) {
return indexFromPos(pos);
}
if (posLe(change.to, pos)) {
return indexFromPos({
line: pos.line + change.text.length - 1 - (change.to.line - change.from.line),
ch: (change.to.line < pos.line) ?
pos.ch : (change.text.length <= 1) ?
pos.ch - (change.to.ch - change.from.ch) + sumLengths(change.text) : pos.ch - change.to.ch + last(change.text).length
}) + sumLengths(change.removed) - sumLengths(change.text);
}
if (change.from.line === pos.line) {
return indexFromPos(change.from) + pos.ch - change.from.ch;
}
return indexFromPos(change.from) +
sumLengths(change.removed.slice(0, pos.line - change.from.line)) +
1 + pos.ch;
};
}
for (var i = changes.length - 1; i >= 0; i--) {
var change = changes[i];
indexFromPos = updateIndexFromPos(indexFromPos, change);
var fromIndex = indexFromPos(change.from);
var restLength = docEndLength - fromIndex - sumLengths(change.text);
operation = new TextOperation()
.retain(fromIndex)['delete'](sumLengths(change.removed))
.insert(change.text.join('\n'))
.retain(restLength)
.compose(operation);
inverse = inverse.compose(new TextOperation()
.retain(fromIndex)['delete'](sumLengths(change.text))
.insert(change.removed.join('\n'))
.retain(restLength)
);
docEndLength += sumLengths(change.removed) - sumLengths(change.text);
}
return [operation, inverse];
};
// Singular form for backwards compatibility.
CodeMirrorAdapter.operationFromCodeMirrorChange =
CodeMirrorAdapter.operationFromCodeMirrorChanges;
// Apply an operation to a CodeMirror instance.
CodeMirrorAdapter.applyOperationToCodeMirror = function (operation, cm) {
cm.operation(function () {
var ops = operation.ops;
var index = 0; // holds the current index into CodeMirror's content
for (var i = 0, l = ops.length; i < l; i++) {
var op = ops[i];
if (TextOperation.isRetain(op)) {
index += op;
} else if (TextOperation.isInsert(op)) {
cm.replaceRange(op, cm.posFromIndex(index), null, 'ignoreHistory');
index += op.length;
} else if (TextOperation.isDelete(op)) {
var from = cm.posFromIndex(index);
var to = cm.posFromIndex(index - op);
cm.replaceRange('', from, to, 'ignoreHistory');
}
}
});
};
CodeMirrorAdapter.prototype.registerCallbacks = function (cb) {
this.callbacks = cb;
};
CodeMirrorAdapter.prototype.onChange = function () {
// By default, CodeMirror's event order is the following:
// 1. 'change', 2. 'cursorActivity', 3. 'changes'.
// We want to fire the 'selectionChange' event after the 'change' event,
// but need the information from the 'changes' event. Therefore, we detect
// when a change is in progress by listening to the change event, setting
// a flag that makes this adapter defer all 'cursorActivity' events.
this.changeInProgress = true;
};
CodeMirrorAdapter.prototype.onChanges = function (_, changes) {
if (!this.ignoreNextChange) {
var pair = CodeMirrorAdapter.operationFromCodeMirrorChanges(changes, this.cm);
this.trigger('change', pair[0], pair[1]);
}
if (this.selectionChanged) {
this.trigger('selectionChange');
}
this.changeInProgress = false;
this.ignoreNextChange = false;
};
CodeMirrorAdapter.prototype.onCursorActivity =
CodeMirrorAdapter.prototype.onFocus = function () {
if (this.changeInProgress) {
this.selectionChanged = true;
} else {
this.trigger('selectionChange');
}
};
CodeMirrorAdapter.prototype.onBlur = function () {
if (!this.cm.somethingSelected()) {
this.trigger('blur');
}
};
CodeMirrorAdapter.prototype.getValue = function () {
return this.cm.getValue();
};
CodeMirrorAdapter.prototype.getSelection = function () {
var cm = this.cm;
var selectionList = cm.listSelections();
var ranges = [];
for (var i = 0; i < selectionList.length; i++) {
ranges[i] = new Selection.Range(
cm.indexFromPos(selectionList[i].anchor),
cm.indexFromPos(selectionList[i].head)
);
}
return new Selection(ranges);
};
CodeMirrorAdapter.prototype.setSelection = function (selection) {
var ranges = [];
for (var i = 0; selection && i < selection.ranges.length; i++) {
var range = selection.ranges[i];
ranges[i] = {
anchor: this.cm.posFromIndex(range.anchor),
head: this.cm.posFromIndex(range.head)
};
}
this.cm.setSelections(ranges);
};
var addStyleRule = (function () {
var added = {};
var styleElement = document.createElement('style');
document.documentElement.getElementsByTagName('head')[0].appendChild(styleElement);
var styleSheet = styleElement.sheet;
return function (css) {
if (added[css]) {
return;
}
added[css] = true;
styleSheet.insertRule(css, (styleSheet.cssRules || styleSheet.rules).length);
};
}());
CodeMirrorAdapter.prototype.setOtherCursor = function (position, color, clientId) {
var cursorPos = this.cm.posFromIndex(position);
var cursorCoords = this.cm.cursorCoords(cursorPos);
var cursorEl = document.createElement('span');
cursorEl.className = 'other-client';
cursorEl.style.display = 'none';
/*
cursorEl.style.padding = '0';
cursorEl.style.marginLeft = cursorEl.style.marginRight = '-1px';
cursorEl.style.borderLeftWidth = '2px';
cursorEl.style.borderLeftStyle = 'solid';
cursorEl.style.borderLeftColor = color;
cursorEl.style.height = (cursorCoords.bottom - cursorCoords.top) * 0.9 + 'px';
cursorEl.style.zIndex = 0;
*/
cursorEl.setAttribute('data-clientid', clientId);
return this.cm.setBookmark(cursorPos, {
widget: cursorEl,
insertLeft: true
});
};
CodeMirrorAdapter.prototype.setOtherSelectionRange = function (range, color, clientId) {
var match = /^#([0-9a-fA-F]{6})$/.exec(color);
if (!match) {
throw new Error("only six-digit hex colors are allowed.");
}
var selectionClassName = 'selection-' + match[1];
var rgbcolor = hex2rgb(color);
var rule = '.' + selectionClassName + ' { background: rgba(' + rgbcolor.red + ',' + rgbcolor.green + ',' + rgbcolor.blue + ',0.2); }';
addStyleRule(rule);
var anchorPos = this.cm.posFromIndex(range.anchor);
var headPos = this.cm.posFromIndex(range.head);
return this.cm.markText(
minPos(anchorPos, headPos),
maxPos(anchorPos, headPos), {
className: selectionClassName
}
);
};
CodeMirrorAdapter.prototype.setOtherSelection = function (selection, color, clientId) {
var selectionObjects = [];
for (var i = 0; i < selection.ranges.length; i++) {
var range = selection.ranges[i];
if (range.isEmpty()) {
//selectionObjects[i] = this.setOtherCursor(range.head, color, clientId);
} else {
selectionObjects[i] = this.setOtherSelectionRange(range, color, clientId);
}
}
return {
clear: function () {
for (var i = 0; i < selectionObjects.length; i++) {
if (selectionObjects[i]) selectionObjects[i].clear();
}
}
};
};
CodeMirrorAdapter.prototype.trigger = function (event) {
var args = Array.prototype.slice.call(arguments, 1);
var action = this.callbacks && this.callbacks[event];
if (action) {
action.apply(this, args);
}
};
CodeMirrorAdapter.prototype.applyOperation = function (operation) {
if (!operation.isNoop()) {
this.ignoreNextChange = true;
}
CodeMirrorAdapter.applyOperationToCodeMirror(operation, this.cm);
};
CodeMirrorAdapter.prototype.registerUndo = function (undoFn) {
this.cm.undo = undoFn;
};
CodeMirrorAdapter.prototype.registerRedo = function (redoFn) {
this.cm.redo = redoFn;
};
// Throws an error if the first argument is falsy. Useful for debugging.
function assert(b, msg) {
if (!b) {
throw new Error(msg || "assertion error");
}
}
// Bind a method to an object, so it doesn't matter whether you call
// object.method() directly or pass object.method as a reference to another
// function.
function bind(obj, method) {
var fn = obj[method];
obj[method] = function () {
fn.apply(obj, arguments);
};
}
return CodeMirrorAdapter;
}(this));
function hex2rgb(hex) {
if (hex[0] == "#") hex = hex.substr(1);
if (hex.length == 3) {
var temp = hex;
hex = '';
temp = /^([a-f0-9])([a-f0-9])([a-f0-9])$/i.exec(temp).slice(1);
for (var i = 0; i < 3; i++) hex += temp[i] + temp[i];
}
var triplets = /^([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.exec(hex).slice(1);
return {
red: parseInt(triplets[0], 16),
green: parseInt(triplets[1], 16),
blue: parseInt(triplets[2], 16)
}
}