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');
|
var verse = event.target.closest('.v');
|
||||||
if (verse && verse.id) { event.preventDefault(); openFor(verse); return; }
|
if (verse && verse.id) { event.preventDefault(); openFor(verse); return; }
|
||||||
if (event.target.closest('[data-action="copy"]')) copyLink(currentUrl);
|
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) {
|
document.addEventListener('keydown', function (event) {
|
||||||
@@ -134,7 +136,9 @@
|
|||||||
// Close the modal once a bookmark write has come back and swapped, leaving
|
// Close the modal once a bookmark write has come back and swapped, leaving
|
||||||
// the status line up long enough to be read.
|
// the status line up long enough to be read.
|
||||||
if (target.id === 'bookmark') return setTimeout(hideModal, 900);
|
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
|
// A history restore does not go through the request pipeline -- htmx swaps
|
||||||
@@ -145,6 +149,7 @@
|
|||||||
document.body.addEventListener('htmx:historyRestore', function () {
|
document.body.addEventListener('htmx:historyRestore', function () {
|
||||||
markVerses();
|
markVerses();
|
||||||
land();
|
land();
|
||||||
|
showVersion();
|
||||||
});
|
});
|
||||||
|
|
||||||
// -- startup ------------------------------------------------------------
|
// -- startup ------------------------------------------------------------
|
||||||
@@ -162,8 +167,28 @@
|
|||||||
// :root { scroll-behavior: smooth }, so the default animates. Bootstrap
|
// :root { scroll-behavior: smooth }, so the default animates. Bootstrap
|
||||||
// wraps that in prefers-reduced-motion, which is the right call for the
|
// wraps that in prefers-reduced-motion, which is the right call for the
|
||||||
// chapter nav's own anchors; these are not that.
|
// 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) {
|
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
|
// A worker-composed page is an ordinary document and the browser scrolls to
|
||||||
@@ -192,7 +217,133 @@
|
|||||||
function land() {
|
function land() {
|
||||||
var el = hashTarget();
|
var el = hashTarget();
|
||||||
if (el) jumpTo(el);
|
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
|
// api/bookmark has no origin server -- it is answered by the worker -- so
|
||||||
@@ -210,6 +361,7 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
markVerses();
|
markVerses();
|
||||||
scrollToHash();
|
scrollToHash();
|
||||||
|
showVersion();
|
||||||
|
|
||||||
// Registered from the page it is going to serve, not from a page of its
|
// 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
|
// own: there is nothing to wait for and nothing to reload. This load is
|
||||||
|
|||||||
+70
-7
@@ -24,6 +24,26 @@
|
|||||||
// template that extends or includes another names only the file.
|
// template that extends or includes another names only the file.
|
||||||
var TEMPLATES = './templates/';
|
var TEMPLATES = './templates/';
|
||||||
|
|
||||||
|
// All of them. Named here rather than in either caller because this file is
|
||||||
|
// the one that knows what a template is: sw.js precaches them from this list
|
||||||
|
// and both renderers load them from it, so there is one place to add the
|
||||||
|
// next one. A template missing from here is a template nunjucks would have
|
||||||
|
// to fetch mid-render, which is the one thing loadAll() exists to prevent.
|
||||||
|
var NAMES = [
|
||||||
|
'base.njk',
|
||||||
|
'page.njk',
|
||||||
|
'book.njk',
|
||||||
|
'index.njk',
|
||||||
|
'bookmark.njk',
|
||||||
|
'reference.njk',
|
||||||
|
'verses.njk',
|
||||||
|
'version.njk',
|
||||||
|
'status.njk',
|
||||||
|
'notice.njk'
|
||||||
|
];
|
||||||
|
|
||||||
|
scope.TEMPLATE_URLS = NAMES.map(function (name) { return TEMPLATES + name; });
|
||||||
|
|
||||||
// readText(path) -> Promise<string> is the only thing the two callers do
|
// readText(path) -> Promise<string> is the only thing the two callers do
|
||||||
// differently: the worker reads through its cache, the page over the network.
|
// differently: the worker reads through its cache, the page over the network.
|
||||||
scope.createRenderer = function (readText) {
|
scope.createRenderer = function (readText) {
|
||||||
@@ -42,10 +62,50 @@
|
|||||||
// worker, so they never go stale.
|
// worker, so they never go stale.
|
||||||
var env = new nunjucks.Environment(new Loader(), { autoescape: true });
|
var env = new nunjucks.Environment(new Loader(), { autoescape: true });
|
||||||
|
|
||||||
|
// Every template, in nunjucks' hands before the first render, because an
|
||||||
|
// include that has to wait for one does not come out whole.
|
||||||
|
//
|
||||||
|
// getTemplate answers from the loader's cache by calling straight back,
|
||||||
|
// and only reaches this loader -- and this loader's promise -- for a
|
||||||
|
// template it has never seen. Nunjucks compiles {% include %} into a
|
||||||
|
// waterfall, and the rest of the block it sits in is the waterfall's
|
||||||
|
// callback while the code after that block runs on regardless. Answered
|
||||||
|
// in the same tick the two happen in order and the output is whole;
|
||||||
|
// answered a tick later the render is finished and handed back first,
|
||||||
|
// and what comes back is missing the include and everything between it
|
||||||
|
// and the end of its block. The verse list, and then the </select> after
|
||||||
|
// it -- a picker with a chapter and no verses, on whichever navigation
|
||||||
|
// happened to be the first to need the template.
|
||||||
|
//
|
||||||
|
// Which is why it is all of them and not just the ones that get included:
|
||||||
|
// whether a template has been reached yet depends on which pages have
|
||||||
|
// been rendered, and in the worker on whether it has been restarted since.
|
||||||
|
// That is the whole shape of the bug -- intermittent, and never twice in a
|
||||||
|
// row. One pass over a few kilobytes already in the cache buys the
|
||||||
|
// question being settled.
|
||||||
|
//
|
||||||
|
// Failures resolve rather than reject: a template that cannot be read now
|
||||||
|
// will fail again in the render that actually needs it, where the error
|
||||||
|
// belongs to a request and can be answered with the bare fragment.
|
||||||
|
var loaded = null;
|
||||||
|
|
||||||
|
function loadAll() {
|
||||||
|
if (!loaded) {
|
||||||
|
loaded = Promise.all(NAMES.map(function (name) {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
env.getTemplate(name, true, null, false, function () { resolve(); });
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return loaded;
|
||||||
|
}
|
||||||
|
|
||||||
function render(name, context) {
|
function render(name, context) {
|
||||||
return new Promise(function (resolve, reject) {
|
return loadAll().then(function () {
|
||||||
env.render(name, context, function (err, out) {
|
return new Promise(function (resolve, reject) {
|
||||||
return err ? reject(err) : resolve(out);
|
env.render(name, context, function (err, out) {
|
||||||
|
return err ? reject(err) : resolve(out);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -61,9 +121,11 @@
|
|||||||
// base.njk is for.
|
// base.njk is for.
|
||||||
//
|
//
|
||||||
// chrome is what the shell needs and the fragment cannot carry: the
|
// chrome is what the shell needs and the fragment cannot carry: the
|
||||||
// bookmark, and the navbar picker's model. Both are null from boot.html,
|
// bookmark, the navbar picker's model, and the two versions the worker
|
||||||
// which has no worker to ask for either, and both render as a hidden
|
// was built with. All three are null from boot.html, which has no worker
|
||||||
// slot that fills itself if one ever appears.
|
// to ask for any of them; the first two render as a hidden slot that
|
||||||
|
// fills itself if one ever appears, and the third renders as nothing,
|
||||||
|
// because no worker is the whole of what there is to report.
|
||||||
compose: function (fragment, chrome) {
|
compose: function (fragment, chrome) {
|
||||||
var title = (TITLE_RE.exec(fragment) || ['', ''])[1].trim();
|
var title = (TITLE_RE.exec(fragment) || ['', ''])[1].trim();
|
||||||
var content = fragment.replace(TITLE_RE, '');
|
var content = fragment.replace(TITLE_RE, '');
|
||||||
@@ -72,7 +134,8 @@
|
|||||||
title: title,
|
title: title,
|
||||||
content: content,
|
content: content,
|
||||||
mark: (chrome && chrome.mark) || null,
|
mark: (chrome && chrome.mark) || null,
|
||||||
reference: (chrome && chrome.reference) || null
|
reference: (chrome && chrome.reference) || null,
|
||||||
|
version: (chrome && chrome.version) || null
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
+26
-21
@@ -83,6 +83,11 @@ const LIBRARY = [];
|
|||||||
const SHELL_CACHE = 'bible-shell-' + VERSION;
|
const SHELL_CACHE = 'bible-shell-' + VERSION;
|
||||||
const LIBRARY_CACHE = 'bible-library-' + LIBRARY_VERSION;
|
const LIBRARY_CACHE = 'bible-library-' + LIBRARY_VERSION;
|
||||||
|
|
||||||
|
// The same two values, handed to the shell so a device can be asked what it is
|
||||||
|
// running. A phone that has kept an old worker is the hardest thing here to
|
||||||
|
// see from the outside, and these are what say so. See templates/version.njk.
|
||||||
|
const VERSIONS = { shell: VERSION, library: LIBRARY_VERSION };
|
||||||
|
|
||||||
// Resolved once, and the set is the test for which cache a response belongs
|
// Resolved once, and the set is the test for which cache a response belongs
|
||||||
// in. Membership, not a path pattern: the split is whichever directory the
|
// in. Membership, not a path pattern: the split is whichever directory the
|
||||||
// file came from, which is a fact only the server has.
|
// file came from, which is a fact only the server has.
|
||||||
@@ -103,17 +108,12 @@ const PRECACHE = [
|
|||||||
'./css/bootstrap.min.css',
|
'./css/bootstrap.min.css',
|
||||||
'./js/app.js',
|
'./js/app.js',
|
||||||
'./js/htmx.min.js',
|
'./js/htmx.min.js',
|
||||||
'./js/bootstrap.bundle.min.js',
|
'./js/bootstrap.bundle.min.js'
|
||||||
'./templates/base.njk',
|
// The templates come from render.js, which is the file that knows what one is
|
||||||
'./templates/page.njk',
|
// -- and which loads exactly this list into nunjucks before it renders
|
||||||
'./templates/book.njk',
|
// anything. Two lists would be one list and a way for the renderer to be
|
||||||
'./templates/index.njk',
|
// asked for a template that was never cached.
|
||||||
'./templates/bookmark.njk',
|
].concat(TEMPLATE_URLS);
|
||||||
'./templates/reference.njk',
|
|
||||||
'./templates/verses.njk',
|
|
||||||
'./templates/status.njk',
|
|
||||||
'./templates/notice.njk'
|
|
||||||
];
|
|
||||||
|
|
||||||
function cacheFor(req) {
|
function cacheFor(req) {
|
||||||
return caches.open(LIBRARY_URLS.has(req.url) ? LIBRARY_CACHE : SHELL_CACHE);
|
return caches.open(LIBRARY_URLS.has(req.url) ? LIBRARY_CACHE : SHELL_CACHE);
|
||||||
@@ -409,7 +409,8 @@ async function servePage(path, code, call) {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return await view.compose(fragment, { mark: await getBookmark(),
|
return await view.compose(fragment, { mark: await getBookmark(),
|
||||||
reference: reference });
|
reference: reference,
|
||||||
|
version: VERSIONS });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// A template that will not render is a bug, not a condition to design for,
|
// A template that will not render is a bug, not a condition to design for,
|
||||||
// but it should not take the text down with it: the bare fragment has no
|
// but it should not take the text down with it: the bare fragment has no
|
||||||
@@ -457,17 +458,21 @@ router.get('/api/bookmark', () => bookmarkFragment(''));
|
|||||||
// under a page boot.html composed once a worker turns up, for nothing extra:
|
// under a page boot.html composed once a worker turns up, for nothing extra:
|
||||||
// those requests carry the address bar too.
|
// those requests carry the address bar too.
|
||||||
//
|
//
|
||||||
// What the response headers are doing is the whole of what app.js used to do
|
// Two response headers carry what a change means beyond the options:
|
||||||
// on a change event, and htmx has a name for each part of it:
|
|
||||||
//
|
//
|
||||||
// settle:0 swap the options now. The default waits 20ms, and the point
|
// settle:0 swap the options now. The default waits 20ms, and this
|
||||||
// of this request is a scroll the reader is waiting on.
|
// answer is one the reader is waiting on.
|
||||||
// show:#id:top scroll the named anchor to the top -- htmx's scrollBehavior
|
|
||||||
// is "instant", which is what this app wants everywhere.
|
|
||||||
// Reboot sets scroll-behavior: smooth, so saying nothing here
|
|
||||||
// would animate.
|
|
||||||
// HX-Replace-Url put the anchor in the address bar without stacking a
|
// HX-Replace-Url put the anchor in the address bar without stacking a
|
||||||
// history entry per option spun past.
|
// history entry per option spun past.
|
||||||
|
//
|
||||||
|
// The scroll used to be a third, show:#id:top, and is app.js's again. Not
|
||||||
|
// because the header was wrong but because the moment was: on iOS the change
|
||||||
|
// event arrives while the native picker is still drawn over the page, and
|
||||||
|
// dismissing it restores the scroll offset it opened at -- so a scroll made
|
||||||
|
// inside the swap is undone before it is seen. The page has to be the one
|
||||||
|
// doing it, because only the page is still there afterwards to try again.
|
||||||
|
// See landOnReference() in js/app.js. The address bar above is how it knows
|
||||||
|
// where to go: htmx updates history before it swaps.
|
||||||
router.get('/api/reference', async (call) => {
|
router.get('/api/reference', async (call) => {
|
||||||
const params = call.url.searchParams;
|
const params = call.url.searchParams;
|
||||||
const page = call.currentUrl();
|
const page = call.currentUrl();
|
||||||
@@ -505,7 +510,7 @@ router.get('/api/reference', async (call) => {
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/html; charset=utf-8',
|
'Content-Type': 'text/html; charset=utf-8',
|
||||||
'Cache-Control': 'no-store',
|
'Cache-Control': 'no-store',
|
||||||
'HX-Reswap': 'innerHTML settle:0 show:#' + target + ':top',
|
'HX-Reswap': 'innerHTML settle:0',
|
||||||
'HX-Replace-Url': page.pathname + page.search + '#' + target
|
'HX-Replace-Url': page.pathname + page.search + '#' + target
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -105,6 +105,7 @@
|
|||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% include "version.njk" %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{# Chrome, so it is written once here instead of built in JavaScript on every
|
{# Chrome, so it is written once here instead of built in JavaScript on every
|
||||||
|
|||||||
@@ -44,12 +44,20 @@
|
|||||||
|
|
||||||
settled is what stops the load trigger looping: the answer to it is a
|
settled is what stops the load trigger looping: the answer to it is a
|
||||||
picker already standing in the right place, and it says so by leaving the
|
picker already standing in the right place, and it says so by leaving the
|
||||||
trigger out of the copy it sends back. #}
|
trigger out of the copy it sends back.
|
||||||
|
|
||||||
|
!!location.hash, and not location.hash, because htmx keeps an event only
|
||||||
|
when the filter returns boolean true exactly -- it tests the result with
|
||||||
|
!== true -- and a hash is a string. Written without the coercion the
|
||||||
|
trigger is dead: every link carrying an anchor lands with the picker still
|
||||||
|
reading 1:1, which is where the worker rendered it, because a browser
|
||||||
|
keeps the fragment to itself and the request that would correct it is
|
||||||
|
never sent. #}
|
||||||
<div class="reference navbar-text {% if reference %}d-flex{% else %}d-none{% endif %} align-items-center gap-1 ms-3"
|
<div class="reference navbar-text {% if reference %}d-flex{% else %}d-none{% endif %} align-items-center gap-1 ms-3"
|
||||||
id="reference"
|
id="reference"
|
||||||
{%- if oob %} hx-swap-oob="true"{% endif %}
|
{%- if oob %} hx-swap-oob="true"{% endif %}
|
||||||
hx-get="api/reference"
|
hx-get="api/reference"
|
||||||
hx-trigger="{% if reference and not settled %}load[location.hash], {% endif %}worker-ready from:body, htmx:historyRestore from:body"
|
hx-trigger="{% if reference and not settled %}load[!!location.hash], {% endif %}worker-ready from:body, htmx:historyRestore from:body"
|
||||||
hx-target="this" hx-swap="outerHTML">
|
hx-target="this" hx-swap="outerHTML">
|
||||||
{%- if reference %}
|
{%- if reference %}
|
||||||
<span class="reference-book text-light fw-semibold"
|
<span class="reference-book text-light fw-semibold"
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{# The footer under the book list: what this device is running, and the one
|
||||||
|
button that makes it forget.
|
||||||
|
|
||||||
|
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/,
|
||||||
|
the generated pages. They are the same two values the worker names its
|
||||||
|
caches after, so a device showing a digest is a device serving that cache.
|
||||||
|
|
||||||
|
It says what the worker that composed this document was, not what is
|
||||||
|
controlling right now. An update installs, claims and starts answering
|
||||||
|
without this document being rebuilt, so the value only becomes current on
|
||||||
|
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.
|
||||||
|
|
||||||
|
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:
|
||||||
|
the shell is composed once per full navigation, and a boosted link to the
|
||||||
|
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">
|
||||||
|
{%- if version %}
|
||||||
|
<p class="app-version-digests form-text mt-0 mb-2">
|
||||||
|
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>
|
||||||
Reference in new issue
Block a user