/*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; }());