mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-12-29 19:14:16 +00:00
395 lines
No EOL
14 KiB
JavaScript
395 lines
No EOL
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)
|
|
}
|
|
} |