function encodePickedPath(p) { if (p === '/' || p === '') return '/'; return '/' + p.replace(/^\/+/, '').split('/').map(encodeURIComponent).join('/'); } // postReplace POSTs to action with the optional form body, then loads target // into the current history entry — so the action and its result occupy one // entry instead of two, and back-navigation skips past the stale pre-mutation // snapshot in bfcache. body may be null for empty POSTs. // // We can't just call window.location.replace(target): when target differs from // the current URL only by fragment, the browser updates the URL bar without // re-fetching, so a server-side mutation wouldn't be reflected. Instead, // rewrite the current entry's URL via history.replaceState, then reload — the // reload always re-fetches and preserves the (new) URL including its fragment. function navigateReplace(target) { window.history.replaceState(null, '', target); window.location.reload(); } function postReplace(action, body, target) { var init = { method: 'POST', redirect: 'manual' }; if (body) { init.headers = { 'Content-Type': 'application/x-www-form-urlencoded' }; init.body = body; } fetch(action, init).then(function (res) { if (res.type === 'opaqueredirect' || res.ok) { navigateReplace(target); return; } return res.text().then(function (msg) { alert(msg || ('Request failed (' + res.status + ')')); }); }).catch(function () { alert('Network error'); }); } function promptPageName(title, initial, confirmLabel, onName) { var input = document.createElement('input'); input.type = 'text'; input.className = 'input'; input.placeholder = 'Page name'; if (initial) input.value = initial; openModal({ title: title, body: input, confirm: { label: confirmLabel, onConfirm: function () { var name = input.value.trim(); if (!name) return; onName(name); } } }); } function newPage() { var current = decodeURIComponent(window.location.pathname).replace(/\/+$/, '') || '/'; openTreePicker({ title: 'New page — where?', mode: 'folder', initialPath: current, preselect: current, hideFiles: true, confirmLabel: 'NEXT', onSelect: function (parentPath) { promptPageName('New page — name?', '', 'CREATE', function (name) { var base = parentPath === '/' ? '/' : encodePickedPath(parentPath) + '/'; window.location.href = base + encodeURIComponent(name) + '/?edit'; }); } }); } function movePage() { var current = decodeURIComponent(window.location.pathname).replace(/\/+$/, ''); if (!current) return; var parent = '/' + current.split('/').filter(Boolean).slice(0, -1).join('/'); openTreePicker({ title: 'Move — new parent?', mode: 'folder', initialPath: parent, preselect: parent, hideFiles: true, confirmLabel: 'NEXT', onSelect: function (newParent) { // The server renders the name step and handles what follows // (merge confirmation, errors, redirect). closeModal(); htmx.ajax('GET', window.location.pathname + '?dialog=move', { target: '#modal-host', values: { parent: newParent } }); } }); }