From 556338a9c6964d110c1351a402b425c71c2571fa Mon Sep 17 00:00:00 2001 From: Wu Cheng-Han Date: Sat, 11 Jul 2015 12:43:08 +0800 Subject: Added support of operational transformation --- public/vendor/ot/editor-client.js | 354 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 354 insertions(+) create mode 100755 public/vendor/ot/editor-client.js (limited to 'public/vendor/ot/editor-client.js') diff --git a/public/vendor/ot/editor-client.js b/public/vendor/ot/editor-client.js new file mode 100755 index 00000000..b01afb46 --- /dev/null +++ b/public/vendor/ot/editor-client.js @@ -0,0 +1,354 @@ +/*global ot */ + +ot.EditorClient = (function () { + 'use strict'; + + var Client = ot.Client; + var Selection = ot.Selection; + var UndoManager = ot.UndoManager; + var TextOperation = ot.TextOperation; + var WrappedOperation = ot.WrappedOperation; + + + function SelfMeta (selectionBefore, selectionAfter) { + this.selectionBefore = selectionBefore; + this.selectionAfter = selectionAfter; + } + + SelfMeta.prototype.invert = function () { + return new SelfMeta(this.selectionAfter, this.selectionBefore); + }; + + SelfMeta.prototype.compose = function (other) { + return new SelfMeta(this.selectionBefore, other.selectionAfter); + }; + + SelfMeta.prototype.transform = function (operation) { + return new SelfMeta( + this.selectionBefore.transform(operation), + this.selectionAfter.transform(operation) + ); + }; + + + function OtherMeta (clientId, selection) { + this.clientId = clientId; + this.selection = selection; + } + + OtherMeta.fromJSON = function (obj) { + return new OtherMeta( + obj.clientId, + obj.selection && Selection.fromJSON(obj.selection) + ); + }; + + OtherMeta.prototype.transform = function (operation) { + return new OtherMeta( + this.clientId, + this.selection && this.selection.transform(operation) + ); + }; + + + function OtherClient (id, listEl, editorAdapter, name, color, selection) { + this.id = id; + this.listEl = listEl; + this.editorAdapter = editorAdapter; + this.name = name; + this.color = color; + + this.li = document.createElement('li'); + if (name) { + this.li.textContent = name; + this.listEl.appendChild(this.li); + } + + if(!color) + this.setColor(name ? hueFromName(name) : Math.random()); + else + this.setForceColor(color); + if (selection) { this.updateSelection(selection); } + } + + OtherClient.prototype.setColor = function (hue) { + this.hue = hue; + this.color = hsl2hex(hue, 0.75, 0.5); + this.lightColor = hsl2hex(hue, 0.5, 0.9); + if (this.li) { this.li.style.color = this.color; } + }; + + OtherClient.prototype.setForceColor = function (color) { + this.hue = null; + this.color = color; + this.lightColor = color; + if (this.li) { this.li.style.color = this.color; } + }; + + OtherClient.prototype.setName = function (name) { + if (this.name === name) { return; } + this.name = name; + + this.li.textContent = name; + if (!this.li.parentNode) { + this.listEl.appendChild(this.li); + } + + this.setColor(hueFromName(name)); + }; + + OtherClient.prototype.updateSelection = function (selection) { + this.removeSelection(); + this.selection = selection; + this.mark = this.editorAdapter.setOtherSelection( + selection, + selection.position === selection.selectionEnd ? this.color : this.lightColor, + this.id + ); + }; + + OtherClient.prototype.remove = function () { + if (this.li) { removeElement(this.li); } + this.removeSelection(); + }; + + OtherClient.prototype.removeSelection = function () { + if (this.mark) { + this.mark.clear(); + this.mark = null; + } + }; + + + function EditorClient (revision, clients, serverAdapter, editorAdapter) { + Client.call(this, revision); + this.serverAdapter = serverAdapter; + this.editorAdapter = editorAdapter; + this.undoManager = new UndoManager(); + + this.initializeClientList(); + this.initializeClients(clients); + + var self = this; + + this.editorAdapter.registerCallbacks({ + change: function (operation, inverse) { self.onChange(operation, inverse); }, + selectionChange: function () { self.onSelectionChange(); }, + blur: function () { self.onBlur(); } + }); + this.editorAdapter.registerUndo(function () { self.undo(); }); + this.editorAdapter.registerRedo(function () { self.redo(); }); + + this.serverAdapter.registerCallbacks({ + client_left: function (clientId) { self.onClientLeft(clientId); }, + set_name: function (clientId, name) { self.getClientObject(clientId).setName(name); }, + set_color: function (clientId, color) { self.getClientObject(clientId).setForceColor(color); }, + ack: function (revision) { self.serverAck(revision); }, + operation: function (revision, operation) { + self.applyServer(revision, TextOperation.fromJSON(operation)); + }, + operations: function (head, operations) { + self.applyOperations(head, operations); + }, + selection: function (clientId, selection) { + if (selection) { + self.getClientObject(clientId).updateSelection( + self.transformSelection(Selection.fromJSON(selection)) + ); + } else { + self.getClientObject(clientId).removeSelection(); + } + }, + clients: function (clients) { + var clientId; + for (clientId in self.clients) { + if (self.clients.hasOwnProperty(clientId) && !clients.hasOwnProperty(clientId)) { + self.onClientLeft(clientId); + } + } + + for (clientId in clients) { + if (clients.hasOwnProperty(clientId)) { + var clientObject = self.getClientObject(clientId); + + if (clients[clientId].name) { + clientObject.setName(clients[clientId].name); + } + + var selection = clients[clientId].selection; + if (selection) { + self.clients[clientId].updateSelection( + self.transformSelection(Selection.fromJSON(selection)) + ); + } else { + self.clients[clientId].removeSelection(); + } + } + } + }, + reconnect: function () { self.serverReconnect(); } + }); + } + + inherit(EditorClient, Client); + + EditorClient.prototype.addClient = function (clientId, clientObj) { + this.clients[clientId] = new OtherClient( + clientId, + this.clientListEl, + this.editorAdapter, + clientObj.name || clientId, + clientObj.color || null, + clientObj.selection ? Selection.fromJSON(clientObj.selection) : null + ); + }; + + EditorClient.prototype.initializeClients = function (clients) { + this.clients = {}; + for (var clientId in clients) { + if (clients.hasOwnProperty(clientId)) { + this.addClient(clientId, clients[clientId]); + } + } + }; + + EditorClient.prototype.getClientObject = function (clientId) { + var client = this.clients[clientId]; + if (client) { return client; } + return this.clients[clientId] = new OtherClient( + clientId, + this.clientListEl, + this.editorAdapter + ); + }; + + EditorClient.prototype.onClientLeft = function (clientId) { + //console.log("User disconnected: " + clientId); + var client = this.clients[clientId]; + if (!client) { return; } + client.remove(); + delete this.clients[clientId]; + }; + + EditorClient.prototype.initializeClientList = function () { + this.clientListEl = document.createElement('ul'); + }; + + EditorClient.prototype.applyUnredo = function (operation) { + this.undoManager.add(operation.invert(this.editorAdapter.getValue())); + this.editorAdapter.applyOperation(operation.wrapped); + this.selection = operation.meta.selectionAfter; + this.editorAdapter.setSelection(this.selection); + this.applyClient(operation.wrapped); + }; + + EditorClient.prototype.undo = function () { + var self = this; + if (!this.undoManager.canUndo()) { return; } + this.undoManager.performUndo(function (o) { self.applyUnredo(o); }); + }; + + EditorClient.prototype.redo = function () { + var self = this; + if (!this.undoManager.canRedo()) { return; } + this.undoManager.performRedo(function (o) { self.applyUnredo(o); }); + }; + + EditorClient.prototype.onChange = function (textOperation, inverse) { + var selectionBefore = this.selection; + this.updateSelection(); + var meta = new SelfMeta(selectionBefore, this.selection); + var operation = new WrappedOperation(textOperation, meta); + + var compose = this.undoManager.undoStack.length > 0 && + inverse.shouldBeComposedWithInverted(last(this.undoManager.undoStack).wrapped); + var inverseMeta = new SelfMeta(this.selection, selectionBefore); + this.undoManager.add(new WrappedOperation(inverse, inverseMeta), compose); + this.applyClient(textOperation); + }; + + EditorClient.prototype.updateSelection = function () { + this.selection = this.editorAdapter.getSelection(); + }; + + EditorClient.prototype.onSelectionChange = function () { + var oldSelection = this.selection; + this.updateSelection(); + if (oldSelection && this.selection.equals(oldSelection)) { return; } + this.sendSelection(this.selection); + }; + + EditorClient.prototype.onBlur = function () { + this.selection = null; + this.sendSelection(null); + }; + + EditorClient.prototype.sendSelection = function (selection) { + if (this.state instanceof Client.AwaitingWithBuffer) { return; } + this.serverAdapter.sendSelection(selection); + }; + + EditorClient.prototype.sendOperation = function (revision, operation) { + this.serverAdapter.sendOperation(revision, operation.toJSON(), this.selection); + }; + + EditorClient.prototype.getOperations = function (base, head) { + this.serverAdapter.getOperations(base, head); + }; + + EditorClient.prototype.applyOperation = function (operation) { + this.editorAdapter.applyOperation(operation); + this.updateSelection(); + this.undoManager.transform(new WrappedOperation(operation, null)); + }; + + function rgb2hex (r, g, b) { + function digits (n) { + var m = Math.round(255*n).toString(16); + return m.length === 1 ? '0'+m : m; + } + return '#' + digits(r) + digits(g) + digits(b); + } + + function hsl2hex (h, s, l) { + if (s === 0) { return rgb2hex(l, l, l); } + var var2 = l < 0.5 ? l * (1+s) : (l+s) - (s*l); + var var1 = 2 * l - var2; + var hue2rgb = function (hue) { + if (hue < 0) { hue += 1; } + if (hue > 1) { hue -= 1; } + if (6*hue < 1) { return var1 + (var2-var1)*6*hue; } + if (2*hue < 1) { return var2; } + if (3*hue < 2) { return var1 + (var2-var1)*6*(2/3 - hue); } + return var1; + }; + return rgb2hex(hue2rgb(h+1/3), hue2rgb(h), hue2rgb(h-1/3)); + } + + function hueFromName (name) { + var a = 1; + for (var i = 0; i < name.length; i++) { + a = 17 * (a+name.charCodeAt(i)) % 360; + } + return a/360; + } + + // Set Const.prototype.__proto__ to Super.prototype + function inherit (Const, Super) { + function F () {} + F.prototype = Super.prototype; + Const.prototype = new F(); + Const.prototype.constructor = Const; + } + + function last (arr) { return arr[arr.length - 1]; } + + // Remove an element from the DOM. + function removeElement (el) { + if (el.parentNode) { + el.parentNode.removeChild(el); + } + } + + return EditorClient; +}()); -- cgit v1.2.3