From f829d90bfb3cfe4b5152b55314b0e197e086949c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9?= Date: Wed, 29 Jun 2022 19:21:15 +0300 Subject: [PATCH] Adding AceEditor-adapter. Changed Gruntfile to build adapters in separation of main code. Changed tests to work with that structure. --- Gruntfile.js | 32 ++- dist/aceeditor-adapter.js | 453 +++++++++++++++++++++++++++++++++ dist/aceeditor-adapter.min.js | 9 + dist/codemirror-adapter.js | 336 ++++++++++++++++++++++++ dist/codemirror-adapter.min.js | 9 + dist/ot-min.js | 10 - dist/ot.js | 339 +----------------------- dist/ot.min.js | 9 + lib/aceeditor-adapter.js | 453 +++++++++++++++++++++++++++++++++ package.json | 12 +- test/phantomjs/test.html | 4 +- 11 files changed, 1305 insertions(+), 361 deletions(-) create mode 100644 dist/aceeditor-adapter.js create mode 100644 dist/aceeditor-adapter.min.js create mode 100644 dist/codemirror-adapter.js create mode 100644 dist/codemirror-adapter.min.js delete mode 100644 dist/ot-min.js create mode 100644 dist/ot.min.js create mode 100644 lib/aceeditor-adapter.js diff --git a/Gruntfile.js b/Gruntfile.js index c49feed..71cfadd 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -24,6 +24,18 @@ module.exports = function (grunt) { qunit: { files: ['test/phantomjs/test.html'] }, + copy: { + adapters: { + files: [ + { + cwd: 'lib', + src: ['codemirror-adapter.js', 'aceeditor-adapter.js'], + dest: 'dist', + expand: true, + } + ] + } + }, concat: { options: { banner: '<%= banner %>' @@ -35,7 +47,6 @@ module.exports = function (grunt) { 'lib/wrapped-operation.js', 'lib/undo-manager.js', 'lib/client.js', - 'lib/codemirror-adapter.js', 'lib/socketio-adapter.js', 'lib/ajax-adapter.js', 'lib/editor-client.js' @@ -47,9 +58,16 @@ module.exports = function (grunt) { options: { banner: '<%= banner %>' }, - dist: { - src: ['<%= concat.dist.dest %>'], - dest: 'dist/ot-min.js' + all: { + files: [ + { + expand: true, + cwd: 'dist/', + src: ['**.js'], + dest: 'dist/', + ext: '.min.js', + } + ] } }, watch: { @@ -71,7 +89,8 @@ module.exports = function (grunt) { eqnull: true, node: true, browser: true, - strict: false + strict: false, + multistr: true, } } }); @@ -80,11 +99,12 @@ module.exports = function (grunt) { grunt.loadNpmTasks('grunt-contrib-qunit'); grunt.loadNpmTasks('grunt-contrib-nodeunit'); grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-contrib-copy'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-connect'); // Default task. - grunt.registerTask('default', ['jshint', 'nodeunit', 'concat', 'uglify', 'qunit']); + grunt.registerTask('default', ['jshint', 'nodeunit', 'copy', 'concat', 'uglify', 'qunit']); }; diff --git a/dist/aceeditor-adapter.js b/dist/aceeditor-adapter.js new file mode 100644 index 0000000..4ea1715 --- /dev/null +++ b/dist/aceeditor-adapter.js @@ -0,0 +1,453 @@ +/*global ot */ +/*global ace */ + +/* + * /\ + * / \ Addon AceEditorAdapter for library "ot 0.0.14" (http://operational-transformation.github.com) + * / \ Written by Sergey Tyapkin (https://github.com/SergTyapkin) + * / \ + * \ / Based on CodeMirrorAdapter from "ot" lib that was written by Tim Baumann (http://timbaumann.info) + * \ / + * \ / (c) 2022 Sergey Tyapkin + * \/ This addon may be freely distributed under the MIT license. + */ + +ot.AceEditorAdapter = (function (global) { + 'use strict'; + + var TextOperation = ot.TextOperation; + var Selection = ot.Selection; + + var otherCursorClassName = 'ot-ace-addon-other-cursor'; + var otherSelectionClassName = 'ot-ace-addon-other-selection'; + var clientNameAttributeName = 'data-client-name'; + + function AceEditorAdapter (ae) { + this.ae = ae; + this.aeElement = ae.renderer.container; + + this.ignoreNextChange = false; + this.changeInProgress = false; + this.selectionChanged = false; + + this.updateAceEditorSizes(); + this.addStyles(); + + this.markersElement = document.createElement('div'); + this.markersElement.id = 'ot-ace-addon-markers'; + this.markersElement.style.position = 'absolute'; + this.markersElement.style.inset = '0'; + this.markersElement.style.left = this.LEFT_MARGIN + 'px'; + this.markersElement.style.pointerEvents = 'none'; + this.aeElement.querySelector('.ace_scroller').appendChild(this.markersElement); + + bind(this, 'onChange'); + bind(this, 'onCursorActivity'); + bind(this, 'onFocus'); + bind(this, 'onBlur'); + bind(this, 'onScrollVertical'); + bind(this, 'onScrollHorizontal'); + + ae.on('change', this.onChange); + ae.on('changeSelection', this.onCursorActivity); + ae.on('focus', this.onFocus); + ae.on('blur', this.onBlur); + ae.session.on('changeScrollTop', this.onScrollVertical); + ae.session.on('changeScrollLeft', this.onScrollHorizontal); + } + + AceEditorAdapter.prototype.updateAceEditorSizes = function () { + this.LINE_HEIGHT = this.aeElement.querySelector('.ace_line').scrollHeight; + this.LEFT_MARGIN = Number(this.aeElement.querySelector('.ace_text-layer').style.getPropertyValue('margin-left').replace(/px$/, '')); + + // To get symbol width in current monospace(!) font we need + // to create new element inside .ace_editor (root element) + // with 100 characters and measure it's width, then divide it by 100 + var el = document.createElement('span'); + el.innerText = 'x'.repeat(100); + el.style.visibility = 'hidden'; + this.aeElement.appendChild(el); // append el to root el + this.SYMBOL_WIDTH = el.offsetWidth / 100; // measure width + el.remove(); // remove el + }; + + // Adding styles of classes for other's selection + AceEditorAdapter.prototype.addStyles = function () { + var addStyleRules = (function () { + var added = {}; + var styleElement = document.createElement('style'); + document.documentElement.getElementsByTagName('head')[0].appendChild(styleElement); + var styleSheet = styleElement.sheet; + + return function (cssRules) { + for (var i = 0; i < cssRules.length; i++) { + var css = cssRules[i]; + if (added[css]) { + return; + } + + added[css] = true; + styleSheet.insertRule(css, (styleSheet.cssRules || styleSheet.rules).length); + } + }; + }()); + + var collapsedHintSize = 8; + var hintFontSize = 14; + addStyleRules([ + // To make hint expanded when cursor moving + '@keyframes ot-ace-addon-keep-hint-open { \ + 0%, 80% { \ + content: attr(' + clientNameAttributeName + '); \ + width: unset; \ + height: ' + hintFontSize + 'px; \ + top: -20px; \ + padding: 4px; \ + opacity: 0.7; \ + } \ + 99% { \ + top: ' + (-collapsedHintSize) + 'px; \ + height: ' + collapsedHintSize + 'px; \ + width: ' + collapsedHintSize + 'px; \ + color: black; \ + opacity: 1; \ + padding: 0; \ + content: ""; \ + } \ + 100% { \ + } \ + }', + // Make cursors + '.' + otherCursorClassName + ' { \ + display: inline-block; \ + padding: 0px; \ + margin-right: -1px; \ + margin-left: -1px; \ + z-index: 0; \ + position: absolute; \ + border-left-width: 2px; \ + border-left-style: solid; \ + }', + // For make hover works in bigger area around cursor + '.' + otherCursorClassName + '::after { \ + content: ""; \ + pointer-events: all; \ + position: absolute; \ + inset: -5px -5px -5px -10px; \ + }', + // Make circle above the cursor + '.' + otherCursorClassName + '::before { \ + content: ""; \ + position: absolute; \ + top: ' + (-collapsedHintSize) + 'px; \ + left: ' + (-collapsedHintSize / 2 - 1) + 'px; \ + height: ' + collapsedHintSize + 'px; \ + width: ' + collapsedHintSize + 'px; \ + background: inherit; \ + font-size: ' + hintFontSize + 'px; \ + transition: all 0.2s ease; \ + line-height: ' + hintFontSize + 'px; \ + border-radius: ' + (hintFontSize / 2) + 'px; \ + color: black; \ + white-space: nowrap; \ + animation: 2s ease ot-ace-addon-keep-hint-open; \ + }', + // Transform circle into username hint + '.' + otherCursorClassName + ':hover::before { \ + content: attr(' + clientNameAttributeName + '); \ + width: unset; \ + height: ' + hintFontSize + 'px; \ + top: -20px; \ + padding: 4px; \ + opacity: 0.85; \ + }', + + // Make other selections + '.' + otherSelectionClassName + ' { \ + display: inline-block; \ + padding: 0px; \ + z-index: 0; \ + position: absolute; \ + inset: 0; \ + opacity: 0.3; \ + }' + ]); + }; + + // Removes all event listeners from the AceEditor instance. + AceEditorAdapter.prototype.detach = function () { + this.markersElement.remove(); + + this.ae.off('change', this.onChange); + this.ae.off('changeSelection', this.onCursorActivity); + this.ae.off('focus', this.onFocus); + this.ae.off('blur', this.onBlur); + this.ae.session.off('changeScrollTop', this.onScrollVertical); + this.ae.session.off('changeScrollLeft', this.onScrollHorizontal); + }; + + 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; } + + // Converts a AceEditor change (as returned + // by the 'change' event in AceEditor) into a + // TextOperation and its inverse and returns them as a two-element array. + // TextOperation and its inverse and returns them as a two-element array. + AceEditorAdapter.operationFromAceEditorChanges = function (change, doc) { + // Approach: Replay the changes, beginning with the most recent one, and + // construct the operation and its inverse. + + var docEndLength = doc.getValue().length; + var operation = new TextOperation().retain(docEndLength); + var inverse = new TextOperation().retain(docEndLength); + + var positionToIndex = function (pos) { + return doc.session.doc.positionToIndex(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; + } + + var fromIndex = positionToIndex(change.start); + var restLength = docEndLength - fromIndex; + + if (change.action === 'insert') { + restLength -= sumLengths(change.lines); + + operation = new TextOperation() + .retain(fromIndex) + .insert(change.lines.join('\n')) + .retain(restLength) + .compose(operation); + + inverse = inverse.compose(new TextOperation() + .retain(fromIndex) + ['delete'](sumLengths(change.lines)) + .retain(restLength) + ); + } else if (change.action === 'remove') { + operation = new TextOperation() + .retain(fromIndex) + ['delete'](sumLengths(change.lines)) + .retain(restLength) + .compose(operation); + + inverse = inverse.compose(new TextOperation() + .retain(fromIndex) + .insert(change.lines.join('\n')) + .retain(restLength) + ); + } + + return [operation, inverse]; + }; + + // Singular form for backwards compatibility. + AceEditorAdapter.operationFromAceEditorChange = + AceEditorAdapter.operationFromAceEditorChanges; + + // Apply an operation to a AceEditor instance. + AceEditorAdapter.applyOperationToAceEditor = function (operation, ae) { + var ops = operation.ops; + var index = 0; // holds the current index into AceEditor's content + for (var i = 0, l = ops.length; i < l; i++) { + var op = ops[i]; + var to; + if (TextOperation.isRetain(op)) { + index += op; + } else if (TextOperation.isInsert(op)) { + to = ae.session.doc.indexToPosition(index); + ae.session.doc.insert(to, op); + index += op.length; + } else if (TextOperation.isDelete(op)) { + var from = ae.session.doc.indexToPosition(index); + to = ae.session.doc.indexToPosition(index - op); + ae.session.doc.remove(new ace.Range(from.row, from.column, to.row, to.column)); + } + } + }; + + AceEditorAdapter.prototype.registerCallbacks = function (cb) { + this.callbacks = cb; + }; + + AceEditorAdapter.prototype.onChange = function (change) { + this.changeInProgress = true; + if (!this.ignoreNextChange) { + var pair = AceEditorAdapter.operationFromAceEditorChanges(change, this.ae); + this.trigger('change', pair[0], pair[1]); + } + if (this.selectionChanged) { this.trigger('selectionChange'); } + this.changeInProgress = false; + this.ignoreNextChange = false; + }; + + AceEditorAdapter.prototype.onFocus = + AceEditorAdapter.prototype.onCursorActivity = + function () { + if (this.changeInProgress) { + this.selectionChanged = true; + } else { + this.trigger('selectionChange'); + } + }; + + AceEditorAdapter.prototype.onBlur = function () { + if (this.ae.selection.isEmpty()) { this.trigger('blur'); } + }; + + AceEditorAdapter.prototype.onScrollVertical = function (scroll) { + this.markersElement.style.top = -scroll + 'px'; + }; + AceEditorAdapter.prototype.onScrollHorizontal = function (scroll) { + this.markersElement.style.left = this.LEFT_MARGIN - scroll + 'px'; + }; + + + AceEditorAdapter.prototype.getValue = function () { + return this.ae.getValue(); + }; + + AceEditorAdapter.prototype.getSelection = function () { + var ae = this.ae; + + var selectionList = ae.selection.getAllRanges(); + + var ranges = []; + var isAllRangesNotEmpty = true; + for (var i = 0; i < selectionList.length; i++) { + var sel = selectionList[i]; + ranges[i] = new Selection.Range( + ae.session.doc.positionToIndex({row: sel.start.row, column: sel.start.column}), + ae.session.doc.positionToIndex({row: sel.end.row, column: sel.end.column}) + ); + if (ranges[i].isEmpty()) { + isAllRangesNotEmpty = false; + } + } + + // Need to add empty range on cursor position to draw it in other side as cursor + if (isAllRangesNotEmpty) { + var cursorPosIndex = ae.session.doc.positionToIndex(ae.selection.getCursor()); + ranges[ranges.length] = new Selection.Range(cursorPosIndex, cursorPosIndex); + } + + return new Selection(ranges); + }; + + AceEditorAdapter.prototype.setSelection = function (selection) { + this.ae.selection.clearSelection(); + for (var i = 0; i < selection.ranges.length; i++) { + var range = selection.ranges[i]; + var start = this.ae.session.doc.indexToPosition(range.anchor); + var end = this.ae.session.doc.indexToPosition(range.head); + this.ae.selection.addRange(new ace.Range(start.row, start.column, end.row, end.column)); + } + }; + + AceEditorAdapter.prototype.setOtherCursor = function (position, color, clientId, clientName) { + var cursorPos = this.ae.session.doc.indexToPosition(position); + var cursorEl = document.createElement('span'); + cursorEl.classList.add(otherCursorClassName); + cursorEl.style.background = color; + cursorEl.style.borderLeftColor = color; + cursorEl.style.height = this.LINE_HEIGHT + 'px'; + cursorEl.style.top = this.LINE_HEIGHT * cursorPos.row + 'px'; + cursorEl.style.left = this.SYMBOL_WIDTH * cursorPos.column + 'px'; + cursorEl.setAttribute(clientNameAttributeName, clientName); + this.markersElement.appendChild(cursorEl); + return cursorEl; + }; + + AceEditorAdapter.prototype.setOtherSelectionRange = function (range, color, clientId, clientName) { + //var match = /^#([0-9a-fA-F]{6})$/.exec(color); + //if (!match) { throw new Error("only six-digit hex colors are allowed."); } + + var anchorPos = this.ae.session.doc.indexToPosition(range.anchor); + var headPos = this.ae.session.doc.indexToPosition(range.head); + + var selEl = document.createElement('span'); + selEl.classList.add(otherSelectionClassName); + selEl.style.background = color; + selEl.setAttribute(clientNameAttributeName, clientName); + var clipPathStart = 'path("M ' + (this.SYMBOL_WIDTH * anchorPos.column) + ' ' + (this.LINE_HEIGHT * anchorPos.row) + ' ' + + 'v ' + this.LINE_HEIGHT; + var clipPathCenter = ''; + for (var i = anchorPos.row + 1; i < headPos.row + 1; i++) { + clipPathCenter += 'h 10000 v ' + -this.LINE_HEIGHT + ' ' + + 'M 0 ' + (this.LINE_HEIGHT * i) + ' v ' + this.LINE_HEIGHT; + } + var clipPathEnd = 'L ' + (this.SYMBOL_WIDTH * headPos.column) + ' ' + (this.LINE_HEIGHT * headPos.row + this.LINE_HEIGHT) + ' v ' + -this.LINE_HEIGHT + ' Z")'; + selEl.style.clipPath = clipPathStart + clipPathCenter + clipPathEnd; + this.markersElement.appendChild(selEl); + + return selEl; + }; + + AceEditorAdapter.prototype.setOtherSelection = function (selection, color, clientId, clientName) { + 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, clientName); + } else { + selectionObjects[i] = this.setOtherSelectionRange(range, color, clientId, clientName); + } + } + return { + clear: function () { + for (var i = 0; i < selectionObjects.length; i++) { + selectionObjects[i].remove(); // Remove old selection HTML elements + } + } + }; + }; + + AceEditorAdapter.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); } + }; + + AceEditorAdapter.prototype.applyOperation = function (operation) { + this.ignoreNextChange = true; + AceEditorAdapter.applyOperationToAceEditor(operation, this.ae); + }; + + AceEditorAdapter.prototype.registerUndo = function (undoFn) { + this.ae.undo = undoFn; + }; + + AceEditorAdapter.prototype.registerRedo = function (redoFn) { + this.ae.redo = redoFn; + }; + + // 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 AceEditorAdapter; + +}(this)); diff --git a/dist/aceeditor-adapter.min.js b/dist/aceeditor-adapter.min.js new file mode 100644 index 0000000..4c25072 --- /dev/null +++ b/dist/aceeditor-adapter.min.js @@ -0,0 +1,9 @@ +/* + * /\ + * / \ ot 0.0.15 + * / \ http://operational-transformation.github.com + * \ / + * \ / (c) 2012-2022 Tim Baumann (http://timbaumann.info) + * \/ ot may be freely distributed under the MIT license. + */ +ot.AceEditorAdapter=function(){"use strict";var c=ot.TextOperation,a=ot.Selection,r="ot-ace-addon-other-cursor",l="ot-ace-addon-other-selection",h="data-client-name";function t(e){this.ae=e,this.aeElement=e.renderer.container,this.ignoreNextChange=!1,this.changeInProgress=!1,this.selectionChanged=!1,this.updateAceEditorSizes(),this.addStyles(),this.markersElement=document.createElement("div"),this.markersElement.id="ot-ace-addon-markers",this.markersElement.style.position="absolute",this.markersElement.style.inset="0",this.markersElement.style.left=this.LEFT_MARGIN+"px",this.markersElement.style.pointerEvents="none",this.aeElement.querySelector(".ace_scroller").appendChild(this.markersElement),o(this,"onChange"),o(this,"onCursorActivity"),o(this,"onFocus"),o(this,"onBlur"),o(this,"onScrollVertical"),o(this,"onScrollHorizontal"),e.on("change",this.onChange),e.on("changeSelection",this.onCursorActivity),e.on("focus",this.onFocus),e.on("blur",this.onBlur),e.session.on("changeScrollTop",this.onScrollVertical),e.session.on("changeScrollLeft",this.onScrollHorizontal)}function o(e,t){var o=e[t];e[t]=function(){o.apply(e,arguments)}}return t.prototype.updateAceEditorSizes=function(){this.LINE_HEIGHT=this.aeElement.querySelector(".ace_line").scrollHeight,this.LEFT_MARGIN=Number(this.aeElement.querySelector(".ace_text-layer").style.getPropertyValue("margin-left").replace(/px$/,""));var e=document.createElement("span");e.innerText="x".repeat(100),e.style.visibility="hidden",this.aeElement.appendChild(e),this.SYMBOL_WIDTH=e.offsetWidth/100,e.remove()},t.prototype.addStyles=function(){e={},t=document.createElement("style"),document.documentElement.getElementsByTagName("head")[0].appendChild(t),o=t.sheet;for(var e,t,o,n=["@keyframes ot-ace-addon-keep-hint-open { 0%, 80% { content: attr("+h+"); width: unset; height: 14px; top: -20px; padding: 4px; opacity: 0.7; } 99% { top: "+-8+'px; height: 8px; width: 8px; color: black; opacity: 1; padding: 0; content: ""; } 100% { } }',"."+r+" { display: inline-block; padding: 0px; margin-right: -1px; margin-left: -1px; z-index: 0; position: absolute; border-left-width: 2px; border-left-style: solid; }","."+r+'::after { content: ""; pointer-events: all; position: absolute; inset: -5px -5px -5px -10px; }',"."+r+'::before { content: ""; position: absolute; top: '+-8+"px; left: "+-5+"px; height: 8px; width: 8px; background: inherit; font-size: 14px; transition: all 0.2s ease; line-height: 14px; border-radius: 7px; color: black; white-space: nowrap; animation: 2s ease ot-ace-addon-keep-hint-open; }","."+r+":hover::before { content: attr("+h+"); width: unset; height: 14px; top: -20px; padding: 4px; opacity: 0.85; }","."+l+" { display: inline-block; padding: 0px; z-index: 0; position: absolute; inset: 0; opacity: 0.3; }"],i=0;i 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)); + index += op.length; + } else if (TextOperation.isDelete(op)) { + var from = cm.posFromIndex(index); + var to = cm.posFromIndex(index - op); + cm.replaceRange('', from, to); + } + } + }); + }; + + 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; 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 = 'inline-block'; + 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 rule = '.' + selectionClassName + ' { background: ' + color + '; }'; + 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) { + 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)); diff --git a/dist/codemirror-adapter.min.js b/dist/codemirror-adapter.min.js new file mode 100644 index 0000000..120f958 --- /dev/null +++ b/dist/codemirror-adapter.min.js @@ -0,0 +1,9 @@ +/* + * /\ + * / \ ot 0.0.15 + * / \ http://operational-transformation.github.com + * \ / + * \ / (c) 2012-2022 Tim Baumann (http://timbaumann.info) + * \/ ot may be freely distributed under the MIT license. + */ +ot.CodeMirrorAdapter=function(){"use strict";var p=ot.TextOperation,r=ot.Selection;function n(e){this.cm=e,this.ignoreNextChange=!1,this.changeInProgress=!1,this.selectionChanged=!1,h(this,"onChanges"),h(this,"onChange"),h(this,"onCursorActivity"),h(this,"onFocus"),h(this,"onBlur"),e.on("changes",this.onChanges),e.on("change",this.onChange),e.on("cursorActivity",this.onCursorActivity),e.on("focus",this.onFocus),e.on("blur",this.onBlur)}function o(e,t){return e.linet.line?1:e.cht.ch?1:0}function m(e,t){return o(e,t)<=0}n.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)},n.operationFromCodeMirrorChange=n.operationFromCodeMirrorChanges=function(e,t){var n,o=(n=t).indexFromPos({line:n.lastLine(),ch:0})+n.getLine(n.lastLine()).length,r=(new p).retain(o),i=(new p).retain(o),s=function(e){return t.indexFromPos(e)};function h(e){if(0===e.length)return 0;for(var t=0,n=0;n (http://timbaumann.info) - * \/ ot may be freely distributed under the MIT license. - */ - -if("undefined"==typeof ot)var ot={};if(ot.TextOperation=function(){"use strict";function a(){return this&&this.constructor===a?(this.ops=[],this.baseLength=0,void(this.targetLength=0)):new a}function b(b,c){var d=b.ops,e=a.isRetain;switch(d.length){case 1:return d[0];case 2:return e(d[0])?d[1]:e(d[1])?d[0]:null;case 3:if(e(d[0])&&e(d[2]))return d[1]}return null}function c(a){return d(a.ops[0])?a.ops[0]:0}a.prototype.equals=function(a){if(this.baseLength!==a.baseLength)return!1;if(this.targetLength!==a.targetLength)return!1;if(this.ops.length!==a.ops.length)return!1;for(var b=0;b0},e=a.isInsert=function(a){return"string"==typeof a},f=a.isDelete=function(a){return"number"==typeof a&&0>a};return a.prototype.retain=function(a){if("number"!=typeof a)throw new Error("retain expects an integer");return 0===a?this:(this.baseLength+=a,this.targetLength+=a,d(this.ops[this.ops.length-1])?this.ops[this.ops.length-1]+=a:this.ops.push(a),this)},a.prototype.insert=function(a){if("string"!=typeof a)throw new Error("insert expects a string");if(""===a)return this;this.targetLength+=a.length;var b=this.ops;return e(b[b.length-1])?b[b.length-1]+=a:f(b[b.length-1])?e(b[b.length-2])?b[b.length-2]+=a:(b[b.length]=b[b.length-1],b[b.length-2]=a):b.push(a),this},a.prototype["delete"]=function(a){if("string"==typeof a&&(a=a.length),"number"!=typeof a)throw new Error("delete expects an integer or a string");return 0===a?this:(a>0&&(a=-a),this.baseLength-=a,f(this.ops[this.ops.length-1])?this.ops[this.ops.length-1]+=a:this.ops.push(a),this)},a.prototype.isNoop=function(){return 0===this.ops.length||1===this.ops.length&&d(this.ops[0])},a.prototype.toString=function(){var a=Array.prototype.map||function(a){for(var b=this,c=[],d=0,e=b.length;e>d;d++)c[d]=a(b[d]);return c};return a.call(this.ops,function(a){return d(a)?"retain "+a:e(a)?"insert '"+a+"'":"delete "+-a}).join(", ")},a.prototype.toJSON=function(){return this.ops},a.fromJSON=function(b){for(var c=new a,g=0,h=b.length;h>g;g++){var i=b[g];if(d(i))c.retain(i);else if(e(i))c.insert(i);else{if(!f(i))throw new Error("unknown operation: "+JSON.stringify(i));c["delete"](i)}}return c},a.prototype.apply=function(a){var b=this;if(a.length!==b.baseLength)throw new Error("The operation's base length must be equal to the string's length.");for(var c=[],f=0,g=0,h=this.ops,i=0,j=h.length;j>i;i++){var k=h[i];if(d(k)){if(g+k>a.length)throw new Error("Operation can't retain more characters than are left in the string.");c[f++]=a.slice(g,g+k),g+=k}else e(k)?c[f++]=k:g-=k}if(g!==a.length)throw new Error("The operation didn't operate on the whole string.");return c.join("")},a.prototype.invert=function(b){for(var c=0,f=new a,g=this.ops,h=0,i=g.length;i>h;h++){var j=g[h];d(j)?(f.retain(j),c+=j):e(j)?f["delete"](j.length):(f.insert(b.slice(c,c-j)),c-=j)}return f},a.prototype.compose=function(b){var c=this;if(c.targetLength!==b.baseLength)throw new Error("The base length of the second operation has to be the target length of the first operation");for(var g=new a,h=c.ops,i=b.ops,j=0,k=0,l=h[j++],m=i[k++];;){if("undefined"==typeof l&&"undefined"==typeof m)break;if(f(l))g["delete"](l),l=h[j++];else if(e(m))g.insert(m),m=i[k++];else{if("undefined"==typeof l)throw new Error("Cannot compose operations: first operation is too short.");if("undefined"==typeof m)throw new Error("Cannot compose operations: first operation is too long.");if(d(l)&&d(m))l>m?(g.retain(m),l-=m,m=i[k++]):l===m?(g.retain(l),l=h[j++],m=i[k++]):(g.retain(l),m-=l,l=h[j++]);else if(e(l)&&f(m))l.length>-m?(l=l.slice(-m),m=i[k++]):l.length===-m?(l=h[j++],m=i[k++]):(m+=l.length,l=h[j++]);else if(e(l)&&d(m))l.length>m?(g.insert(l.slice(0,m)),l=l.slice(m),m=i[k++]):l.length===m?(g.insert(l),l=h[j++],m=i[k++]):(g.insert(l),m-=l.length,l=h[j++]);else{if(!d(l)||!f(m))throw new Error("This shouldn't happen: op1: "+JSON.stringify(l)+", op2: "+JSON.stringify(m));l>-m?(g["delete"](m),l+=m,m=i[k++]):l===-m?(g["delete"](m),l=h[j++],m=i[k++]):(g["delete"](l),m+=l,l=h[j++])}}}return g},a.prototype.shouldBeComposedWith=function(a){if(this.isNoop()||a.isNoop())return!0;var d=c(this),g=c(a),h=b(this),i=b(a);return h&&i?e(h)&&e(i)?d+h.length===g:f(h)&&f(i)?g-i===d||d===g:!1:!1},a.prototype.shouldBeComposedWithInverted=function(a){if(this.isNoop()||a.isNoop())return!0;var d=c(this),g=c(a),h=b(this),i=b(a);return h&&i?e(h)&&e(i)?d+h.length===g||d===g:f(h)&&f(i)?g-i===d:!1:!1},a.transform=function(b,c){if(b.baseLength!==c.baseLength)throw new Error("Both operations have to have the same base length");for(var g=new a,h=new a,i=b.ops,j=c.ops,k=0,l=0,m=i[k++],n=j[l++];;){if("undefined"==typeof m&&"undefined"==typeof n)break;if(e(m))g.insert(m),h.retain(m.length),m=i[k++];else if(e(n))g.retain(n.length),h.insert(n),n=j[l++];else{if("undefined"==typeof m)throw new Error("Cannot compose operations: first operation is too short.");if("undefined"==typeof n)throw new Error("Cannot compose operations: first operation is too long.");var o;if(d(m)&&d(n))m>n?(o=n,m-=n,n=j[l++]):m===n?(o=n,m=i[k++],n=j[l++]):(o=m,n-=m,m=i[k++]),g.retain(o),h.retain(o);else if(f(m)&&f(n))-m>-n?(m-=n,n=j[l++]):m===n?(m=i[k++],n=j[l++]):(n-=m,m=i[k++]);else if(f(m)&&d(n))-m>n?(o=n,m+=n,n=j[l++]):-m===n?(o=n,m=i[k++],n=j[l++]):(o=-m,n+=m,m=i[k++]),g["delete"](o);else{if(!d(m)||!f(n))throw new Error("The two operations aren't compatible");m>-n?(o=-n,m+=n,n=j[l++]):m===-n?(o=m,m=i[k++],n=j[l++]):(o=m,n+=m,m=i[k++]),h["delete"](o)}}}return[g,h]},a}(),"object"==typeof module&&(module.exports=ot.TextOperation),"undefined"==typeof ot)var ot={};if(ot.Selection=function(a){"use strict";function b(a,b){this.anchor=a,this.head=b}function c(a){this.ranges=a||[]}var d=a.ot?a.ot.TextOperation:require("./text-operation");return b.fromJSON=function(a){return new b(a.anchor,a.head)},b.prototype.equals=function(a){return this.anchor===a.anchor&&this.head===a.head},b.prototype.isEmpty=function(){return this.anchor===this.head},b.prototype.transform=function(a){function c(b){for(var c=b,e=a.ops,f=0,g=a.ops.length;g>f&&(d.isRetain(e[f])?b-=e[f]:d.isInsert(e[f])?c+=e[f].length:(c-=Math.min(b,-e[f]),b+=e[f]),!(0>b));f++);return c}var e=c(this.anchor);return this.anchor===this.head?new b(e,e):new b(e,c(this.head))},c.Range=b,c.createCursor=function(a){return new c([new b(a,a)])},c.fromJSON=function(a){for(var d=a.ranges||a,e=0,f=[];e=0;e--){var f=d.transform(a[e],b);"function"==typeof f[0].isNoop&&f[0].isNoop()||c.push(f[0]),b=f[1]}return c.reverse()}var c="normal",d="undoing",e="redoing";return a.prototype.add=function(a,b){if(this.state===d)this.redoStack.push(a),this.dontCompose=!0;else if(this.state===e)this.undoStack.push(a),this.dontCompose=!0;else{var c=this.undoStack;!this.dontCompose&&b&&c.length>0?c.push(a.compose(c.pop())):(c.push(a),c.length>this.maxItems&&c.shift()),this.dontCompose=!1,this.redoStack=[]}},a.prototype.transform=function(a){this.undoStack=b(this.undoStack,a),this.redoStack=b(this.redoStack,a)},a.prototype.performUndo=function(a){if(this.state=d,0===this.undoStack.length)throw new Error("undo not possible");a(this.undoStack.pop()),this.state=c},a.prototype.performRedo=function(a){if(this.state=e,0===this.redoStack.length)throw new Error("redo not possible");a(this.redoStack.pop()),this.state=c},a.prototype.canUndo=function(){return 0!==this.undoStack.length},a.prototype.canRedo=function(){return 0!==this.redoStack.length},a.prototype.isUndoing=function(){return this.state===d},a.prototype.isRedoing=function(){return this.state===e},a}(),"object"==typeof module&&(module.exports=ot.UndoManager),"undefined"==typeof ot)var ot={};ot.Client=function(a){"use strict";function b(a){this.revision=a,this.state=f}function c(){}function d(a){this.outstanding=a}function e(a,b){this.outstanding=a,this.buffer=b}b.prototype.setState=function(a){this.state=a},b.prototype.applyClient=function(a){this.setState(this.state.applyClient(this,a))},b.prototype.applyServer=function(a){this.revision++,this.setState(this.state.applyServer(this,a))},b.prototype.serverAck=function(){this.revision++,this.setState(this.state.serverAck(this))},b.prototype.serverReconnect=function(){"function"==typeof this.state.resend&&this.state.resend(this)},b.prototype.transformSelection=function(a){return this.state.transformSelection(a)},b.prototype.sendOperation=function(a,b){throw new Error("sendOperation must be defined in child class")},b.prototype.applyOperation=function(a){throw new Error("applyOperation must be defined in child class")},b.Synchronized=c,c.prototype.applyClient=function(a,b){return a.sendOperation(a.revision,b),new d(b)},c.prototype.applyServer=function(a,b){return a.applyOperation(b),this},c.prototype.serverAck=function(a){throw new Error("There is no pending operation.")},c.prototype.transformSelection=function(a){return a};var f=new c;return b.AwaitingConfirm=d,d.prototype.applyClient=function(a,b){return new e(this.outstanding,b)},d.prototype.applyServer=function(a,b){var c=b.constructor.transform(this.outstanding,b);return a.applyOperation(c[1]),new d(c[0])},d.prototype.serverAck=function(a){return f},d.prototype.transformSelection=function(a){return a.transform(this.outstanding)},d.prototype.resend=function(a){a.sendOperation(a.revision,this.outstanding)},b.AwaitingWithBuffer=e,e.prototype.applyClient=function(a,b){var c=this.buffer.compose(b);return new e(this.outstanding,c)},e.prototype.applyServer=function(a,b){var c=b.constructor.transform,d=c(this.outstanding,b),f=c(this.buffer,d[1]);return a.applyOperation(f[1]),new e(d[0],f[0])},e.prototype.serverAck=function(a){return a.sendOperation(a.revision,this.buffer),new d(this.buffer)},e.prototype.transformSelection=function(a){return a.transform(this.outstanding).transform(this.buffer)},e.prototype.resend=function(a){a.sendOperation(a.revision,this.outstanding)},b}(this),"object"==typeof module&&(module.exports=ot.Client),ot.CodeMirrorAdapter=function(a){"use strict";function b(a){this.cm=a,this.ignoreNextChange=!1,this.changeInProgress=!1,this.selectionChanged=!1,h(this,"onChanges"),h(this,"onChange"),h(this,"onCursorActivity"),h(this,"onFocus"),h(this,"onBlur"),a.on("changes",this.onChanges),a.on("change",this.onChange),a.on("cursorActivity",this.onCursorActivity),a.on("focus",this.onFocus),a.on("blur",this.onBlur)}function c(a,b){return a.lineb.line?1:a.chb.ch?1:0}function d(a,b){return c(a,b)<=0}function e(a,b){return d(a,b)?a:b}function f(a,b){return d(a,b)?b:a}function g(a){return a.indexFromPos({line:a.lastLine(),ch:0})+a.getLine(a.lastLine()).length}function h(a,b){var c=a[b];a[b]=function(){c.apply(a,arguments)}}var i=ot.TextOperation,j=ot.Selection;b.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)},b.operationFromCodeMirrorChanges=function(a,b){function c(a){return a[a.length-1]}function e(a){if(0===a.length)return 0;for(var b=0,c=0;c=0;m--){var n=a[m];l=f(l,n);var o=l(n.from),p=h-o-e(n.text);j=(new i).retain(o)["delete"](e(n.removed)).insert(n.text.join("\n")).retain(p).compose(j),k=k.compose((new i).retain(o)["delete"](e(n.text)).insert(n.removed.join("\n")).retain(p)),h+=e(n.removed)-e(n.text)}return[j,k]},b.operationFromCodeMirrorChange=b.operationFromCodeMirrorChanges,b.applyOperationToCodeMirror=function(a,b){b.operation(function(){for(var c=a.ops,d=0,e=0,f=c.length;f>e;e++){var g=c[e];if(i.isRetain(g))d+=g;else if(i.isInsert(g))b.replaceRange(g,b.posFromIndex(d)),d+=g.length;else if(i.isDelete(g)){var h=b.posFromIndex(d),j=b.posFromIndex(d-g);b.replaceRange("",h,j)}}})},b.prototype.registerCallbacks=function(a){this.callbacks=a},b.prototype.onChange=function(){this.changeInProgress=!0},b.prototype.onChanges=function(a,c){if(!this.ignoreNextChange){var d=b.operationFromCodeMirrorChanges(c,this.cm);this.trigger("change",d[0],d[1])}this.selectionChanged&&this.trigger("selectionChange"),this.changeInProgress=!1,this.ignoreNextChange=!1},b.prototype.onCursorActivity=b.prototype.onFocus=function(){this.changeInProgress?this.selectionChanged=!0:this.trigger("selectionChange")},b.prototype.onBlur=function(){this.cm.somethingSelected()||this.trigger("blur")},b.prototype.getValue=function(){return this.cm.getValue()},b.prototype.getSelection=function(){for(var a=this.cm,b=a.listSelections(),c=[],d=0;d0&&(this.majorRevision+=c.length,this.minorRevision=0);var d=a.events;if(d){for(b=0;bc?c*(1+b):c+b-b*c,f=2*c-d,g=function(a){return 0>a&&(a+=1),a>1&&(a-=1),1>6*a?f+6*(d-f)*a:1>2*a?d:2>3*a?f+6*(d-f)*(2/3-a):f};return e(g(a+1/3),g(a),g(a-1/3))}function g(a){for(var b=1,c=0;c0&&c.shouldBeComposedWithInverted(i(this.undoManager.undoStack).wrapped)),g=new a(this.selection,d);this.undoManager.add(new o(c,g),f),this.applyClient(b)},d.prototype.updateSelection=function(){this.selection=this.editorAdapter.getSelection()},d.prototype.onSelectionChange=function(){var a=this.selection;this.updateSelection(),a&&this.selection.equals(a)||this.sendSelection(this.selection)},d.prototype.onBlur=function(){this.selection=null,this.sendSelection(null)},d.prototype.sendSelection=function(a){this.state instanceof k.AwaitingWithBuffer||this.serverAdapter.sendSelection(a)},d.prototype.sendOperation=function(a,b){this.serverAdapter.sendOperation(a,b.toJSON(),this.selection)},d.prototype.applyOperation=function(a){this.editorAdapter.applyOperation(a),this.updateSelection(),this.undoManager.transform(new o(a,null))},d}(); \ No newline at end of file diff --git a/dist/ot.js b/dist/ot.js index a00fe30..1a16817 100644 --- a/dist/ot.js +++ b/dist/ot.js @@ -3,7 +3,7 @@ * / \ ot 0.0.15 * / \ http://operational-transformation.github.com * \ / - * \ / (c) 2012-2016 Tim Baumann (http://timbaumann.info) + * \ / (c) 2012-2022 Tim Baumann (http://timbaumann.info) * \/ ot may be freely distributed under the MIT license. */ @@ -1052,343 +1052,6 @@ if (typeof module === 'object') { /*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)); - index += op.length; - } else if (TextOperation.isDelete(op)) { - var from = cm.posFromIndex(index); - var to = cm.posFromIndex(index - op); - cm.replaceRange('', from, to); - } - } - }); - }; - - 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; 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 = 'inline-block'; - 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 rule = '.' + selectionClassName + ' { background: ' + color + '; }'; - 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) { - 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)); - -/*global ot */ - ot.SocketIOAdapter = (function () { 'use strict'; diff --git a/dist/ot.min.js b/dist/ot.min.js new file mode 100644 index 0000000..97b79ea --- /dev/null +++ b/dist/ot.min.js @@ -0,0 +1,9 @@ +/* + * /\ + * / \ ot 0.0.15 + * / \ http://operational-transformation.github.com + * \ / + * \ / (c) 2012-2022 Tim Baumann (http://timbaumann.info) + * \/ ot may be freely distributed under the MIT license. + */ +var ot;(ot=void 0===ot?{}:ot).TextOperation=function(){"use strict";function l(){if(!this||this.constructor!==l)return new l;this.ops=[],this.baseLength=0,this.targetLength=0}l.prototype.equals=function(t){if(this.baseLength!==t.baseLength)return!1;if(this.targetLength!==t.targetLength)return!1;if(this.ops.length!==t.ops.length)return!1;for(var e=0;et.length)throw new Error("Operation can't retain more characters than are left in the string.");e[n++]=t.slice(o,o+a),o+=a}else f(a)?e[n++]=a:o-=a}if(o!==t.length)throw new Error("The operation didn't operate on the whole string.");return e.join("")},l.prototype.invert=function(t){for(var e=0,n=new l,o=this.ops,i=0,r=o.length;i-a?(s=s.slice(-a),a=o[r++]):s.length===-a?(s=n[i++],a=o[r++]):(a+=s.length,s=n[i++]);else if(f(s)&&u(a))s.length>a?(e.insert(s.slice(0,a)),s=s.slice(a),a=o[r++]):s.length===a?(e.insert(s),s=n[i++],a=o[r++]):(e.insert(s),a-=s.length,s=n[i++]);else{if(!u(s)||!d(a))throw new Error("This shouldn't happen: op1: "+JSON.stringify(s)+", op2: "+JSON.stringify(a));-athis.maxItems&&n.shift()),this.dontCompose=!1,this.redoStack=[])},t.prototype.transform=function(t){this.undoStack=n(this.undoStack,t),this.redoStack=n(this.redoStack,t)},t.prototype.performUndo=function(t){if(this.state=o,0===this.undoStack.length)throw new Error("undo not possible");t(this.undoStack.pop()),this.state=e},t.prototype.performRedo=function(t){if(this.state=i,0===this.redoStack.length)throw new Error("redo not possible");t(this.redoStack.pop()),this.state=e},t.prototype.canUndo=function(){return 0!==this.undoStack.length},t.prototype.canRedo=function(){return 0!==this.redoStack.length},t.prototype.isUndoing=function(){return this.state===o},t.prototype.isRedoing=function(){return this.state===i},t}(),"object"==typeof module&&(module.exports=ot.UndoManager),(ot=void 0===ot?{}:ot).Client=function(){"use strict";function t(t){this.revision=t,this.state=n}function e(){}t.prototype.setState=function(t){this.state=t},t.prototype.applyClient=function(t){this.setState(this.state.applyClient(this,t))},t.prototype.applyServer=function(t){this.revision++,this.setState(this.state.applyServer(this,t))},t.prototype.serverAck=function(){this.revision++,this.setState(this.state.serverAck(this))},t.prototype.serverReconnect=function(){"function"==typeof this.state.resend&&this.state.resend(this)},t.prototype.transformSelection=function(t){return this.state.transformSelection(t)},t.prototype.sendOperation=function(t,e){throw new Error("sendOperation must be defined in child class")},t.prototype.applyOperation=function(t){throw new Error("applyOperation must be defined in child class")},(t.Synchronized=e).prototype.applyClient=function(t,e){return t.sendOperation(t.revision,e),new o(e)},e.prototype.applyServer=function(t,e){return t.applyOperation(e),this},e.prototype.serverAck=function(t){throw new Error("There is no pending operation.")},e.prototype.transformSelection=function(t){return t};var n=new e;function o(t){this.outstanding=t}function i(t,e){this.outstanding=t,this.buffer=e}return(t.AwaitingConfirm=o).prototype.applyClient=function(t,e){return new i(this.outstanding,e)},o.prototype.applyServer=function(t,e){e=e.constructor.transform(this.outstanding,e);return t.applyOperation(e[1]),new o(e[0])},o.prototype.serverAck=function(t){return n},o.prototype.transformSelection=function(t){return t.transform(this.outstanding)},o.prototype.resend=function(t){t.sendOperation(t.revision,this.outstanding)},(t.AwaitingWithBuffer=i).prototype.applyClient=function(t,e){e=this.buffer.compose(e);return new i(this.outstanding,e)},i.prototype.applyServer=function(t,e){var n=e.constructor.transform,e=n(this.outstanding,e),n=n(this.buffer,e[1]);return t.applyOperation(n[1]),new i(e[0],n[0])},i.prototype.serverAck=function(t){return t.sendOperation(t.revision,this.buffer),new o(this.buffer)},i.prototype.transformSelection=function(t){return t.transform(this.outstanding).transform(this.buffer)},i.prototype.resend=function(t){t.sendOperation(t.revision,this.outstanding)},t}(),"object"==typeof module&&(module.exports=ot.Client),ot.SocketIOAdapter=function(){"use strict";function t(t){this.socket=t;var o=this;t.on("client_left",function(t){o.trigger("client_left",t)}).on("set_name",function(t,e){o.trigger("set_name",t,e)}).on("ack",function(){o.trigger("ack")}).on("operation",function(t,e,n){o.trigger("operation",e),o.trigger("selection",t,n)}).on("selection",function(t,e){o.trigger("selection",t,e)}).on("reconnect",function(){o.trigger("reconnect")})}return t.prototype.sendOperation=function(t,e,n){this.socket.emit("operation",t,e,n)},t.prototype.sendSelection=function(t){this.socket.emit("selection",t)},t.prototype.registerCallbacks=function(t){this.callbacks=t},t.prototype.trigger=function(t){var e=Array.prototype.slice.call(arguments,1),t=this.callbacks&&this.callbacks[t];t&&t.apply(this,e)},t}(),ot.AjaxAdapter=function(){"use strict";function t(t,e,n){"/"!==t[t.length-1]&&(t+="/"),this.path=t,this.ownUserName=e,this.majorRevision=n.major||0,this.minorRevision=n.minor||0,this.poll()}return t.prototype.renderRevisionPath=function(){return"revision/"+this.majorRevision+"-"+this.minorRevision},t.prototype.handleResponse=function(t){for(var e=t.operations,n=0;n 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; } + + // Converts a AceEditor change (as returned + // by the 'change' event in AceEditor) into a + // TextOperation and its inverse and returns them as a two-element array. + // TextOperation and its inverse and returns them as a two-element array. + AceEditorAdapter.operationFromAceEditorChanges = function (change, doc) { + // Approach: Replay the changes, beginning with the most recent one, and + // construct the operation and its inverse. + + var docEndLength = doc.getValue().length; + var operation = new TextOperation().retain(docEndLength); + var inverse = new TextOperation().retain(docEndLength); + + var positionToIndex = function (pos) { + return doc.session.doc.positionToIndex(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; + } + + var fromIndex = positionToIndex(change.start); + var restLength = docEndLength - fromIndex; + + if (change.action === 'insert') { + restLength -= sumLengths(change.lines); + + operation = new TextOperation() + .retain(fromIndex) + .insert(change.lines.join('\n')) + .retain(restLength) + .compose(operation); + + inverse = inverse.compose(new TextOperation() + .retain(fromIndex) + ['delete'](sumLengths(change.lines)) + .retain(restLength) + ); + } else if (change.action === 'remove') { + operation = new TextOperation() + .retain(fromIndex) + ['delete'](sumLengths(change.lines)) + .retain(restLength) + .compose(operation); + + inverse = inverse.compose(new TextOperation() + .retain(fromIndex) + .insert(change.lines.join('\n')) + .retain(restLength) + ); + } + + return [operation, inverse]; + }; + + // Singular form for backwards compatibility. + AceEditorAdapter.operationFromAceEditorChange = + AceEditorAdapter.operationFromAceEditorChanges; + + // Apply an operation to a AceEditor instance. + AceEditorAdapter.applyOperationToAceEditor = function (operation, ae) { + var ops = operation.ops; + var index = 0; // holds the current index into AceEditor's content + for (var i = 0, l = ops.length; i < l; i++) { + var op = ops[i]; + var to; + if (TextOperation.isRetain(op)) { + index += op; + } else if (TextOperation.isInsert(op)) { + to = ae.session.doc.indexToPosition(index); + ae.session.doc.insert(to, op); + index += op.length; + } else if (TextOperation.isDelete(op)) { + var from = ae.session.doc.indexToPosition(index); + to = ae.session.doc.indexToPosition(index - op); + ae.session.doc.remove(new ace.Range(from.row, from.column, to.row, to.column)); + } + } + }; + + AceEditorAdapter.prototype.registerCallbacks = function (cb) { + this.callbacks = cb; + }; + + AceEditorAdapter.prototype.onChange = function (change) { + this.changeInProgress = true; + if (!this.ignoreNextChange) { + var pair = AceEditorAdapter.operationFromAceEditorChanges(change, this.ae); + this.trigger('change', pair[0], pair[1]); + } + if (this.selectionChanged) { this.trigger('selectionChange'); } + this.changeInProgress = false; + this.ignoreNextChange = false; + }; + + AceEditorAdapter.prototype.onFocus = + AceEditorAdapter.prototype.onCursorActivity = + function () { + if (this.changeInProgress) { + this.selectionChanged = true; + } else { + this.trigger('selectionChange'); + } + }; + + AceEditorAdapter.prototype.onBlur = function () { + if (this.ae.selection.isEmpty()) { this.trigger('blur'); } + }; + + AceEditorAdapter.prototype.onScrollVertical = function (scroll) { + this.markersElement.style.top = -scroll + 'px'; + }; + AceEditorAdapter.prototype.onScrollHorizontal = function (scroll) { + this.markersElement.style.left = this.LEFT_MARGIN - scroll + 'px'; + }; + + + AceEditorAdapter.prototype.getValue = function () { + return this.ae.getValue(); + }; + + AceEditorAdapter.prototype.getSelection = function () { + var ae = this.ae; + + var selectionList = ae.selection.getAllRanges(); + + var ranges = []; + var isAllRangesNotEmpty = true; + for (var i = 0; i < selectionList.length; i++) { + var sel = selectionList[i]; + ranges[i] = new Selection.Range( + ae.session.doc.positionToIndex({row: sel.start.row, column: sel.start.column}), + ae.session.doc.positionToIndex({row: sel.end.row, column: sel.end.column}) + ); + if (ranges[i].isEmpty()) { + isAllRangesNotEmpty = false; + } + } + + // Need to add empty range on cursor position to draw it in other side as cursor + if (isAllRangesNotEmpty) { + var cursorPosIndex = ae.session.doc.positionToIndex(ae.selection.getCursor()); + ranges[ranges.length] = new Selection.Range(cursorPosIndex, cursorPosIndex); + } + + return new Selection(ranges); + }; + + AceEditorAdapter.prototype.setSelection = function (selection) { + this.ae.selection.clearSelection(); + for (var i = 0; i < selection.ranges.length; i++) { + var range = selection.ranges[i]; + var start = this.ae.session.doc.indexToPosition(range.anchor); + var end = this.ae.session.doc.indexToPosition(range.head); + this.ae.selection.addRange(new ace.Range(start.row, start.column, end.row, end.column)); + } + }; + + AceEditorAdapter.prototype.setOtherCursor = function (position, color, clientId, clientName) { + var cursorPos = this.ae.session.doc.indexToPosition(position); + var cursorEl = document.createElement('span'); + cursorEl.classList.add(otherCursorClassName); + cursorEl.style.background = color; + cursorEl.style.borderLeftColor = color; + cursorEl.style.height = this.LINE_HEIGHT + 'px'; + cursorEl.style.top = this.LINE_HEIGHT * cursorPos.row + 'px'; + cursorEl.style.left = this.SYMBOL_WIDTH * cursorPos.column + 'px'; + cursorEl.setAttribute(clientNameAttributeName, clientName); + this.markersElement.appendChild(cursorEl); + return cursorEl; + }; + + AceEditorAdapter.prototype.setOtherSelectionRange = function (range, color, clientId, clientName) { + //var match = /^#([0-9a-fA-F]{6})$/.exec(color); + //if (!match) { throw new Error("only six-digit hex colors are allowed."); } + + var anchorPos = this.ae.session.doc.indexToPosition(range.anchor); + var headPos = this.ae.session.doc.indexToPosition(range.head); + + var selEl = document.createElement('span'); + selEl.classList.add(otherSelectionClassName); + selEl.style.background = color; + selEl.setAttribute(clientNameAttributeName, clientName); + var clipPathStart = 'path("M ' + (this.SYMBOL_WIDTH * anchorPos.column) + ' ' + (this.LINE_HEIGHT * anchorPos.row) + ' ' + + 'v ' + this.LINE_HEIGHT; + var clipPathCenter = ''; + for (var i = anchorPos.row + 1; i < headPos.row + 1; i++) { + clipPathCenter += 'h 10000 v ' + -this.LINE_HEIGHT + ' ' + + 'M 0 ' + (this.LINE_HEIGHT * i) + ' v ' + this.LINE_HEIGHT; + } + var clipPathEnd = 'L ' + (this.SYMBOL_WIDTH * headPos.column) + ' ' + (this.LINE_HEIGHT * headPos.row + this.LINE_HEIGHT) + ' v ' + -this.LINE_HEIGHT + ' Z")'; + selEl.style.clipPath = clipPathStart + clipPathCenter + clipPathEnd; + this.markersElement.appendChild(selEl); + + return selEl; + }; + + AceEditorAdapter.prototype.setOtherSelection = function (selection, color, clientId, clientName) { + 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, clientName); + } else { + selectionObjects[i] = this.setOtherSelectionRange(range, color, clientId, clientName); + } + } + return { + clear: function () { + for (var i = 0; i < selectionObjects.length; i++) { + selectionObjects[i].remove(); // Remove old selection HTML elements + } + } + }; + }; + + AceEditorAdapter.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); } + }; + + AceEditorAdapter.prototype.applyOperation = function (operation) { + this.ignoreNextChange = true; + AceEditorAdapter.applyOperationToAceEditor(operation, this.ae); + }; + + AceEditorAdapter.prototype.registerUndo = function (undoFn) { + this.ae.undo = undoFn; + }; + + AceEditorAdapter.prototype.registerRedo = function (redoFn) { + this.ae.redo = redoFn; + }; + + // 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 AceEditorAdapter; + +}(this)); diff --git a/package.json b/package.json index b46c7c4..1bac5b2 100644 --- a/package.json +++ b/package.json @@ -15,17 +15,17 @@ "url": "https://github.com/operational-transformation/ot.js/issues" }, "main": "./lib/index.js", - "dependencies": {}, "devDependencies": { - "socket.io": "1.2.1", "grunt": "*", + "grunt-contrib-concat": "*", + "grunt-contrib-connect": "*", + "grunt-contrib-copy": "^1.0.0", "grunt-contrib-jshint": "*", - "grunt-contrib-qunit": "*", "grunt-contrib-nodeunit": "*", - "grunt-contrib-watch": "*", + "grunt-contrib-qunit": "*", "grunt-contrib-uglify": "*", - "grunt-contrib-concat": "*", - "grunt-contrib-connect": "*" + "grunt-contrib-watch": "*", + "socket.io": "1.2.1" }, "scripts": { "test": "grunt jshint nodeunit qunit" diff --git a/test/phantomjs/test.html b/test/phantomjs/test.html index 5c1f64b..c3f424c 100644 --- a/test/phantomjs/test.html +++ b/test/phantomjs/test.html @@ -13,10 +13,12 @@ + + - \ No newline at end of file +