393 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			393 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++) {
 | |
|                     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) {
 | |
|         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)
 | |
|     }
 | |
| } |