(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); } // isValidWikiTarget mirrors the Go validator in wikilinks.go — absolute // path, no empty/dot segments. Used to gate the INSERT confirm button. function isValidWikiTarget(p) { if (!p || p[0] !== '/') return false; var trimmed = p.replace(/^\/+|\/+$/g, ''); if (trimmed === '') return true; var segs = trimmed.split('/'); for (var i = 0; i < segs.length; i++) { if (segs[i] === '' || segs[i] === '.' || segs[i] === '..') return false; } return true; } function insertWikilink() { var sel = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd); var container = document.createElement('div'); var targetWrap = document.createElement('div'); var targetInput = document.createElement('input'); targetInput.type = 'text'; targetInput.className = 'input'; targetInput.placeholder = 'Page path or search…'; targetWrap.appendChild(targetInput); var displayInput = document.createElement('input'); displayInput.type = 'text'; displayInput.className = 'input'; displayInput.placeholder = 'Display text (optional)'; if (sel) displayInput.value = sel; container.appendChild(targetWrap); container.appendChild(displayInput); var handle = openModal({ title: 'Insert link', body: container, confirm: { label: 'INSERT', initiallyDisabled: true, onConfirm: function () { var target = targetInput.value.trim(); if (!isValidWikiTarget(target)) return; var display = displayInput.value.trim(); handle.close(); insertAtCursor(display ? '[[' + target + '::' + display + ']]' : '[[' + target + ']]'); } } }); function updateConfirm() { handle.setConfirmDisabled(!isValidWikiTarget(targetInput.value.trim())); } targetInput.addEventListener('input', updateConfirm); window.attachSuggestions(targetInput, { showFooter: false, container: targetWrap, onPick: function (r) { targetInput.value = '/' + r.path; updateConfirm(); displayInput.focus(); displayInput.select(); } }); } // --- 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); })();