Remove comments
This commit is contained in:
1 parent
872175153a
commit
057b4be7d7
18 files changed
+66
-1350
No files matched your search
@@ -1,52 +1,14 @@
|
||||
{# The shell every page is wrapped in. Rendered by sw.js, not by the build,
|
||||
so editing this file changes every page without regenerating any of them.
|
||||
|
||||
Context: title, content (safe HTML fragment), mark (bookmark or null),
|
||||
reference (the navbar picker's model, or null).
|
||||
|
||||
Bootstrap is the whole stylesheet. The fragments carry semantic USFM
|
||||
classes and no Bootstrap ones, so the body text is styled by Reboot; the
|
||||
chrome here is what uses the framework. #}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
{# No viewport-fit=cover, deliberately. It extends the page into the notch
|
||||
and the home indicator, which only makes sense paired with safe-area-inset
|
||||
padding in the stylesheet -- and without that padding on iOS the first line
|
||||
of scripture sits under the clock. Paired with the status bar style below:
|
||||
change one and the other has to change too. #}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}{{ title | safe }}{% endblock %}</title>
|
||||
{# Vendored rather than linked from a CDN: sw.js only caches same-origin
|
||||
responses, so a CDN stylesheet would leave the app unstyled offline. #}
|
||||
<link rel="stylesheet" href="css/bootstrap.min.css">
|
||||
{# app.css comes second so its variable overrides win. It styles the
|
||||
scripture; Bootstrap styles the chrome. #}
|
||||
<link rel="stylesheet" href="css/app.css">
|
||||
{# History entries are whole books, and htmx keeps its history cache in
|
||||
localStorage -- a couple of them would blow the quota. Turned off, so every
|
||||
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"
|
||||
content='{"historyCacheSize":0,"refreshOnHistoryMiss":false}'>
|
||||
{# 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
|
||||
in the precache either way. Deferred scripts run in order, so `bootstrap`
|
||||
is defined by the time app.js runs. #}
|
||||
<script src="js/bootstrap.bundle.min.js" defer></script>
|
||||
{# htmx is not deferred: hx-boost has to be in place before the first click. #}
|
||||
<script src="js/htmx.min.js"></script>
|
||||
<script src="js/app.js" defer></script>
|
||||
<link rel="manifest" href="manifest.webmanifest">
|
||||
@@ -54,22 +16,9 @@
|
||||
<link rel="apple-touch-icon" href="icons/apple-touch-icon.png">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
{# black, not black-translucent. Translucent makes iOS draw the status bar
|
||||
over the web view and reserve no space for it, so the navbar starts at the
|
||||
very top of the screen with the clock and battery on top of it. black keeps
|
||||
the status bar opaque and the view below it, which is what this app wants:
|
||||
there is no full-bleed artwork here to show through. #}
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="Bible">
|
||||
</head>
|
||||
{# Boosted links swap only the fragment, so the navbar, the modal and the
|
||||
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"
|
||||
hx-boost="true" hx-target="#content" hx-swap="innerHTML">
|
||||
|
||||
@@ -94,31 +43,8 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{# col-xl-8 keeps a line of verse near a readable measure instead of running
|
||||
the full width of a desktop window. #}
|
||||
<main class="container-xl my-4">
|
||||
<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.
|
||||
|
||||
It is also where a verse click becomes a request. htmx's bootstrap-modal
|
||||
example puts hx-get on the element that opens the dialog; the element
|
||||
here is a verse number in a generated book, and there are 38,000 of
|
||||
them -- an attribute apiece would cost more than the whole shell, and a
|
||||
listener apiece more than the reading. So one handler sits on the
|
||||
container they all swap into, and names the verse it found. Delegated,
|
||||
so it survives every swap: this element is never replaced, only filled.
|
||||
|
||||
hx-vals cannot do this. htmx evaluates it as Function("return (...)")()
|
||||
with nothing passed in, so an expression there can read the document but
|
||||
not the click -- and which verse was clicked is the one thing that has
|
||||
to travel.
|
||||
|
||||
Enter and space go through the same door rather than a second 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]');
|
||||
if (verse) htmx.ajax('GET', 'api/verse?at=' + verse.id,
|
||||
@@ -131,37 +57,8 @@
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "version.njk" %}
|
||||
</main>
|
||||
|
||||
{# The verse dialog: empty in the page, filled per request. Bootstrap owns
|
||||
showing it, dismissing it, the backdrop and Escape; sw.js owns everything
|
||||
inside, and templates/verse.njk is what lands in the modal-content below.
|
||||
|
||||
Opened by the arrival of the content rather than by data-bs-toggle on
|
||||
whatever was clicked. The example puts that attribute on its trigger, which
|
||||
is not available here for the same reason hx-get is not -- see #content
|
||||
above -- and the swap is the honest signal anyway: there is nothing worth
|
||||
showing until it is here. A verse the corpus does not have is answered 204,
|
||||
which swaps nothing, which opens nothing.
|
||||
|
||||
after-swap and not after-settle: settling is the 20ms attribute pass and
|
||||
this dialog has nothing to settle. Only the modal-content swap can reach
|
||||
here -- the bookmark write targets the navbar slot, and the toast it sends
|
||||
back rides in out of band, which fires its own event and not this one.
|
||||
|
||||
verse-done is the dialog saying it has finished its job, and it arrives
|
||||
from both of the things in it: sw.js sends it as a trigger header on a
|
||||
bookmark write that committed, and the copy button dispatches it when the
|
||||
clipboard has actually taken the link. Neither sends it when the action
|
||||
failed -- the field in the dialog is the fallback for a copy that was
|
||||
refused, and there is nothing to take down the dialog for when there is
|
||||
nothing to celebrate.
|
||||
|
||||
Closed at once rather than after a pause. The pause was there to let the
|
||||
status line inside be read before the dialog took it away; the line is a
|
||||
toast now, outside and above all of this, so it outlives the dialog and
|
||||
there is nothing left to wait for. #}
|
||||
<div class="modal fade" id="verse-dialog" tabindex="-1"
|
||||
aria-labelledby="verse-dialog-ref" aria-hidden="true"
|
||||
hx-on::after-swap="bootstrap.Modal.getOrCreateInstance(this).show();"
|
||||
@@ -171,21 +68,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Where confirmations land. Empty in the page and filled a toast at a time:
|
||||
api/toast appends one when the copy button asks for it, and a bookmark
|
||||
write sends one out of band on its own answer. templates/toast.njk is what
|
||||
lands here, and each one shows and then removes itself, so this is empty
|
||||
again a few seconds later.
|
||||
|
||||
Fixed, and outside #content, for the same reason the dialog is: it has to
|
||||
survive the swap under it. It also has to survive the dialog -- a copy
|
||||
confirmed by a toast inside the modal would go when the modal did.
|
||||
|
||||
Bottom right, and last in the document, so it covers the text rather than
|
||||
moving it, and so nothing in the reading order comes after it. It sits
|
||||
above the modal without being asked to: Bootstrap gives .toast a z-index of
|
||||
1090 against the modal's 1055, and this container takes none of its own,
|
||||
which is what leaves that comparison to the toast. #}
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3" id="toasts"></div>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
{# The navbar's bookmark slot. Rendered into the shell with whatever mark the
|
||||
worker holds, and returned on its own by api/bookmark after a write. Both
|
||||
paths must produce the same element for the swap to be seamless.
|
||||
|
||||
Always filled: getBookmark() answers with the default when nothing has been
|
||||
set and when storage cannot be read at all, so there is no empty state to
|
||||
render. There was one -- a hidden slot that fetched itself once a worker
|
||||
turned up -- for pages boot.html composed before there was a worker to ask.
|
||||
Nothing composes a page but the worker now, and the worker always knows. #}
|
||||
<span class="bookmark navbar-text d-flex align-items-center gap-1" id="bookmark">
|
||||
{# A word and a colon, not a badge: this is one line of text that happens to
|
||||
end in a link, and a pill in front of it read as a control. Outside the
|
||||
anchor so that only the reference is underlined -- Reboot underlines an
|
||||
<a>, and the label is not the part you can go to. It takes the muted
|
||||
navbar colour from .navbar-text on the parent without asking. #}
|
||||
<span class="bookmark-label">Bookmark:</span>
|
||||
{# Not boosted: a real navigation lets the browser scroll to the verse
|
||||
anchor, which a swap into #content would not do. #}
|
||||
<a class="bookmark-link link-light text-truncate" href="{{ mark.href }}"
|
||||
hx-boost="false">{{ mark.ref }}</a>
|
||||
</span>
|
||||
@@ -1,7 +1,5 @@
|
||||
{% extends "base.njk" %}
|
||||
{% block body_class %}browsing{% endblock %}
|
||||
{# Already on the index, so the nav entry marks the current page instead of
|
||||
offering a link to it. #}
|
||||
{% block navbar_nav %}
|
||||
{% endblock %}
|
||||
{% block content %}{{ content | safe }}{% endblock %}
|
||||
@@ -1,9 +1,3 @@
|
||||
{# A page that could not be delivered, written in the same shape as a real
|
||||
one: a title and one root element naming the template that wraps it.
|
||||
|
||||
Sent with 200 on purpose. htmx only swaps a 2xx body, and an error status
|
||||
would leave the reader looking at the page they came from with nothing to
|
||||
say what happened. #}
|
||||
<title>{{ heading }}</title>
|
||||
<div class="notice" data-template="page">
|
||||
<h1>{{ heading }}</h1>
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
{# Fallback for a fragment that names no template of its own. #}
|
||||
{% extends "base.njk" %}
|
||||
{% block content %}{{ content | safe }}{% endblock %}
|
||||
@@ -1,77 +1,15 @@
|
||||
{# The navbar's reference picker: which book you are in, and where in it to go.
|
||||
|
||||
Context: reference (or null), oob.
|
||||
|
||||
reference is built by sw.js from bible/verses.json, which compile_bible.py
|
||||
writes from the same tree it renders the books from -- so an option can
|
||||
never name an anchor that is not in the page it lands on.
|
||||
|
||||
It used to be built in the browser, by app.js, reading the chapters and
|
||||
verses off the book already on screen. That was free, and it only worked
|
||||
where there was a document to read: this shell is composed in a service
|
||||
worker, which has no DOM, before any document exists. What the picker needs
|
||||
is a fact on disk now, and everything below is markup and htmx.
|
||||
|
||||
Outside the collapse deliberately -- it is the main way of moving around a
|
||||
book now that the chapter list is gone from the page, and a chapter picker
|
||||
behind a hamburger is no use on a phone.
|
||||
|
||||
d-none for the empty state rather than the hidden attribute: Bootstrap
|
||||
declares .d-flex after Reboot's [hidden] rule, both !important and at the
|
||||
same specificity, so the utility would win.
|
||||
|
||||
Three things bring it back into step with the page, because the navbar is
|
||||
never the swap target -- every boosted link lands in #content:
|
||||
|
||||
a navigation sw.js appends this template to the fragment with oob set,
|
||||
the same way a bookmark write carries the modal's status
|
||||
line, so one response moves both.
|
||||
a restore htmx swaps the history element straight in and processes
|
||||
no out-of-band elements at all, so the picker asks for
|
||||
itself instead.
|
||||
an anchor a link to GEN#v3_16 should open with the picker reading
|
||||
3:16, and the fragment never reaches the worker: a browser
|
||||
asks for /GEN and keeps the rest. So a picker that arrived
|
||||
with the page asks once, on load, if there is an anchor to
|
||||
ask about.
|
||||
|
||||
None of them carries the book or the anchor. The request does, in
|
||||
HX-Current-URL, which htmx sets from location.href -- the address the page
|
||||
is actually showing, fragment and all. Baking either into these attributes
|
||||
is the one thing here that can go stale, because the picker outlives the
|
||||
page it was rendered for.
|
||||
|
||||
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
|
||||
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"
|
||||
id="reference"
|
||||
{%- if oob %} hx-swap-oob="true"{% endif %}
|
||||
hx-get="api/reference"
|
||||
{# htmx keeps an event only when the filter returns true exactly, so a
|
||||
string is discarded. #}
|
||||
hx-trigger="{% if reference and not settled %}load[!!location.hash], {% endif %}htmx:historyRestore from:body"
|
||||
hx-target="this" hx-swap="outerHTML">
|
||||
{%- if reference %}
|
||||
<span class="reference-book text-light fw-semibold"
|
||||
id="reference-book">{{ reference.book }}</span>
|
||||
<label class="visually-hidden" for="reference-chapter">Chapter</label>
|
||||
{# The value is the id of the element to land on, so nothing downstream has
|
||||
to know how a verse anchor is spelled -- including the worker, which
|
||||
reads the chapter back out of the id it sent.
|
||||
|
||||
Which is why both selects post under the same name. ch3 and v3_16 are
|
||||
told apart by their shape, so a second name saying which select it came
|
||||
from would only be repeating what the value already says.
|
||||
|
||||
hx-target is spelled out on both selects because the one on the div above
|
||||
is "this", and an inherited "this" would resolve to the select. #}
|
||||
<select class="reference-chapter form-select form-select-sm"
|
||||
id="reference-chapter" name="at"
|
||||
hx-get="api/reference" hx-target="#reference-verse"
|
||||
@@ -83,9 +21,6 @@
|
||||
</select>
|
||||
<span class="reference-sep text-light">:</span>
|
||||
<label class="visually-hidden" for="reference-verse">Verse</label>
|
||||
{# Answers with its own options again rather than nothing, so the response
|
||||
has something to hang the scroll on -- and so the option the reader chose
|
||||
comes back marked selected instead of being reset by the swap. #}
|
||||
<select class="reference-verse form-select form-select-sm"
|
||||
id="reference-verse" name="at"
|
||||
hx-get="api/reference" hx-target="this" hx-swap="innerHTML">
|
||||
|
||||
@@ -1,29 +1,3 @@
|
||||
{# One confirmation: something the reader just did, said once and then gone.
|
||||
|
||||
Bootstrap's custom-content shape -- a flex row of body and close button,
|
||||
with no toast-header above it. The header carries a title, an icon and a
|
||||
timestamp, which is what a notification arriving from elsewhere needs; this
|
||||
is the answer to a button pressed a second ago, and the sentence is the
|
||||
whole of it.
|
||||
|
||||
Context: text, bad (it did not happen), oob.
|
||||
|
||||
Two ways in, the same element either way. api/toast renders it alone and
|
||||
htmx appends it to the container; the bookmark write renders it with oob
|
||||
set and it rides along on the answer that updates the navbar slot -- one
|
||||
response, both halves of the page, the way the modal's status line used to.
|
||||
The wrapper is how an out-of-band element with no id of its own says where
|
||||
it goes: htmx appends the wrapper's children to the named container and
|
||||
drops the wrapper itself.
|
||||
|
||||
It shows itself on arrival and removes itself once it has gone. hx-on::load
|
||||
is htmx's own event, fired on content it has just inserted and processed,
|
||||
so it covers both ways in -- and it is the same move base.njk makes to open
|
||||
the dialog: the arrival of the content is the signal to show it. Bootstrap
|
||||
owns the timer, the fade, the close button and Escape from there. Removing
|
||||
the element on hidden.bs.toast is what keeps the container from filling up
|
||||
with dead markup: a toast that has been read is not history, and nothing
|
||||
here ever looks at it again. #}
|
||||
{% if oob %}<div hx-swap-oob="beforeend:#toasts">{% endif %}
|
||||
<div class="toast align-items-center{% if bad %} text-bg-danger{% endif %}"
|
||||
role="alert" aria-live="assertive" aria-atomic="true"
|
||||
@@ -31,9 +5,6 @@
|
||||
hx-on:hidden.bs.toast="this.remove();">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">{{ text }}</div>
|
||||
{# me-2 m-auto, not ms-auto: the row is body then button, and the body is
|
||||
what grows -- the button only has to sit centred against however many
|
||||
lines it turns out to be. #}
|
||||
<button type="button"
|
||||
class="btn-close{% if bad %} btn-close-white{% endif %} me-2 m-auto"
|
||||
data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
|
||||
@@ -1,24 +1,3 @@
|
||||
{# The verse dialog's contents: what this verse is called, a link to it, and
|
||||
the one thing you can do with it.
|
||||
|
||||
Rendered by sw.js for the verse that was clicked and swapped into the empty
|
||||
modal-content in base.njk -- the shape htmx's bootstrap-modal example uses.
|
||||
Bootstrap owns showing, dismissing, the backdrop and Escape; everything
|
||||
here is what is inside.
|
||||
|
||||
Context: verse (ref, href, url).
|
||||
|
||||
All three are built in the worker, from the book in the address bar and the
|
||||
anchor that was clicked. app.js used to read them off the page -- the book
|
||||
out of article[data-name], the chapter out of the enclosing section -- which
|
||||
put the reference in two places and left the spelling of an anchor known to
|
||||
both the file that generates them and the file that scrapes them back.
|
||||
|
||||
The link is a field rather than a line of text because it is the fallback
|
||||
for the button beside it: navigator.clipboard exists only in a secure
|
||||
context, so over plain http:// from a LAN address there is nothing to write
|
||||
with and a selected field is the whole of what is left. Readonly, so it
|
||||
reads as something to take rather than something to fill in. #}
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title fs-5 verse-dialog-ref" id="verse-dialog-ref">{{ verse.ref }}</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"
|
||||
@@ -30,27 +9,10 @@
|
||||
type="text" readonly value="{{ verse.url }}">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
{# The form is the bookmark write and nothing else. ref and href are hidden
|
||||
because they are this dialog's whole subject: the reader chose them by
|
||||
clicking a verse, and there is nothing here to edit. #}
|
||||
<form class="verse-dialog-actions d-flex flex-wrap gap-2 m-0"
|
||||
hx-post="api/bookmark" hx-target="#bookmark" hx-swap="outerHTML">
|
||||
<input type="hidden" name="ref" value="{{ verse.ref }}">
|
||||
<input type="hidden" name="href" value="{{ verse.href }}">
|
||||
{# Selected first, written second: the selection is what makes the field
|
||||
useful when the write is refused, and it costs nothing when it is not.
|
||||
The toast is only raised once the clipboard has actually taken the link,
|
||||
which is why the promise is answered rather than assumed.
|
||||
|
||||
Both outcomes are a toast, and neither is written here: api/toast is
|
||||
asked for one by name, so the wording lives in the worker with the rest
|
||||
of the app's words rather than in the middle of an attribute. It is the
|
||||
same request the dialog itself arrives on -- htmx, the worker, a
|
||||
fragment -- with the container in base.njk as the target.
|
||||
|
||||
Only a copy that happened ends the dialog. A refused one leaves it
|
||||
standing, because the field above it, selected, is the whole of what is
|
||||
left to copy with. #}
|
||||
<button class="btn btn-outline-secondary" type="button"
|
||||
hx-on:click="var button = this;
|
||||
var field = document.getElementById('verse-dialog-url');
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
{# The verse select's options. Rendered into the picker by reference.njk, and
|
||||
returned on their own by api/reference after a chapter or verse change.
|
||||
Both paths must produce the same options for the swap to be seamless. #}
|
||||
{% for verse in reference.verses -%}
|
||||
<option value="{{ verse.id }}"
|
||||
{%- if verse.id == reference.verse %} selected{% endif %}>{{ verse.label }}</option>
|
||||
|
||||
@@ -1,23 +1,4 @@
|
||||
{# 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/,
|
||||
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.
|
||||
|
||||
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. #}
|
||||
<div class="app-version d-none text-center mt-5 mb-2" id="app-version">
|
||||
<div class="app-version text-center mt-5 mb-2">
|
||||
<p class="app-version-digests form-text m-0">
|
||||
shell {{ version.shell }} · library {{ version.library }}
|
||||
</p>
|
||||
|
||||
Reference in new issue
Block a user