script cache invalidation and verse menu

This commit is contained in:
2026-10-19 15:32:06 -07:00
parent 317f5bf746
commit 9d95ae3ec2
7 changed files with 317 additions and 58 deletions
+128 -14
View File
@@ -1,8 +1,9 @@
// Tap a verse to open a small menu: favorite it, look up the 1828 Webster
// definition of its words, or see its cross references. Everything saved
// lives in this browser's localStorage (read back on the /saved page).
// Every verse card gets an actions button (and the card itself is tappable)
// that opens a small menu: favorite the verse, write a note on it, look up the
// 1828 Webster definition of its words, or see its cross references.
// Everything saved lives in this browser's localStorage (see the /saved page).
(function () {
var KEYS = { fav: "kjv.favorites", defs: "kjv.definitions", xrefs: "kjv.crossrefs" };
var KEYS = { fav: "kjv.favorites", defs: "kjv.definitions", xrefs: "kjv.crossrefs", notes: "kjv.notes" };
var open = null; // { box, menu }
function load(key) {
@@ -71,12 +72,72 @@
return "/find?kw=" + encodeURIComponent(ref) + "&view=rich";
}
function markFavorites(root) {
function noteFor(ref) {
var list = load(KEYS.notes);
var at = indexOf(list, function (n) { return n.ref === ref; });
return at >= 0 ? list[at].note : "";
}
// Shows the saved note (if any) under the verse text.
function renderNote(box) {
var old = box.querySelector(".verse-note");
if (old) {
old.remove();
}
var info = verseInfo(box);
var note = noteFor(info.ref);
box.classList.toggle("has-note", !!note);
if (!note) {
return;
}
var text = box.querySelector(".verse-text");
var node = el("div", "verse-note", note);
node.setAttribute("title", "Your note");
if (text) {
text.after(node);
} else {
box.appendChild(node);
}
}
function markSaved(root) {
var favs = load(KEYS.fav);
var boxes = (root || document).querySelectorAll(".verse-container");
for (var i = 0; i < boxes.length; i++) {
var ref = verseInfo(boxes[i]).ref;
boxes[i].classList.toggle("is-fav", indexOf(favs, function (f) { return f.ref === ref; }) >= 0);
renderNote(boxes[i]);
}
}
// A visible way in to the menu on every verse, so nobody has to guess that the card is tappable.
function addMenuButtons() {
var boxes = document.querySelectorAll(".verse-container");
for (var i = 0; i < boxes.length; i++) {
var header = boxes[i].querySelector(".verse-reference");
if (!header || header.querySelector(".vm-more")) {
continue;
}
var button = el("button", "vm-more", "\u22ef");
button.type = "button";
button.setAttribute("aria-label", "Verse actions: notes, favorite, define, cross references");
button.setAttribute("title", "Notes, favorite, define, cross references");
button.addEventListener("click", (function (box) {
return function () { toggleMenu(box); };
})(boxes[i]));
header.appendChild(button);
}
var count = document.querySelector(".results-count");
if (count && boxes.length && !document.querySelector(".verse-hint")) {
count.after(el("div", "verse-hint", "Tap a verse, or its \u22ef button, to add a note, favorite it, define words or see cross references."));
}
}
function toggleMenu(box) {
if (open && open.box === box) {
closeMenu();
} else {
openMenu(box);
}
}
@@ -181,6 +242,49 @@
.catch(function () { panel.textContent = "Could not load cross references."; });
}
// Notes save as you type; clearing the box deletes the note.
function showNote(info, panel, box, onChange) {
panel.textContent = "";
var area = el("textarea", "vm-note-input");
area.rows = 4;
area.placeholder = "Your note on " + info.ref + "...";
area.setAttribute("aria-label", "Note for " + info.ref);
area.value = noteFor(info.ref);
var status = el("div", "vm-hint", "Saved in this browser as you type.");
var timer = null;
function commit() {
var value = area.value.trim();
var list = load(KEYS.notes);
var at = indexOf(list, function (n) { return n.ref === info.ref; });
if (!value) {
if (at >= 0) {
list.splice(at, 1);
}
} else if (at >= 0) {
list[at].note = value;
list[at].t = Date.now();
} else {
list.unshift({ ref: info.ref, text: info.text, note: value, t: Date.now() });
}
save(KEYS.notes, list);
renderNote(box);
onChange();
status.textContent = value ? "Saved." : "Note removed.";
}
area.addEventListener("input", function () {
status.textContent = "Saving\u2026";
clearTimeout(timer);
timer = setTimeout(commit, 400);
});
area.addEventListener("blur", function () {
clearTimeout(timer);
commit();
});
panel.appendChild(area);
panel.appendChild(status);
area.focus();
}
function openMenu(box) {
closeMenu();
var info = verseInfo(box);
@@ -206,6 +310,19 @@
paintFav();
});
var noteBtn = el("button", "vm-btn");
noteBtn.type = "button";
function paintNote() {
var has = !!noteFor(info.ref);
noteBtn.textContent = has ? "\u270e Edit note" : "\u270e Note";
noteBtn.classList.toggle("saved", has);
}
paintNote();
noteBtn.addEventListener("click", function () {
panel.hidden = false;
showNote(info, panel, box, paintNote);
});
var define = el("button", "vm-btn", "Define words");
define.type = "button";
define.addEventListener("click", function () {
@@ -225,7 +342,7 @@
close.setAttribute("aria-label", "Close menu");
close.addEventListener("click", closeMenu);
[fav, define, xref, close].forEach(function (b) { actions.appendChild(b); });
[fav, noteBtn, define, xref, close].forEach(function (b) { actions.appendChild(b); });
menu.appendChild(actions);
menu.appendChild(panel);
box.appendChild(menu);
@@ -241,25 +358,22 @@
return;
}
// links, buttons and the open menu keep their own behaviour; so does selecting text
if (event.target.closest("a, button, input, .verse-menu")) {
if (event.target.closest("a, button, input, textarea, .verse-menu")) {
return;
}
var selection = window.getSelection && window.getSelection().toString();
if (selection) {
return;
}
if (open && open.box === box) {
closeMenu();
} else {
openMenu(box);
}
toggleMenu(box);
});
document.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
if (event.key === "Escape" && !(event.target && event.target.tagName === "TEXTAREA")) {
closeMenu();
}
});
markFavorites();
markSaved();
addMenuButtons();
})();