(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: "", displayPreferencesBySource: {} }; let sourceListEl; let sourceCountEl; let globalSearchFormEl; let globalSearchInputEl; let localSearchFormEl; let localSearchInputEl; let workSelectEl; let sectionSelectEl; let detailNameEl; let detailSubEl; let detailBodyEl; let textLayoutEl; 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"); localSearchFormEl = document.getElementById("alpha-text-local-search-form"); localSearchInputEl = document.getElementById("alpha-text-local-search-input"); 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"); textLayoutEl = sourceListEl?.closest?.(".planet-layout") || detailBodyEl?.closest?.(".planet-layout") || null; ensureLexiconPopup(); } function showDetailOnlyMode() { if (!(textLayoutEl instanceof HTMLElement)) { return; } window.TarotChromeUi?.initializeSidebarPopouts?.(); window.TarotChromeUi?.initializeDetailPopouts?.(); window.TarotChromeUi?.initializeSidebarAutoCollapse?.(); window.TarotChromeUi?.showDetailOnly?.(textLayoutEl); } 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 normalizeTextValue(value) { return String(value || "").trim(); } const GREEK_TRANSLITERATION_MAP = { α: "a", β: "b", γ: "g", δ: "d", ε: "e", ζ: "z", η: "e", θ: "th", ι: "i", κ: "k", λ: "l", μ: "m", ν: "n", ξ: "x", ο: "o", π: "p", ρ: "r", σ: "s", ς: "s", τ: "t", υ: "u", φ: "ph", χ: "ch", ψ: "ps", ω: "o" }; const HEBREW_TRANSLITERATION_MAP = { א: "a", ב: "b", ג: "g", ד: "d", ה: "h", ו: "v", ז: "z", ח: "ch", ט: "t", י: "y", כ: "k", ך: "k", ל: "l", מ: "m", ם: "m", נ: "n", ן: "n", ס: "s", ע: "a", פ: "p", ף: "p", צ: "ts", ץ: "ts", ק: "q", ר: "r", ש: "sh", ת: "t" }; function stripSourceScriptMarks(value) { return String(value || "") .normalize("NFD") .replace(/[\u0300-\u036f\u0591-\u05c7]/g, ""); } function transliterateSourceScriptText(value) { const stripped = stripSourceScriptMarks(value); let result = ""; for (const character of stripped) { const lowerCharacter = character.toLowerCase(); const mapped = GREEK_TRANSLITERATION_MAP[lowerCharacter] || HEBREW_TRANSLITERATION_MAP[character] || HEBREW_TRANSLITERATION_MAP[lowerCharacter]; result += mapped != null ? mapped : character; } return result .replace(/\s+([,.;:!?])/g, "$1") .replace(/\s+/g, " ") .trim(); } function buildTokenDerivedTransliteration(verse) { const tokenText = (Array.isArray(verse?.tokens) ? verse.tokens : []) .map((token) => normalizeTextValue(token?.original)) .filter(Boolean) .join(" ") .replace(/\s+([,.;:!?])/g, "$1") .trim(); return tokenText ? transliterateSourceScriptText(tokenText) : ""; } function getVerseTransliteration(verse, source = null) { const metadata = verse?.metadata && typeof verse.metadata === "object" ? verse.metadata : {}; const explicit = [ verse?.transliteration, verse?.xlit, metadata?.transliteration, metadata?.transliterationText, metadata?.xlit, metadata?.romanizedText, metadata?.romanized ].map(normalizeTextValue).find(Boolean) || ""; if (explicit) { return explicit; } if (source?.features?.hasTokenAnnotations) { return buildTokenDerivedTransliteration(verse); } return ""; } 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() { return; } function clearActiveSearchUi(options = {}) { const preserveHighlight = options.preserveHighlight === true; const scope = state.activeSearchScope === "source" ? "source" : "global"; setStoredSearchQuery(scope, ""); const input = getSearchInput(scope); if (input instanceof HTMLInputElement) { input.value = ""; } state.searchQuery = ""; state.searchResults = null; state.searchLoading = false; state.searchError = ""; state.searchRequestId += 1; if (!preserveHighlight) { state.highlightedVerseId = ""; } updateSearchControls(); } function getSourceDisplayCapabilities(source, passage) { const verses = Array.isArray(passage?.verses) ? passage.verses : []; const hasOriginal = verses.some((verse) => normalizeTextValue(verse?.originalText)); const hasTransliteration = verses.some((verse) => getVerseTransliteration(verse, source)); const hasInterlinear = Boolean(source?.features?.hasTokenAnnotations); const textModeCount = 1 + (hasOriginal ? 1 : 0) + (hasTransliteration ? 1 : 0); return { hasTranslation: true, hasOriginal, hasTransliteration, hasInterlinear, hasAnyExtras: hasOriginal || hasTransliteration || hasInterlinear, supportsAllTextMode: textModeCount > 1 }; } function getDefaultTextDisplayMode(capabilities) { if (capabilities?.hasTranslation) { return "translation"; } if (capabilities?.hasOriginal) { return "original"; } if (capabilities?.hasTransliteration) { return "transliteration"; } return "translation"; } function getAvailableTextDisplayModes(capabilities) { const modes = []; if (capabilities?.hasTranslation) { modes.push("translation"); } if (capabilities?.hasOriginal) { modes.push("original"); } if (capabilities?.hasTransliteration) { modes.push("transliteration"); } if (capabilities?.supportsAllTextMode) { modes.push("all"); } return modes; } function getSourceDisplayPreferences(source, passage) { const sourceId = normalizeId(source?.id); const capabilities = getSourceDisplayCapabilities(source, passage); const availableTextModes = getAvailableTextDisplayModes(capabilities); const stored = sourceId ? state.displayPreferencesBySource[sourceId] : null; let textMode = stored?.textMode; if (!availableTextModes.includes(textMode)) { textMode = getDefaultTextDisplayMode(capabilities); } const preferences = { textMode, showInterlinear: capabilities.hasInterlinear ? Boolean(stored?.showInterlinear) : false }; if (sourceId) { state.displayPreferencesBySource[sourceId] = preferences; } return { ...preferences, capabilities, availableTextModes }; } function updateSourceDisplayPreferences(source, patch) { const sourceId = normalizeId(source?.id); if (!sourceId) { return; } const current = state.displayPreferencesBySource[sourceId] || {}; state.displayPreferencesBySource[sourceId] = { ...current, ...patch }; } function formatTextDisplayModeLabel(mode) { switch (mode) { case "translation": return "Translation"; case "original": return "Original"; case "transliteration": return "Transliteration"; case "all": return "All"; default: return "Display"; } } 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 = "detail-meta-card 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 navigateToPassageTarget(target) { if (!target) { return; } state.selectedWorkId = target.workId; state.selectedSectionId = target.sectionId; state.lexiconEntry = null; renderSelectors(); void loadSelectedPassage(); } function getPassageLocationLabel(passage) { const source = passage?.source || getSelectedSource(); const work = passage?.work || getSelectedWork(source); const section = passage?.section || getSelectedSection(source, work); return `${work?.title || "--"} · ${section?.title || section?.label || "--"}`; } 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)) { showDetailOnlyMode(); return; } state.selectedSourceId = source.id; state.currentPassage = null; state.lexiconEntry = null; state.highlightedVerseId = ""; syncSelectionForSource(getSelectedSource()); renderSourceList(); renderSelectors(); showDetailOnlyMode(); 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 displayPreferences = getSourceDisplayPreferences(source, passage); 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); if (displayPreferences.capabilities.hasAnyExtras) { const extraCard = createCard("Extra"); extraCard.classList.add("alpha-text-extra-card"); if (displayPreferences.availableTextModes.length > 1) { const displayGroup = document.createElement("div"); displayGroup.className = "alpha-text-extra-group"; const displayLabel = document.createElement("span"); displayLabel.className = "alpha-text-extra-label"; displayLabel.textContent = "Display"; const displayButtons = document.createElement("div"); displayButtons.className = "alpha-nav-btns alpha-text-extra-actions"; displayPreferences.availableTextModes.forEach((mode) => { const button = document.createElement("button"); button.type = "button"; button.className = "alpha-nav-btn"; button.textContent = formatTextDisplayModeLabel(mode); button.setAttribute("aria-pressed", displayPreferences.textMode === mode ? "true" : "false"); button.classList.toggle("is-selected", displayPreferences.textMode === mode); button.addEventListener("click", () => { updateSourceDisplayPreferences(source, { textMode: mode }); renderDetail(); }); displayButtons.appendChild(button); }); displayGroup.append(displayLabel, displayButtons); extraCard.appendChild(displayGroup); } if (displayPreferences.capabilities.hasInterlinear) { const interlinearGroup = document.createElement("div"); interlinearGroup.className = "alpha-text-extra-group"; const interlinearLabel = document.createElement("span"); interlinearLabel.className = "alpha-text-extra-label"; interlinearLabel.textContent = "Reader"; const interlinearButtons = document.createElement("div"); interlinearButtons.className = "alpha-nav-btns alpha-text-extra-actions"; const interlinearButton = document.createElement("button"); interlinearButton.type = "button"; interlinearButton.className = "alpha-nav-btn"; interlinearButton.textContent = "Interlinear"; interlinearButton.setAttribute("aria-pressed", displayPreferences.showInterlinear ? "true" : "false"); interlinearButton.classList.toggle("is-selected", displayPreferences.showInterlinear); interlinearButton.addEventListener("click", () => { updateSourceDisplayPreferences(source, { showInterlinear: !displayPreferences.showInterlinear }); renderDetail(); }); interlinearButtons.appendChild(interlinearButton); interlinearGroup.append(interlinearLabel, interlinearButtons); extraCard.appendChild(interlinearGroup); } metaGrid.appendChild(extraCard); } 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 source = getSelectedSource(); const displayPreferences = getSourceDisplayPreferences(source, state.currentPassage); 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}` : ""); head.append(reference); article.append(head); appendVerseTextLines(article, verse, source, displayPreferences, verse.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 appendVerseTextLines(target, verse, source, displayPreferences, translationText) { if (!(target instanceof HTMLElement)) { return; } const mode = displayPreferences?.textMode || "translation"; const originalText = normalizeTextValue(verse?.originalText); const transliterationText = getVerseTransliteration(verse, source); const lines = []; const appendLine = (text, variant) => { const normalizedText = normalizeTextValue(text); if (!normalizedText || lines.some((entry) => entry.text === normalizedText)) { return; } lines.push({ text: normalizedText, variant }); }; if (mode === "all") { appendLine(translationText, "translation"); appendLine(originalText, "original"); appendLine(transliterationText, "transliteration"); } else if (mode === "original") { appendLine(originalText || translationText, originalText ? "original" : "translation"); } else if (mode === "transliteration") { appendLine(transliterationText || translationText, transliterationText ? "transliteration" : "translation"); } else { appendLine(translationText, "translation"); } if (!lines.length) { appendLine(translationText, "translation"); } lines.forEach((line) => { const text = document.createElement("p"); text.className = `alpha-text-verse-text alpha-text-verse-text--${line.variant}`; appendHighlightedText(text, line.text, isHighlightedVerse(verse) ? state.searchQuery : ""); target.appendChild(text); }); } function createTokenVerse(verse, lexiconId, displayPreferences, source) { const article = document.createElement("article"); article.className = "alpha-text-verse"; article.classList.toggle("alpha-text-verse--interlinear", Boolean(displayPreferences?.showInterlinear)); 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 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); appendVerseTextLines(article, verse, source, displayPreferences, buildTokenTranslationText(verse?.tokens, verse?.text)); if (displayPreferences?.showInterlinear) { article.appendChild(tokenGrid); } return article; } function createReaderCard(passage) { const source = passage?.source || getSelectedSource(); const displayPreferences = getSourceDisplayPreferences(source, passage); const card = createCard(getPassageLocationLabel(passage)); card.classList.add("alpha-text-reader-card"); 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] || "", displayPreferences, source) : createPlainVerse(verse); reader.appendChild(verseEl); }); card.appendChild(reader); const navigation = document.createElement("div"); navigation.className = "alpha-text-reader-navigation"; if (passage?.navigation?.previous) { const previousButton = document.createElement("button"); previousButton.type = "button"; previousButton.className = "alpha-nav-btn alpha-text-reader-nav-btn"; previousButton.textContent = "← Previous"; previousButton.addEventListener("click", () => { navigateToPassageTarget(passage.navigation.previous); }); navigation.appendChild(previousButton); } if (passage?.navigation?.next) { const nextButton = document.createElement("button"); nextButton.type = "button"; nextButton.className = "alpha-nav-btn alpha-text-reader-nav-btn alpha-text-reader-nav-btn--next"; nextButton.textContent = "Next →"; nextButton.addEventListener("click", () => { navigateToPassageTarget(passage.navigation.next); }); navigation.appendChild(nextButton); } if (navigation.childElementCount) { card.appendChild(navigation); } 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(); showDetailOnlyMode(); await loadSelectedPassage(); clearActiveSearchUi({ preserveHighlight: true }); renderDetail(); } 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 (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 (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(); window.TarotChromeUi?.initializeSidebarPopouts?.(); window.TarotChromeUi?.initializeDetailPopouts?.(); 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 }; })();