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}} +