Serve delete, move and merge dialogs as htmx fragments
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>
This commit is contained in:
+7
-82
@@ -75,45 +75,10 @@ function newPage() {
|
||||
});
|
||||
}
|
||||
|
||||
// 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 = '/';
|
||||
var parent = '/' + current.split('/').filter(Boolean).slice(0, -1).join('/');
|
||||
|
||||
openTreePicker({
|
||||
title: 'Move — new parent?',
|
||||
@@ -123,53 +88,13 @@ function movePage() {
|
||||
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;
|
||||
|
||||
openModal({
|
||||
title: 'Move — new name?',
|
||||
body: input,
|
||||
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);
|
||||
var target = encodePickedPath(dest) + '/';
|
||||
closeModal();
|
||||
submitMove(action, target);
|
||||
}
|
||||
}
|
||||
// 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 }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
{{/* Server-rendered dialogs, fetched with ?dialog=<name> and swapped into
|
||||
#modal-host by htmx; modal.js supplies the chrome (backdrop, Escape, drag,
|
||||
focus). Each dialog is a form that posts the action it offers, so the client
|
||||
never builds action URLs itself. Responses to those posts either redirect
|
||||
(HX-Redirect), replace the dialog with the next step (e.g. the merge
|
||||
confirmation), or fail with a plain-text error. */}}
|
||||
|
||||
{{define "delete"}}<div class="modal-backdrop">
|
||||
<form class="modal panel panel-floating" role="dialog" aria-modal="true" aria-labelledby="modal-title" hx-post="?delete=1" hx-target="#modal-host">
|
||||
<div class="panel-header"><span id="modal-title">Delete page</span></div>
|
||||
<div class="panel-body">Delete {{.Path}} and everything inside it?</div>
|
||||
<div class="panel-footer">
|
||||
<button class="btn danger" type="submit">DELETE</button>
|
||||
<button class="btn" type="button" data-modal-close autofocus>CANCEL</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>{{end}}
|
||||
|
||||
{{define "move"}}<div class="modal-backdrop">
|
||||
<form class="modal panel panel-floating" role="dialog" aria-modal="true" aria-labelledby="modal-title" hx-post="?move" hx-target="#modal-host">
|
||||
<div class="panel-header"><span id="modal-title">Move — new name?</span></div>
|
||||
<div class="panel-body">
|
||||
<input type="hidden" name="parent" value="{{.Parent}}" />
|
||||
<input class="input" type="text" name="name" value="{{.Name}}" placeholder="Page name" required autofocus />
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<button class="btn" type="button" data-modal-close>CANCEL</button>
|
||||
<button class="btn" type="submit">MOVE</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>{{end}}
|
||||
|
||||
{{define "merge"}}<div class="modal-backdrop">
|
||||
<form class="modal panel panel-floating" role="dialog" aria-modal="true" aria-labelledby="modal-title" hx-post="?move" hx-target="#modal-host">
|
||||
<div class="panel-header"><span id="modal-title">Merge folders?</span></div>
|
||||
<div class="panel-body">
|
||||
The destination folder already exists but has no page of its own. Merge this page and its contents into it?
|
||||
<input type="hidden" name="move" value="{{.Dest}}" />
|
||||
<input type="hidden" name="merge" value="1" />
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<button class="btn" type="button" data-modal-close>CANCEL</button>
|
||||
<button class="btn" type="submit" autofocus>MERGE</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>{{end}}
|
||||
@@ -5,7 +5,7 @@
|
||||
<button class="btn btn-block" data-companion-reveal hidden title="Reveal in file manager"><span class="btn-icon">{{icon "reveal"}}</span><span class="btn-label">REVEAL</span></button>
|
||||
{{if not .IsRoot}}
|
||||
<button class="btn btn-block" onclick="movePage()" title="Move page (M)"><span class="btn-icon">{{icon "move"}}</span><span class="btn-label">MOVE</span></button>
|
||||
<button class="btn btn-block danger" onclick="deletePage()" title="Delete page"><span class="btn-icon">{{icon "delete"}}</span><span class="btn-label">DELETE</span></button>
|
||||
<button class="btn btn-block danger" hx-get="?dialog=delete" hx-target="#modal-host" title="Delete page"><span class="btn-icon">{{icon "delete"}}</span><span class="btn-label">DELETE</span></button>
|
||||
{{end}}
|
||||
<a class="btn btn-block" href="?edit" title="Edit page (E)"><span class="btn-icon">{{icon "edit"}}</span><span class="btn-label">EDIT</span></a>
|
||||
{{end}}{{end}}
|
||||
|
||||
Reference in New Issue
Block a user