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