5a015d438b
Unify dropdown handling, refactor crowded toolbar
201 lines
7.7 KiB
JavaScript
201 lines
7.7 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. '); },
|
|
task: function () { linePrefix('- [ ] '); },
|
|
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); },
|
|
wide: function () {
|
|
var enabled = !document.body.classList.contains('editor-wide');
|
|
document.body.classList.toggle('editor-wide', enabled);
|
|
sessionStorage.setItem('editor-wide', enabled ? '1' : '0');
|
|
},
|
|
};
|
|
|
|
// --- 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];
|
|
}
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
// --- Dropdowns ---
|
|
|
|
document.querySelectorAll('.dropdown-toggle').forEach(wireDropdown);
|
|
})();
|