Merge branch 'cursor' into staging
This commit is contained in:
commit
bfd94c7f2b
@ -149,3 +149,38 @@ a > img {
|
|||||||
border: none;
|
border: none;
|
||||||
outline: 1px solid #0782C1;
|
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.
|
// their length.
|
||||||
var newOffset = offset;
|
var newOffset = offset;
|
||||||
for (var i = 0; i < el.childNodes.length; i++) {
|
for (var i = 0; i < el.childNodes.length; i++) {
|
||||||
|
try {
|
||||||
newOffset -= (getTextNodeValue(el.childNodes[i]) || el.childNodes[i].outerHTML).length;
|
newOffset -= (getTextNodeValue(el.childNodes[i]) || el.childNodes[i].outerHTML).length;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(el);
|
||||||
|
console.log(el.childNodes[i]);
|
||||||
|
}
|
||||||
if (newOffset <= 0) {
|
if (newOffset <= 0) {
|
||||||
return getFinalRange(el.childNodes[i], offset);
|
return getFinalRange(el.childNodes[i], offset);
|
||||||
}
|
}
|
||||||
@ -216,6 +221,19 @@ define([
|
|||||||
return range;
|
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.
|
// Restore the cursor position after applying the changes.
|
||||||
cursor.restoreOffset = function (ops) {
|
cursor.restoreOffset = function (ops) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -332,7 +332,11 @@ define([
|
|||||||
if (!readOnly && cursorGetter) {
|
if (!readOnly && cursorGetter) {
|
||||||
common.openCursorChannel(onLocal);
|
common.openCursorChannel(onLocal);
|
||||||
cursor = common.createCursor();
|
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);
|
UI.removeLoadingScreen(emitResize);
|
||||||
@ -654,7 +658,9 @@ define([
|
|||||||
onCursorUpdate: evCursorUpdate.reg,
|
onCursorUpdate: evCursorUpdate.reg,
|
||||||
updateCursor: function () {
|
updateCursor: function () {
|
||||||
if (cursor && cursorGetter) {
|
if (cursor && cursorGetter) {
|
||||||
cursor.updateCursor(cursorGetter());
|
var newContentStr = cpNfInner.chainpad.getUserDoc();
|
||||||
|
var data = normalize(JSON.parse(newContentStr));
|
||||||
|
cursor.updateCursor(cursorGetter(data));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
154
www/pad/cursor.js
Normal file
154
www/pad/cursor.js
Normal file
@ -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',
|
'/customize/messages.js',
|
||||||
'/pad/links.js',
|
'/pad/links.js',
|
||||||
'/pad/export.js',
|
'/pad/export.js',
|
||||||
|
'/pad/cursor.js',
|
||||||
'/bower_components/nthen/index.js',
|
'/bower_components/nthen/index.js',
|
||||||
'/common/media-tag.js',
|
'/common/media-tag.js',
|
||||||
'/api/config',
|
'/api/config',
|
||||||
@ -51,6 +52,7 @@ define([
|
|||||||
Messages,
|
Messages,
|
||||||
Links,
|
Links,
|
||||||
Exporter,
|
Exporter,
|
||||||
|
Cursors,
|
||||||
nThen,
|
nThen,
|
||||||
MediaTag,
|
MediaTag,
|
||||||
ApiConfig,
|
ApiConfig,
|
||||||
@ -109,8 +111,10 @@ define([
|
|||||||
el.getAttribute('class').split(' ').indexOf('non-realtime') !== -1);
|
el.getAttribute('class').split(' ').indexOf('non-realtime') !== -1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var isCursor = Cursors.isCursor;
|
||||||
|
|
||||||
var shouldSerialize = function (el) {
|
var shouldSerialize = function (el) {
|
||||||
return isNotMagicLine(el) && !isWidget(el);
|
return isNotMagicLine(el) && !isWidget(el) && !isCursor(el);
|
||||||
};
|
};
|
||||||
|
|
||||||
// MEDIATAG: Filter attributes in the serialized elements
|
// MEDIATAG: Filter attributes in the serialized elements
|
||||||
@ -217,6 +221,10 @@ define([
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Other users cursor
|
||||||
|
if (Cursors.preDiffApply(info)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// MEDIATAG
|
// MEDIATAG
|
||||||
// Never modify widget ids
|
// Never modify widget ids
|
||||||
@ -454,8 +462,12 @@ define([
|
|||||||
|
|
||||||
var inner = window.inner = documentBody;
|
var inner = window.inner = documentBody;
|
||||||
|
|
||||||
|
// My cursor
|
||||||
var cursor = module.cursor = Cursor(inner);
|
var cursor = module.cursor = Cursor(inner);
|
||||||
|
|
||||||
|
// Display other users cursor
|
||||||
|
var cursors = Cursors.create(inner, hjsonToDom, cursor);
|
||||||
|
|
||||||
var openLink = function (e) {
|
var openLink = function (e) {
|
||||||
var el = e.currentTarget;
|
var el = e.currentTarget;
|
||||||
if (!el || el.nodeName !== 'A') { return; }
|
if (!el || el.nodeName !== 'A') { return; }
|
||||||
@ -496,10 +508,17 @@ define([
|
|||||||
|
|
||||||
var DD = new DiffDom(mkDiffOptions(cursor, framework.isReadOnly()));
|
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!
|
// apply patches, and try not to lose the cursor in the process!
|
||||||
framework.onContentUpdate(function (hjson) {
|
framework.onContentUpdate(function (hjson) {
|
||||||
if (!Array.isArray(hjson)) { throw new Error(Messages.typeError); }
|
if (!Array.isArray(hjson)) { throw new Error(Messages.typeError); }
|
||||||
var userDocStateDom = hjsonToDom(hjson);
|
var userDocStateDom = hjsonToDom(hjson);
|
||||||
|
cursorStopped = true;
|
||||||
|
|
||||||
userDocStateDom.setAttribute("contenteditable",
|
userDocStateDom.setAttribute("contenteditable",
|
||||||
inner.getAttribute('contenteditable'));
|
inner.getAttribute('contenteditable'));
|
||||||
@ -527,6 +546,9 @@ define([
|
|||||||
var ops = ChainPad.Diff.diff(oldText, newText);
|
var ops = ChainPad.Diff.diff(oldText, newText);
|
||||||
cursor.restoreOffset(ops);
|
cursor.restoreOffset(ops);
|
||||||
|
|
||||||
|
cursorStopped = false;
|
||||||
|
updateCursor();
|
||||||
|
|
||||||
// MEDIATAG: Migrate old mediatags to the widget system
|
// MEDIATAG: Migrate old mediatags to the widget system
|
||||||
$(inner).find('media-tag:not(.cke_widget_element)').each(function (i, el) {
|
$(inner).find('media-tag:not(.cke_widget_element)').each(function (i, el) {
|
||||||
var element = new window.CKEDITOR.dom.element(el);
|
var element = new window.CKEDITOR.dom.element(el);
|
||||||
@ -561,9 +583,15 @@ define([
|
|||||||
$(el).remove();
|
$(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);
|
displayMediaTags(framework, inner, mediaTagMap);
|
||||||
inner.normalize();
|
inner.normalize();
|
||||||
return Hyperjson.fromDOM(inner, shouldSerialize, hjsonFilters);
|
var hjson = Hyperjson.fromDOM(inner, shouldSerialize, hjsonFilters);
|
||||||
|
|
||||||
|
return hjson;
|
||||||
});
|
});
|
||||||
|
|
||||||
$bar.find('#cke_1_toolbar_collapser').hide();
|
$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
|
/* hitting enter makes a new line, but places the cursor inside
|
||||||
of the <br> instead of the <p>. This makes it such that you
|
of the <br> instead of the <p>. This makes it such that you
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user