Files
kjv_api/js/verse_menu.js
T
2026-10-19 15:10:59 -07:00

266 lines
9.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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).
(function () {
var KEYS = { fav: "kjv.favorites", defs: "kjv.definitions", xrefs: "kjv.crossrefs" };
var open = null; // { box, menu }
function load(key) {
try {
var value = JSON.parse(localStorage.getItem(key));
return Array.isArray(value) ? value : [];
} catch (e) {
return [];
}
}
function save(key, list) {
try {
localStorage.setItem(key, JSON.stringify(list));
return true;
} catch (e) {
return false;
}
}
function indexOf(list, match) {
for (var i = 0; i < list.length; i++) {
if (match(list[i])) {
return i;
}
}
return -1;
}
// Adds the item if it is not stored yet, removes it if it is. Returns the new "saved" state.
function toggle(key, item, match) {
var list = load(key);
var at = indexOf(list, match);
if (at >= 0) {
list.splice(at, 1);
} else {
item.t = Date.now();
list.unshift(item);
}
save(key, list);
return at < 0;
}
function el(tag, className, text) {
var node = document.createElement(tag);
if (className) {
node.className = className;
}
if (text !== undefined) {
node.textContent = text;
}
return node;
}
function verseInfo(box) {
var link = box.querySelector(".verse-reference a");
var text = box.querySelector(".verse-text");
return {
ref: link ? link.textContent.trim() : "",
text: text ? text.textContent.trim() : "",
id: box.getAttribute("data-verse-id") || ""
};
}
function findUrl(ref) {
return "/find?kw=" + encodeURIComponent(ref) + "&view=rich";
}
function markFavorites(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);
}
}
function closeMenu() {
if (open) {
open.box.classList.remove("selected");
open.box.removeAttribute("aria-expanded");
open.menu.remove();
open = null;
}
}
function saveButton(isSaved, onToggle) {
var button = el("button", "vm-save");
button.type = "button";
function paint(saved) {
button.textContent = saved ? "Saved ✓" : "Save";
button.classList.toggle("saved", saved);
}
paint(isSaved);
button.addEventListener("click", function () { paint(onToggle()); });
return button;
}
function showDefinitions(info, panel) {
panel.textContent = "Looking up words…";
var seen = {};
var words = (info.text.toLowerCase().match(/[a-z]+/g) || []).filter(function (w) {
if (w.length < 3 || seen[w]) {
return false;
}
seen[w] = true;
return true;
});
fetch("/defs?words=" + encodeURIComponent(words.join(",")))
.then(function (r) { return r.json(); })
.then(function (defs) {
panel.textContent = "";
var found = words.filter(function (w) { return defs[w]; });
if (!found.length) {
panel.textContent = "No definitions found for the words in this verse.";
return;
}
var chips = el("div", "vm-chips");
var detail = el("div", "vm-detail");
detail.hidden = true;
found.forEach(function (word) {
var chip = el("button", "vm-chip", word);
chip.type = "button";
chip.addEventListener("click", function () {
detail.hidden = false;
detail.textContent = "";
detail.appendChild(el("strong", "", word));
defs[word].forEach(function (d) { detail.appendChild(el("p", "", d)); });
var match = function (s) { return s.word === word; };
detail.appendChild(saveButton(
indexOf(load(KEYS.defs), match) >= 0,
function () {
return toggle(KEYS.defs, { word: word, defs: defs[word], ref: info.ref }, match);
}
));
});
chips.appendChild(chip);
});
panel.appendChild(el("div", "vm-hint", "Tap a word for its 1828 Webster definition."));
panel.appendChild(chips);
panel.appendChild(detail);
})
.catch(function () { panel.textContent = "Could not load definitions."; });
}
function showCrossRefs(info, panel) {
if (!info.id) {
panel.textContent = "Cross references are not available here.";
return;
}
panel.textContent = "Loading cross references…";
fetch("/xref/" + encodeURIComponent(info.id))
.then(function (r) { return r.json(); })
.then(function (refs) {
panel.textContent = "";
if (!refs.length) {
panel.textContent = "No cross references for this verse.";
return;
}
refs.forEach(function (x) {
var row = el("div", "vm-xref");
var link = el("a", "", x.ref);
link.href = findUrl(x.ref);
row.appendChild(link);
row.appendChild(el("div", "vm-xref-text", x.text));
var match = function (s) { return s.from === info.ref && s.ref === x.ref; };
row.appendChild(saveButton(
indexOf(load(KEYS.xrefs), match) >= 0,
function () {
return toggle(KEYS.xrefs, { from: info.ref, ref: x.ref, text: x.text }, match);
}
));
panel.appendChild(row);
});
})
.catch(function () { panel.textContent = "Could not load cross references."; });
}
function openMenu(box) {
closeMenu();
var info = verseInfo(box);
var menu = el("div", "verse-menu");
menu.setAttribute("role", "group");
menu.setAttribute("aria-label", "Actions for " + info.ref);
var actions = el("div", "vm-actions");
var panel = el("div", "vm-panel");
panel.hidden = true;
var favMatch = function (f) { return f.ref === info.ref; };
var fav = el("button", "vm-btn");
fav.type = "button";
function paintFav() {
var saved = indexOf(load(KEYS.fav), favMatch) >= 0;
fav.textContent = saved ? "★ Favorited" : "☆ Favorite";
fav.classList.toggle("saved", saved);
box.classList.toggle("is-fav", saved);
}
paintFav();
fav.addEventListener("click", function () {
toggle(KEYS.fav, { ref: info.ref, text: info.text }, favMatch);
paintFav();
});
var define = el("button", "vm-btn", "Define words");
define.type = "button";
define.addEventListener("click", function () {
panel.hidden = false;
showDefinitions(info, panel);
});
var xref = el("button", "vm-btn", "Cross references");
xref.type = "button";
xref.addEventListener("click", function () {
panel.hidden = false;
showCrossRefs(info, panel);
});
var close = el("button", "vm-btn vm-close", "×");
close.type = "button";
close.setAttribute("aria-label", "Close menu");
close.addEventListener("click", closeMenu);
[fav, define, xref, close].forEach(function (b) { actions.appendChild(b); });
menu.appendChild(actions);
menu.appendChild(panel);
box.appendChild(menu);
box.classList.add("selected");
box.setAttribute("aria-expanded", "true");
open = { box: box, menu: menu };
}
document.addEventListener("click", function (event) {
var box = event.target.closest && event.target.closest(".verse-container");
if (!box) {
closeMenu();
return;
}
// links, buttons and the open menu keep their own behaviour; so does selecting text
if (event.target.closest("a, button, input, .verse-menu")) {
return;
}
var selection = window.getSelection && window.getSelection().toString();
if (selection) {
return;
}
if (open && open.box === box) {
closeMenu();
} else {
openMenu(box);
}
});
document.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
closeMenu();
}
});
markFavorites();
})();