The server renders the dialogs (?dialog=delete|move) and answers a move that needs confirmation with the merge dialog itself, so the client no longer builds action URLs or interprets X-Merge-Available. Successful mutations send HX-Redirect, which hx.js turns into a history replace. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
101 lines
3.5 KiB
JavaScript
101 lines
3.5 KiB
JavaScript
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 }
|
|
});
|
|
}
|
|
});
|
|
}
|