mirror of https://github.com/xwiki-labs/cryptpad
191 lines
6.9 KiB
JavaScript
191 lines
6.9 KiB
JavaScript
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
define([
|
|
'jquery',
|
|
'/common/common-ui-elements.js',
|
|
'/common/common-interface.js',
|
|
'/components/chainpad/chainpad.dist.js',
|
|
'/customize/messages.js',
|
|
'/common/inner/common-mediatag.js',
|
|
], function ($, UIElements, UI, ChainPad, Messages, MT) {
|
|
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;
|
|
}
|
|
}
|
|
};
|
|
|
|
var removeNode = function (el) {
|
|
if (!el) { return; }
|
|
if (typeof el.remove === "function") {
|
|
return void el.remove();
|
|
}
|
|
if (el.parentNode) {
|
|
el.parentNode.removeChild(el);
|
|
return;
|
|
}
|
|
$(el).remove();
|
|
};
|
|
|
|
Cursor.create = function (inner, hjsonToDom, cursorModule) {
|
|
var exp = {};
|
|
|
|
var cursors = {};
|
|
|
|
// FIXME despite the name of this function this doesn't actually render as a tippy tooltip
|
|
// that means that emojis will use the system font that shows up in native tooltips
|
|
// so this might be of limited value/aesthetic appeal compared to other apps' cursors
|
|
var makeTippy = function (cursor) {
|
|
if (typeof(cursor.uid) === 'string' && (!cursor.name || cursor.name === Messages.anonymous)) {
|
|
var animal = MT.getPseudorandomAnimal(cursor.uid);
|
|
if (animal) {
|
|
return animal + ' ' + Messages.anonymous;
|
|
}
|
|
}
|
|
return cursor.name || Messages.anonymous;
|
|
};
|
|
|
|
var makeCursor = function (id, cursor) {
|
|
if (cursors[id]) {
|
|
removeNode(cursors[id].el);
|
|
removeNode(cursors[id].elstart);
|
|
removeNode(cursors[id].elend);
|
|
}
|
|
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; }
|
|
removeNode(cursors[id].el);
|
|
removeNode(cursors[id].elstart);
|
|
removeNode(cursors[id].elend);
|
|
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 (inner) {
|
|
for (var id in cursors) {
|
|
deleteCursor(id);
|
|
}
|
|
// If diffdom has changed the cursor element somehow, we'll have cursor elements
|
|
// in the dom but not in memory: remove them
|
|
$(inner).find('.cp-cursor-position').remove();
|
|
};
|
|
|
|
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.reset) {
|
|
return void exp.removeCursors(inner);
|
|
}
|
|
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);
|
|
['start', 'end'].forEach(function (t) {
|
|
// Prevent the cursor from creating a new line at the beginning
|
|
if (r[t].el.nodeName.toUpperCase() === 'BODY') {
|
|
if (!r[t].el.childNodes.length) { r[t] = null; return; }
|
|
r[t].el = r[t].el.childNodes[0];
|
|
r[t].offset = 0;
|
|
}
|
|
});
|
|
if (!r.start || !r.end) { return; }
|
|
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;
|
|
});
|