355 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			355 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /*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 ? this.selectionBefore.transform(operation) : null),
 | |
|       (this.selectionAfter ? this.selectionAfter.transform(operation) : null)
 | |
|     );
 | |
|   };
 | |
| 
 | |
| 
 | |
|   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;
 | |
| }());
 |