Fixes
This commit is contained in:
1 parent
9407c26d2d
commit
bfb3c87558
5 files changed
+204
-129
No files matched your search
+54
-25
@@ -100,39 +100,68 @@
|
||||
<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>
|
||||
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:
|
||||
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
|
||||
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,
|
||||
{ target: '#verse-dialog .modal-content',
|
||||
swap: 'innerHTML' });"
|
||||
hx-on:keydown="if (event.key !== 'Enter' && event.key !== ' ') return;
|
||||
if (!event.target.matches('.v[id]')) return;
|
||||
event.preventDefault();
|
||||
event.target.click();">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "version.njk" %}
|
||||
</main>
|
||||
|
||||
{# Chrome, so it is written once here instead of built in JavaScript on every
|
||||
page. app.js only fills in the verse it was opened for and shows it. #}
|
||||
{# 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 status line it
|
||||
sends back rides in out of band, which fires its own event and not this one.
|
||||
|
||||
bookmark-saved comes from sw.js once the write has committed, and only then:
|
||||
a dialog that closed itself on a failed write would take the message
|
||||
explaining the failure with it. #}
|
||||
<div class="modal fade" id="verse-dialog" tabindex="-1"
|
||||
aria-labelledby="verse-dialog-ref" aria-hidden="true">
|
||||
aria-labelledby="verse-dialog-ref" aria-hidden="true"
|
||||
hx-on::after-swap="bootstrap.Modal.getOrCreateInstance(this).show();"
|
||||
hx-on:bookmark-saved="var dialog = this;
|
||||
setTimeout(function () {
|
||||
bootstrap.Modal.getOrCreateInstance(dialog).hide();
|
||||
}, 900);">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title fs-5 verse-dialog-ref" id="verse-dialog-ref"></h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% include "status.njk" %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<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="">
|
||||
<input type="hidden" name="href" value="">
|
||||
<button class="btn btn-outline-secondary" type="button"
|
||||
data-action="copy">Copy link</button>
|
||||
<button class="btn btn-primary" type="submit">Set bookmark</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
{# 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"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label class="visually-hidden" for="verse-dialog-url">Link to {{ verse.ref }}</label>
|
||||
<input class="verse-dialog-url form-control form-control-sm" id="verse-dialog-url"
|
||||
type="text" readonly value="{{ verse.url }}">
|
||||
{% include "status.njk" %}
|
||||
</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 status line is only told once the clipboard has actually taken it,
|
||||
which is why the promise is answered rather than assumed. #}
|
||||
<button class="btn btn-outline-secondary" type="button"
|
||||
hx-on:click="var field = document.getElementById('verse-dialog-url');
|
||||
var line = document.getElementById('verse-dialog-status');
|
||||
field.focus();
|
||||
field.select();
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(field.value).then(
|
||||
function () { line.textContent = 'Link copied.'; },
|
||||
function () { line.textContent = 'Copy the link above.'; });
|
||||
} else {
|
||||
line.textContent = document.execCommand('copy')
|
||||
? 'Link copied.' : 'Copy the link above.';
|
||||
}">Copy link</button>
|
||||
<button class="btn btn-primary" type="submit">Set bookmark</button>
|
||||
</form>
|
||||
</div>
|
||||
Reference in new issue
Block a user