Fixes
This commit is contained in:
1 parent
ce73c76126
commit
84c07a912b
5 files changed
+44
-133
No files matched your search
+2
-1
@@ -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;
|
||||
|
||||
+17
-112
@@ -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();
|
||||
|
||||
|
||||
@@ -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. #}
|
||||
<div id="content" class="col-12 col-xl-8" hx-history-elt
|
||||
hx-on:click="var verse = event.target.closest('.v[id]');
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{# The footer under the book list: what this device is running, and the one
|
||||
button that makes it forget.
|
||||
{# The footer under the book list: what this device is running.
|
||||
|
||||
Both digests are stamped into sw.js by server.py from the files on disk:
|
||||
shell covers assets/, the hand-written chrome, and library covers bible/,
|
||||
@@ -12,10 +11,9 @@
|
||||
the next full load -- which is the reload you would do to check it anyway.
|
||||
|
||||
version is null in one case: boot.html composed this page, so there was no
|
||||
worker to ask. The line is then absent, which is the honest answer -- no
|
||||
worker means no version, and the bookmark slot in the navbar is empty for
|
||||
the same reason. The button stays, because a device with no worker and a
|
||||
full cache is exactly the state worth clearing.
|
||||
worker to ask. Nothing is rendered at all then, which is the honest answer
|
||||
-- no worker means no version, and the bookmark slot in the navbar is empty
|
||||
for the same reason. app.js finds no element and leaves it at that.
|
||||
|
||||
Rendered on every page and shown under the book list alone. Populating it
|
||||
only for the index would look like the smaller change and would not work:
|
||||
@@ -23,17 +21,11 @@
|
||||
book list swaps #content without rebuilding anything around it. So the page
|
||||
decides, not the composer -- app.js looks for the index in #content after
|
||||
every swap. The body class this was rendered with cannot be asked, for the
|
||||
same reason: it goes stale the moment a link is clicked.
|
||||
|
||||
The button is a plain <button> outside any form, which is the whole of why
|
||||
hx-boost on <body> leaves it alone: htmx boosts links, forms, and submits
|
||||
belonging to one. resetApp() in js/app.js is what it runs. #}
|
||||
<div class="app-version d-none text-center mt-5 mb-2" id="app-version">
|
||||
same reason: it goes stale the moment a link is clicked. #}
|
||||
{%- if version %}
|
||||
<p class="app-version-digests form-text mt-0 mb-2">
|
||||
<div class="app-version d-none text-center mt-5 mb-2" id="app-version">
|
||||
<p class="app-version-digests form-text m-0">
|
||||
shell {{ version.shell }} · library {{ version.library }}
|
||||
</p>
|
||||
{%- endif %}
|
||||
<button type="button" class="app-reset btn btn-outline-secondary btn-sm"
|
||||
id="app-reset">Reset app</button>
|
||||
</div>
|
||||
{%- endif %}
|
||||
+15
-2
@@ -588,8 +588,21 @@ def render_inline(nodes, book, chapter, out):
|
||||
elif kind == "verse":
|
||||
# Trailing space keeps the number off the text before any CSS
|
||||
# lands; style .v with padding and it stays correct either way.
|
||||
out.append('<span class="v" id="%s">%s</span> ' % (
|
||||
verse_id(chapter, node["label"]), esc(node["label"])))
|
||||
#
|
||||
# A control, said here rather than by a pass over the page once it
|
||||
# has landed. role is what announces it and what earns Reboot's
|
||||
# [role=button] pointer cursor, tabindex is what puts it in the tab
|
||||
# order, and the label is what a screen reader says in place of a
|
||||
# bare number. All three are true of every verse in the corpus and
|
||||
# none of them depends on anything only the browser knows, so
|
||||
# there is nothing here for a script to work out: app.js used to
|
||||
# set them on load, after every htmx swap and after every history
|
||||
# restore, walking a whole book each time. The click itself never
|
||||
# needed that pass -- it is delegated at #content in base.njk.
|
||||
label = esc(node["label"])
|
||||
out.append('<span class="v" id="%s" role="button" tabindex="0" '
|
||||
'aria-label="Verse %s, options">%s</span> '
|
||||
% (verse_id(chapter, node["label"]), label, label))
|
||||
elif kind == "noteref":
|
||||
n = node["id"].replace("FN", "")
|
||||
out.append('<a class="noteref" id="fnref%s_%s" href="#fn%s_%s">'
|
||||
|
||||
Reference in new issue
Block a user