266 lines
9.4 KiB
JavaScript
266 lines
9.4 KiB
JavaScript
// 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();
|
||
})();
|