Frontend Features and refactor

This commit is contained in:
2026-10-19 15:10:59 -07:00
parent 7a0dbe19ea
commit 317f5bf746
28 changed files with 3915 additions and 530 deletions
+160
View File
@@ -0,0 +1,160 @@
// Book name suggestions for reference/search boxes: type "joh" and get
// John, 1 John...; Tab (or tap/click) completes. Only offered while the text
// could still be the start of a book name, so keyword searches are unaffected.
(function () {
var BOOKS = __BOOKS__;
var MAX_SHOWN = 8;
var list = null;
var current = { input: null, matches: [], active: 0, explicit: false };
function norm(text) {
return text.toLowerCase().replace(/[^a-z0-9]/g, "");
}
function injectStyle() {
var style = document.createElement("style");
style.textContent =
".book-suggest{position:absolute;z-index:2000;margin:4px 0 0;padding:4px 0;list-style:none;" +
"background:var(--color-bg-secondary,#fff);color:var(--color-text-primary,#111);" +
"border:1px solid var(--color-border,#ccc);border-radius:6px;" +
"box-shadow:0 6px 18px var(--color-shadow,rgba(0,0,0,.25));font:1rem var(--font-ui,sans-serif)}" +
".book-suggest li{padding:10px 14px;cursor:pointer;min-height:44px;display:flex;align-items:center}" +
".book-suggest li[aria-selected=true]{background:var(--color-accent-primary,#2b5c94);" +
"color:var(--color-on-accent,#fff)}" +
".book-suggest .hint{min-height:0;padding:4px 14px;font-size:.8rem;cursor:default;" +
"color:var(--color-text-muted,#666);background:none}" +
"@media (hover:none){.book-suggest .hint{display:none}}";
document.head.appendChild(style);
}
function ensureList() {
if (list) {
return list;
}
injectStyle();
list = document.createElement("ul");
list.className = "book-suggest";
list.id = "book-suggest-list";
list.setAttribute("role", "listbox");
list.hidden = true;
// keep focus in the input while clicking an item
list.addEventListener("mousedown", function (event) { event.preventDefault(); });
document.body.appendChild(list);
return list;
}
function close() {
if (list) {
list.hidden = true;
}
if (current.input) {
current.input.setAttribute("aria-expanded", "false");
current.input.removeAttribute("aria-activedescendant");
}
current.matches = [];
}
function accept(book) {
var input = current.input;
input.value = book + " ";
input.setSelectionRange(input.value.length, input.value.length);
close();
input.focus();
}
function paint() {
var items = list.querySelectorAll("li[role=option]");
for (var i = 0; i < items.length; i++) {
var selected = i === current.active;
items[i].setAttribute("aria-selected", selected ? "true" : "false");
if (selected) {
current.input.setAttribute("aria-activedescendant", items[i].id);
}
}
}
function show(input, matches) {
var box = input.getBoundingClientRect();
ensureList();
list.textContent = "";
matches.forEach(function (book, i) {
var item = document.createElement("li");
item.setAttribute("role", "option");
item.id = "book-suggest-" + i;
item.textContent = book;
item.addEventListener("click", function () { accept(book); });
list.appendChild(item);
});
var hint = document.createElement("li");
hint.className = "hint";
hint.textContent = "Tab to complete";
list.appendChild(hint);
list.style.left = box.left + window.pageXOffset + "px";
list.style.top = box.bottom + window.pageYOffset + "px";
list.style.minWidth = Math.min(box.width, 320) + "px";
list.hidden = false;
input.setAttribute("aria-expanded", "true");
paint();
}
function update(input) {
current.input = input;
var value = input.value.replace(/^\s+/, "");
// "1 jo", "song of sol": optional leading book number, then letters only
var n = /^[1-3]?\s?[A-Za-z ]*$/.test(value) ? norm(value) : "";
var matches = n ? BOOKS.filter(function (b) { return norm(b).indexOf(n) === 0; }) : [];
// already typed a whole book name: nothing left to complete
if (matches.length === 1 && norm(matches[0]) === n) {
matches = [];
}
matches = matches.slice(0, MAX_SHOWN);
current.matches = matches;
current.active = 0;
current.explicit = false;
if (matches.length) {
show(input, matches);
} else {
close();
}
}
function onKeydown(event) {
var open = list && !list.hidden && current.matches.length;
if (!open) {
return;
}
var count = current.matches.length;
if (event.key === "Tab" && !event.shiftKey) {
event.preventDefault();
accept(current.matches[current.active]);
} else if (event.key === "ArrowDown" || event.key === "ArrowUp") {
event.preventDefault();
current.active = (current.active + (event.key === "ArrowDown" ? 1 : count - 1)) % count;
current.explicit = true;
paint();
} else if (event.key === "Enter" && current.explicit) {
event.preventDefault(); // pick the highlighted book instead of submitting
accept(current.matches[current.active]);
} else if (event.key === "Escape") {
close();
}
}
function attach(input) {
input.setAttribute("autocomplete", "off");
input.setAttribute("aria-autocomplete", "list");
input.setAttribute("aria-controls", "book-suggest-list");
input.addEventListener("input", function () { update(input); });
input.addEventListener("keydown", function (event) {
current.input = input;
onKeydown(event);
});
input.addEventListener("blur", close);
}
var inputs = document.querySelectorAll("input[data-book-suggest]");
for (var i = 0; i < inputs.length; i++) {
attach(inputs[i]);
}
window.addEventListener("resize", close);
})();