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