1322 lines
43 KiB
JavaScript
1322 lines
43 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
const dataService = window.TarotDataService || {};
|
|
|
|
const state = {
|
|
initialized: false,
|
|
catalog: null,
|
|
selectedSourceId: "",
|
|
selectedWorkId: "",
|
|
selectedSectionId: "",
|
|
currentPassage: null,
|
|
lexiconEntry: null,
|
|
lexiconRequestId: 0,
|
|
lexiconOccurrenceResults: null,
|
|
lexiconOccurrenceLoading: false,
|
|
lexiconOccurrenceError: "",
|
|
lexiconOccurrenceVisible: false,
|
|
lexiconOccurrenceRequestId: 0,
|
|
globalSearchQuery: "",
|
|
localSearchQuery: "",
|
|
activeSearchScope: "global",
|
|
searchQuery: "",
|
|
searchResults: null,
|
|
searchLoading: false,
|
|
searchError: "",
|
|
searchRequestId: 0,
|
|
highlightedVerseId: ""
|
|
};
|
|
|
|
let sourceListEl;
|
|
let sourceCountEl;
|
|
let globalSearchFormEl;
|
|
let globalSearchInputEl;
|
|
let globalSearchClearEl;
|
|
let localSearchFormEl;
|
|
let localSearchInputEl;
|
|
let localSearchClearEl;
|
|
let workSelectEl;
|
|
let sectionSelectEl;
|
|
let detailNameEl;
|
|
let detailSubEl;
|
|
let detailBodyEl;
|
|
let lexiconPopupEl;
|
|
let lexiconPopupTitleEl;
|
|
let lexiconPopupSubtitleEl;
|
|
let lexiconPopupBodyEl;
|
|
let lexiconPopupCloseEl;
|
|
let lexiconReturnFocusEl = null;
|
|
|
|
function normalizeId(value) {
|
|
return String(value || "")
|
|
.trim()
|
|
.toLowerCase();
|
|
}
|
|
|
|
function getElements() {
|
|
sourceListEl = document.getElementById("alpha-text-source-list");
|
|
sourceCountEl = document.getElementById("alpha-text-source-count");
|
|
globalSearchFormEl = document.getElementById("alpha-text-global-search-form");
|
|
globalSearchInputEl = document.getElementById("alpha-text-global-search-input");
|
|
globalSearchClearEl = document.getElementById("alpha-text-global-search-clear");
|
|
localSearchFormEl = document.getElementById("alpha-text-local-search-form");
|
|
localSearchInputEl = document.getElementById("alpha-text-local-search-input");
|
|
localSearchClearEl = document.getElementById("alpha-text-local-search-clear");
|
|
workSelectEl = document.getElementById("alpha-text-work-select");
|
|
sectionSelectEl = document.getElementById("alpha-text-section-select");
|
|
detailNameEl = document.getElementById("alpha-text-detail-name");
|
|
detailSubEl = document.getElementById("alpha-text-detail-sub");
|
|
detailBodyEl = document.getElementById("alpha-text-detail-body");
|
|
ensureLexiconPopup();
|
|
}
|
|
|
|
function ensureLexiconPopup() {
|
|
if (lexiconPopupEl instanceof HTMLElement) {
|
|
return;
|
|
}
|
|
|
|
const popup = document.createElement("div");
|
|
popup.className = "alpha-text-lexicon-popup";
|
|
popup.hidden = true;
|
|
popup.setAttribute("aria-hidden", "true");
|
|
|
|
const backdrop = document.createElement("div");
|
|
backdrop.className = "alpha-text-lexicon-popup-backdrop";
|
|
backdrop.addEventListener("click", closeLexiconEntry);
|
|
|
|
const card = document.createElement("section");
|
|
card.className = "alpha-text-lexicon-popup-card";
|
|
card.setAttribute("role", "dialog");
|
|
card.setAttribute("aria-modal", "true");
|
|
card.setAttribute("aria-labelledby", "alpha-text-lexicon-popup-title");
|
|
card.setAttribute("tabindex", "-1");
|
|
|
|
const header = document.createElement("div");
|
|
header.className = "alpha-text-lexicon-popup-header";
|
|
|
|
const headingWrap = document.createElement("div");
|
|
headingWrap.className = "alpha-text-lexicon-popup-heading";
|
|
|
|
const title = document.createElement("h3");
|
|
title.id = "alpha-text-lexicon-popup-title";
|
|
title.textContent = "Lexicon Entry";
|
|
|
|
const subtitle = document.createElement("p");
|
|
subtitle.className = "alpha-text-lexicon-popup-subtitle";
|
|
subtitle.textContent = "Strong's definition";
|
|
|
|
headingWrap.append(title, subtitle);
|
|
|
|
const closeButton = document.createElement("button");
|
|
closeButton.type = "button";
|
|
closeButton.className = "alpha-text-lexicon-popup-close";
|
|
closeButton.textContent = "Close";
|
|
closeButton.addEventListener("click", closeLexiconEntry);
|
|
|
|
header.append(headingWrap, closeButton);
|
|
|
|
const body = document.createElement("div");
|
|
body.className = "alpha-text-lexicon-popup-body";
|
|
|
|
card.append(header, body);
|
|
popup.append(backdrop, card);
|
|
document.body.appendChild(popup);
|
|
|
|
lexiconPopupEl = popup;
|
|
lexiconPopupTitleEl = title;
|
|
lexiconPopupSubtitleEl = subtitle;
|
|
lexiconPopupBodyEl = body;
|
|
lexiconPopupCloseEl = closeButton;
|
|
}
|
|
|
|
function getSources() {
|
|
return Array.isArray(state.catalog?.sources) ? state.catalog.sources : [];
|
|
}
|
|
|
|
function findById(entries, value) {
|
|
const needle = normalizeId(value);
|
|
return (Array.isArray(entries) ? entries : []).find((entry) => normalizeId(entry?.id) === needle) || null;
|
|
}
|
|
|
|
function getSelectedSource() {
|
|
return findById(getSources(), state.selectedSourceId);
|
|
}
|
|
|
|
function getSelectedWork(source = getSelectedSource()) {
|
|
return findById(source?.works, state.selectedWorkId);
|
|
}
|
|
|
|
function getSelectedSection(source = getSelectedSource(), work = getSelectedWork(source)) {
|
|
return findById(work?.sections, state.selectedSectionId);
|
|
}
|
|
|
|
function getSearchInput(scope) {
|
|
return scope === "source" ? localSearchInputEl : globalSearchInputEl;
|
|
}
|
|
|
|
function getStoredSearchQuery(scope) {
|
|
return scope === "source" ? state.localSearchQuery : state.globalSearchQuery;
|
|
}
|
|
|
|
function setStoredSearchQuery(scope, value) {
|
|
if (scope === "source") {
|
|
state.localSearchQuery = value;
|
|
return;
|
|
}
|
|
|
|
state.globalSearchQuery = value;
|
|
}
|
|
|
|
function updateSearchControls() {
|
|
if (globalSearchClearEl instanceof HTMLButtonElement) {
|
|
globalSearchClearEl.disabled = !String(globalSearchInputEl?.value || state.globalSearchQuery || "").trim();
|
|
}
|
|
|
|
if (localSearchClearEl instanceof HTMLButtonElement) {
|
|
localSearchClearEl.disabled = !String(localSearchInputEl?.value || state.localSearchQuery || "").trim();
|
|
}
|
|
}
|
|
|
|
function clearSearchState() {
|
|
state.searchQuery = "";
|
|
state.searchResults = null;
|
|
state.searchLoading = false;
|
|
state.searchError = "";
|
|
state.highlightedVerseId = "";
|
|
state.searchRequestId += 1;
|
|
|
|
updateSearchControls();
|
|
}
|
|
|
|
function clearScopedSearch(scope) {
|
|
setStoredSearchQuery(scope, "");
|
|
|
|
const input = getSearchInput(scope);
|
|
if (input instanceof HTMLInputElement) {
|
|
input.value = "";
|
|
}
|
|
|
|
if (state.activeSearchScope === scope) {
|
|
clearSearchState();
|
|
} else {
|
|
updateSearchControls();
|
|
}
|
|
}
|
|
|
|
function escapeRegExp(value) {
|
|
return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
}
|
|
|
|
function appendHighlightedText(target, text, query) {
|
|
if (!(target instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
|
|
const sourceText = String(text || "");
|
|
const normalizedQuery = String(query || "").trim();
|
|
target.replaceChildren();
|
|
if (!normalizedQuery) {
|
|
target.textContent = sourceText;
|
|
return;
|
|
}
|
|
|
|
const matcher = new RegExp(escapeRegExp(normalizedQuery), "ig");
|
|
let lastIndex = 0;
|
|
let match = matcher.exec(sourceText);
|
|
while (match) {
|
|
if (match.index > lastIndex) {
|
|
target.appendChild(document.createTextNode(sourceText.slice(lastIndex, match.index)));
|
|
}
|
|
|
|
const mark = document.createElement("mark");
|
|
mark.className = "alpha-text-mark";
|
|
mark.textContent = sourceText.slice(match.index, match.index + match[0].length);
|
|
target.appendChild(mark);
|
|
|
|
lastIndex = match.index + match[0].length;
|
|
match = matcher.exec(sourceText);
|
|
}
|
|
|
|
if (lastIndex < sourceText.length) {
|
|
target.appendChild(document.createTextNode(sourceText.slice(lastIndex)));
|
|
}
|
|
}
|
|
|
|
function isHighlightedVerse(verse) {
|
|
return normalizeId(verse?.id) && normalizeId(verse?.id) === normalizeId(state.highlightedVerseId);
|
|
}
|
|
|
|
function scrollHighlightedVerseIntoView() {
|
|
const highlightedVerse = detailBodyEl?.querySelector?.(".alpha-text-verse.is-highlighted");
|
|
const detailPanel = highlightedVerse?.closest?.(".planet-detail-panel");
|
|
if (!(highlightedVerse instanceof HTMLElement) || !(detailPanel instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
|
|
const verseRect = highlightedVerse.getBoundingClientRect();
|
|
const panelRect = detailPanel.getBoundingClientRect();
|
|
const targetTop = detailPanel.scrollTop
|
|
+ (verseRect.top - panelRect.top)
|
|
- (detailPanel.clientHeight / 2)
|
|
+ (verseRect.height / 2);
|
|
|
|
detailPanel.scrollTo({
|
|
top: Math.max(0, targetTop),
|
|
behavior: "smooth"
|
|
});
|
|
}
|
|
|
|
function createCard(title) {
|
|
const card = document.createElement("div");
|
|
card.className = "planet-meta-card";
|
|
if (title) {
|
|
const heading = document.createElement("strong");
|
|
heading.textContent = title;
|
|
card.appendChild(heading);
|
|
}
|
|
return card;
|
|
}
|
|
|
|
function createEmptyMessage(text) {
|
|
const message = document.createElement("div");
|
|
message.className = "alpha-text-empty";
|
|
message.textContent = text;
|
|
return message;
|
|
}
|
|
|
|
function renderPlaceholder(title, subtitle, message) {
|
|
if (detailNameEl) {
|
|
detailNameEl.textContent = title;
|
|
}
|
|
if (detailSubEl) {
|
|
detailSubEl.textContent = subtitle;
|
|
}
|
|
if (!detailBodyEl) {
|
|
return;
|
|
}
|
|
|
|
detailBodyEl.replaceChildren();
|
|
const card = createCard("Text Reader");
|
|
card.appendChild(createEmptyMessage(message));
|
|
detailBodyEl.appendChild(card);
|
|
}
|
|
|
|
function syncSelectionForSource(source) {
|
|
const works = Array.isArray(source?.works) ? source.works : [];
|
|
if (!works.length) {
|
|
state.selectedWorkId = "";
|
|
state.selectedSectionId = "";
|
|
return;
|
|
}
|
|
|
|
if (!findById(works, state.selectedWorkId)) {
|
|
state.selectedWorkId = works[0].id;
|
|
}
|
|
|
|
const work = getSelectedWork(source);
|
|
const sections = Array.isArray(work?.sections) ? work.sections : [];
|
|
if (!findById(sections, state.selectedSectionId)) {
|
|
state.selectedSectionId = sections[0]?.id || "";
|
|
}
|
|
}
|
|
|
|
async function ensureCatalogLoaded(forceRefresh = false) {
|
|
if (!forceRefresh && state.catalog) {
|
|
return state.catalog;
|
|
}
|
|
|
|
const payload = await dataService.loadTextLibrary?.(forceRefresh);
|
|
state.catalog = payload && typeof payload === "object"
|
|
? payload
|
|
: { meta: {}, sources: [], lexicons: [] };
|
|
|
|
if (!state.selectedSourceId) {
|
|
state.selectedSourceId = getSources()[0]?.id || "";
|
|
}
|
|
|
|
syncSelectionForSource(getSelectedSource());
|
|
return state.catalog;
|
|
}
|
|
|
|
function fillSelect(selectEl, entries, selectedValue, labelBuilder) {
|
|
if (!(selectEl instanceof HTMLSelectElement)) {
|
|
return;
|
|
}
|
|
|
|
selectEl.replaceChildren();
|
|
(Array.isArray(entries) ? entries : []).forEach((entry) => {
|
|
const option = document.createElement("option");
|
|
option.value = entry.id;
|
|
option.textContent = typeof labelBuilder === "function" ? labelBuilder(entry) : String(entry?.label || entry?.title || entry?.id || "");
|
|
option.selected = normalizeId(entry.id) === normalizeId(selectedValue);
|
|
selectEl.appendChild(option);
|
|
});
|
|
|
|
selectEl.disabled = !selectEl.options.length;
|
|
}
|
|
|
|
function renderSourceList() {
|
|
if (!sourceListEl) {
|
|
return;
|
|
}
|
|
|
|
sourceListEl.replaceChildren();
|
|
const sources = getSources();
|
|
sources.forEach((source) => {
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.className = "planet-list-item alpha-text-source-btn";
|
|
button.dataset.sourceId = source.id;
|
|
button.setAttribute("role", "option");
|
|
|
|
const isSelected = normalizeId(source.id) === normalizeId(state.selectedSourceId);
|
|
button.classList.toggle("is-selected", isSelected);
|
|
button.setAttribute("aria-selected", isSelected ? "true" : "false");
|
|
|
|
const name = document.createElement("span");
|
|
name.className = "planet-list-name";
|
|
name.textContent = source.title;
|
|
|
|
const meta = document.createElement("span");
|
|
meta.className = "alpha-text-source-meta";
|
|
const sectionLabel = source.sectionLabel || "Section";
|
|
meta.textContent = `${source.shortTitle || source.title} · ${source.stats?.workCount || 0} ${source.workLabel || "Works"} · ${source.stats?.sectionCount || 0} ${sectionLabel.toLowerCase()}s`;
|
|
|
|
button.append(name, meta);
|
|
button.addEventListener("click", () => {
|
|
if (normalizeId(source.id) === normalizeId(state.selectedSourceId)) {
|
|
return;
|
|
}
|
|
|
|
state.selectedSourceId = source.id;
|
|
state.currentPassage = null;
|
|
state.lexiconEntry = null;
|
|
state.highlightedVerseId = "";
|
|
syncSelectionForSource(getSelectedSource());
|
|
renderSourceList();
|
|
renderSelectors();
|
|
|
|
if (state.searchQuery && state.activeSearchScope === "source") {
|
|
void Promise.all([loadSelectedPassage(), runSearch("source")]);
|
|
return;
|
|
}
|
|
|
|
void loadSelectedPassage();
|
|
});
|
|
|
|
sourceListEl.appendChild(button);
|
|
});
|
|
|
|
if (!sources.length) {
|
|
sourceListEl.appendChild(createEmptyMessage("No text sources are available."));
|
|
}
|
|
|
|
if (sourceCountEl) {
|
|
sourceCountEl.textContent = `${sources.length} sources`;
|
|
}
|
|
}
|
|
|
|
function renderSelectors() {
|
|
const source = getSelectedSource();
|
|
const work = getSelectedWork(source);
|
|
const works = Array.isArray(source?.works) ? source.works : [];
|
|
const sections = Array.isArray(work?.sections) ? work.sections : [];
|
|
|
|
fillSelect(workSelectEl, works, state.selectedWorkId, (entry) => `${entry.title} (${entry.sectionCount} ${String(source?.sectionLabel || "section").toLowerCase()}s)`);
|
|
fillSelect(sectionSelectEl, sections, state.selectedSectionId, (entry) => `${entry.label} · ${entry.verseCount} verses`);
|
|
}
|
|
|
|
function closeLexiconEntry() {
|
|
dismissLexiconEntry();
|
|
}
|
|
|
|
function clearLexiconOccurrenceState() {
|
|
state.lexiconOccurrenceResults = null;
|
|
state.lexiconOccurrenceLoading = false;
|
|
state.lexiconOccurrenceError = "";
|
|
state.lexiconOccurrenceVisible = false;
|
|
state.lexiconOccurrenceRequestId += 1;
|
|
}
|
|
|
|
function dismissLexiconEntry(options = {}) {
|
|
const shouldRestoreFocus = options.restoreFocus !== false;
|
|
state.lexiconRequestId += 1;
|
|
state.lexiconEntry = null;
|
|
clearLexiconOccurrenceState();
|
|
renderLexiconPopup();
|
|
|
|
const returnFocusEl = lexiconReturnFocusEl;
|
|
lexiconReturnFocusEl = null;
|
|
|
|
if (shouldRestoreFocus && returnFocusEl instanceof HTMLElement && returnFocusEl.isConnected) {
|
|
requestAnimationFrame(() => {
|
|
if (returnFocusEl.isConnected) {
|
|
returnFocusEl.focus();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
async function toggleLexiconOccurrences() {
|
|
const lexiconId = state.lexiconEntry?.lexicon?.id || state.lexiconEntry?.lexiconId || "";
|
|
const entryId = state.lexiconEntry?.entryId || "";
|
|
if (!lexiconId || !entryId) {
|
|
return;
|
|
}
|
|
|
|
if (state.lexiconOccurrenceVisible && !state.lexiconOccurrenceLoading) {
|
|
state.lexiconOccurrenceVisible = false;
|
|
renderLexiconPopup();
|
|
return;
|
|
}
|
|
|
|
state.lexiconOccurrenceVisible = true;
|
|
if (state.lexiconOccurrenceResults || state.lexiconOccurrenceError) {
|
|
renderLexiconPopup();
|
|
return;
|
|
}
|
|
|
|
const requestId = state.lexiconOccurrenceRequestId + 1;
|
|
state.lexiconOccurrenceRequestId = requestId;
|
|
state.lexiconOccurrenceLoading = true;
|
|
state.lexiconOccurrenceError = "";
|
|
renderLexiconPopup();
|
|
|
|
try {
|
|
const payload = await dataService.loadTextLexiconOccurrences?.(lexiconId, entryId, { limit: 100 });
|
|
if (requestId !== state.lexiconOccurrenceRequestId) {
|
|
return;
|
|
}
|
|
|
|
state.lexiconOccurrenceResults = payload;
|
|
state.lexiconOccurrenceLoading = false;
|
|
renderLexiconPopup();
|
|
} catch (error) {
|
|
if (requestId !== state.lexiconOccurrenceRequestId) {
|
|
return;
|
|
}
|
|
|
|
state.lexiconOccurrenceLoading = false;
|
|
state.lexiconOccurrenceError = error?.message || "Unable to load verse occurrences for this Strong's entry.";
|
|
renderLexiconPopup();
|
|
}
|
|
}
|
|
|
|
async function openLexiconOccurrence(result) {
|
|
dismissLexiconEntry({ restoreFocus: false });
|
|
await openSearchResult(result);
|
|
}
|
|
|
|
function appendLexiconOccurrencePreview(target, result) {
|
|
if (!(target instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
|
|
target.replaceChildren();
|
|
|
|
const previewTokens = Array.isArray(result?.previewTokens) ? result.previewTokens : [];
|
|
if (!previewTokens.length) {
|
|
target.textContent = result?.preview || result?.reference || "";
|
|
return;
|
|
}
|
|
|
|
previewTokens.forEach((token, index) => {
|
|
const text = String(token?.text || "").trim();
|
|
if (!text) {
|
|
return;
|
|
}
|
|
|
|
const previousText = String(previewTokens[index - 1]?.text || "").trim();
|
|
if (index > 0 && text !== "..." && previousText !== "...") {
|
|
target.appendChild(document.createTextNode(" "));
|
|
}
|
|
|
|
if (token?.isMatch) {
|
|
const mark = document.createElement("mark");
|
|
mark.className = "alpha-text-mark alpha-text-mark--lexicon";
|
|
mark.textContent = text;
|
|
target.appendChild(mark);
|
|
return;
|
|
}
|
|
|
|
target.appendChild(document.createTextNode(text));
|
|
});
|
|
}
|
|
|
|
function renderLexiconPopup() {
|
|
ensureLexiconPopup();
|
|
|
|
if (!(lexiconPopupEl instanceof HTMLElement) || !(lexiconPopupBodyEl instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
|
|
const payload = state.lexiconEntry;
|
|
const wasHidden = lexiconPopupEl.hidden;
|
|
|
|
if (!payload) {
|
|
lexiconPopupEl.hidden = true;
|
|
lexiconPopupEl.setAttribute("aria-hidden", "true");
|
|
lexiconPopupTitleEl.textContent = "Lexicon Entry";
|
|
lexiconPopupSubtitleEl.textContent = "Strong's definition";
|
|
lexiconPopupBodyEl.replaceChildren();
|
|
return;
|
|
}
|
|
|
|
lexiconPopupTitleEl.textContent = payload.entryId ? `Strong's ${payload.entryId}` : "Lexicon Entry";
|
|
lexiconPopupSubtitleEl.textContent = payload.loading
|
|
? "Loading definition..."
|
|
: "Strong's definition";
|
|
lexiconPopupBodyEl.replaceChildren();
|
|
|
|
if (payload.loading) {
|
|
lexiconPopupBodyEl.appendChild(createEmptyMessage(`Loading ${payload.entryId}...`));
|
|
} else if (payload.error) {
|
|
lexiconPopupBodyEl.appendChild(createEmptyMessage(payload.error));
|
|
} else {
|
|
const entry = payload.entry || {};
|
|
const head = document.createElement("div");
|
|
head.className = "alpha-text-lexicon-head";
|
|
|
|
const idPill = document.createElement("button");
|
|
idPill.type = "button";
|
|
idPill.className = "alpha-text-lexicon-id alpha-text-lexicon-id--button";
|
|
idPill.textContent = payload.entryId || "--";
|
|
idPill.setAttribute("aria-expanded", state.lexiconOccurrenceVisible ? "true" : "false");
|
|
idPill.addEventListener("click", () => {
|
|
void toggleLexiconOccurrences();
|
|
});
|
|
head.appendChild(idPill);
|
|
|
|
if (entry.lemma) {
|
|
const lemma = document.createElement("span");
|
|
lemma.className = "alpha-text-token-original";
|
|
lemma.textContent = entry.lemma;
|
|
head.appendChild(lemma);
|
|
}
|
|
|
|
lexiconPopupBodyEl.appendChild(head);
|
|
|
|
const rows = [
|
|
["Transliteration", entry.xlit],
|
|
["Pronunciation", entry.pron],
|
|
["Derivation", entry.derivation],
|
|
["Strong's Definition", entry.strongs_def],
|
|
["KJV Definition", entry.kjv_def]
|
|
].filter(([, value]) => String(value || "").trim());
|
|
|
|
if (rows.length) {
|
|
const dl = document.createElement("dl");
|
|
dl.className = "alpha-dl";
|
|
rows.forEach(([label, value]) => {
|
|
const dt = document.createElement("dt");
|
|
dt.textContent = label;
|
|
const dd = document.createElement("dd");
|
|
dd.textContent = String(value || "").trim();
|
|
dl.append(dt, dd);
|
|
});
|
|
lexiconPopupBodyEl.appendChild(dl);
|
|
}
|
|
|
|
const occurrenceHint = document.createElement("p");
|
|
occurrenceHint.className = "alpha-text-lexicon-hint";
|
|
occurrenceHint.textContent = "Click the Strong's number to show verses that use this entry.";
|
|
lexiconPopupBodyEl.appendChild(occurrenceHint);
|
|
|
|
if (state.lexiconOccurrenceVisible) {
|
|
const occurrenceSection = document.createElement("section");
|
|
occurrenceSection.className = "alpha-text-lexicon-occurrences";
|
|
|
|
const occurrenceTitle = document.createElement("strong");
|
|
occurrenceTitle.textContent = "Verse Occurrences";
|
|
occurrenceSection.appendChild(occurrenceTitle);
|
|
|
|
if (state.lexiconOccurrenceLoading) {
|
|
occurrenceSection.appendChild(createEmptyMessage(`Loading verses for ${payload.entryId}...`));
|
|
} else if (state.lexiconOccurrenceError) {
|
|
occurrenceSection.appendChild(createEmptyMessage(state.lexiconOccurrenceError));
|
|
} else {
|
|
const occurrencePayload = state.lexiconOccurrenceResults;
|
|
const totalMatches = Number(occurrencePayload?.totalMatches) || 0;
|
|
const summary = document.createElement("p");
|
|
summary.className = "alpha-text-search-summary";
|
|
summary.textContent = totalMatches
|
|
? `${totalMatches} verses use ${payload.entryId}.${occurrencePayload?.truncated ? ` Showing the first ${occurrencePayload.resultCount} results.` : ""}`
|
|
: `No verses found for ${payload.entryId}.`;
|
|
occurrenceSection.appendChild(summary);
|
|
|
|
if (Array.isArray(occurrencePayload?.results) && occurrencePayload.results.length) {
|
|
const occurrenceList = document.createElement("div");
|
|
occurrenceList.className = "alpha-text-lexicon-occurrence-list";
|
|
|
|
occurrencePayload.results.forEach((result) => {
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.className = "alpha-text-lexicon-occurrence";
|
|
|
|
const headRow = document.createElement("div");
|
|
headRow.className = "alpha-text-search-result-head";
|
|
|
|
const reference = document.createElement("span");
|
|
reference.className = "alpha-text-search-reference";
|
|
reference.textContent = result.reference || `${result.workTitle} ${result.sectionLabel}:${result.verseNumber}`;
|
|
|
|
const location = document.createElement("span");
|
|
location.className = "alpha-text-search-location";
|
|
location.textContent = `${result.sourceShortTitle || result.sourceTitle} · ${result.workTitle} · ${result.sectionLabel}`;
|
|
|
|
const preview = document.createElement("p");
|
|
preview.className = "alpha-text-search-preview alpha-text-search-preview--compact";
|
|
appendLexiconOccurrencePreview(preview, result);
|
|
|
|
button.addEventListener("click", () => {
|
|
void openLexiconOccurrence(result);
|
|
});
|
|
|
|
headRow.append(reference, location);
|
|
button.append(headRow, preview);
|
|
occurrenceList.appendChild(button);
|
|
});
|
|
|
|
occurrenceSection.appendChild(occurrenceList);
|
|
}
|
|
}
|
|
|
|
lexiconPopupBodyEl.appendChild(occurrenceSection);
|
|
}
|
|
}
|
|
|
|
lexiconPopupEl.hidden = false;
|
|
lexiconPopupEl.setAttribute("aria-hidden", "false");
|
|
|
|
if (wasHidden && lexiconPopupCloseEl instanceof HTMLButtonElement) {
|
|
requestAnimationFrame(() => {
|
|
lexiconPopupCloseEl.focus();
|
|
});
|
|
}
|
|
}
|
|
|
|
async function loadLexiconEntry(lexiconId, entryId, triggerElement) {
|
|
if (!lexiconId || !entryId) {
|
|
return;
|
|
}
|
|
|
|
if (triggerElement instanceof HTMLElement) {
|
|
lexiconReturnFocusEl = triggerElement;
|
|
}
|
|
|
|
const requestId = state.lexiconRequestId + 1;
|
|
state.lexiconRequestId = requestId;
|
|
clearLexiconOccurrenceState();
|
|
state.lexiconEntry = {
|
|
loading: true,
|
|
lexiconId,
|
|
entryId: String(entryId).toUpperCase()
|
|
};
|
|
renderDetail();
|
|
|
|
try {
|
|
const payload = await dataService.loadTextLexiconEntry?.(lexiconId, entryId);
|
|
if (requestId !== state.lexiconRequestId) {
|
|
return;
|
|
}
|
|
state.lexiconEntry = payload;
|
|
renderDetail();
|
|
} catch (error) {
|
|
if (requestId !== state.lexiconRequestId) {
|
|
return;
|
|
}
|
|
state.lexiconEntry = {
|
|
error: error?.message || "Unable to load lexicon entry.",
|
|
lexiconId,
|
|
entryId: String(entryId).toUpperCase()
|
|
};
|
|
renderDetail();
|
|
}
|
|
}
|
|
|
|
function createMetaGrid(passage) {
|
|
const source = passage?.source || getSelectedSource();
|
|
const work = passage?.work || getSelectedWork(source);
|
|
const section = passage?.section || getSelectedSection(source, work);
|
|
const metaGrid = document.createElement("div");
|
|
metaGrid.className = "alpha-text-meta-grid";
|
|
|
|
const overviewCard = createCard("Source Overview");
|
|
overviewCard.innerHTML += `
|
|
<dl class="alpha-dl">
|
|
<dt>Source</dt><dd>${source?.title || "--"}</dd>
|
|
<dt>Tradition</dt><dd>${source?.tradition || "--"}</dd>
|
|
<dt>Language</dt><dd>${source?.language || "--"}</dd>
|
|
<dt>Script</dt><dd>${source?.script || "--"}</dd>
|
|
<dt>${source?.workLabel || "Work"}</dt><dd>${work?.title || "--"}</dd>
|
|
<dt>${source?.sectionLabel || "Section"}</dt><dd>${section?.label || "--"}</dd>
|
|
</dl>
|
|
`;
|
|
metaGrid.appendChild(overviewCard);
|
|
|
|
const navigationCard = createCard("Navigation");
|
|
const toolbar = document.createElement("div");
|
|
toolbar.className = "alpha-text-toolbar";
|
|
|
|
const previousButton = document.createElement("button");
|
|
previousButton.type = "button";
|
|
previousButton.className = "alpha-nav-btn";
|
|
previousButton.textContent = "← Previous";
|
|
previousButton.disabled = !passage?.navigation?.previous;
|
|
previousButton.addEventListener("click", () => {
|
|
if (!passage?.navigation?.previous) {
|
|
return;
|
|
}
|
|
state.selectedWorkId = passage.navigation.previous.workId;
|
|
state.selectedSectionId = passage.navigation.previous.sectionId;
|
|
state.lexiconEntry = null;
|
|
renderSelectors();
|
|
void loadSelectedPassage();
|
|
});
|
|
|
|
const nextButton = document.createElement("button");
|
|
nextButton.type = "button";
|
|
nextButton.className = "alpha-nav-btn";
|
|
nextButton.textContent = "Next →";
|
|
nextButton.disabled = !passage?.navigation?.next;
|
|
nextButton.addEventListener("click", () => {
|
|
if (!passage?.navigation?.next) {
|
|
return;
|
|
}
|
|
state.selectedWorkId = passage.navigation.next.workId;
|
|
state.selectedSectionId = passage.navigation.next.sectionId;
|
|
state.lexiconEntry = null;
|
|
renderSelectors();
|
|
void loadSelectedPassage();
|
|
});
|
|
|
|
const location = document.createElement("div");
|
|
location.className = "planet-text";
|
|
location.textContent = `${work?.title || "--"} · ${section?.title || "--"}`;
|
|
|
|
toolbar.append(previousButton, nextButton);
|
|
navigationCard.append(toolbar, location);
|
|
metaGrid.appendChild(navigationCard);
|
|
|
|
if (source?.features?.hasTokenAnnotations) {
|
|
const noteCard = createCard("Reader Mode");
|
|
noteCard.appendChild(createEmptyMessage("This source is tokenized. Click a Strong's code chip to open its lexicon entry."));
|
|
metaGrid.appendChild(noteCard);
|
|
}
|
|
|
|
return metaGrid;
|
|
}
|
|
|
|
function createPlainVerse(verse) {
|
|
const article = document.createElement("article");
|
|
article.className = "alpha-text-verse";
|
|
article.classList.toggle("is-highlighted", isHighlightedVerse(verse));
|
|
|
|
const head = document.createElement("div");
|
|
head.className = "alpha-text-verse-head";
|
|
|
|
const reference = document.createElement("span");
|
|
reference.className = "alpha-text-verse-reference";
|
|
reference.textContent = verse.reference || (verse.number ? `Verse ${verse.number}` : "");
|
|
|
|
const text = document.createElement("p");
|
|
text.className = "alpha-text-verse-text";
|
|
appendHighlightedText(text, verse.text || "", isHighlightedVerse(verse) ? state.searchQuery : "");
|
|
|
|
head.append(reference);
|
|
article.append(head, text);
|
|
return article;
|
|
}
|
|
|
|
function buildTokenTranslationText(tokens, fallbackText) {
|
|
const glossText = (Array.isArray(tokens) ? tokens : [])
|
|
.map((token) => String(token?.gloss || "").trim())
|
|
.filter(Boolean)
|
|
.join(" ")
|
|
.replace(/\s+([,.;:!?])/g, "$1")
|
|
.trim();
|
|
|
|
return glossText || String(fallbackText || "").trim();
|
|
}
|
|
|
|
function createTokenVerse(verse, lexiconId) {
|
|
const article = document.createElement("article");
|
|
article.className = "alpha-text-verse alpha-text-verse--interlinear";
|
|
article.classList.toggle("is-highlighted", isHighlightedVerse(verse));
|
|
|
|
const head = document.createElement("div");
|
|
head.className = "alpha-text-verse-head";
|
|
|
|
const reference = document.createElement("span");
|
|
reference.className = "alpha-text-verse-reference";
|
|
reference.textContent = verse.reference || (verse.number ? `Verse ${verse.number}` : "");
|
|
|
|
const gloss = document.createElement("p");
|
|
gloss.className = "alpha-text-verse-text";
|
|
appendHighlightedText(
|
|
gloss,
|
|
buildTokenTranslationText(verse?.tokens, verse?.text),
|
|
isHighlightedVerse(verse) ? state.searchQuery : ""
|
|
);
|
|
|
|
const tokenGrid = document.createElement("div");
|
|
tokenGrid.className = "alpha-text-token-grid";
|
|
|
|
(Array.isArray(verse?.tokens) ? verse.tokens : []).forEach((token) => {
|
|
const strongId = Array.isArray(token?.strongs) ? token.strongs[0] : "";
|
|
const tokenEl = document.createElement(strongId ? "button" : "div");
|
|
tokenEl.className = `alpha-text-token${strongId ? " alpha-text-token--interactive" : ""}`;
|
|
if (tokenEl instanceof HTMLButtonElement) {
|
|
tokenEl.type = "button";
|
|
tokenEl.addEventListener("click", () => {
|
|
void loadLexiconEntry(lexiconId, strongId, tokenEl);
|
|
});
|
|
}
|
|
|
|
const glossEl = document.createElement("span");
|
|
glossEl.className = "alpha-text-token-gloss";
|
|
glossEl.textContent = token?.gloss || "—";
|
|
|
|
const originalEl = document.createElement("span");
|
|
originalEl.className = "alpha-text-token-original";
|
|
originalEl.textContent = token?.original || "—";
|
|
|
|
tokenEl.append(glossEl, originalEl);
|
|
|
|
if (strongId) {
|
|
const strongsEl = document.createElement("span");
|
|
strongsEl.className = "alpha-text-token-strongs";
|
|
strongsEl.textContent = Array.isArray(token.strongs) ? token.strongs.join(" · ") : strongId;
|
|
tokenEl.appendChild(strongsEl);
|
|
}
|
|
|
|
tokenGrid.appendChild(tokenEl);
|
|
});
|
|
|
|
head.append(reference);
|
|
article.append(head, gloss, tokenGrid);
|
|
return article;
|
|
}
|
|
|
|
function createReaderCard(passage) {
|
|
const source = passage?.source || getSelectedSource();
|
|
const card = createCard(`${source?.title || "Text"} Reader`);
|
|
const reader = document.createElement("div");
|
|
reader.className = "alpha-text-reader";
|
|
|
|
if (passage?.errorMessage) {
|
|
reader.appendChild(createEmptyMessage(passage.errorMessage));
|
|
card.appendChild(reader);
|
|
return card;
|
|
}
|
|
|
|
const verses = Array.isArray(passage?.verses) ? passage.verses : [];
|
|
if (!verses.length) {
|
|
reader.appendChild(createEmptyMessage("No verses were found for this section."));
|
|
card.appendChild(reader);
|
|
return card;
|
|
}
|
|
|
|
verses.forEach((verse) => {
|
|
const verseEl = source?.features?.hasTokenAnnotations
|
|
? createTokenVerse(verse, source.features.lexiconIds?.[0] || "")
|
|
: createPlainVerse(verse);
|
|
reader.appendChild(verseEl);
|
|
});
|
|
|
|
card.appendChild(reader);
|
|
return card;
|
|
}
|
|
|
|
function createSearchCard() {
|
|
const hasSearchState = state.searchLoading || state.searchError || state.searchResults || state.searchQuery;
|
|
if (!hasSearchState) {
|
|
return null;
|
|
}
|
|
|
|
const card = createCard("Search Results");
|
|
const scopeLabel = state.activeSearchScope === "global"
|
|
? "all texts"
|
|
: (state.searchResults?.scope?.source?.title || getSelectedSource()?.title || "current source");
|
|
|
|
const summary = document.createElement("p");
|
|
summary.className = "alpha-text-search-summary";
|
|
|
|
if (state.searchLoading) {
|
|
summary.textContent = `Searching ${scopeLabel} for \"${state.searchQuery}\"...`;
|
|
card.appendChild(summary);
|
|
return card;
|
|
}
|
|
|
|
if (state.searchError) {
|
|
summary.textContent = `Search scope: ${scopeLabel}`;
|
|
card.append(summary, createEmptyMessage(state.searchError));
|
|
return card;
|
|
}
|
|
|
|
const payload = state.searchResults;
|
|
const totalMatches = Number(payload?.totalMatches) || 0;
|
|
const truncatedNote = payload?.truncated ? ` Showing the first ${payload.resultCount} results.` : "";
|
|
summary.textContent = `${totalMatches} matches in ${scopeLabel}.${truncatedNote}`;
|
|
card.appendChild(summary);
|
|
|
|
if (!Array.isArray(payload?.results) || !payload.results.length) {
|
|
card.appendChild(createEmptyMessage(`No matches found for \"${state.searchQuery}\".`));
|
|
return card;
|
|
}
|
|
|
|
const resultsEl = document.createElement("div");
|
|
resultsEl.className = "alpha-text-search-results";
|
|
|
|
payload.results.forEach((result) => {
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.className = "alpha-text-search-result";
|
|
button.classList.toggle(
|
|
"is-active",
|
|
normalizeId(result?.sourceId) === normalizeId(state.selectedSourceId)
|
|
&& normalizeId(result?.workId) === normalizeId(state.selectedWorkId)
|
|
&& normalizeId(result?.sectionId) === normalizeId(state.selectedSectionId)
|
|
&& normalizeId(result?.verseId) === normalizeId(state.highlightedVerseId)
|
|
);
|
|
|
|
const head = document.createElement("div");
|
|
head.className = "alpha-text-search-result-head";
|
|
|
|
const reference = document.createElement("span");
|
|
reference.className = "alpha-text-search-reference";
|
|
reference.textContent = result.reference || `${result.workTitle} ${result.sectionLabel}:${result.verseNumber}`;
|
|
|
|
const location = document.createElement("span");
|
|
location.className = "alpha-text-search-location";
|
|
location.textContent = state.activeSearchScope === "global"
|
|
? `${result.sourceShortTitle || result.sourceTitle} · ${result.workTitle} · ${result.sectionLabel}`
|
|
: `${result.workTitle} · ${result.sectionLabel}`;
|
|
|
|
const preview = document.createElement("p");
|
|
preview.className = "alpha-text-search-preview";
|
|
appendHighlightedText(preview, result.preview || result.reference || "", state.searchQuery);
|
|
|
|
button.addEventListener("click", () => {
|
|
void openSearchResult(result);
|
|
});
|
|
|
|
head.append(reference, location);
|
|
button.append(head, preview);
|
|
resultsEl.appendChild(button);
|
|
});
|
|
|
|
card.appendChild(resultsEl);
|
|
return card;
|
|
}
|
|
|
|
function isGlobalSearchOnlyMode() {
|
|
return state.activeSearchScope === "global"
|
|
&& Boolean(state.searchQuery)
|
|
&& !state.highlightedVerseId;
|
|
}
|
|
|
|
function renderDetail() {
|
|
const source = getSelectedSource();
|
|
const work = getSelectedWork(source);
|
|
const section = getSelectedSection(source, work);
|
|
const globalSearchOnlyMode = isGlobalSearchOnlyMode();
|
|
|
|
if (!source || !work || !section) {
|
|
renderPlaceholder("Text Reader", "Select a source to begin", "Choose a text source and section from the left panel.");
|
|
renderLexiconPopup();
|
|
return;
|
|
}
|
|
|
|
if (detailNameEl) {
|
|
detailNameEl.textContent = globalSearchOnlyMode
|
|
? `Global Search${state.searchQuery ? `: ${state.searchQuery}` : ""}`
|
|
: (state.currentPassage?.section?.title || section.title);
|
|
}
|
|
if (detailSubEl) {
|
|
detailSubEl.textContent = globalSearchOnlyMode
|
|
? "All text sources"
|
|
: `${source.title} · ${work.title}`;
|
|
}
|
|
if (!detailBodyEl) {
|
|
return;
|
|
}
|
|
|
|
detailBodyEl.replaceChildren();
|
|
|
|
const searchCard = createSearchCard();
|
|
if (searchCard) {
|
|
detailBodyEl.appendChild(searchCard);
|
|
}
|
|
|
|
if (globalSearchOnlyMode) {
|
|
renderLexiconPopup();
|
|
return;
|
|
}
|
|
|
|
if (!state.currentPassage) {
|
|
const loadingCard = createCard("Text Reader");
|
|
loadingCard.appendChild(createEmptyMessage("Loading section…"));
|
|
detailBodyEl.appendChild(loadingCard);
|
|
renderLexiconPopup();
|
|
return;
|
|
}
|
|
|
|
detailBodyEl.appendChild(createMetaGrid(state.currentPassage));
|
|
detailBodyEl.appendChild(createReaderCard(state.currentPassage));
|
|
renderLexiconPopup();
|
|
}
|
|
|
|
async function loadSelectedPassage() {
|
|
const source = getSelectedSource();
|
|
const work = getSelectedWork(source);
|
|
const section = getSelectedSection(source, work);
|
|
if (!source || !work || !section) {
|
|
state.currentPassage = null;
|
|
renderDetail();
|
|
return;
|
|
}
|
|
|
|
state.currentPassage = null;
|
|
renderDetail();
|
|
|
|
try {
|
|
state.currentPassage = await dataService.loadTextSection?.(source.id, work.id, section.id);
|
|
renderDetail();
|
|
if (state.highlightedVerseId) {
|
|
requestAnimationFrame(scrollHighlightedVerseIntoView);
|
|
}
|
|
} catch (error) {
|
|
state.currentPassage = {
|
|
source,
|
|
work,
|
|
section,
|
|
verses: [],
|
|
errorMessage: error?.message || "Unable to load this section."
|
|
};
|
|
renderDetail();
|
|
}
|
|
}
|
|
|
|
async function runSearch(scope, forceRefresh = false) {
|
|
const searchFn = dataService.searchTextLibrary;
|
|
if (typeof searchFn !== "function") {
|
|
state.searchError = "Text search is unavailable.";
|
|
state.searchLoading = false;
|
|
state.searchResults = null;
|
|
renderDetail();
|
|
return;
|
|
}
|
|
|
|
const normalizedScope = scope === "source" ? "source" : "global";
|
|
const query = String(getSearchInput(normalizedScope)?.value || getStoredSearchQuery(normalizedScope) || "").trim();
|
|
setStoredSearchQuery(normalizedScope, query);
|
|
state.activeSearchScope = normalizedScope;
|
|
state.searchQuery = query;
|
|
state.searchError = "";
|
|
state.searchResults = null;
|
|
state.highlightedVerseId = "";
|
|
updateSearchControls();
|
|
|
|
if (!query) {
|
|
clearSearchState();
|
|
renderDetail();
|
|
return;
|
|
}
|
|
|
|
const requestId = state.searchRequestId + 1;
|
|
state.searchRequestId = requestId;
|
|
state.searchLoading = true;
|
|
renderDetail();
|
|
|
|
try {
|
|
const payload = await searchFn(query, {
|
|
sourceId: normalizedScope === "source" ? state.selectedSourceId : "",
|
|
limit: 50
|
|
}, forceRefresh);
|
|
|
|
if (requestId !== state.searchRequestId) {
|
|
return;
|
|
}
|
|
|
|
state.searchResults = payload;
|
|
state.searchLoading = false;
|
|
renderDetail();
|
|
} catch (error) {
|
|
if (requestId !== state.searchRequestId) {
|
|
return;
|
|
}
|
|
|
|
state.searchLoading = false;
|
|
state.searchError = error?.message || "Unable to search this text library.";
|
|
renderDetail();
|
|
}
|
|
}
|
|
|
|
async function openSearchResult(result) {
|
|
if (!result) {
|
|
return;
|
|
}
|
|
|
|
state.selectedSourceId = result.sourceId;
|
|
state.selectedWorkId = result.workId;
|
|
state.selectedSectionId = result.sectionId;
|
|
state.highlightedVerseId = result.verseId;
|
|
dismissLexiconEntry({ restoreFocus: false });
|
|
syncSelectionForSource(getSelectedSource());
|
|
renderSourceList();
|
|
renderSelectors();
|
|
await loadSelectedPassage();
|
|
}
|
|
|
|
function bindControls() {
|
|
if (state.initialized) {
|
|
return;
|
|
}
|
|
|
|
if (globalSearchFormEl instanceof HTMLFormElement) {
|
|
globalSearchFormEl.addEventListener("submit", (event) => {
|
|
event.preventDefault();
|
|
void runSearch("global");
|
|
});
|
|
}
|
|
|
|
if (globalSearchInputEl instanceof HTMLInputElement) {
|
|
globalSearchInputEl.addEventListener("input", () => {
|
|
state.globalSearchQuery = String(globalSearchInputEl.value || "").trim();
|
|
updateSearchControls();
|
|
if (!state.globalSearchQuery && state.activeSearchScope === "global" && state.searchQuery) {
|
|
clearSearchState();
|
|
renderDetail();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (globalSearchClearEl instanceof HTMLButtonElement) {
|
|
globalSearchClearEl.addEventListener("click", () => {
|
|
clearScopedSearch("global");
|
|
renderDetail();
|
|
});
|
|
}
|
|
|
|
if (localSearchFormEl instanceof HTMLFormElement) {
|
|
localSearchFormEl.addEventListener("submit", (event) => {
|
|
event.preventDefault();
|
|
void runSearch("source");
|
|
});
|
|
}
|
|
|
|
if (localSearchInputEl instanceof HTMLInputElement) {
|
|
localSearchInputEl.addEventListener("input", () => {
|
|
state.localSearchQuery = String(localSearchInputEl.value || "").trim();
|
|
updateSearchControls();
|
|
if (!state.localSearchQuery && state.activeSearchScope === "source" && state.searchQuery) {
|
|
clearSearchState();
|
|
renderDetail();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (localSearchClearEl instanceof HTMLButtonElement) {
|
|
localSearchClearEl.addEventListener("click", () => {
|
|
clearScopedSearch("source");
|
|
renderDetail();
|
|
});
|
|
}
|
|
|
|
if (workSelectEl) {
|
|
workSelectEl.addEventListener("change", () => {
|
|
state.selectedWorkId = String(workSelectEl.value || "");
|
|
const source = getSelectedSource();
|
|
syncSelectionForSource(source);
|
|
state.currentPassage = null;
|
|
state.lexiconEntry = null;
|
|
state.highlightedVerseId = "";
|
|
renderSelectors();
|
|
void loadSelectedPassage();
|
|
});
|
|
}
|
|
|
|
if (sectionSelectEl) {
|
|
sectionSelectEl.addEventListener("change", () => {
|
|
state.selectedSectionId = String(sectionSelectEl.value || "");
|
|
state.currentPassage = null;
|
|
state.lexiconEntry = null;
|
|
state.highlightedVerseId = "";
|
|
void loadSelectedPassage();
|
|
});
|
|
}
|
|
|
|
document.addEventListener("keydown", (event) => {
|
|
if (event.key === "Escape" && state.lexiconEntry) {
|
|
closeLexiconEntry();
|
|
}
|
|
});
|
|
|
|
state.initialized = true;
|
|
}
|
|
|
|
async function ensureAlphabetTextSection() {
|
|
getElements();
|
|
bindControls();
|
|
|
|
if (!sourceListEl || !detailBodyEl) {
|
|
return;
|
|
}
|
|
|
|
await ensureCatalogLoaded();
|
|
renderSourceList();
|
|
renderSelectors();
|
|
updateSearchControls();
|
|
|
|
if (!state.currentPassage) {
|
|
await loadSelectedPassage();
|
|
return;
|
|
}
|
|
|
|
renderDetail();
|
|
}
|
|
|
|
function resetState() {
|
|
state.catalog = null;
|
|
state.currentPassage = null;
|
|
state.lexiconEntry = null;
|
|
state.selectedSourceId = "";
|
|
state.selectedWorkId = "";
|
|
state.selectedSectionId = "";
|
|
state.lexiconRequestId = 0;
|
|
state.lexiconOccurrenceResults = null;
|
|
state.lexiconOccurrenceLoading = false;
|
|
state.lexiconOccurrenceError = "";
|
|
state.lexiconOccurrenceVisible = false;
|
|
state.lexiconOccurrenceRequestId = 0;
|
|
state.globalSearchQuery = "";
|
|
state.localSearchQuery = "";
|
|
state.activeSearchScope = "global";
|
|
state.searchQuery = "";
|
|
state.searchResults = null;
|
|
state.searchLoading = false;
|
|
state.searchError = "";
|
|
state.searchRequestId = 0;
|
|
state.highlightedVerseId = "";
|
|
lexiconReturnFocusEl = null;
|
|
|
|
if (globalSearchInputEl instanceof HTMLInputElement) {
|
|
globalSearchInputEl.value = "";
|
|
}
|
|
|
|
if (localSearchInputEl instanceof HTMLInputElement) {
|
|
localSearchInputEl.value = "";
|
|
}
|
|
|
|
updateSearchControls();
|
|
renderLexiconPopup();
|
|
}
|
|
|
|
document.addEventListener("connection:updated", resetState);
|
|
|
|
window.AlphabetTextUi = {
|
|
ensureAlphabetTextSection
|
|
};
|
|
})(); |