Move verse selector to htmx

This commit is contained in:
mark committed 2026-09-07 08:05:45 -04:00
1 parent dd0021ce15
commit ba3b588ac5
9 files changed
+394 -135

No files matched your search

+4 -25
View File
@@ -1,7 +1,8 @@
{# The shell every page is wrapped in. Rendered by sw.js, not by the build,
so editing this file changes every page without regenerating any of them.
Context: title, content (safe HTML fragment), mark (bookmark or null).
Context: title, content (safe HTML fragment), mark (bookmark or null),
reference (the navbar picker's model, or null).
Bootstrap is the whole stylesheet. The fragments carry semantic USFM
classes and no Bootstrap ones, so the body text is styled by Reboot; the
@@ -76,29 +77,7 @@
data-bs-theme="dark">
<div class="container-xl">
<a class="navbar-brand" href="./">Bible</a>
{# The reference picker: which book you are in, and where in it to go.
Outside the collapse deliberately -- it is the main way of moving around
a book now that the chapter list is gone from the page, and a chapter
picker behind a hamburger is no use on a phone.
Empty here and filled by app.js from the book on the page. The verse
list has to be rebuilt in the browser every time the chapter changes, so
there is nothing for a template to usefully render first.
d-none for the empty state rather than the hidden attribute: Bootstrap
declares .d-flex after Reboot's [hidden] rule, both !important and at
the same specificity, so the utility would win. #}
<div class="reference navbar-text d-none align-items-center gap-1 ms-3"
id="reference">
<span class="reference-book text-light fw-semibold" id="reference-book"></span>
<label class="visually-hidden" for="reference-chapter">Chapter</label>
<select class="reference-chapter form-select form-select-sm"
id="reference-chapter"></select>
<span class="reference-sep text-light">:</span>
<label class="visually-hidden" for="reference-verse">Verse</label>
<select class="reference-verse form-select form-select-sm"
id="reference-verse"></select>
</div>
{% include "reference.njk" %}
<button class="navbar-toggler" type="button"
data-bs-toggle="collapse" data-bs-target="#site-nav"
aria-controls="site-nav" aria-expanded="false"
@@ -129,7 +108,7 @@
</main>
{# Chrome, so it is written once here instead of built in JavaScript on every
page. app.js only fills in the reference and shows it. #}
page. app.js only fills in the verse it was opened for and shows it. #}
<div class="modal fade" id="verse-dialog" tabindex="-1"
aria-labelledby="verse-dialog-ref" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
+74
View File
@@ -0,0 +1,74 @@
{# The navbar's reference picker: which book you are in, and where in it to go.
Context: reference (or null), oob.
reference is built by sw.js from bible/verses.json, which compile_bible.py
writes from the same tree it renders the books from -- so an option can
never name an anchor that is not in the page it lands on.
It used to be built in the browser, by app.js, reading the chapters and
verses off the book already on screen. That was free, and it only worked
where there was a document to read: this shell is composed in a service
worker, which has no DOM, before any document exists. What the picker needs
is a fact on disk now, and everything below is markup and htmx.
Outside the collapse deliberately -- it is the main way of moving around a
book now that the chapter list is gone from the page, and a chapter picker
behind a hamburger is no use on a phone.
d-none for the empty state rather than the hidden attribute: Bootstrap
declares .d-flex after Reboot's [hidden] rule, both !important and at the
same specificity, so the utility would win.
Two things bring it back into step with the page, because the navbar is
never the swap target -- every boosted link lands in #content:
a navigation sw.js appends this template to the fragment with oob set,
the same way a bookmark write carries the modal's status
line, so one response moves both.
a restore htmx swaps the history element straight in and processes
no out-of-band elements at all, so the picker asks for
itself instead. worker-ready is in the same trigger for
the empty state below.
Neither carries the book: the request does, in HX-Current-URL, which is the
address the page is actually showing. An address baked into this attribute
is the one thing here that can go stale, because the picker outlives the
page it was rendered for. #}
<div class="reference navbar-text {% if reference %}d-flex{% else %}d-none{% endif %} align-items-center gap-1 ms-3"
id="reference"
{%- if oob %} hx-swap-oob="true"{% endif %}
hx-get="api/reference"
hx-trigger="worker-ready from:body, htmx:historyRestore from:body"
hx-target="this" hx-swap="outerHTML">
{%- if reference %}
<span class="reference-book text-light fw-semibold"
id="reference-book">{{ reference.book }}</span>
<label class="visually-hidden" for="reference-chapter">Chapter</label>
{# The value is the id of the element to land on, so nothing downstream has
to know how a verse anchor is spelled -- including the worker, which
reads the chapter back out of the id it sent.
hx-target is spelled out on both selects because the one on the div above
is "this", and an inherited "this" would resolve to the select. #}
<select class="reference-chapter form-select form-select-sm"
id="reference-chapter" name="chapter"
hx-get="api/reference" hx-target="#reference-verse"
hx-swap="innerHTML">
{%- for chapter in reference.chapters %}
<option value="{{ chapter.id }}"
{%- if chapter.id == reference.chapter %} selected{% endif %}>{{ chapter.label }}</option>
{%- endfor %}
</select>
<span class="reference-sep text-light">:</span>
<label class="visually-hidden" for="reference-verse">Verse</label>
{# Answers with its own options again rather than nothing, so the response
has something to hang the scroll on -- and so the option the reader chose
comes back marked selected instead of being reset by the swap. #}
<select class="reference-verse form-select form-select-sm"
id="reference-verse" name="verse"
hx-get="api/reference" hx-target="this" hx-swap="innerHTML">
{% include "verses.njk" %}
</select>
{%- endif %}
</div>
+7
View File
@@ -0,0 +1,7 @@
{# The verse select's options. Rendered into the picker by reference.njk, and
returned on their own by api/reference after a chapter or verse change.
Both paths must produce the same options for the swap to be seamless. #}
{% for verse in reference.verses -%}
<option value="{{ verse.id }}"
{%- if verse.id == reference.verse %} selected{% endif %}>{{ verse.label }}</option>
{% endfor -%}