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'; }); } }); } // submitMove POSTs a move and navigates on success. When the server reports // the destination folder already exists but can be merged (the source carries // a page and the destination has none), it asks the user to confirm and // retries the same move with &merge=1. function submitMove(action, target) { fetch(action, { method: 'POST', redirect: 'manual' }).then(function (res) { if (res.type === 'opaqueredirect' || res.ok) { navigateReplace(target); return; } if (res.status === 409 && res.headers.get('X-Merge-Available') === '1') { openModal({ title: 'Merge folders?', body: 'The destination folder already exists but has no page of its own. Merge this page and its contents into it?', confirm: { label: 'MERGE', onConfirm: function () { closeModal(); submitMove(action + '&merge=1', target); } } }); return; } return res.text().then(function (msg) { alert(msg || ('Request failed (' + res.status + ')')); }); }).catch(function () { alert('Network error'); }); } function movePage() { var current = decodeURIComponent(window.location.pathname).replace(/\/+$/, ''); if (!current) return; var segs = current.split('/').filter(Boolean); var currentName = segs[segs.length - 1] || ''; var parent = '/' + segs.slice(0, -1).join('/'); if (parent === '/') parent = '/'; openTreePicker({ title: 'Move — new parent?', mode: 'folder', initialPath: parent, preselect: parent, hideFiles: true, confirmLabel: 'NEXT', onSelect: function (newParent) { var input = document.createElement('input'); input.type = 'text'; input.className = 'input'; input.placeholder = 'Page name'; input.value = currentName; var linksCheckbox = document.createElement('input'); linksCheckbox.type = 'checkbox'; linksCheckbox.id = 'move-update-links'; var linksLabel = document.createElement('label'); linksLabel.htmlFor = linksCheckbox.id; linksLabel.className = 'row'; linksLabel.appendChild(linksCheckbox); linksLabel.appendChild(document.createTextNode('Update links')); var body = document.createDocumentFragment(); body.appendChild(input); body.appendChild(linksLabel); openModal({ title: 'Move — new name?', body: body, confirm: { label: 'MOVE', onConfirm: function () { var name = input.value.trim(); if (!name) return; var dest = (newParent === '/' ? '' : newParent) + '/' + name; var action = window.location.pathname + '?move=' + encodeURIComponent(dest); if (linksCheckbox.checked) action += '&links=1'; var target = encodePickedPath(dest) + '/'; closeModal(); submitMove(action, target); } } }); } }); } function deletePage() { 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 p = window.location.pathname.replace(/\/+$/, ''); var idx = p.lastIndexOf('/'); var parent = idx > 0 ? p.substring(0, idx + 1) : '/'; closeModal(); postReplace(window.location.pathname + '?delete=1', null, parent); } }, cancel: { autofocus: true }, swapButtons: true }); }