// 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(); })();