+
shell {{ version.shell }} · library {{ version.library }}
-{%- endif %} -diff --git a/assets/css/app.css b/assets/css/app.css index 42876b1..9697104 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -25,7 +25,8 @@ user-select: none; } -/* app.js turns these into real buttons, so they need a visible focus ring. */ +/* These are real buttons -- compile_bible.py writes role and tabindex on every + one of them -- so they need a visible focus ring. */ .v:focus-visible { outline: 2px solid var(--bs-primary); outline-offset: 2px; diff --git a/assets/js/app.js b/assets/js/app.js index ae182de..02c4a56 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -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(); diff --git a/assets/templates/base.njk b/assets/templates/base.njk index 01fa049..f93bc6d 100644 --- a/assets/templates/base.njk +++ b/assets/templates/base.njk @@ -116,8 +116,8 @@ to travel. Enter and space go through the same door rather than a second one: - markVerses() in app.js gives every verse role=button and tabindex, and - a button that answers the keyboard by being clicked is a button with one + compile_bible.py writes role=button and tabindex on every verse, and a + button that answers the keyboard by being clicked is a button with one behaviour instead of two. #}
+
shell {{ version.shell }} · library {{ version.library }}
-{%- endif %} -