From c8dbeaeca04b3767df71564406fb205f9e65d44f Mon Sep 17 00:00:00 2001 From: Mark Wissink Date: Fri, 4 Sep 2026 10:51:29 -0400 Subject: [PATCH] BETTER --- assets/css/app.css | 16 ++++ assets/js/app.js | 90 +++++++++++++++++++++- assets/templates/base.njk | 23 ++++++ books/1CH.html | 34 --------- books/1CO.html | 21 ------ books/1ES.html | 14 ---- books/1JN.html | 10 --- books/1KI.html | 27 ------- books/1MA.html | 21 ------ books/1PE.html | 10 --- books/1SA.html | 36 --------- books/1TH.html | 10 --- books/1TI.html | 11 --- books/2CH.html | 41 ---------- books/2CO.html | 18 ----- books/2ES.html | 21 ------ books/2JN.html | 6 -- books/2KI.html | 30 -------- books/2MA.html | 20 ----- books/2PE.html | 8 -- books/2SA.html | 29 ------- books/2TH.html | 8 -- books/2TI.html | 9 --- books/3JN.html | 6 -- books/3MA.html | 12 --- books/4MA.html | 23 ------ books/ACT.html | 33 -------- books/AMO.html | 14 ---- books/BAR.html | 11 --- books/COL.html | 9 --- books/DAG.html | 19 ----- books/DAN.html | 17 ----- books/DEU.html | 39 ---------- books/ECC.html | 17 ----- books/EPH.html | 11 --- books/ESG.html | 15 ---- books/EST.html | 15 ---- books/EXO.html | 45 ----------- books/EZK.html | 53 ------------- books/EZR.html | 15 ---- books/FRT.html | 6 -- books/GAL.html | 11 --- books/GEN.html | 55 -------------- books/GLO.html | 6 -- books/HAB.html | 8 -- books/HAG.html | 7 -- books/HEB.html | 18 ----- books/HOS.html | 19 ----- books/ISA.html | 71 ----------------- books/JAS.html | 10 --- books/JDG.html | 26 ------- books/JDT.html | 21 ------ books/JER.html | 57 -------------- books/JHN.html | 26 ------- books/JOB.html | 47 ------------ books/JOL.html | 8 -- books/JON.html | 9 --- books/JOS.html | 29 ------- books/JUD.html | 6 -- books/LAM.html | 10 --- books/LEV.html | 32 -------- books/LUK.html | 29 ------- books/MAL.html | 9 --- books/MAN.html | 6 -- books/MAT.html | 33 -------- books/MIC.html | 12 --- books/MRK.html | 21 ------ books/NAM.html | 8 -- books/NEH.html | 18 ----- books/NUM.html | 41 ---------- books/OBA.html | 6 -- books/PHM.html | 6 -- books/PHP.html | 9 --- books/PRO.html | 36 --------- books/PS2.html | 6 -- books/PSA.html | 155 -------------------------------------- books/REV.html | 27 ------- books/ROM.html | 21 ------ books/RUT.html | 9 --- books/SIR.html | 56 -------------- books/SNG.html | 13 ---- books/TIT.html | 8 -- books/TOB.html | 19 ----- books/WIS.html | 24 ------ books/ZEC.html | 19 ----- books/ZEP.html | 8 -- render_books.py | 14 ++-- 87 files changed, 135 insertions(+), 1827 deletions(-) diff --git a/assets/css/app.css b/assets/css/app.css index fdadbed..291b653 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -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; +} diff --git a/assets/js/app.js b/assets/js/app.js index a8593fd..11a3455 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -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 diff --git a/assets/templates/base.njk b/assets/templates/base.njk index f89d486..6c2c76f 100644 --- a/assets/templates/base.njk +++ b/assets/templates/base.njk @@ -76,6 +76,29 @@ data-bs-theme="dark">
Bible + {# 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. #} +