(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 += `
Source
${source?.title || "--"}
Tradition
${source?.tradition || "--"}
Language
${source?.language || "--"}
Script
${source?.script || "--"}
${source?.workLabel || "Work"}
${work?.title || "--"}
${source?.sectionLabel || "Section"}
${section?.label || "--"}
`; 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 }; })();