Move verse selector to htmx
This commit is contained in:
1 parent
dd0021ce15
commit
ba3b588ac5
9 files changed
+394
-135
No files matched your search
+16
-97
@@ -2,15 +2,21 @@
|
||||
//
|
||||
// Pages arrive whole -- composed by the service worker, or by boot.html on the
|
||||
// visit before there is one -- so there is nothing here that builds a page.
|
||||
// What is left is the three things a composed document cannot do for itself:
|
||||
// mark up the verse numbers as controls, run the modal, and tell the bookmark
|
||||
// slot when a worker has appeared to answer it.
|
||||
// What is left is the four things a composed document cannot do for itself:
|
||||
// mark up the verse numbers as controls, run the modal, put a swapped-in page
|
||||
// where it belongs before it is painted, and tell the chrome when a worker has
|
||||
// appeared to answer it.
|
||||
//
|
||||
// The reference picker used to be here as well, built by reading the chapters
|
||||
// and verses off the book on the page. It is markup and htmx now: sw.js
|
||||
// renders it from bible/verses.json and answers api/reference, and the scroll
|
||||
// and the address-bar update that a change event used to run through here are
|
||||
// response headers on the answer. See templates/reference.njk.
|
||||
//
|
||||
// The modal's markup is in templates/base.njk and the bookmark is in the
|
||||
// worker, so
|
||||
// all this file does for it is decide which verse was clicked, fill the form,
|
||||
// and show it -- Bootstrap handles opening, dismissing and focus, and htmx
|
||||
// carries the save.
|
||||
// worker, so all this file does for it is decide which verse was clicked, fill
|
||||
// the form, and show it -- Bootstrap handles opening, dismissing and focus,
|
||||
// and htmx carries the save.
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
@@ -128,102 +134,16 @@
|
||||
// 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') { fillReference(); land(); }
|
||||
});
|
||||
|
||||
// -- reference picker ---------------------------------------------------
|
||||
|
||||
// The "GEN 1:1" in the navbar. Built from the book on the page rather than
|
||||
// from a table shipped beside it: the chapters are the sections and a
|
||||
// chapter's verses are the anchors inside it, so an option can never name a
|
||||
// target that is not there to scroll to, and there is no second file to keep
|
||||
// in step, cache, or version.
|
||||
//
|
||||
// It is not kept in step with the scroll position, on purpose. It says where
|
||||
// you are going, not where you are, and it starts at 1:1 in every book.
|
||||
|
||||
function byId(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function fillOptions(select, items) {
|
||||
select.textContent = '';
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var option = document.createElement('option');
|
||||
// The value is the id of the element to land on, so nothing downstream
|
||||
// has to know how a verse anchor is spelled.
|
||||
option.value = items[i].value;
|
||||
option.textContent = items[i].label;
|
||||
select.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
function fillVerses(chapterId) {
|
||||
var section = byId(chapterId);
|
||||
var verses = section ? section.querySelectorAll('.v') : [];
|
||||
var items = [];
|
||||
for (var i = 0; i < verses.length; i++) {
|
||||
items.push({ value: verses[i].id,
|
||||
label: (verses[i].textContent || '').trim() });
|
||||
}
|
||||
fillOptions(byId('reference-verse'), items);
|
||||
}
|
||||
|
||||
function fillReference() {
|
||||
var picker = byId('reference');
|
||||
if (!picker) return;
|
||||
var book = document.querySelector('#content article.book');
|
||||
picker.classList.toggle('d-none', !book);
|
||||
picker.classList.toggle('d-flex', !!book);
|
||||
if (!book) return;
|
||||
|
||||
byId('reference-book').textContent = book.getAttribute('data-book') || '';
|
||||
|
||||
var chapters = book.querySelectorAll('.chapter[data-chapter]');
|
||||
var items = [];
|
||||
for (var i = 0; i < chapters.length; i++) {
|
||||
items.push({ value: chapters[i].id,
|
||||
label: chapters[i].getAttribute('data-chapter') });
|
||||
}
|
||||
fillOptions(byId('reference-chapter'), items);
|
||||
if (items.length) fillVerses(items[0].value);
|
||||
}
|
||||
|
||||
// The URL is the whole mechanism: name the anchor, then land on it. Nothing
|
||||
// is stored anywhere else, so a reload or a shared link ends up in the same
|
||||
// place.
|
||||
//
|
||||
// replaceState rather than assigning location.hash: spinning through a
|
||||
// select should not stack one history entry per option, and assigning the
|
||||
// hash hands the scroll to the browser, which Bootstrap has set to smooth.
|
||||
function goTo(id) {
|
||||
var target = byId(id);
|
||||
if (!target) return;
|
||||
history.replaceState(null, '', '#' + id);
|
||||
jumpTo(target);
|
||||
}
|
||||
|
||||
// Delegated at the document, so it survives every swap and binds once.
|
||||
// Changing the chapter rebuilds the verses and lands on the chapter heading,
|
||||
// which is what 1 is directly below; changing the verse lands on the verse.
|
||||
document.addEventListener('change', function (event) {
|
||||
var target = event.target;
|
||||
if (!target || !target.id) return;
|
||||
if (target.id === 'reference-chapter') {
|
||||
fillVerses(target.value);
|
||||
goTo(target.value);
|
||||
} else if (target.id === 'reference-verse') {
|
||||
goTo(target.value);
|
||||
}
|
||||
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.
|
||||
// with dead verse numbers. The picker listens for the same event itself,
|
||||
// from the navbar, because a restore reaches nothing outside #content.
|
||||
document.body.addEventListener('htmx:historyRestore', function () {
|
||||
markVerses();
|
||||
fillReference();
|
||||
land();
|
||||
});
|
||||
|
||||
@@ -289,7 +209,6 @@
|
||||
// and it lands after htmx has processed the shell.
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
markVerses();
|
||||
fillReference();
|
||||
scrollToHash();
|
||||
|
||||
// Registered from the page it is going to serve, not from a page of its
|
||||
|
||||
+8
-2
@@ -59,14 +59,20 @@
|
||||
// content is already escaped -- compile_bible.py escapes as it writes and
|
||||
// notice.njk comes out of nunjucks -- which is what the |safe on it in
|
||||
// base.njk is for.
|
||||
compose: function (fragment, mark) {
|
||||
//
|
||||
// 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,
|
||||
// which has no worker to ask for either, and both render as a hidden
|
||||
// slot that fills itself if one ever appears.
|
||||
compose: function (fragment, chrome) {
|
||||
var title = (TITLE_RE.exec(fragment) || ['', ''])[1].trim();
|
||||
var content = fragment.replace(TITLE_RE, '');
|
||||
var name = (TEMPLATE_RE.exec(content) || ['', 'page'])[1] + '.njk';
|
||||
return render(name, {
|
||||
title: title,
|
||||
content: content,
|
||||
mark: mark || null
|
||||
mark: (chrome && chrome.mark) || null,
|
||||
reference: (chrome && chrome.reference) || null
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -37,8 +37,20 @@
|
||||
// out of the response itself.
|
||||
this.restore =
|
||||
this.request.headers.get('HX-History-Restore-Request') === 'true';
|
||||
// The address the page making this request is showing. htmx stamps it on
|
||||
// everything it sends; a navigation carries nothing, because there is no
|
||||
// page yet. It is how a handler answering the chrome -- which htmx never
|
||||
// swaps, and which therefore outlives the page it was rendered for -- can
|
||||
// still find out which page that is.
|
||||
this.current = this.request.headers.get('HX-Current-URL');
|
||||
}
|
||||
|
||||
// That address as a URL, resolved against this request's own so a relative
|
||||
// one still works. Null when there was no page.
|
||||
Call.prototype.currentUrl = function () {
|
||||
return this.current ? new URL(this.current, this.url) : null;
|
||||
};
|
||||
|
||||
Call.prototype.form = function () {
|
||||
return this.request.formData();
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user