diff --git a/README.md b/README.md
index 7b552e8..53bbc19 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,47 @@
# Share-CodeMirror [](http://travis-ci.org/share/share-codemirror) [](https://david-dm.org/share/share-codemirror) [](https://david-dm.org/share/share-codemirror#info=devDependencies)
CodeMirror bindings for ShareJS >= 0.7.x.
+## Dependencies
+
+You need [lodash](http://lodash.com/) loaded before this library.
+
## Usage
```javascript
var cm = CodeMirror.fromTextArea(elem);
-shareDoc.attachCodeMirror(cm);
+
+var ctx = shareDoc.createContext();
+shareDoc.attachCodeMirror(cm, ctx);
+shareDoc.attachCodeMirrorCursor(cm, ctx);
```
-That's it. You now have 2-way sync between your ShareJS and CodeMirror.
+That's it. You now have 2-way sync between your ShareJS and CodeMirror.
+
+### Configuration
+
+The `attachCodeMirrorCursor` takes an optional 3rd `options` argument where the
+following options may be set:
+
+* `inactiveTimeout` - how long the "name" part of a cursor is visible after inactivity
+* `color` - the color of the cursor
+* `selectionColor` - the color of text selection
+* `textColor` - the color of the "name" text
+
+These attributes can also be set on a per-user basis with presence properties:
+
+```javascript
+shareDoc.setPresenceProperty("name", name);
+shareDoc.setPresenceProperty("color", color);
+shareDoc.setPresenceProperty("selectionColor", selectionColor);
+shareDoc.setPresenceProperty("textColor", textColor);
+shareDoc.setPresenceProperty("inactiveTimeout", 10000);
+```
+
+Share-Codemirror will provide default values for all of these properties if you
+don't set them explicitly. You may want to use a color library such as
+[TinyColor](http://bgrins.github.io/TinyColor/)
+to manipulate colors, for example setting `selectionColor` a bit brighter than
+`color`, or finding a legible `textColor` based on `color`.
## Install with Bower
@@ -22,7 +55,7 @@ bower install share-codemirror
npm install share-codemirror
```
-On Node.js you can mount the `scriptsDir` (where `share-codemirror.js` lives) as a static resource
+On Node.js you can mount the `scriptsDir` (where `share-codemirror.js` lives) as a static resource
in your web server:
```javascript
@@ -41,6 +74,7 @@ In the HTML:
```
npm install
+npm test
node examples/server.js
# in a couple of browsers...
open http://localhost:7007
@@ -78,4 +112,3 @@ git push --tags
```
There is no `bower publish` - the existance of a git tag is enough.
-
diff --git a/examples/index.html b/examples/index.html
index 940e116..dadb3a7 100644
--- a/examples/index.html
+++ b/examples/index.html
@@ -1,66 +1,89 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
diff --git a/examples/server.js b/examples/server.js
index c37edc5..f9bfdc4 100644
--- a/examples/server.js
+++ b/examples/server.js
@@ -10,6 +10,8 @@ var webserver = connect(
connect["static"](__dirname),
connect["static"](shareCodeMirror.scriptsDir),
connect["static"](__dirname + '/../node_modules/codemirror/lib'),
+ connect["static"](__dirname + '/../node_modules/tinycolor2/dist'),
+ connect["static"](__dirname + '/../node_modules/lodash/dist'),
connect["static"](sharejs.scriptsDir)
);
@@ -19,7 +21,12 @@ var backend = livedb.client(livedbMongo('localhost:27017/test?auto_reconnect', {
var share = sharejs.server.createClient({backend: backend});
-webserver.use(browserChannel({webserver: webserver}, function (client) {
+var clientsById = {};
+
+webserver.use(browserChannel({webserver: webserver, sessionTimeoutInterval: 5000}, function (client) {
+ clientsById[client.id] = client;
+ //client.send({_type: 'connectionId', connectionId: client.id});
+
var stream = new Duplex({objectMode: true});
stream._write = function (chunk, encoding, callback) {
if (client.state !== 'closed') {
@@ -35,12 +42,15 @@ webserver.use(browserChannel({webserver: webserver}, function (client) {
stream.push(data);
});
stream.on('error', function (msg) {
+ console.log('ERROR', msg, client.id);
client.stop();
});
client.on('close', function (reason) {
+ console.log('CLOSE', reason, client.id);
stream.emit('close');
stream.emit('end');
stream.end();
+ delete clientsById[client.id];
});
return share.listen(stream);
}));
diff --git a/package.json b/package.json
index 564d64b..3efc38b 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"description": "CodeMirror bindings for ShareJS",
"main": "share-codemirror.js",
"scripts": {
- "pretest": "cd node_modules/share && npm install",
+ "pretest": "cd node_modules/share && UGLIFY=../../node_modules/.bin/uglifyjs make webclient/share.uncompressed.js",
"test": "node_modules/.bin/mocha"
},
"repository": {
@@ -21,14 +21,17 @@
"url": "https://github.com/share/share-codemirror/issues"
},
"devDependencies": {
- "share": "v0.7.0-alpha9",
- "codemirror": "~4.0.3",
+ "browserchannel": "~1.2.0",
+ "codemirror": "~4.2.0",
"connect": "~2.11.0",
- "browserchannel": "~1.0.8",
- "livedb": "~0.2.6",
- "livedb-mongo": "~0.2.5",
- "mocha": "~1.14.0",
- "jsdom": "~0.8.8",
- "istanbul": "~0.1.44"
+ "istanbul": "~0.2.10",
+ "jsdom": "~0.10.6",
+ "livedb": "git://github.com/share/livedb.git#f03086cdf51ffc80c725f105c7236c2997d74eac",
+ "livedb-mongo": "~0.3.2",
+ "lodash": "~2.4.1",
+ "mocha": "~1.20.0",
+ "share": "git://github.com/share/ShareJS.git#94dd9e0659c9b6953ba888f5dde8ce223a71f2ec",
+ "tinycolor2": "^0.10.0",
+ "uglify-js": "~2.4.13"
}
}
diff --git a/share-codemirror-cursor.js b/share-codemirror-cursor.js
new file mode 100644
index 0000000..8b6a223
--- /dev/null
+++ b/share-codemirror-cursor.js
@@ -0,0 +1,182 @@
+(function () {
+ 'use strict';
+
+ function shareCodeMirrorCursor(cm, ctx, options) {
+ options = options || {};
+ options.selectionColor = options.selectionColor || 'yellow';
+ options.color = options.color || '#dddddd';
+ options.textColor = options.textColor || 'black';
+ options.inactiveTimeout = options.inactiveTimeout || 5000;
+
+ cm.on('cursorActivity', function (editor) {
+ if (ctx.suppress) return;
+
+ var start = editor.indexFromPos(editor.getCursor('start'));
+ var end = editor.indexFromPos(editor.getCursor('end'));
+ ctx.setSelection([start, end]);
+ });
+
+ var cursorsBySessionId = {};
+
+ ctx.onPresence = function (presence) {
+ var sessionIds = Object.keys(presence);
+ makeUserStyles(presence);
+ sessionIds.forEach(function (sessionId) {
+ var session = presence[sessionId];
+ displayCursor(sessionId, session);
+ });
+ var cursorIds = Object.keys(cursorsBySessionId);
+ cursorIds.forEach(function (cid) {
+ if (sessionIds.indexOf(cid) < 0) {
+ cursorsBySessionId[cid].remove();
+ delete cursorsBySessionId[cid];
+ }
+ })
+ };
+
+ function makeUserStyles(sessions) {
+ var ids = Object.keys(sessions);
+ var headStyle = document.getElementById("user-styles");
+ if (!headStyle) {
+ var head = document.getElementsByTagName("head")[0];
+ headStyle = document.createElement("style");
+ headStyle.setAttribute("id", "user-styles");
+ head.appendChild(headStyle);
+ }
+ var style = "";
+ for (var i = 0; i < ids.length; i++) {
+ var session = sessions[ids[i]];
+ var selectionColor = session.selectionColor || session.color || options.selectionColor;
+ style += ".user-" + ids[i] + " { background: " + selectionColor + "; }";
+ }
+ headStyle.innerHTML = style;
+ }
+
+ function displayCursor(sessionId, session) {
+ // we make a cursor widget to display where the other user's cursor is
+ var selection = session._selection;
+ if (!selection) return;
+ if (typeof selection == "number") selection = [selection, selection];
+ var from = cm.posFromIndex(selection[0]);
+ var to = cm.posFromIndex(selection[1]);
+
+ var cursor = cursorsBySessionId[sessionId];
+ if (cursor === undefined) {
+ cursor = new Cursor(cm, sessionId);
+ cursorsBySessionId[sessionId] = cursor;
+ }
+ cursor.update(session, from, to);
+ }
+
+ var ownerFactor = 0.8; // Relative size of owner font
+
+ function Cursor(cm, sessionId) {
+ // TODO: Make similar to this:
+ // https://github.com/quilljs/quill/blob/develop/src/modules/multi-cursor.coffee
+ // http://quilljs.com/docs/modules/multi-cursors/
+
+ // The parent element of the cursor
+ var widget = document.createElement('div');
+ widget.style.position = 'absolute';
+ widget.style.zIndex = 1000;
+
+ // The caret
+ var caret = document.createElement('pre');
+ caret.style.borderLeftWidth = '2px';
+ caret.style.borderLeftStyle = 'solid';
+ caret.style.height = cm.defaultTextHeight() + 'px';
+ caret.style.marginTop = '-' + cm.defaultTextHeight() + 'px';
+ caret.innerHTML = ' ';
+ widget.appendChild(caret);
+
+ // The name
+ var owner = document.createElement('div');
+ owner.style.height = cm.defaultTextHeight() * ownerFactor + 'px';
+ owner.style['font-size'] = cm.defaultTextHeight() * ownerFactor + 'px';
+ widget.appendChild(owner);
+
+ var dot = document.createElement('pre');
+ var dotFactor = 0.5; // Relative size of owner font
+ dot.style.position = 'relative';
+ dot.style.borderLeftWidth = '6px';
+ dot.style.borderLeftStyle = 'solid';
+ dot.style.height = cm.defaultTextHeight() * dotFactor + 'px';
+ dot.style.marginTop = '-' + ((1 + dotFactor) * cm.defaultTextHeight()) + 'px';
+ dot.style.marginLeft = '-2px';
+ widget.appendChild(dot);
+
+ var lastSession;
+ var marker;
+ var inactiveTimer;
+
+ this.update = function (session, from, to) {
+ if(_.isEqual(session, lastSession)) {
+ return;
+ }
+ lastSession = _.cloneDeep(session);
+
+ caret.style.borderLeftColor = session.color || options.color;
+ dot.style.borderLeftColor = session.color || options.color;
+ owner.style.background = session.color || options.color;
+ owner.style.color = session.textColor || options.textColor;
+ owner.innerHTML = session.name || sessionId;
+
+ if(to.line === 0) {
+ // Display the owner text below the caret.
+ // This is to prevent owner from being invisible (outside editor)
+ owner.style.marginTop = '0';
+ } else {
+ // Display the owner text above the caret
+ owner.style.marginTop = '-' + ((1 + ownerFactor) * cm.defaultTextHeight()) + 'px';
+ }
+
+ // We mark up the range of text the other user has highlighted
+ if (marker) {
+ marker.clear();
+ }
+ marker = cm.markText(from, to, { className: "user-" + sessionId });
+
+ cm.addWidget(to, widget);
+
+ if(inactiveTimer) {
+ clearTimeout(inactiveTimer);
+ }
+ dot.style.display = 'none';
+ owner.style.display = 'block';
+ var inactiveTimeout = session.inactiveTimeout || options.inactiveTimeout;
+ inactiveTimer = setTimeout(function() {
+ dot.style.display = 'block';
+ owner.style.display = 'none';
+ }, inactiveTimeout);
+ };
+
+ this.remove = function () {
+ widget.parentElement.removeChild(widget);
+ if (marker) {
+ marker.clear();
+ }
+ }
+ }
+ }
+
+ // Exporting
+ if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
+ // Node.js
+ module.exports = shareCodeMirrorCursor;
+ module.exports.scriptsDir = __dirname;
+ } else {
+ if (typeof define === 'function' && define.amd) {
+ // AMD
+ define([], function () {
+ return shareCodeMirrorCursor;
+ });
+ } else {
+ // Browser, no AMD
+ window.sharejs.Doc.prototype.attachCodeMirrorCursor = function (cm, ctx, options) {
+ if (!ctx) ctx = this.createContext();
+ shareCodeMirrorCursor(cm, ctx, options);
+ };
+ }
+ }
+
+})();
diff --git a/share-codemirror.js b/share-codemirror.js
index 9f66c7c..57a81ca 100644
--- a/share-codemirror.js
+++ b/share-codemirror.js
@@ -8,7 +8,6 @@
function shareCodeMirror(cm, ctx) {
if (!ctx.provides.text) throw new Error('Cannot attach to non-text document');
- var suppress = false;
var text = ctx.get() || ''; // Due to a bug in share - get() returns undefined for empty docs.
cm.setValue(text);
check();
@@ -16,26 +15,28 @@
// *** remote -> local changes
ctx.onInsert = function (pos, text) {
- suppress = true;
+ ctx.suppress = true;
cm.replaceRange(text, cm.posFromIndex(pos));
- suppress = false;
+ ctx.suppress = false;
check();
};
ctx.onRemove = function (pos, length) {
- suppress = true;
+ ctx.suppress = true;
var from = cm.posFromIndex(pos);
var to = cm.posFromIndex(pos + length);
cm.replaceRange('', from, to);
- suppress = false;
+ ctx.suppress = false;
check();
};
// *** local -> remote changes
cm.on('change', function (cm, change) {
- if (suppress) return;
+ if (ctx.suppress) return;
+ ctx.suppress = true;
applyToShareJS(cm, change);
+ ctx.suppress = false;
check();
});
@@ -84,7 +85,9 @@
console.error("cm: " + cmText);
console.error("ot: " + otText);
// Replace the editor text with the ctx snapshot.
+ ctx.suppress = true;
cm.setValue(ctx.get() || '');
+ ctx.suppress = false;
}
}, 0);
}