Merge branch 'cursor' into staging

pull/1/head
yflory 6 years ago
commit bfd94c7f2b

@ -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%;
}

@ -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 {

@ -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));
}
},

@ -0,0 +1,154 @@
define([
'jquery',
'/common/common-ui-elements.js',
'/common/common-interface.js',
'/bower_components/chainpad/chainpad.dist.js',
], function ($, UIElements, UI, ChainPad) {
var Cursor = {};
Cursor.isCursor = function (el) {
return typeof (el.getAttribute) === "function" &&
el.getAttribute('class') &&
/cp-cursor-position/.test(el.getAttribute('class'));
};
Cursor.preDiffApply = function (info) {
if (info.node && info.node.tagName === 'SPAN' &&
info.node.getAttribute('class') &&
/cp-cursor-position/.test(info.node.getAttribute('class'))) {
if (info.diff.action === 'removeElement') {
console.error('PREVENTING REMOVAL OF CURSOR', info.node);
return true;
}
}
};
Cursor.create = function (inner, hjsonToDom, cursorModule) {
var exp = {};
var cursors = {};
var makeTippy = function (cursor) {
/*var html = '<span class="cp-cursor-avatar">';
if (cursor.avatar && UIElements.getAvatar(cursor.avatar)) {
html += UIElements.getAvatar(cursor.avatar);
}
html += cursor.name + '</span>';
return html;*/
return cursor.name;
};
var makeCursor = function (id, cursor) {
if (cursors[id]) {
cursors[id].el.remove();
cursors[id].elstart.remove();
cursors[id].elend.remove();
}
cursors[id] = {
el: $('<span>', {
'id': id,
'data-type': '',
title: makeTippy(cursor),
'class': 'cp-cursor-position'
})[0],
elstart: $('<span>', {
'id': id,
'data-type': 'start',
title: makeTippy(cursor),
'class': 'cp-cursor-position'
})[0],
elend: $('<span>', {
'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;
});

@ -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 <br> instead of the <p>. This makes it such that you

Loading…
Cancel
Save