Fixes
This commit is contained in:
1 parent
f041daeda3
commit
9407c26d2d
6 files changed
+301
-33
No files matched your search
+155
-3
@@ -116,6 +116,8 @@
|
||||
var verse = event.target.closest('.v');
|
||||
if (verse && verse.id) { event.preventDefault(); openFor(verse); return; }
|
||||
if (event.target.closest('[data-action="copy"]')) copyLink(currentUrl);
|
||||
var reset = event.target.closest('#app-reset');
|
||||
if (reset) resetApp(reset);
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (event) {
|
||||
@@ -134,7 +136,9 @@
|
||||
// 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();
|
||||
// Both selects answer into the verse list, so this is either of them.
|
||||
if (target.id === 'reference-verse') return landOnReference();
|
||||
if (target.id === 'content') { land(); showVersion(); }
|
||||
});
|
||||
|
||||
// A history restore does not go through the request pipeline -- htmx swaps
|
||||
@@ -145,6 +149,7 @@
|
||||
document.body.addEventListener('htmx:historyRestore', function () {
|
||||
markVerses();
|
||||
land();
|
||||
showVersion();
|
||||
});
|
||||
|
||||
// -- startup ------------------------------------------------------------
|
||||
@@ -162,8 +167,28 @@
|
||||
// :root { scroll-behavior: smooth }, so the default animates. Bootstrap
|
||||
// wraps that in prefers-reduced-motion, which is the right call for the
|
||||
// chapter nav's own anchors; these are not that.
|
||||
//
|
||||
// Asked for twice, because "instant" is a later addition to an old enum and
|
||||
// a browser that does not know the value rejects the whole call with a
|
||||
// TypeError rather than ignoring the word -- so on an older WebKit this did
|
||||
// not scroll slowly, it did not scroll at all. "auto" is what the enum has
|
||||
// always had; it animates here, which is worse than instant and far better
|
||||
// than staying put.
|
||||
function jumpTo(el) {
|
||||
el.scrollIntoView({ block: 'start', behavior: 'instant' });
|
||||
try {
|
||||
el.scrollIntoView({ block: 'start', behavior: 'instant' });
|
||||
} catch (err) {
|
||||
el.scrollIntoView({ block: 'start', behavior: 'auto' });
|
||||
}
|
||||
}
|
||||
|
||||
// The same, for the top of the page and the same reason.
|
||||
function jumpToTop() {
|
||||
try {
|
||||
window.scrollTo({ top: 0, behavior: 'instant' });
|
||||
} catch (err) {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// A worker-composed page is an ordinary document and the browser scrolls to
|
||||
@@ -192,7 +217,133 @@
|
||||
function land() {
|
||||
var el = hashTarget();
|
||||
if (el) jumpTo(el);
|
||||
else window.scrollTo({ top: 0, behavior: 'instant' });
|
||||
else jumpToTop();
|
||||
}
|
||||
|
||||
// Where the reference picker lands, which is the one scroll in the app that
|
||||
// is a gesture: the reader asked for chapter 3 and is waiting to be taken
|
||||
// there.
|
||||
//
|
||||
// It used to ride along on the answer, as show: on HX-Reswap, and be htmx's
|
||||
// scroll rather than this file's. Two reasons it is here now. The smaller
|
||||
// one is that htmx's own scrollIntoView says "instant" with nothing around
|
||||
// it, so the enum above takes the whole scroll down with it. The larger one
|
||||
// is the phone: a select on iOS is a picker drawn over the page, the change
|
||||
// event arrives while it is still up, and dismissing it puts the page back
|
||||
// where it stood when it opened -- undoing anything scrolled in between.
|
||||
// The options were swapping correctly and the book never moved.
|
||||
//
|
||||
// So: once now, for a pointer, and once more after the picker has had time
|
||||
// to finish, but only if the page did not stay where it was put. That test
|
||||
// is also what keeps the second attempt from yanking back a reader who has
|
||||
// started scrolling on their own.
|
||||
//
|
||||
// The anchor comes from the address bar. The worker sends HX-Replace-Url
|
||||
// with the answer and htmx updates history before it swaps, so by the time
|
||||
// this runs location.hash is already naming the chapter or verse chosen.
|
||||
var PICKER_SETTLE_MS = 350;
|
||||
|
||||
function landOnReference() {
|
||||
var el = hashTarget();
|
||||
if (!el) return;
|
||||
jumpTo(el);
|
||||
var landed = window.pageYOffset;
|
||||
setTimeout(function () {
|
||||
var again = hashTarget();
|
||||
if (again && Math.abs(window.pageYOffset - landed) > 2) jumpTo(again);
|
||||
}, PICKER_SETTLE_MS);
|
||||
}
|
||||
|
||||
// The version readout, shown under the book list and nowhere else.
|
||||
//
|
||||
// It is chrome: it sits outside #content, so it outlives every swap -- and
|
||||
// so does the body class it was rendered with, which is exactly why that
|
||||
// cannot be what decides. What is in #content can, because that is the one
|
||||
// thing a swap changes: the book list is the fragment rooted at .index.
|
||||
//
|
||||
// Toggling the class rather than the style because Bootstrap declares
|
||||
// .d-none !important, which an inline display would lose to.
|
||||
//
|
||||
// Only the digest line is missing on a page boot.html composed -- there was
|
||||
// no worker to name a version -- so the element itself is always here.
|
||||
function showVersion() {
|
||||
var el = document.getElementById('app-version');
|
||||
if (!el) return;
|
||||
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
|
||||
@@ -210,6 +361,7 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
markVerses();
|
||||
scrollToHash();
|
||||
showVersion();
|
||||
|
||||
// Registered from the page it is going to serve, not from a page of its
|
||||
// own: there is nothing to wait for and nothing to reload. This load is
|
||||
|
||||
Reference in new issue
Block a user