Fix
This commit is contained in:
1 parent
ba3b588ac5
commit
d74385ca3c
2 files changed
+80
-24
No files matched your search
+63
-17
@@ -311,9 +311,19 @@ function verseLabels(chapters, number) {
|
|||||||
// no verse anchors to offer.
|
// no verse anchors to offer.
|
||||||
//
|
//
|
||||||
// anchor says which chapter to show the verses of, and which verse to mark
|
// 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
|
// selected. Anything this cannot read -- nothing at all, an empty select, a
|
||||||
// where you are going, not where you are, and the address bar is the only
|
// footnote id, a chapter past the end of the book -- is chapter 1, and chapter
|
||||||
// record of the latter.
|
// 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) {
|
function referenceFor(map, code, anchor) {
|
||||||
const chapters = map && Object.prototype.hasOwnProperty.call(map, code)
|
const chapters = map && Object.prototype.hasOwnProperty.call(map, code)
|
||||||
? map[code] : null;
|
? map[code] : null;
|
||||||
@@ -328,12 +338,15 @@ function referenceFor(map, code, anchor) {
|
|||||||
|
|
||||||
const at = parseAnchor(anchor) || { chapter: 1, verse: null };
|
const at = parseAnchor(anchor) || { chapter: 1, verse: null };
|
||||||
const number = at.chapter >= 1 && at.chapter <= chapters.length ? at.chapter : 1;
|
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 {
|
return {
|
||||||
book: code,
|
book: code,
|
||||||
chapter: 'ch' + number,
|
chapter: 'ch' + number,
|
||||||
chapters: chapters.map((_, i) => ({ id: 'ch' + (i + 1), label: String(i + 1) })),
|
chapters: chapters.map((_, i) => ({ id: 'ch' + (i + 1), label: String(i + 1) })),
|
||||||
verse: at.verse,
|
verse: verse,
|
||||||
verses: verseLabels(chapters, number)
|
verses: verseLabels(chapters, number)
|
||||||
.map((label) => ({ id: 'v' + number + '_' + label, label: label }))
|
.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.
|
// The picker, and the two moves it makes.
|
||||||
//
|
//
|
||||||
// no parameters the whole picker, for whichever book is on screen. Asked
|
// no parameters the whole picker, standing where the anchor in the address
|
||||||
// for by the picker itself, after a history restore -- which
|
// bar puts it. Asked for by the picker itself: on load,
|
||||||
// swaps the history element straight in and processes no
|
// after a history restore -- which swaps the history element
|
||||||
// out-of-band elements -- and when a worker first appears
|
// straight in and processes no out-of-band elements -- and
|
||||||
// under a page boot.html composed.
|
// when a worker first appears under a page boot.html
|
||||||
|
// composed.
|
||||||
// chapter=ch3 the verses of that chapter, and land on the chapter.
|
// 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.
|
// 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,
|
// Neither the book nor the anchor is in the URL, deliberately. Both come from
|
||||||
// which is the address the page is showing now rather than the address it was
|
// HX-Current-URL, which htmx sets from location.href on every request it
|
||||||
// showing when the picker was last rendered. The picker sits in the navbar and
|
// makes: the whole address, fragment included.
|
||||||
// outlives the page under it, so anything baked into its attributes is the one
|
//
|
||||||
// thing here that can go stale.
|
// 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
|
// 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:
|
// 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.
|
// history entry per option spun past.
|
||||||
router.get('/api/reference', async (call) => {
|
router.get('/api/reference', async (call) => {
|
||||||
const params = call.url.searchParams;
|
const params = call.url.searchParams;
|
||||||
const anchor = params.get('verse') || params.get('chapter');
|
|
||||||
const page = call.currentUrl();
|
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
|
// 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
|
// 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.
|
// than swapping in options for a book that is no longer on screen.
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
declares .d-flex after Reboot's [hidden] rule, both !important and at the
|
declares .d-flex after Reboot's [hidden] rule, both !important and at the
|
||||||
same specificity, so the utility would win.
|
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:
|
never the swap target -- every boosted link lands in #content:
|
||||||
|
|
||||||
a navigation sw.js appends this template to the fragment with oob set,
|
a navigation sw.js appends this template to the fragment with oob set,
|
||||||
@@ -28,18 +28,28 @@
|
|||||||
line, so one response moves both.
|
line, so one response moves both.
|
||||||
a restore htmx swaps the history element straight in and processes
|
a restore htmx swaps the history element straight in and processes
|
||||||
no out-of-band elements at all, so the picker asks for
|
no out-of-band elements at all, so the picker asks for
|
||||||
itself instead. worker-ready is in the same trigger for
|
itself instead. worker-ready is in the same trigger, for a
|
||||||
the empty state below.
|
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
|
None of them carries the book or the anchor. The request does, in
|
||||||
address the page is actually showing. An address baked into this attribute
|
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
|
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"
|
<div class="reference navbar-text {% if reference %}d-flex{% else %}d-none{% endif %} align-items-center gap-1 ms-3"
|
||||||
id="reference"
|
id="reference"
|
||||||
{%- if oob %} hx-swap-oob="true"{% endif %}
|
{%- if oob %} hx-swap-oob="true"{% endif %}
|
||||||
hx-get="api/reference"
|
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">
|
hx-target="this" hx-swap="outerHTML">
|
||||||
{%- if reference %}
|
{%- if reference %}
|
||||||
<span class="reference-book text-light fw-semibold"
|
<span class="reference-book text-light fw-semibold"
|
||||||
|
|||||||
Reference in new issue
Block a user