cryptpad/www/pad/cursor.js

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