This commit is contained in:
mark committed 2026-09-09 10:12:39 -04:00
1 parent ce73c76126
commit 84c07a912b
5 files changed
+44 -133

No files matched your search

+17 -112
View File
@@ -2,13 +2,14 @@
//
// Pages arrive whole -- composed by the service worker, or by boot.html on the
// visit before there is one -- so there is nothing here that builds a page.
// What is left is the four things a composed document cannot do for itself:
// mark up the verse numbers as controls, put a swapped-in page where it
// belongs before it is painted, land the reference picker where the reader
// asked to go, and tell the chrome when a worker has appeared to answer it.
// What is left is the three things a composed document cannot do for itself:
// put a swapped-in page where it belongs before it is painted, land the
// reference picker where the reader asked to go, and tell the chrome when a
// worker has appeared to answer it.
//
// Two things used to live here and no longer do, both for the same reason:
// they were reading the page to work out something the worker already knew.
// Three things used to live here and no longer do, all for the same reason:
// they were reading the page to work out something that was already known
// somewhere better.
//
// The reference picker was built by reading the chapters and verses off the
// book on screen. It is markup and htmx now: sw.js renders it from
@@ -24,39 +25,18 @@
// delegated because the trigger is one of 38,000 generated verse numbers and
// cannot carry attributes of its own. See templates/verse.njk.
//
// What is left of the dialog here is markVerses, and only because the numbers
// have to look and behave like controls before anyone can click one.
// The third was marking those numbers as controls: role, tabindex and an
// aria-label, set on every verse on load, after every swap and after every
// history restore -- a walk over a whole book, three times over, to arrive at
// the same three attributes every time. They are in the markup now, written
// by compile_bible.py, because nothing about them depends on anything only the
// browser knows. It costs the corpus a couple of megabytes and saves the pass.
(function () {
'use strict';
// -- verse numbers ------------------------------------------------------
// Applied here rather than baked into the HTML: without this script the
// numbers are plain text, and announcing them as buttons would lie. It also
// earns Reboot's [role=button]{cursor:pointer}. Runs again after every htmx
// swap, because the new verses arrive bare.
function markVerses(root) {
var verses = (root || document).querySelectorAll('.chapter .v');
for (var i = 0; i < verses.length; i++) {
verses[i].setAttribute('role', 'button');
verses[i].setAttribute('tabindex', '0');
verses[i].setAttribute('aria-label',
'Verse ' + (verses[i].textContent || '').trim() + ', options');
}
}
// Delegated at the document, so it survives swaps and is bound only once.
// The verse numbers and the dialog's own buttons are not here: opening the
// dialog is an hx-on handler on #content, and everything inside it is
// markup that sw.js sends. This is the reset button and nothing else.
document.addEventListener('click', function (event) {
if (!event.target.closest) return;
var reset = event.target.closest('#app-reset');
if (reset) resetApp(reset);
});
// -- events -------------------------------------------------------------
document.body.addEventListener('htmx:afterSwap', function (event) {
markVerses(event.target);
var target = event.detail.target;
if (!target) return;
// Both selects answer into the verse list, so this is either of them.
@@ -67,10 +47,10 @@
// A history restore does not go through the request pipeline -- htmx swaps
// the element straight in and fires this instead -- so the work afterSwap
// does has to be repeated here or a page reached by the back button arrives
// with dead verse numbers. The picker listens for the same event itself,
// from the navbar, because a restore reaches nothing outside #content.
// scrolled to wherever the last one was. The picker listens for the same
// event itself, from the navbar, because a restore reaches nothing outside
// #content.
document.body.addEventListener('htmx:historyRestore', function () {
markVerses();
land();
showVersion();
});
@@ -195,80 +175,6 @@
el.classList.toggle('d-none', !document.querySelector('#content .index'));
}
// -- reset --------------------------------------------------------------
// Everything this app has put on the device, cleared in the order that makes
// the next load a first visit.
//
// The worker goes first, so that nothing is left answering out of a cache
// while the caches themselves are being deleted. Unregistering does not stop
// the copy already controlling this page -- an active worker keeps its
// clients until they unload -- which is what the reload at the end is for:
// the registration is gone by then, the navigation reaches server.py, and
// boot.html registers a new worker against whatever is on disk now.
//
// Then the caches, both of them, by whatever names they carry: the point of
// this button is the case where the names are not the ones this build would
// have chosen. Then the bookmark, which is the only thing here a reader
// would miss -- hence the question first.
//
// One after another rather than all at once, so that the order above is the
// order it happens in, and so that a step cannot be reading storage another
// is halfway through deleting.
//
// It still races the worker's own background download, which writes a book
// into the cache on any request that arrives while this is running -- there
// is no way to call that off from here. The reload is what settles it: the
// worker is gone by then, and the one that installs in its place opens
// caches under its own names regardless of what survived.
//
// Each step is allowed to fail on its own. A browser with no caches API, or
// storage the user has locked down, should still get everything else
// cleared and still reload.
var RESET_QUESTION =
'Clear this app from the device? The offline copy of the books and the ' +
'bookmark go with it, and the app reloads as a first visit.';
function settle(promise) {
return promise.catch(function () {});
}
function forgetWorker() {
if (!('serviceWorker' in navigator)) return Promise.resolve();
return navigator.serviceWorker.getRegistrations().then(function (regs) {
return Promise.all(regs.map(function (reg) { return reg.unregister(); }));
});
}
function forgetCaches() {
if (!window.caches) return Promise.resolve();
return caches.keys().then(function (keys) {
return Promise.all(keys.map(function (key) { return caches.delete(key); }));
});
}
// Resolved on blocked as well as on done. Blocked means another tab still
// has the database open, and there is nothing useful to do about it from
// here: this tab is about to reload, and the delete completes when the last
// one lets go.
function forgetBookmark() {
return new Promise(function (resolve) {
if (!window.indexedDB) return resolve();
var req = indexedDB.deleteDatabase('bible');
req.onsuccess = req.onerror = req.onblocked = function () { resolve(); };
});
}
function resetApp(button) {
if (!window.confirm(RESET_QUESTION)) return;
button.disabled = true;
button.textContent = 'Clearing…';
settle(forgetWorker())
.then(function () { return settle(forgetCaches()); })
.then(function () { return settle(forgetBookmark()); })
.then(function () { location.reload(); });
}
// api/bookmark has no origin server -- it is answered by the worker -- so
// there is nothing to ask until the worker is controlling this page.
// controller, not ready: ready can resolve a tick before clients.claim()
@@ -282,7 +188,6 @@
// Deferred scripts run before DOMContentLoaded fires, so this always lands,
// and it lands after htmx has processed the shell.
document.addEventListener('DOMContentLoaded', function () {
markVerses();
scrollToHash();
showVersion();