diff --git a/customize.dist/ckeditor-contents.css b/customize.dist/ckeditor-contents.css
index 378d61793..75d054230 100644
--- a/customize.dist/ckeditor-contents.css
+++ b/customize.dist/ckeditor-contents.css
@@ -149,3 +149,38 @@ a > img {
border: none;
outline: 1px solid #0782C1;
}
+
+.cp-cursor-position {
+ cursor: default;
+ background-color: red;
+ background-clip: padding-box;
+ padding: 0 1px;
+ border: 2px solid red;
+ border-right-color: transparent !important;
+ border-left-color: transparent !important;
+}
+.cp-cursor-position[data-type="start"] {
+ border-left: none;
+ border-right-width: 4px;
+}
+.cp-cursor-position[data-type="end"] {
+ border-right: none;
+ border-left-width: 4px;
+}
+.cp-cursor-avatar {
+ display: flex;
+ align-items: center;
+}
+.cp-cursor-avatar media-tag {
+ min-height: 32px;
+ max-height: 32px;
+ min-width: 32px;
+ max-width: 32px;
+ margin-right: 10px;
+}
+.cp-cursor-avatar media-tag img {
+ border-radius: 4px;
+ max-height: 100%;
+ max-width: 100%;
+}
+
diff --git a/www/common/cursor.js b/www/common/cursor.js
index fffbe1db6..01f71d8d5 100644
--- a/www/common/cursor.js
+++ b/www/common/cursor.js
@@ -173,7 +173,12 @@ define([
// their length.
var newOffset = offset;
for (var i = 0; i < el.childNodes.length; i++) {
+ try {
newOffset -= (getTextNodeValue(el.childNodes[i]) || el.childNodes[i].outerHTML).length;
+ } catch (e) {
+ console.log(el);
+ console.log(el.childNodes[i]);
+ }
if (newOffset <= 0) {
return getFinalRange(el.childNodes[i], offset);
}
@@ -216,6 +221,19 @@ define([
return range;
};
+ cursor.getNewOffset = function (ops) {
+ return {
+ selectionStart: offsetTransformRange(offsetRange.start, ops),
+ selectionEnd: offsetTransformRange(offsetRange.end, ops)
+ };
+ };
+ cursor.getNewRange = function (data, ops) {
+ offsetRange.start = offsetTransformRange(data.start, ops);
+ offsetRange.end = offsetTransformRange(data.end, ops);
+ var range = getRangeFromOffset(inner);
+ return range;
+ };
+
// Restore the cursor position after applying the changes.
cursor.restoreOffset = function (ops) {
try {
diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js
index c16805b2b..8bba4d111 100644
--- a/www/common/sframe-app-framework.js
+++ b/www/common/sframe-app-framework.js
@@ -332,7 +332,11 @@ define([
if (!readOnly && cursorGetter) {
common.openCursorChannel(onLocal);
cursor = common.createCursor();
- cursor.onCursorUpdate(evCursorUpdate.fire);
+ cursor.onCursorUpdate(function (data) {
+ var newContentStr = cpNfInner.chainpad.getUserDoc();
+ var hjson = normalize(JSON.parse(newContentStr));
+ evCursorUpdate.fire(data, hjson);
+ });
}
UI.removeLoadingScreen(emitResize);
@@ -654,7 +658,9 @@ define([
onCursorUpdate: evCursorUpdate.reg,
updateCursor: function () {
if (cursor && cursorGetter) {
- cursor.updateCursor(cursorGetter());
+ var newContentStr = cpNfInner.chainpad.getUserDoc();
+ var data = normalize(JSON.parse(newContentStr));
+ cursor.updateCursor(cursorGetter(data));
}
},
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