This commit is contained in:
mark committed 2026-09-08 08:24:21 -04:00
1 parent 9407c26d2d
commit bfb3c87558
5 files changed
+204 -129

No files matched your search

+54 -25
View File
@@ -100,39 +100,68 @@
<div class="row justify-content-center">
{# The swap target. hx-target on <body> names this id, so every boosted
link lands here -- remove it and a click does nothing at all while a
full navigation still works. #}
<div id="content" class="col-12 col-xl-8" hx-history-elt>
full navigation still works.
It is also where a verse click becomes a request. htmx's bootstrap-modal
example puts hx-get on the element that opens the dialog; the element
here is a verse number in a generated book, and there are 38,000 of
them -- an attribute apiece would cost more than the whole shell, and a
listener apiece more than the reading. So one handler sits on the
container they all swap into, and names the verse it found. Delegated,
so it survives every swap: this element is never replaced, only filled.
hx-vals cannot do this. htmx evaluates it as Function("return (...)")()
with nothing passed in, so an expression there can read the document but
not the click -- and which verse was clicked is the one thing that has
to travel.
Enter and space go through the same door rather than a second one:
markVerses() in app.js gives every verse role=button and tabindex, and
a button that answers the keyboard by being clicked is a button with one
behaviour instead of two. #}
<div id="content" class="col-12 col-xl-8" hx-history-elt
hx-on:click="var verse = event.target.closest('.v[id]');
if (verse) htmx.ajax('GET', 'api/verse?at=' + verse.id,
{ target: '#verse-dialog .modal-content',
swap: 'innerHTML' });"
hx-on:keydown="if (event.key !== 'Enter' &amp;&amp; event.key !== ' ') return;
if (!event.target.matches('.v[id]')) return;
event.preventDefault();
event.target.click();">
{% block content %}{% endblock %}
</div>
</div>
{% include "version.njk" %}
</main>
{# Chrome, so it is written once here instead of built in JavaScript on every
page. app.js only fills in the verse it was opened for and shows it. #}
{# The verse dialog: empty in the page, filled per request. Bootstrap owns
showing it, dismissing it, the backdrop and Escape; sw.js owns everything
inside, and templates/verse.njk is what lands in the modal-content below.
Opened by the arrival of the content rather than by data-bs-toggle on
whatever was clicked. The example puts that attribute on its trigger, which
is not available here for the same reason hx-get is not -- see #content
above -- and the swap is the honest signal anyway: there is nothing worth
showing until it is here. A verse the corpus does not have is answered 204,
which swaps nothing, which opens nothing.
after-swap and not after-settle: settling is the 20ms attribute pass and
this dialog has nothing to settle. Only the modal-content swap can reach
here -- the bookmark write targets the navbar slot, and the status line it
sends back rides in out of band, which fires its own event and not this one.
bookmark-saved comes from sw.js once the write has committed, and only then:
a dialog that closed itself on a failed write would take the message
explaining the failure with it. #}
<div class="modal fade" id="verse-dialog" tabindex="-1"
aria-labelledby="verse-dialog-ref" aria-hidden="true">
aria-labelledby="verse-dialog-ref" aria-hidden="true"
hx-on::after-swap="bootstrap.Modal.getOrCreateInstance(this).show();"
hx-on:bookmark-saved="var dialog = this;
setTimeout(function () {
bootstrap.Modal.getOrCreateInstance(dialog).hide();
}, 900);">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title fs-5 verse-dialog-ref" id="verse-dialog-ref"></h2>
<button type="button" class="btn-close" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">
{% include "status.njk" %}
</div>
<div class="modal-footer">
<form class="verse-dialog-actions d-flex flex-wrap gap-2 m-0"
hx-post="api/bookmark" hx-target="#bookmark" hx-swap="outerHTML">
<input type="hidden" name="ref" value="">
<input type="hidden" name="href" value="">
<button class="btn btn-outline-secondary" type="button"
data-action="copy">Copy link</button>
<button class="btn btn-primary" type="submit">Set bookmark</button>
</form>
</div>
</div>
<div class="modal-content"></div>
</div>
</div>