This commit is contained in:
mark committed 2026-09-04 10:51:29 -04:00
1 parent 4c59f077d5
commit c8dbeaeca0
87 files changed
+135 -1827

No files matched your search

+16
View File
@@ -66,3 +66,19 @@
}
.footnotes-title { font-size: 1rem; }
.marker { font-weight: 700; }
/* The navbar is sticky, so an element scrolled to the top of the viewport
lands underneath it. These two are everything an anchor in this app points
at -- chapter headings and verse numbers -- so the offset lives on them
rather than on a scroll container. Roughly the navbar's height. */
.chapter,
.v { scroll-margin-top: 4.5rem; }
/* The navbar's reference picker. form-select is width:100% by default, which
in a flex row means both selects try to take the whole navbar; and the
default right padding leaves the caret sitting on top of a three-digit
chapter number. */
.reference .form-select {
width: auto;
padding-right: 2rem;
}
+89 -1
View File
@@ -128,7 +128,93 @@
// Close the modal once a bookmark write has come back and swapped, leaving
// the status line up long enough to be read.
if (target.id === 'bookmark') return setTimeout(hideModal, 900);
if (target.id === 'content') land();
if (target.id === 'content') { fillReference(); land(); }
});
// -- reference picker ---------------------------------------------------
// The "GEN 1:1" in the navbar. Built from the book on the page rather than
// from a table shipped beside it: the chapters are the sections and a
// chapter's verses are the anchors inside it, so an option can never name a
// target that is not there to scroll to, and there is no second file to keep
// in step, cache, or version.
//
// It is not kept in step with the scroll position, on purpose. It says where
// you are going, not where you are, and it starts at 1:1 in every book.
function byId(id) {
return document.getElementById(id);
}
function fillOptions(select, items) {
select.textContent = '';
for (var i = 0; i < items.length; i++) {
var option = document.createElement('option');
// The value is the id of the element to land on, so nothing downstream
// has to know how a verse anchor is spelled.
option.value = items[i].value;
option.textContent = items[i].label;
select.appendChild(option);
}
}
function fillVerses(chapterId) {
var section = byId(chapterId);
var verses = section ? section.querySelectorAll('.v') : [];
var items = [];
for (var i = 0; i < verses.length; i++) {
items.push({ value: verses[i].id,
label: (verses[i].textContent || '').trim() });
}
fillOptions(byId('reference-verse'), items);
}
function fillReference() {
var picker = byId('reference');
if (!picker) return;
var book = document.querySelector('#content article.book');
picker.classList.toggle('d-none', !book);
picker.classList.toggle('d-flex', !!book);
if (!book) return;
byId('reference-book').textContent = book.getAttribute('data-book') || '';
var chapters = book.querySelectorAll('.chapter[data-chapter]');
var items = [];
for (var i = 0; i < chapters.length; i++) {
items.push({ value: chapters[i].id,
label: chapters[i].getAttribute('data-chapter') });
}
fillOptions(byId('reference-chapter'), items);
if (items.length) fillVerses(items[0].value);
}
// The URL is the whole mechanism: name the anchor, then land on it. Nothing
// is stored anywhere else, so a reload or a shared link ends up in the same
// place.
//
// replaceState rather than assigning location.hash: spinning through a
// select should not stack one history entry per option, and assigning the
// hash hands the scroll to the browser, which Bootstrap has set to smooth.
function goTo(id) {
var target = byId(id);
if (!target) return;
history.replaceState(null, '', '#' + id);
jumpTo(target);
}
// Delegated at the document, so it survives every swap and binds once.
// Changing the chapter rebuilds the verses and lands on the chapter heading,
// which is what 1 is directly below; changing the verse lands on the verse.
document.addEventListener('change', function (event) {
var target = event.target;
if (!target || !target.id) return;
if (target.id === 'reference-chapter') {
fillVerses(target.value);
goTo(target.value);
} else if (target.id === 'reference-verse') {
goTo(target.value);
}
});
// A history restore does not go through the request pipeline -- htmx swaps
@@ -137,6 +223,7 @@
// with dead verse numbers.
document.body.addEventListener('htmx:historyRestore', function () {
markVerses();
fillReference();
land();
});
@@ -202,6 +289,7 @@
// and it lands after htmx has processed the shell.
document.addEventListener('DOMContentLoaded', function () {
markVerses();
fillReference();
scrollToHash();
// Registered from the page it is going to serve, not from a page of its
+23
View File
@@ -76,6 +76,29 @@
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>
<button class="navbar-toggler" type="button"
data-bs-toggle="collapse" data-bs-target="#site-nav"
aria-controls="site-nav" aria-expanded="false"