diff --git a/assets/editor.js b/assets/editor.js index 0273797..0e87f22 100644 --- a/assets/editor.js +++ b/assets/editor.js @@ -142,11 +142,11 @@ function makeDropdown(triggerBtn, items) { var menu = document.createElement('div'); - menu.className = 'toolbar-dropdown-menu'; + menu.className = 'dropdown-menu'; items.forEach(function (item) { var btn = document.createElement('button'); btn.type = 'button'; - btn.className = 'btn btn-tool toolbar-dropdown-item'; + btn.className = 'btn btn-tool dropdown-item'; btn.textContent = item.label; btn.addEventListener('mousedown', function (e) { e.preventDefault(); diff --git a/assets/global-shortcuts.js b/assets/global-shortcuts.js index 0720fce..1082910 100644 --- a/assets/global-shortcuts.js +++ b/assets/global-shortcuts.js @@ -1,10 +1,3 @@ -function newPage() { - const name = prompt('New page name:'); - if (!name || !name.trim()) return; - const slug = name.trim().replace(/\s+/g, '-'); - window.location.href = window.location.pathname + slug + '/?edit'; -} - (function () { document.addEventListener('keydown', function (e) { if (!e.altKey || !e.shiftKey) return; @@ -12,11 +5,15 @@ function newPage() { case 'E': e.preventDefault(); window.location.href = window.location.pathname + '?edit'; - break; + break; case 'N': - e.preventDefault(); - newPage(); - break; + e.preventDefault(); + if (typeof newPage === 'function') newPage(); + break; + case 'M': + e.preventDefault(); + if (window.location.pathname !== '/' && typeof movePage === 'function') movePage(); + break; } }); })(); diff --git a/assets/modal.js b/assets/modal.js new file mode 100644 index 0000000..22658c3 --- /dev/null +++ b/assets/modal.js @@ -0,0 +1,146 @@ +(function () { + var backdrop = null; + var modal = null; + var titleEl = null; + var bodyEl = null; + var cancelBtn = null; + var confirmBtn = null; + var footerEl = null; + var prevFocus = null; + var currentOpts = null; + var onKeydown = null; + + function build() { + backdrop = document.createElement('div'); + backdrop.className = 'modal-backdrop'; + + modal = document.createElement('div'); + modal.className = 'modal'; + modal.setAttribute('role', 'dialog'); + modal.setAttribute('aria-modal', 'true'); + + var header = document.createElement('div'); + header.className = 'modal-header'; + titleEl = document.createElement('span'); + header.appendChild(titleEl); + + bodyEl = document.createElement('div'); + bodyEl.className = 'modal-body'; + + footerEl = document.createElement('div'); + footerEl.className = 'modal-footer'; + + cancelBtn = document.createElement('button'); + cancelBtn.type = 'button'; + confirmBtn = document.createElement('button'); + confirmBtn.type = 'button'; + + modal.appendChild(header); + modal.appendChild(bodyEl); + modal.appendChild(footerEl); + backdrop.appendChild(modal); + + backdrop.addEventListener('mousedown', function (e) { + if (e.target === backdrop) close(); + }); + cancelBtn.addEventListener('click', close); + confirmBtn.addEventListener('click', function () { + if (confirmBtn.disabled) return; + if (currentOpts && currentOpts.confirm && currentOpts.confirm.onConfirm) { + currentOpts.confirm.onConfirm(); + } + }); + } + + function open(opts) { + if (backdrop && backdrop.parentNode) close(); + if (!backdrop) build(); + + currentOpts = opts; + prevFocus = document.activeElement; + + titleEl.textContent = opts.title || ''; + + bodyEl.textContent = ''; + if (opts.body instanceof Node) { + bodyEl.appendChild(opts.body); + } else if (typeof opts.body === 'string') { + bodyEl.textContent = opts.body; + } + + var confirmOpts = opts.confirm || {}; + var cancelOpts = opts.cancel || {}; + + confirmBtn.textContent = confirmOpts.label || 'OK'; + confirmBtn.className = 'btn' + (confirmOpts.danger ? ' danger' : ''); + confirmBtn.disabled = !!confirmOpts.initiallyDisabled; + + cancelBtn.textContent = cancelOpts.label || 'CANCEL'; + cancelBtn.className = 'btn'; + + footerEl.textContent = ''; + if (opts.swapButtons) { + footerEl.appendChild(confirmBtn); + footerEl.appendChild(cancelBtn); + } else { + footerEl.appendChild(cancelBtn); + footerEl.appendChild(confirmBtn); + } + + document.body.appendChild(backdrop); + + setTimeout(function () { + if (cancelOpts.autofocus) { + cancelBtn.focus(); + return; + } + var firstInput = bodyEl.querySelector('input, textarea, select'); + if (firstInput) { + firstInput.focus(); + if (firstInput.select) firstInput.select(); + } else { + confirmBtn.focus(); + } + }, 0); + + var enterConfirms = confirmOpts.enterConfirms !== false; + onKeydown = function (e) { + if (e.key === 'Escape') { + e.preventDefault(); + close(); + return; + } + if (e.key === 'Enter' && enterConfirms) { + var tag = (e.target && e.target.tagName) || ''; + if (tag === 'TEXTAREA') return; + e.preventDefault(); + if (!confirmBtn.disabled) confirmBtn.click(); + } + }; + document.addEventListener('keydown', onKeydown); + + return { + close: close, + setConfirmDisabled: function (d) { confirmBtn.disabled = !!d; }, + confirmButton: confirmBtn + }; + } + + function close() { + if (!backdrop || !backdrop.parentNode) return; + if (onKeydown) { + document.removeEventListener('keydown', onKeydown); + onKeydown = null; + } + backdrop.parentNode.removeChild(backdrop); + currentOpts = null; + var toRestore = prevFocus; + prevFocus = null; + if (toRestore && toRestore.focus) { + try { toRestore.focus(); } catch (e) {} + } + } + + window.openModal = open; + window.closeModal = close; +})(); diff --git a/assets/page-actions.js b/assets/page-actions.js index 2864898..7cb90f9 100644 --- a/assets/page-actions.js +++ b/assets/page-actions.js @@ -1,25 +1,91 @@ +function newPage() { + var input = document.createElement('input'); + input.type = 'text'; + input.className = 'modal-input'; + input.placeholder = 'New page name'; + + openModal({ + title: 'New page', + body: input, + confirm: { + label: 'CREATE', + onConfirm: function () { + var name = input.value.trim(); + if (!name) return; + window.location.href = window.location.pathname + + encodeURIComponent(name) + '/?edit'; + } + } + }); +} + function movePage() { - const current = window.location.pathname; - const target = prompt('Move this page to (absolute path):', current); - if (target === null) return; - const clean = target.trim(); - if (!clean || !clean.startsWith('/')) { - alert('Move target must be an absolute path starting with /'); - return; - } - const form = document.createElement('form'); - form.method = 'POST'; - form.action = current + '?move=' + encodeURIComponent(clean); - document.body.appendChild(form); - form.submit(); + var input = document.createElement('input'); + input.type = 'text'; + input.className = 'modal-input'; + input.value = decodeURIComponent(window.location.pathname); + + openModal({ + title: 'Move page', + body: input, + confirm: { + label: 'MOVE', + onConfirm: function () { + var target = input.value.trim(); + if (!target || !target.startsWith('/')) return; + var form = document.createElement('form'); + form.method = 'POST'; + form.action = window.location.pathname + '?move=' + + encodeURIComponent(target); + document.body.appendChild(form); + form.submit(); + } + } + }); } function deletePage() { - const current = window.location.pathname; - if (!confirm('Delete ' + current + ' and everything inside it?')) return; - const form = document.createElement('form'); - form.method = 'POST'; - form.action = current + '?delete=1'; - document.body.appendChild(form); - form.submit(); + var decodedPath = decodeURIComponent(window.location.pathname); + + openModal({ + title: 'Delete page', + body: 'Delete ' + decodedPath + ' and everything inside it?', + confirm: { + label: 'DELETE', + danger: true, + enterConfirms: false, + onConfirm: function () { + var form = document.createElement('form'); + form.method = 'POST'; + form.action = window.location.pathname + '?delete=1'; + document.body.appendChild(form); + form.submit(); + } + }, + cancel: { autofocus: true }, + swapButtons: true + }); } + +document.addEventListener('DOMContentLoaded', function () { + var trigger = document.querySelector('[data-action="actions-drop"]'); + if (!trigger) return; + var menu = trigger.parentElement.querySelector('.dropdown-menu'); + if (!menu) return; + + trigger.addEventListener('click', function (e) { + e.stopPropagation(); + menu.classList.toggle('is-open'); + }); + menu.addEventListener('click', function () { + menu.classList.remove('is-open'); + }); + document.addEventListener('click', function (e) { + if (!trigger.contains(e.target) && !menu.contains(e.target)) { + menu.classList.remove('is-open'); + } + }); + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape') menu.classList.remove('is-open'); + }); +}); diff --git a/assets/page.html b/assets/page.html index 8f156da..7520d54 100644 --- a/assets/page.html +++ b/assets/page.html @@ -8,6 +8,7 @@ + @@ -23,12 +24,17 @@ CANCEL {{else if .CanEdit}} - - EDIT - {{if not .IsRoot}} - - - {{end}} + {{end}}
@@ -52,8 +58,8 @@ - - + + diff --git a/assets/style.css b/assets/style.css index 0098107..f50b19d 100644 --- a/assets/style.css +++ b/assets/style.css @@ -22,6 +22,8 @@ --secondary: #c48401; --link: #01b6c4; --link-hover: #d6d24d; + --danger: #c40141; + --danger-hover: #d03467; } /* === Base === */ @@ -135,10 +137,10 @@ header { /* Destructive action */ .danger { - color: var(--primary-hover); + color: var(--danger); } .danger:hover { - color: var(--link-hover); + color: var(--danger-hover); } /* === Main === */ @@ -291,12 +293,12 @@ main { align-self: stretch; } -/* === Toolbar dropdowns === */ -.toolbar-dropdown { +/* === Dropdowns === */ +.dropdown { position: relative; } -.toolbar-dropdown-menu { +.dropdown-menu { position: absolute; top: 100%; left: 0; @@ -307,17 +309,23 @@ main { min-width: 9rem; } -.toolbar-dropdown-menu.is-open { +.dropdown-menu.align-right { + left: auto; + right: 0; +} + +.dropdown-menu.is-open { display: block; } -.toolbar-dropdown-item { +.dropdown-item { display: block; width: 100%; text-align: left; border: none; border-radius: 0; - padding: 0.2rem 0.5rem; + padding: 0.3rem 0.75rem; + white-space: nowrap; } /* === Edit form === */ @@ -451,6 +459,63 @@ hr { display: none; } +/* === Modal === */ +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 1rem; +} + +.modal { + background: var(--bg-panel); + border: 1px solid var(--secondary); + width: 100%; + max-width: 500px; + display: flex; + flex-direction: column; +} + +.modal-header { + padding: 0.6rem 1rem; + border-bottom: 1px dashed var(--secondary); + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text); +} + +.modal-body { + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.75rem; + word-break: break-word; +} + +.modal-footer { + padding: 0.6rem 1rem; + border-top: 1px dashed var(--secondary); + display: flex; + justify-content: space-between; + gap: 0.5rem; +} + +.modal-input { + width: 100%; + background: var(--bg); + border: 1px solid var(--secondary); + color: var(--text); + font: inherit; + font-family: "Iosevka Slab", monospace; + padding: 0.4rem 0.6rem; + outline: none; +} + /* === Responsive === */ @media (max-width: 1100px) { .toc-toggle { @@ -502,4 +567,12 @@ hr { .toc.is-open { width: calc(100% - 1.5rem); } + .modal-backdrop { + padding: 0.5rem; + align-items: flex-start; + } + .modal { + max-width: none; + margin-top: 1rem; + } }