// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team 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: $('', { 'id': id, 'data-type': '', title: makeTippy(cursor), 'class': 'cp-cursor-position' })[0], elstart: $('', { 'id': id, 'data-type': 'start', title: makeTippy(cursor), 'class': 'cp-cursor-position' })[0], elend: $('', { '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; });