', {
+ 'id': id,
+ 'data-type': 'end',
+ title: makeTippy(cursor),
+ 'class': 'cp-cursor-position'
+ })[0],
+ };
+ return cursors[id];
+ };
+ var deleteCursor = function (id) {
+ if (!cursors[id]) { return; }
+ cursors[id].el.remove();
+ cursors[id].elstart.remove();
+ cursors[id].elend.remove();
+ delete cursors[id];
+ };
+
+
+ var addCursorAtRange = function (cursorEl, r, cursor, type) {
+ var pos = type || 'start';
+ var p = r[pos].el.parentNode;
+ var el = cursorEl['el'+type];
+ if (cursor.color) {
+ $(el).css('border-color', cursor.color);
+ $(el).css('background-color', cursor.color);
+ }
+ if (r[pos].offset === 0) {
+ if (r[pos].el.nodeType === r[pos].el.TEXT_NODE) {
+ // Text node, insert at the beginning
+ p.insertBefore(el, p.childNodes[0] || null);
+ } else {
+ // Other node, insert as first child
+ r[pos].el.insertBefore(el, r[pos].el.childNodes[0] || null);
+ }
+ } else {
+ if (r[pos].el.nodeType !== r[pos].el.TEXT_NODE) { return; }
+ // Text node, we have to split...
+ var newNode = r[pos].el.splitText(r[pos].offset);
+ p.insertBefore(el, newNode);
+ }
+ };
+
+ exp.removeCursors = function () {
+ for (var id in cursors) {
+ deleteCursor(id);
+ }
+ };
+
+ exp.cursorGetter = function (hjson) {
+ cursorModule.offsetUpdate();
+ var userDocStateDom = hjsonToDom(hjson);
+ var ops = ChainPad.Diff.diff(inner.outerHTML, userDocStateDom.outerHTML);
+ return cursorModule.getNewOffset(ops);
+ };
+
+ exp.onCursorUpdate = function (data, hjson) {
+ if (data.leave) {
+ if (data.id.length === 32) {
+ Object.keys(cursors).forEach(function (id) {
+ if (id.indexOf(data.id) === 0) { deleteCursor(id); }
+ });
+ }
+ deleteCursor(data.id);
+ return;
+ }
+ var id = data.id;
+ var cursorObj = data.cursor;
+
+ if (!cursorObj.selectionStart) { return; }
+
+ // 1. Transform the cursor to get the offset relative to our doc
+ // 2. Turn it into a range
+ var userDocStateDom = hjsonToDom(hjson);
+ var ops = ChainPad.Diff.diff(userDocStateDom.outerHTML, inner.outerHTML);
+ var r = cursorModule.getNewRange({
+ start: cursorObj.selectionStart,
+ end: cursorObj.selectionEnd
+ }, ops);
+ var cursorEl = makeCursor(id, cursorObj);
+ if (r.start.el === r.end.el && r.start.offset === r.end.offset) {
+ // Cursor
+ addCursorAtRange(cursorEl, r, cursorObj, '');
+ } else {
+ // Selection
+ addCursorAtRange(cursorEl, r, cursorObj, 'end');
+ addCursorAtRange(cursorEl, r, cursorObj, 'start');
+ }
+ inner.normalize();
+ };
+
+ return exp;
+ };
+
+ return Cursor;
+});
diff --git a/www/pad/inner.js b/www/pad/inner.js
index 9442eea7a..c1c952683 100644
--- a/www/pad/inner.js
+++ b/www/pad/inner.js
@@ -26,6 +26,7 @@ define([
'/customize/messages.js',
'/pad/links.js',
'/pad/export.js',
+ '/pad/cursor.js',
'/bower_components/nthen/index.js',
'/common/media-tag.js',
'/api/config',
@@ -51,6 +52,7 @@ define([
Messages,
Links,
Exporter,
+ Cursors,
nThen,
MediaTag,
ApiConfig,
@@ -109,8 +111,10 @@ define([
el.getAttribute('class').split(' ').indexOf('non-realtime') !== -1);
};
+ var isCursor = Cursors.isCursor;
+
var shouldSerialize = function (el) {
- return isNotMagicLine(el) && !isWidget(el);
+ return isNotMagicLine(el) && !isWidget(el) && !isCursor(el);
};
// MEDIATAG: Filter attributes in the serialized elements
@@ -217,6 +221,10 @@ define([
}
}
+ // Other users cursor
+ if (Cursors.preDiffApply(info)) {
+ return true;
+ }
// MEDIATAG
// Never modify widget ids
@@ -454,8 +462,12 @@ define([
var inner = window.inner = documentBody;
+ // My cursor
var cursor = module.cursor = Cursor(inner);
+ // Display other users cursor
+ var cursors = Cursors.create(inner, hjsonToDom, cursor);
+
var openLink = function (e) {
var el = e.currentTarget;
if (!el || el.nodeName !== 'A') { return; }
@@ -496,10 +508,17 @@ define([
var DD = new DiffDom(mkDiffOptions(cursor, framework.isReadOnly()));
+ var cursorStopped = false;
+ var updateCursor = function () {
+ if (cursorStopped) { return; }
+ framework.updateCursor();
+ };
+
// apply patches, and try not to lose the cursor in the process!
framework.onContentUpdate(function (hjson) {
if (!Array.isArray(hjson)) { throw new Error(Messages.typeError); }
var userDocStateDom = hjsonToDom(hjson);
+ cursorStopped = true;
userDocStateDom.setAttribute("contenteditable",
inner.getAttribute('contenteditable'));
@@ -527,6 +546,9 @@ define([
var ops = ChainPad.Diff.diff(oldText, newText);
cursor.restoreOffset(ops);
+ cursorStopped = false;
+ updateCursor();
+
// MEDIATAG: Migrate old mediatags to the widget system
$(inner).find('media-tag:not(.cke_widget_element)').each(function (i, el) {
var element = new window.CKEDITOR.dom.element(el);
@@ -561,9 +583,15 @@ define([
$(el).remove();
});
+ // We have to remove the cursors before getting the content because they split
+ // the text nodes and OT/ChainPad would freak out
+ cursors.removeCursors();
+
displayMediaTags(framework, inner, mediaTagMap);
inner.normalize();
- return Hyperjson.fromDOM(inner, shouldSerialize, hjsonFilters);
+ var hjson = Hyperjson.fromDOM(inner, shouldSerialize, hjsonFilters);
+
+ return hjson;
});
$bar.find('#cke_1_toolbar_collapser').hide();
@@ -694,6 +722,12 @@ define([
];
});
+ /* Display the cursor of other users and send our cursor */
+ framework.setCursorGetter(cursors.cursorGetter);
+ framework.onCursorUpdate(cursors.onCursorUpdate);
+ inner.addEventListener('click', updateCursor);
+ inner.addEventListener('keyup', updateCursor);
+
/* hitting enter makes a new line, but places the cursor inside
of the
instead of the . This makes it such that you