This commit is contained in:
mark committed 2026-09-07 08:22:43 -04:00
1 parent ba3b588ac5
commit d74385ca3c
2 files changed
+80 -24

No files matched your search

+63 -17
View File
@@ -311,9 +311,19 @@ function verseLabels(chapters, number) {
// no verse anchors to offer.
//
// anchor says which chapter to show the verses of, and which verse to mark
// selected. Null starts at 1:1, which is where every book starts: this says
// where you are going, not where you are, and the address bar is the only
// record of the latter.
// selected. Anything this cannot read -- nothing at all, an empty select, a
// footnote id, a chapter past the end of the book -- is chapter 1, and chapter
// 1 is filled in as if it had been picked. There is no state where a chapter
// is only sort of chosen: a select showing an option is a select that chose
// it, and the verses beside it have to be that chapter's.
//
// Every book that gets a picker has verses in its first chapter -- the two
// that do not, the front matter and the glossary, are turned away above -- so
// falling back to 1 always fills the verse list rather than emptying it.
//
// The verse is left unselected when the anchor names only a chapter. That is
// not the same as unfilled: the options are chapter 1's, and the select shows
// the first of them. It says where you are going, not where you are.
function referenceFor(map, code, anchor) {
const chapters = map && Object.prototype.hasOwnProperty.call(map, code)
? map[code] : null;
@@ -328,12 +338,15 @@ function referenceFor(map, code, anchor) {
const at = parseAnchor(anchor) || { chapter: 1, verse: null };
const number = at.chapter >= 1 && at.chapter <= chapters.length ? at.chapter : 1;
// Only when the anchor got as far as naming this chapter. A fallback to 1
// has not chosen a verse, it has just run out of anchor.
const verse = at.chapter === number ? at.verse : null;
return {
book: code,
chapter: 'ch' + number,
chapters: chapters.map((_, i) => ({ id: 'ch' + (i + 1), label: String(i + 1) })),
verse: at.verse,
verse: verse,
verses: verseLabels(chapters, number)
.map((label) => ({ id: 'v' + number + '_' + label, label: label }))
};
@@ -415,19 +428,30 @@ router.get('/api/bookmark', () => bookmarkFragment(''));
// The picker, and the two moves it makes.
//
// no parameters the whole picker, for whichever book is on screen. Asked
// for by the picker itself, after a history restore -- which
// swaps the history element straight in and processes no
// out-of-band elements -- and when a worker first appears
// under a page boot.html composed.
// no parameters the whole picker, standing where the anchor in the address
// bar puts it. Asked for by the picker itself: on load,
// after a history restore -- which swaps the history element
// straight in and processes no out-of-band elements -- and
// when a worker first appears under a page boot.html
// composed.
// chapter=ch3 the verses of that chapter, and land on the chapter.
// verse=v3_16 the same verses, that one selected, and land on the verse.
//
// The book is not in the URL, deliberately: it comes from HX-Current-URL,
// which is the address the page is showing now rather than the address it was
// showing when the picker was last rendered. The picker sits in the navbar and
// outlives the page under it, so anything baked into its attributes is the one
// thing here that can go stale.
// Neither the book nor the anchor is in the URL, deliberately. Both come from
// HX-Current-URL, which htmx sets from location.href on every request it
// makes: the whole address, fragment included.
//
// For the book, because the picker sits in the navbar and outlives the page
// under it -- an address baked into its attributes is the one thing here that
// can go stale. For the anchor, because this is the only way a fragment
// reaches this file at all. A browser opening /GEN#v3_16 asks for /GEN and
// keeps the rest to itself, so the shell is composed without it and the picker
// is rendered at 1:1 like any other. The page then asks this route for itself,
// and that request is the one carrying the anchor.
//
// Which also puts the picker where it belongs after a back or forward, and
// under a page boot.html composed once a worker turns up, for nothing extra:
// those requests carry the address bar too.
//
// What the response headers are doing is the whole of what app.js used to do
// on a change event, and htmx has a name for each part of it:
@@ -442,11 +466,33 @@ router.get('/api/bookmark', () => bookmarkFragment(''));
// history entry per option spun past.
router.get('/api/reference', async (call) => {
const params = call.url.searchParams;
const anchor = params.get('verse') || params.get('chapter');
const page = call.currentUrl();
const reference = await referenceOf(bookOf(page), anchor);
// Which select the reader moved, if either did -- by whether the parameter
// is there, not by whether it says anything. A select posts an empty value
// when it has no options, and reading that as "no select moved" answers it
// with a whole picker: htmx swaps a <div> into a <select>, which renders as
// nothing, and the select is then empty for good -- every change after that
// posts the same empty value and gets the same answer.
//
// An empty value is still an answerable question. It names no chapter, so it
// gets chapter 1, which is what everything here does with an anchor it
// cannot read.
const moved = params.has('verse') ? params.get('verse')
: params.has('chapter') ? params.get('chapter')
: null;
if (!anchor) return view.render('reference.njk', { reference: reference });
// The address bar is the fallback, and only that: once a select has been
// used it is the one saying where to go, and HX-Replace-Url has not caught
// up yet.
const reference = await referenceOf(
bookOf(page), moved === null ? (page && page.hash.slice(1)) : moved);
// settled, because this picker is already standing where the anchor put it.
// The one that came with the page is not, and carries a load trigger to ask
// -- which this answer must not carry as well, or it would ask forever.
if (moved === null) {
return view.render('reference.njk', { reference: reference, settled: true });
}
// The options were rendered from this map, so failing to parse them back
// means the map moved under an open page: leave the picker alone rather
// than swapping in options for a book that is no longer on screen.
+17 -7
View File
@@ -20,7 +20,7 @@
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
Three 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,
@@ -28,18 +28,28 @@
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.
itself instead. worker-ready is in the same trigger, for a
page boot.html composed before there was a worker to ask.
an anchor a link to GEN#v3_16 should open with the picker reading
3:16, and the fragment never reaches the worker: a browser
asks for /GEN and keeps the rest. So a picker that arrived
with the page asks once, on load, if there is an anchor to
ask about.
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
None of them carries the book or the anchor. The request does, in
HX-Current-URL, which htmx sets from location.href -- the address the page
is actually showing, fragment and all. Baking either into these attributes
is the one thing here that can go stale, because the picker outlives the
page it was rendered for. #}
page it was rendered for.
settled is what stops the load trigger looping: the answer to it is a
picker already standing in the right place, and it says so by leaving the
trigger out of the copy it sends back. #}
<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-trigger="{% if reference and not settled %}load[location.hash], {% endif %}worker-ready from:body, htmx:historyRestore from:body"
hx-target="this" hx-swap="outerHTML">
{%- if reference %}
<span class="reference-book text-light fw-semibold"