Files
datascape/assets/editor.js
T
2026-05-04 11:54:58 +02:00

261 lines
9.9 KiB
JavaScript

(function () {
var textarea = document.getElementById('editor');
if (!textarea) return;
var form = textarea.closest('form');
// --- DOM helpers ---
// Route every edit through execCommand so the browser's native undo/redo
// stack is preserved. Direct assignment to textarea.value would wipe it.
function replaceRange(start, end, text) {
textarea.focus();
textarea.selectionStart = start;
textarea.selectionEnd = end;
document.execCommand('insertText', false, text);
}
function wrap(before, after, placeholder) {
var start = textarea.selectionStart;
var end = textarea.selectionEnd;
var hadSelection = end > start;
var selected = hadSelection ? textarea.value.slice(start, end) : placeholder;
replaceRange(start, end, before + selected + after);
if (!hadSelection) {
textarea.selectionStart = start + before.length;
textarea.selectionEnd = start + before.length + placeholder.length;
}
}
function linePrefix(prefix) {
var start = textarea.selectionStart;
var lineStart = textarea.value.lastIndexOf('\n', start - 1) + 1;
replaceRange(lineStart, lineStart, prefix);
textarea.selectionStart = textarea.selectionEnd = start + prefix.length;
}
function insertAtCursor(s) {
replaceRange(textarea.selectionStart, textarea.selectionEnd, s);
}
function applyResult(result) {
var oldText = textarea.value;
var newText = result.text;
var prefixLen = 0;
var maxPrefix = Math.min(oldText.length, newText.length);
while (prefixLen < maxPrefix && oldText.charCodeAt(prefixLen) === newText.charCodeAt(prefixLen)) {
prefixLen++;
}
var oldEnd = oldText.length;
var newEnd = newText.length;
while (oldEnd > prefixLen && newEnd > prefixLen
&& oldText.charCodeAt(oldEnd - 1) === newText.charCodeAt(newEnd - 1)) {
oldEnd--;
newEnd--;
}
replaceRange(prefixLen, oldEnd, newText.slice(prefixLen, newEnd));
textarea.selectionStart = textarea.selectionEnd = result.cursor;
}
function applyTableOp(fn, arg) {
var result = arg !== undefined
? fn(textarea.value, textarea.selectionStart, arg)
: fn(textarea.value, textarea.selectionStart);
if (result) applyResult(result);
}
function promptDisplayText(initial, onDone) {
var input = document.createElement('input');
input.type = 'text';
input.className = 'modal-input';
input.placeholder = 'Display text (optional)';
if (initial) input.value = initial;
var handle = openModal({
title: 'Insert link — display text?',
body: input,
confirm: {
label: 'INSERT',
onConfirm: function () {
handle.close();
onDone(input.value.trim());
}
}
});
}
function insertWikilink() {
var sel = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd);
openTreePicker({
title: 'Insert link',
mode: 'any',
initialPath: '/',
confirmLabel: 'NEXT',
onSelect: function (path, kind) {
if (kind === 'folder') {
promptDisplayText(sel, function (display) {
insertAtCursor(display ? '[[' + path + '|' + display + ']]' : '[[' + path + ']]');
});
} else {
var name = path.split('/').pop();
insertAtCursor('[' + (sel || name) + '](' + path + ')');
}
}
});
}
// --- Actions ---
var T = EditorTables;
var L = EditorLists;
var D = EditorDates;
var M = EditorMovie;
var actions = {
save: function () { form.submit(); },
bold: function () { wrap('**', '**', 'bold text'); },
italic: function () { wrap('*', '*', 'italic text'); },
h1: function () { linePrefix('# '); },
h2: function () { linePrefix('## '); },
h3: function () { linePrefix('### '); },
code: function () { wrap('`', '`', 'code'); },
codeblock: function () { wrap('```\n', '\n```', 'code'); },
quote: function () { linePrefix('> '); },
link: function () { wrap('[', '](url)', 'link text'); },
wikilink: insertWikilink,
ul: function () { linePrefix('- '); },
ol: function () { linePrefix('1. '); },
hr: function () { wrap('\n\n---\n\n', '', ''); },
fmttable: function () { applyTableOp(T.formatTableText); },
tblalignleft: function () { applyTableOp(T.setColumnAlignment, 'left'); },
tblaligncenter: function () { applyTableOp(T.setColumnAlignment, 'center'); },
tblalignright: function () { applyTableOp(T.setColumnAlignment, 'right'); },
tblinsertcol: function () { applyTableOp(T.insertColumn); },
tbldeletecol: function () { applyTableOp(T.deleteColumn); },
tblinsertrow: function () { applyTableOp(T.insertRow); },
tbldeleterow: function () { applyTableOp(T.deleteRow); },
dateiso: function () { insertAtCursor(D.isoDate()); },
datelong: function () { insertAtCursor(D.longDate()); },
movie: function () { M.run(textarea); },
};
// --- Keyboard shortcut registration ---
var keyMap = {};
document.querySelectorAll('[data-action]').forEach(function (btn) {
btn.addEventListener('click', function () {
var action = actions[btn.dataset.action];
if (action) action();
});
if (btn.dataset.key) {
keyMap[btn.dataset.key] = actions[btn.dataset.action];
}
});
keyMap['T'] = actions.fmttable;
keyMap['D'] = actions.dateiso;
keyMap['W'] = actions.datelong;
document.addEventListener('keydown', function (e) {
if (!e.altKey || !e.shiftKey) return;
// Shift+digit produces a layout-dependent character in e.key (e.g. "!"
// on US, "!" on DE), so fall back to e.code for digit rows.
var key = /^Digit[0-9]$/.test(e.code) ? e.code.slice(5) : e.key;
var action = keyMap[key];
if (action) {
e.preventDefault();
action();
}
});
// --- Textarea key handling ---
textarea.addEventListener('keydown', function (e) {
if (e.key === 'Delete' && e.shiftKey) {
var result = T.deleteRow(textarea.value, textarea.selectionStart)
|| L.deleteOrderedLine(textarea.value, textarea.selectionStart);
if (!result) return;
e.preventDefault();
applyResult(result);
return;
}
if (e.key === 'Enter' && e.shiftKey) {
var result = T.insertRowBelow(textarea.value, textarea.selectionStart);
if (!result) return;
e.preventDefault();
applyResult(result);
return;
}
if (e.key !== 'Enter') return;
var result = L.handleEnterKey(textarea.value, textarea.selectionStart);
if (!result) return;
e.preventDefault();
applyResult(result);
});
// --- Dropdown helper ---
var openMenus = [];
function makeDropdown(triggerBtn, items) {
var menu = document.createElement('div');
menu.className = 'dropdown-menu';
items.forEach(function (item) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'btn btn-tool dropdown-item';
btn.textContent = item.label;
btn.addEventListener('mousedown', function (e) {
e.preventDefault();
actions[item.action]();
menu.classList.remove('is-open');
});
menu.appendChild(btn);
});
triggerBtn.appendChild(menu);
openMenus.push(menu);
triggerBtn.addEventListener('click', function (e) {
if (e.target !== triggerBtn) return;
var wasOpen = menu.classList.contains('is-open');
openMenus.forEach(function (m) { m.classList.remove('is-open'); });
if (!wasOpen) menu.classList.add('is-open');
});
}
document.addEventListener('click', function (e) {
var insideAny = openMenus.some(function (m) {
return m.parentElement && m.parentElement.contains(e.target);
});
if (!insideAny) openMenus.forEach(function (m) { m.classList.remove('is-open'); });
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') openMenus.forEach(function (m) { m.classList.remove('is-open'); });
});
// --- Table dropdown ---
var tblDropBtn = document.querySelector('[data-action="tbldrop"]');
if (tblDropBtn) {
makeDropdown(tblDropBtn, [
{ label: 'Format table', action: 'fmttable' },
{ label: 'Align left', action: 'tblalignleft' },
{ label: 'Align center', action: 'tblaligncenter' },
{ label: 'Align right', action: 'tblalignright' },
{ label: 'Insert column', action: 'tblinsertcol' },
{ label: 'Delete column', action: 'tbldeletecol' },
{ label: 'Insert row', action: 'tblinsertrow' },
{ label: 'Delete row', action: 'tbldeleterow' },
]);
}
// --- Date dropdown ---
var dateDropBtn = document.querySelector('[data-action="datedrop"]');
if (dateDropBtn) {
makeDropdown(dateDropBtn, [
{ label: 'YYYY-MM-DD', action: 'dateiso' },
{ label: 'DE Long', action: 'datelong' },
]);
}
})();