Best
This commit is contained in:
1 parent
2fea308b38
commit
f5fd8e632d
4 files changed
+91
-18
No files matched your search
+54
-9
@@ -121,26 +121,71 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close the modal once a bookmark write has come back and swapped, leaving
|
|
||||||
// the status line up long enough to be read.
|
|
||||||
document.body.addEventListener('htmx:afterSwap', function (event) {
|
document.body.addEventListener('htmx:afterSwap', function (event) {
|
||||||
markVerses(event.target);
|
markVerses(event.target);
|
||||||
if (event.detail.target && event.detail.target.id === 'bookmark') {
|
var target = event.detail.target;
|
||||||
setTimeout(hideModal, 900);
|
if (!target) return;
|
||||||
}
|
// 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();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
document.body.addEventListener('htmx:historyRestore', function () {
|
||||||
|
markVerses();
|
||||||
|
land();
|
||||||
});
|
});
|
||||||
|
|
||||||
// -- startup ------------------------------------------------------------
|
// -- startup ------------------------------------------------------------
|
||||||
|
|
||||||
|
function hashTarget() {
|
||||||
|
if (!location.hash) return null;
|
||||||
|
return document.getElementById(decodeURIComponent(location.hash.slice(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every scroll here is a correction, not a gesture: the page is meant to
|
||||||
|
// arrive already in position. So they all say instant.
|
||||||
|
//
|
||||||
|
// Saying nothing would not mean instant. The default is "auto", which means
|
||||||
|
// "whatever scroll-behavior computes to" -- and Bootstrap's Reboot sets
|
||||||
|
// :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.
|
||||||
|
function jumpTo(el) {
|
||||||
|
el.scrollIntoView({ block: 'start', behavior: 'instant' });
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
// the anchor itself. One composed by boot.html is not: it replaced a
|
// the anchor itself. One composed by boot.html is not: it replaced a
|
||||||
// document the browser had already finished with, so the fragment identifier
|
// document the browser had already finished with, so the fragment identifier
|
||||||
// has been spent. Repeating it costs nothing in the first case and is what
|
// has been spent. Repeating it costs nothing in the first case and is what
|
||||||
// makes a link to GEN.html#v1_1 land on the verse in the second.
|
// makes a link to GEN#v1_1 land on the verse in the second.
|
||||||
function scrollToHash() {
|
function scrollToHash() {
|
||||||
if (!location.hash) return;
|
var el = hashTarget();
|
||||||
var el = document.getElementById(decodeURIComponent(location.hash.slice(1)));
|
if (el) jumpTo(el);
|
||||||
if (el) el.scrollIntoView();
|
}
|
||||||
|
|
||||||
|
// Where a swapped-in page should sit before it is ever seen.
|
||||||
|
//
|
||||||
|
// htmx does this itself -- scrolling to the anchor a boosted href named, or
|
||||||
|
// to the top of the target when it named none -- but from the settle
|
||||||
|
// callback, which is a setTimeout of defaultSettleDelay, 20ms. The browser
|
||||||
|
// paints the new content at the old scroll offset first, so the jump is
|
||||||
|
// visible every time. Here it runs in the same task as the swap, before any
|
||||||
|
// paint, so the page simply appears where it belongs. htmx's own scroll
|
||||||
|
// still follows and finds nothing left to do.
|
||||||
|
//
|
||||||
|
// htmx has already pushed the new URL by this point -- history is updated
|
||||||
|
// before the swap, not after -- so location.hash is the anchor that was
|
||||||
|
// asked for.
|
||||||
|
function land() {
|
||||||
|
var el = hashTarget();
|
||||||
|
if (el) jumpTo(el);
|
||||||
|
else window.scrollTo({ top: 0, behavior: 'instant' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|||||||
@@ -32,6 +32,11 @@
|
|||||||
// htmx stamps every request it makes; a navigation never carries it.
|
// htmx stamps every request it makes; a navigation never carries it.
|
||||||
this.htmx = this.request.headers.get('HX-Request') === 'true';
|
this.htmx = this.request.headers.get('HX-Request') === 'true';
|
||||||
this.navigate = this.request.mode === 'navigate';
|
this.navigate = this.request.mode === 'navigate';
|
||||||
|
// Going back or forward. htmx asks for the page again and expects a whole
|
||||||
|
// document back, the same as a navigation -- it takes the history element
|
||||||
|
// out of the response itself.
|
||||||
|
this.restore =
|
||||||
|
this.request.headers.get('HX-History-Restore-Request') === 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
Call.prototype.form = function () {
|
Call.prototype.form = function () {
|
||||||
|
|||||||
+4
-4
@@ -257,12 +257,12 @@ async function fragmentFor(path, event) {
|
|||||||
return res.ok ? res.text() : view.notice(...MISSING_NOTICE);
|
return res.ok ? res.text() : view.notice(...MISSING_NOTICE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// One handler for both callers. htmx is swapping into a shell that is already
|
// One handler for every caller. htmx swapping a link into a shell already on
|
||||||
// on screen and wants the fragment; a navigation has no shell yet and gets the
|
// screen wants the bare fragment; a navigation has no shell yet, and a history
|
||||||
// fragment composed into one.
|
// restore is rebuilding one, so both get the fragment composed into a document.
|
||||||
async function servePage(path, call) {
|
async function servePage(path, call) {
|
||||||
const fragment = await fragmentFor(path, call.event);
|
const fragment = await fragmentFor(path, call.event);
|
||||||
if (!call.navigate) return fragment;
|
if (!call.navigate && !call.restore) return fragment;
|
||||||
try {
|
try {
|
||||||
return await view.compose(fragment, await getBookmark());
|
return await view.compose(fragment, await getBookmark());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -19,10 +19,22 @@
|
|||||||
scripture; Bootstrap styles the chrome. #}
|
scripture; Bootstrap styles the chrome. #}
|
||||||
<link rel="stylesheet" href="css/app.css">
|
<link rel="stylesheet" href="css/app.css">
|
||||||
{# History entries are whole books, and htmx keeps its history cache in
|
{# History entries are whole books, and htmx keeps its history cache in
|
||||||
localStorage -- a couple of them would blow the quota. Turned off: going
|
localStorage -- a couple of them would blow the quota. Turned off, so every
|
||||||
back re-requests the fragment, which the worker answers from its cache. #}
|
back or forward is a cache miss, and refreshOnHistoryMiss decides what a
|
||||||
|
miss does.
|
||||||
|
|
||||||
|
False on purpose. True makes htmx call location.reload(true) -- a forced
|
||||||
|
reload, which is exactly the kind of navigation a browser takes past the
|
||||||
|
service worker, so it reaches server.py and comes back as boot.html: the
|
||||||
|
spinner flashes by and then reloads into the real page. False makes htmx
|
||||||
|
fetch the page over XHR instead, which the worker answers itself, from its
|
||||||
|
own cache when offline. No navigation, no boot page, nothing to flash.
|
||||||
|
|
||||||
|
That request carries HX-History-Restore-Request, and the worker answers it
|
||||||
|
with a whole composed document; htmx lifts the element marked
|
||||||
|
hx-history-elt out of the response and puts it back in the page. #}
|
||||||
<meta name="htmx-config"
|
<meta name="htmx-config"
|
||||||
content='{"historyCacheSize":0,"refreshOnHistoryMiss":true}'>
|
content='{"historyCacheSize":0,"refreshOnHistoryMiss":false}'>
|
||||||
{# The bundle carries Popper, which the navbar collapse and the modal do not
|
{# The bundle carries Popper, which the navbar collapse and the modal do not
|
||||||
need -- but a dropdown or tooltip added later would, and this is one file
|
need -- but a dropdown or tooltip added later would, and this is one file
|
||||||
in the precache either way. Deferred scripts run in order, so `bootstrap`
|
in the precache either way. Deferred scripts run in order, so `bootstrap`
|
||||||
@@ -40,9 +52,15 @@
|
|||||||
<meta name="apple-mobile-web-app-title" content="Bible">
|
<meta name="apple-mobile-web-app-title" content="Bible">
|
||||||
</head>
|
</head>
|
||||||
{# Boosted links swap only the fragment, so the navbar, the modal and the
|
{# Boosted links swap only the fragment, so the navbar, the modal and the
|
||||||
scripts above survive navigation and never reload. #}
|
scripts above survive navigation and never reload.
|
||||||
|
|
||||||
|
No show: modifier, deliberately. htmx adds show:top itself for a boosted
|
||||||
|
link, but skips it when the href carries a fragment identifier and scrolls
|
||||||
|
to that anchor after the swap instead. Spelling show:top out here overrides
|
||||||
|
that decision for every link, which leaves a cross-reference like ISA#v61_6
|
||||||
|
sitting at the top of Isaiah. #}
|
||||||
<body class="{% block body_class %}{% endblock %} bg-body-tertiary"
|
<body class="{% block body_class %}{% endblock %} bg-body-tertiary"
|
||||||
hx-boost="true" hx-target="#content" hx-swap="innerHTML show:top">
|
hx-boost="true" hx-target="#content" hx-swap="innerHTML">
|
||||||
|
|
||||||
<nav class="site-header navbar navbar-expand-lg bg-dark sticky-top"
|
<nav class="site-header navbar navbar-expand-lg bg-dark sticky-top"
|
||||||
data-bs-theme="dark">
|
data-bs-theme="dark">
|
||||||
@@ -68,8 +86,13 @@
|
|||||||
the full width of a desktop window. #}
|
the full width of a desktop window. #}
|
||||||
<main class="container-xl my-4">
|
<main class="container-xl my-4">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
|
{# The swap target. hx-target on <body> names this id, so every boosted
|
||||||
|
link lands here -- remove it and a click does nothing at all while a
|
||||||
|
full navigation still works. #}
|
||||||
|
<div id="content" class="col-12 col-xl-8" hx-history-elt>
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</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
|
||||||
|
|||||||
Reference in new issue
Block a user