2026-03-09 23:27:03 -07:00
( function ( ) {
"use strict" ;
const dataService = window . TarotDataService || { } ;
const state = {
initialized : false ,
catalog : null ,
2026-03-14 00:45:15 -07:00
selectedSourceGroupId : "" ,
2026-03-09 23:27:03 -07:00
selectedSourceId : "" ,
2026-03-14 00:45:15 -07:00
selectedSourceIdByGroup : { } ,
compareSourceIdByGroup : { } ,
compareModeByGroup : { } ,
2026-03-09 23:27:03 -07:00
selectedWorkId : "" ,
selectedSectionId : "" ,
currentPassage : null ,
2026-03-14 00:45:15 -07:00
comparePassage : null ,
2026-03-09 23:27:03 -07:00
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 ,
2026-03-10 14:55:01 -07:00
highlightedVerseId : "" ,
displayPreferencesBySource : { }
2026-03-09 23:27:03 -07:00
} ;
let sourceListEl ;
let sourceCountEl ;
let globalSearchFormEl ;
let globalSearchInputEl ;
let localSearchFormEl ;
let localSearchInputEl ;
2026-03-14 00:45:15 -07:00
let translationSelectEl ;
let translationControlEl ;
let compareSelectEl ;
let compareControlEl ;
let compareToggleEl ;
let compareToggleControlEl ;
2026-03-09 23:27:03 -07:00
let workSelectEl ;
let sectionSelectEl ;
2026-03-12 04:37:26 -07:00
let detailHeadingEl ;
2026-03-09 23:27:03 -07:00
let detailNameEl ;
let detailSubEl ;
2026-03-12 04:37:26 -07:00
let detailHeadingToolsEl ;
2026-03-09 23:27:03 -07:00
let detailBodyEl ;
2026-03-12 02:35:02 -07:00
let textLayoutEl ;
2026-03-09 23:27:03 -07:00
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" ) ;
2026-03-14 00:45:15 -07:00
translationSelectEl = document . getElementById ( "alpha-text-translation-select" ) ;
translationControlEl = translationSelectEl ? . closest ? . ( ".alpha-text-control" ) || null ;
compareSelectEl = document . getElementById ( "alpha-text-compare-select" ) ;
compareControlEl = compareSelectEl ? . closest ? . ( ".alpha-text-control" ) || null ;
compareToggleEl = document . getElementById ( "alpha-text-compare-toggle" ) ;
compareToggleControlEl = document . getElementById ( "alpha-text-compare-toggle-control" ) ;
2026-03-09 23:27:03 -07:00
workSelectEl = document . getElementById ( "alpha-text-work-select" ) ;
sectionSelectEl = document . getElementById ( "alpha-text-section-select" ) ;
2026-03-12 04:37:26 -07:00
detailHeadingEl = document . querySelector ( "#alphabet-text-section .alpha-text-detail-heading" ) ;
2026-03-09 23:27:03 -07:00
detailNameEl = document . getElementById ( "alpha-text-detail-name" ) ;
detailSubEl = document . getElementById ( "alpha-text-detail-sub" ) ;
2026-03-12 04:37:26 -07:00
detailHeadingToolsEl = document . querySelector ( "#alphabet-text-section .alpha-text-heading-tools" ) ;
2026-03-09 23:27:03 -07:00
detailBodyEl = document . getElementById ( "alpha-text-detail-body" ) ;
2026-03-12 02:35:02 -07:00
textLayoutEl = sourceListEl ? . closest ? . ( ".planet-layout" ) || detailBodyEl ? . closest ? . ( ".planet-layout" ) || null ;
2026-03-09 23:27:03 -07:00
ensureLexiconPopup ( ) ;
}
2026-03-12 04:37:26 -07:00
function setGlobalSearchHeadingMode ( isGlobalSearchOnly ) {
if ( textLayoutEl instanceof HTMLElement ) {
textLayoutEl . classList . toggle ( "alpha-text-global-search-only" , Boolean ( isGlobalSearchOnly ) ) ;
textLayoutEl . setAttribute ( "data-global-search-only" , isGlobalSearchOnly ? "true" : "false" ) ;
}
if ( detailHeadingEl instanceof HTMLElement ) {
detailHeadingEl . hidden = Boolean ( isGlobalSearchOnly ) ;
detailHeadingEl . setAttribute ( "aria-hidden" , isGlobalSearchOnly ? "true" : "false" ) ;
}
if ( ! ( detailHeadingToolsEl instanceof HTMLElement ) ) {
return ;
}
detailHeadingToolsEl . hidden = Boolean ( isGlobalSearchOnly ) ;
detailHeadingToolsEl . setAttribute ( "aria-hidden" , isGlobalSearchOnly ? "true" : "false" ) ;
}
2026-03-12 02:35:02 -07:00
function showDetailOnlyMode ( ) {
if ( ! ( textLayoutEl instanceof HTMLElement ) ) {
return ;
}
window . TarotChromeUi ? . initializeSidebarPopouts ? . ( ) ;
window . TarotChromeUi ? . initializeDetailPopouts ? . ( ) ;
window . TarotChromeUi ? . initializeSidebarAutoCollapse ? . ( ) ;
window . TarotChromeUi ? . showDetailOnly ? . ( textLayoutEl ) ;
}
2026-03-09 23:27:03 -07:00
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 : [ ] ;
}
2026-03-14 00:45:15 -07:00
function getSourceGroupId ( source ) {
const metadata = getSourceMetadata ( source ) ;
return normalizeId ( metadata . workKey || source ? . id || source ? . title ) ;
}
function buildSourceGroups ( sources ) {
const groupsById = new Map ( ) ;
( Array . isArray ( sources ) ? sources : [ ] ) . forEach ( ( source , index ) => {
const groupId = getSourceGroupId ( source ) || ` source-group- ${ index + 1 } ` ;
if ( ! groupsById . has ( groupId ) ) {
groupsById . set ( groupId , {
id : groupId ,
title : normalizeTextValue ( source ? . title ) || normalizeTextValue ( source ? . shortTitle ) || "Untitled Source" ,
order : index ,
variants : [ ]
} ) ;
}
groupsById . get ( groupId ) . variants . push ( source ) ;
} ) ;
return [ ... groupsById . values ( ) ] . sort ( ( left , right ) => left . order - right . order ) ;
}
function getSourceGroups ( ) {
return Array . isArray ( state . catalog ? . sourceGroups ) ? state . catalog . sourceGroups : [ ] ;
}
2026-03-09 23:27:03 -07:00
function findById ( entries , value ) {
const needle = normalizeId ( value ) ;
return ( Array . isArray ( entries ) ? entries : [ ] ) . find ( ( entry ) => normalizeId ( entry ? . id ) === needle ) || null ;
}
2026-03-14 00:45:15 -07:00
function getSelectedSourceGroup ( ) {
return findById ( getSourceGroups ( ) , state . selectedSourceGroupId ) ;
}
function getSourceVariants ( group = getSelectedSourceGroup ( ) ) {
return Array . isArray ( group ? . variants ) ? group . variants : [ ] ;
}
function getSourceForGroup ( group = getSelectedSourceGroup ( ) , sourceId = state . selectedSourceId ) {
return findById ( getSourceVariants ( group ) , sourceId ) || getSourceVariants ( group ) [ 0 ] || null ;
}
function findSourceGroupBySourceId ( sourceId ) {
const needle = normalizeId ( sourceId ) ;
return getSourceGroups ( ) . find ( ( group ) => getSourceVariants ( group ) . some ( ( source ) => normalizeId ( source ? . id ) === needle ) ) || null ;
}
function rememberSelectedSource ( group , sourceId ) {
const groupId = normalizeId ( group ? . id ) ;
const normalizedSourceId = normalizeTextValue ( sourceId ) ;
if ( ! groupId || ! normalizedSourceId ) {
return ;
}
state . selectedSourceIdByGroup [ groupId ] = normalizedSourceId ;
}
function rememberCompareSource ( group , sourceId ) {
const groupId = normalizeId ( group ? . id ) ;
const normalizedSourceId = normalizeTextValue ( sourceId ) ;
if ( ! groupId || ! normalizedSourceId ) {
return ;
}
state . compareSourceIdByGroup [ groupId ] = normalizedSourceId ;
}
function isCompareAvailable ( group = getSelectedSourceGroup ( ) ) {
return getSourceVariants ( group ) . length > 1 ;
}
function isCompareModeEnabled ( group = getSelectedSourceGroup ( ) ) {
const groupId = normalizeId ( group ? . id ) ;
return Boolean ( groupId && state . compareModeByGroup [ groupId ] && isCompareAvailable ( group ) ) ;
}
function setCompareModeEnabled ( group , isEnabled ) {
const groupId = normalizeId ( group ? . id ) ;
if ( ! groupId ) {
return ;
}
state . compareModeByGroup [ groupId ] = Boolean ( isEnabled ) ;
}
function getCompareCandidates ( group = getSelectedSourceGroup ( ) ) {
const activeSourceId = normalizeId ( state . selectedSourceId ) ;
return getSourceVariants ( group ) . filter ( ( source ) => normalizeId ( source ? . id ) !== activeSourceId ) ;
}
function getCompareSource ( group = getSelectedSourceGroup ( ) ) {
const groupId = normalizeId ( group ? . id ) ;
const candidates = getCompareCandidates ( group ) ;
const rememberedSourceId = groupId ? state . compareSourceIdByGroup [ groupId ] : "" ;
return findById ( candidates , rememberedSourceId ) || candidates [ 0 ] || null ;
}
function syncCompareSelection ( group = getSelectedSourceGroup ( ) ) {
const groupId = normalizeId ( group ? . id ) ;
if ( ! groupId ) {
return ;
}
if ( ! isCompareAvailable ( group ) ) {
delete state . compareSourceIdByGroup [ groupId ] ;
delete state . compareModeByGroup [ groupId ] ;
return ;
}
const compareSource = getCompareSource ( group ) ;
if ( compareSource ? . id ) {
rememberCompareSource ( group , compareSource . id ) ;
}
}
2026-03-09 23:27:03 -07:00
function getSelectedSource ( ) {
2026-03-14 00:45:15 -07:00
return getSourceForGroup ( getSelectedSourceGroup ( ) , state . selectedSourceId )
|| findById ( getSources ( ) , state . selectedSourceId ) ;
2026-03-09 23:27:03 -07:00
}
function getSelectedWork ( source = getSelectedSource ( ) ) {
return findById ( source ? . works , state . selectedWorkId ) ;
}
function getSelectedSection ( source = getSelectedSource ( ) , work = getSelectedWork ( source ) ) {
return findById ( work ? . sections , state . selectedSectionId ) ;
}
2026-03-10 14:55:01 -07:00
function normalizeTextValue ( value ) {
return String ( value || "" ) . trim ( ) ;
}
2026-03-14 00:45:15 -07:00
function buildTranslationOptionLabel ( source ) {
const metadata = getSourceMetadata ( source ) ;
return normalizeTextValue ( metadata . translator )
|| normalizeTextValue ( metadata . versionLabel || metadata . version )
|| normalizeTextValue ( source ? . shortTitle )
|| normalizeTextValue ( source ? . title )
|| "Translation" ;
}
function getSourceMetadata ( source ) {
return source ? . metadata && typeof source . metadata === "object" ? source . metadata : { } ;
}
function includesNormalizedText ( container , value ) {
const containerText = normalizeTextValue ( container ) . toLowerCase ( ) ;
const valueText = normalizeTextValue ( value ) . toLowerCase ( ) ;
return Boolean ( containerText && valueText && containerText . includes ( valueText ) ) ;
}
function formatCountLabel ( count , label ) {
const normalizedCount = Number ( count ) || 0 ;
const baseLabel = normalizeTextValue ( label ) || "item" ;
if ( normalizedCount === 1 ) {
return ` ${ normalizedCount } ${ baseLabel } ` ;
}
return ` ${ normalizedCount } ${ baseLabel . endsWith ( "s" ) ? baseLabel : ` ${ baseLabel } s ` } ` ;
}
function getSourceEditionLabel ( source ) {
const metadata = getSourceMetadata ( source ) ;
const version = normalizeTextValue ( metadata . versionLabel || metadata . version ) ;
const translator = normalizeTextValue ( metadata . translator ) ;
if (
version
&& translator
&& normalizeId ( version ) !== normalizeId ( translator )
&& ! includesNormalizedText ( version , translator )
&& ! includesNormalizedText ( translator , version )
) {
return ` ${ version } · ${ translator } ` ;
}
return version || translator ;
}
function buildSourceListMeta ( source ) {
const shortTitle = normalizeTextValue ( source ? . shortTitle ) ;
const title = normalizeTextValue ( source ? . title ) ;
const editionLabel = getSourceEditionLabel ( source ) ;
const parts = [ ] ;
if ( shortTitle && normalizeId ( shortTitle ) !== normalizeId ( title ) ) {
parts . push ( shortTitle ) ;
}
if ( editionLabel && ! parts . some ( ( part ) => includesNormalizedText ( part , editionLabel ) || includesNormalizedText ( editionLabel , part ) ) ) {
parts . push ( editionLabel ) ;
}
parts . push ( formatCountLabel ( source ? . stats ? . workCount , source ? . workLabel || "Work" ) ) ;
parts . push ( formatCountLabel ( source ? . stats ? . sectionCount , source ? . sectionLabel || "Section" ) ) ;
return parts . join ( " · " ) ;
}
function buildSourceGroupListMeta ( group ) {
const activeSource = getSourceForGroup ( group ) ;
if ( ! group || getSourceVariants ( group ) . length <= 1 ) {
return buildSourceListMeta ( activeSource ) ;
}
const translators = Array . from ( new Set (
getSourceVariants ( group )
. map ( ( source ) => normalizeTextValue ( getSourceMetadata ( source ) . translator ) )
. filter ( Boolean )
) ) ;
const parts = [ ] ;
if ( translators . length ) {
parts . push ( translators . join ( " / " ) ) ;
}
parts . push ( formatCountLabel ( getSourceVariants ( group ) . length , "translation" ) ) ;
parts . push ( formatCountLabel ( activeSource ? . stats ? . sectionCount , activeSource ? . sectionLabel || "Section" ) ) ;
return parts . join ( " · " ) ;
}
function buildSourceDetailSubtitle ( source , work ) {
const parts = [ normalizeTextValue ( source ? . title ) || "--" ] ;
const editionLabel = getSourceEditionLabel ( source ) ;
const workTitle = normalizeTextValue ( work ? . title ) ;
if ( editionLabel ) {
parts . push ( editionLabel ) ;
}
if ( workTitle && normalizeId ( workTitle ) !== normalizeId ( source ? . title ) ) {
parts . push ( workTitle ) ;
}
return parts . join ( " · " ) ;
}
function buildCompareCardTitle ( passage ) {
const source = passage ? . source || getSelectedSource ( ) ;
const section = passage ? . section || getSelectedSection ( source , getSelectedWork ( source ) ) ;
return ` ${ buildTranslationOptionLabel ( source ) } · ${ section ? . title || section ? . label || "--" } ` ;
}
2026-03-12 04:18:26 -07:00
function extractVerseCountText ( verse , source , displayPreferences , translationText = "" ) {
const mode = displayPreferences ? . textMode || "translation" ;
const originalText = normalizeTextValue ( verse ? . originalText ) ;
const transliterationText = getVerseTransliteration ( verse , source ) ;
if ( mode === "original" ) {
return originalText || normalizeTextValue ( translationText ) ;
}
if ( mode === "transliteration" ) {
return transliterationText || normalizeTextValue ( translationText ) ;
}
return normalizeTextValue ( translationText )
|| originalText
|| transliterationText ;
}
function getTextCounts ( value ) {
const normalized = String ( value || "" )
. normalize ( "NFD" )
. replace ( /[\u0300-\u036f]/g , "" ) ;
const words = normalized . match ( /[\p{L}\p{N}]+(?:['’ -][\p{L}\p{N}]+)*/gu ) || [ ] ;
const letters = normalized . match ( /\p{L}/gu ) || [ ] ;
const vowels = normalized . match ( /[AEIOUYaeiouy]/g ) || [ ] ;
const consonants = letters . length - vowels . length ;
return {
words : words . length ,
letters : letters . length ,
consonants : Math . max ( 0 , consonants ) ,
vowels : vowels . length
} ;
}
function formatCountSummary ( counts ) {
return ` W: ${ counts . words } L: ${ counts . letters } C: ${ counts . consonants } V: ${ counts . vowels } ` ;
}
function sumPassageCounts ( passage , source , displayPreferences ) {
const verses = Array . isArray ( passage ? . verses ) ? passage . verses : [ ] ;
return verses . reduce ( ( totals , verse ) => {
const translationText = source ? . features ? . hasTokenAnnotations
? buildTokenTranslationText ( verse ? . tokens , verse ? . text )
: verse ? . text ;
const counts = getTextCounts ( extractVerseCountText ( verse , source , displayPreferences , translationText ) ) ;
totals . words += counts . words ;
totals . letters += counts . letters ;
totals . consonants += counts . consonants ;
totals . vowels += counts . vowels ;
return totals ;
} , {
words : 0 ,
letters : 0 ,
consonants : 0 ,
vowels : 0
} ) ;
}
2026-03-10 14:55:01 -07:00
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 "" ;
}
2026-03-09 23:27:03 -07:00
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 ( ) {
2026-03-10 14:55:01 -07:00
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 ;
2026-03-09 23:27:03 -07:00
}
2026-03-10 14:55:01 -07:00
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" ;
2026-03-09 23:27:03 -07:00
}
}
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" ) ;
2026-03-10 14:55:01 -07:00
card . className = "detail-meta-card planet-meta-card" ;
2026-03-09 23:27:03 -07:00
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 ) ;
}
2026-03-10 14:55:01 -07:00
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 || "--" } ` ;
}
2026-03-09 23:27:03 -07:00
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 || "" ;
}
}
2026-03-14 00:45:15 -07:00
function syncSelectionForGroup ( group = getSelectedSourceGroup ( ) ) {
const variants = getSourceVariants ( group ) ;
if ( ! variants . length ) {
state . selectedSourceGroupId = "" ;
state . selectedSourceId = "" ;
state . selectedWorkId = "" ;
state . selectedSectionId = "" ;
return ;
}
state . selectedSourceGroupId = group . id ;
const rememberedSourceId = state . selectedSourceIdByGroup [ normalizeId ( group . id ) ] || "" ;
const source = findById ( variants , state . selectedSourceId )
|| findById ( variants , rememberedSourceId )
|| variants [ 0 ] ;
state . selectedSourceId = source ? . id || "" ;
rememberSelectedSource ( group , state . selectedSourceId ) ;
syncSelectionForSource ( source ) ;
syncCompareSelection ( group ) ;
}
2026-03-09 23:27:03 -07:00
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 : [ ] } ;
2026-03-14 00:45:15 -07:00
state . catalog . sourceGroups = buildSourceGroups ( getSources ( ) ) ;
if ( ! state . selectedSourceGroupId && state . selectedSourceId ) {
state . selectedSourceGroupId = findSourceGroupBySourceId ( state . selectedSourceId ) ? . id || "" ;
2026-03-09 23:27:03 -07:00
}
2026-03-14 00:45:15 -07:00
if ( ! state . selectedSourceGroupId ) {
state . selectedSourceGroupId = getSourceGroups ( ) [ 0 ] ? . id || "" ;
}
syncSelectionForGroup ( getSelectedSourceGroup ( ) ) ;
2026-03-09 23:27:03 -07:00
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 ( ) ;
2026-03-14 00:45:15 -07:00
const sourceGroups = getSourceGroups ( ) ;
sourceGroups . forEach ( ( group ) => {
const source = getSourceForGroup ( group ) ;
2026-03-09 23:27:03 -07:00
const button = document . createElement ( "button" ) ;
button . type = "button" ;
button . className = "planet-list-item alpha-text-source-btn" ;
2026-03-14 00:45:15 -07:00
button . dataset . sourceGroupId = group . id ;
2026-03-09 23:27:03 -07:00
button . setAttribute ( "role" , "option" ) ;
2026-03-14 00:45:15 -07:00
const isSelected = normalizeId ( group . id ) === normalizeId ( state . selectedSourceGroupId ) ;
2026-03-09 23:27:03 -07:00
button . classList . toggle ( "is-selected" , isSelected ) ;
button . setAttribute ( "aria-selected" , isSelected ? "true" : "false" ) ;
const name = document . createElement ( "span" ) ;
name . className = "planet-list-name" ;
2026-03-14 00:45:15 -07:00
name . textContent = group . title ;
2026-03-09 23:27:03 -07:00
const meta = document . createElement ( "span" ) ;
meta . className = "alpha-text-source-meta" ;
2026-03-14 00:45:15 -07:00
meta . textContent = buildSourceGroupListMeta ( group ) ;
2026-03-09 23:27:03 -07:00
button . append ( name , meta ) ;
button . addEventListener ( "click" , ( ) => {
2026-03-14 00:45:15 -07:00
if ( normalizeId ( group . id ) === normalizeId ( state . selectedSourceGroupId ) ) {
2026-03-12 02:35:02 -07:00
showDetailOnlyMode ( ) ;
2026-03-09 23:27:03 -07:00
return ;
}
2026-03-14 00:45:15 -07:00
state . selectedSourceGroupId = group . id ;
2026-03-09 23:27:03 -07:00
state . currentPassage = null ;
state . lexiconEntry = null ;
state . highlightedVerseId = "" ;
2026-03-14 00:45:15 -07:00
syncSelectionForGroup ( group ) ;
2026-03-09 23:27:03 -07:00
renderSourceList ( ) ;
renderSelectors ( ) ;
2026-03-12 02:35:02 -07:00
showDetailOnlyMode ( ) ;
2026-03-09 23:27:03 -07:00
if ( state . searchQuery && state . activeSearchScope === "source" ) {
void Promise . all ( [ loadSelectedPassage ( ) , runSearch ( "source" ) ] ) ;
return ;
}
void loadSelectedPassage ( ) ;
} ) ;
sourceListEl . appendChild ( button ) ;
} ) ;
2026-03-14 00:45:15 -07:00
if ( ! sourceGroups . length ) {
2026-03-09 23:27:03 -07:00
sourceListEl . appendChild ( createEmptyMessage ( "No text sources are available." ) ) ;
}
if ( sourceCountEl ) {
2026-03-14 00:45:15 -07:00
sourceCountEl . textContent = ` ${ sourceGroups . length } sources ` ;
2026-03-09 23:27:03 -07:00
}
}
function renderSelectors ( ) {
2026-03-14 00:45:15 -07:00
const group = getSelectedSourceGroup ( ) ;
2026-03-09 23:27:03 -07:00
const source = getSelectedSource ( ) ;
const work = getSelectedWork ( source ) ;
2026-03-14 00:45:15 -07:00
const variants = getSourceVariants ( group ) ;
const compareCandidates = getCompareCandidates ( group ) ;
const compareSource = getCompareSource ( group ) ;
const compareEnabled = isCompareModeEnabled ( group ) ;
2026-03-09 23:27:03 -07:00
const works = Array . isArray ( source ? . works ) ? source . works : [ ] ;
const sections = Array . isArray ( work ? . sections ) ? work . sections : [ ] ;
2026-03-14 00:45:15 -07:00
fillSelect ( translationSelectEl , variants , state . selectedSourceId , ( entry ) => buildTranslationOptionLabel ( entry ) ) ;
fillSelect ( compareSelectEl , compareCandidates , compareSource ? . id || "" , ( entry ) => buildTranslationOptionLabel ( entry ) ) ;
2026-03-09 23:27:03 -07:00
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 ` ) ;
2026-03-14 00:45:15 -07:00
if ( translationSelectEl instanceof HTMLSelectElement ) {
translationSelectEl . disabled = variants . length <= 1 ;
}
if ( translationControlEl instanceof HTMLElement ) {
translationControlEl . hidden = variants . length <= 1 ;
}
if ( compareToggleEl instanceof HTMLButtonElement ) {
compareToggleEl . textContent = compareEnabled ? "On" : "Off" ;
compareToggleEl . setAttribute ( "aria-pressed" , compareEnabled ? "true" : "false" ) ;
compareToggleEl . classList . toggle ( "is-selected" , compareEnabled ) ;
}
if ( compareToggleControlEl instanceof HTMLElement ) {
compareToggleControlEl . hidden = ! isCompareAvailable ( group ) ;
}
if ( compareSelectEl instanceof HTMLSelectElement ) {
compareSelectEl . disabled = ! compareEnabled || compareCandidates . length === 0 ;
}
if ( compareControlEl instanceof HTMLElement ) {
compareControlEl . hidden = ! compareEnabled || compareCandidates . length === 0 ;
}
2026-03-09 23:27:03 -07:00
}
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 ) {
2026-03-14 00:45:15 -07:00
const sourceGroup = getSelectedSourceGroup ( ) ;
2026-03-09 23:27:03 -07:00
const source = passage ? . source || getSelectedSource ( ) ;
const work = passage ? . work || getSelectedWork ( source ) ;
const section = passage ? . section || getSelectedSection ( source , work ) ;
2026-03-14 00:45:15 -07:00
const metadata = getSourceMetadata ( source ) ;
const version = normalizeTextValue ( metadata . versionLabel || metadata . version ) ;
const translator = normalizeTextValue ( metadata . translator ) ;
const compareSource = getCompareSource ( sourceGroup ) ;
2026-03-10 14:55:01 -07:00
const displayPreferences = getSourceDisplayPreferences ( source , passage ) ;
2026-03-09 23:27:03 -07:00
const metaGrid = document . createElement ( "div" ) ;
metaGrid . className = "alpha-text-meta-grid" ;
const overviewCard = createCard ( "Source Overview" ) ;
overviewCard . innerHTML += `
< dl class = "alpha-dl" >
< dt > Source < / d t > < d d > $ { s o u r c e ? . t i t l e | | " - - " } < / d d >
2026-03-14 00:45:15 -07:00
$ { version ? ` <dt>Version</dt><dd> ${ version } </dd> ` : "" }
$ { translator ? ` <dt>Translator</dt><dd> ${ translator } </dd> ` : "" }
$ { getSourceVariants ( sourceGroup ) . length > 1 ? ` <dt>Translations</dt><dd> ${ getSourceVariants ( sourceGroup ) . map ( ( entry ) => buildTranslationOptionLabel ( entry ) ) . join ( " / " ) } </dd> ` : "" }
$ { isCompareModeEnabled ( sourceGroup ) && compareSource ? ` <dt>Compare</dt><dd> ${ buildTranslationOptionLabel ( compareSource ) } </dd> ` : "" }
2026-03-09 23:27:03 -07:00
< dt > Tradition < / d t > < d d > $ { s o u r c e ? . t r a d i t i o n | | " - - " } < / d d >
< dt > Language < / d t > < d d > $ { s o u r c e ? . l a n g u a g e | | " - - " } < / d d >
< dt > Script < / d t > < d d > $ { s o u r c e ? . s c r i p t | | " - - " } < / d d >
< dt > $ { source ? . workLabel || "Work" } < / d t > < d d > $ { w o r k ? . t i t l e | | " - - " } < / d d >
< dt > $ { source ? . sectionLabel || "Section" } < / d t > < d d > $ { s e c t i o n ? . l a b e l | | " - - " } < / d d >
< / d l >
` ;
metaGrid . appendChild ( overviewCard ) ;
2026-03-12 04:18:26 -07:00
const totalsCard = createCard ( "Entry Totals" ) ;
const totals = sumPassageCounts ( passage , source , displayPreferences ) ;
totalsCard . innerHTML += `
< dl class = "alpha-dl" >
< dt > Words < / d t > < d d > $ { t o t a l s . w o r d s } < / d d >
< dt > Letters < / d t > < d d > $ { t o t a l s . l e t t e r s } < / d d >
< dt > Consonants < / d t > < d d > $ { t o t a l s . c o n s o n a n t s } < / d d >
< dt > Vowels < / d t > < d d > $ { t o t a l s . v o w e l s } < / d d >
< / d l >
` ;
metaGrid . appendChild ( totalsCard ) ;
2026-03-10 14:55:01 -07:00
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 ) ;
} ) ;
2026-03-09 23:27:03 -07:00
2026-03-10 14:55:01 -07:00
displayGroup . append ( displayLabel , displayButtons ) ;
extraCard . appendChild ( displayGroup ) ;
2026-03-09 23:27:03 -07:00
}
2026-03-10 14:55:01 -07:00
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 ( ) ;
} ) ;
2026-03-09 23:27:03 -07:00
2026-03-10 14:55:01 -07:00
interlinearButtons . appendChild ( interlinearButton ) ;
interlinearGroup . append ( interlinearLabel , interlinearButtons ) ;
extraCard . appendChild ( interlinearGroup ) ;
}
metaGrid . appendChild ( extraCard ) ;
}
2026-03-09 23:27:03 -07:00
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 ;
}
2026-03-14 00:45:15 -07:00
function createPlainVerse ( verse , source , displayPreferences , options = { } ) {
2026-03-12 04:18:26 -07:00
const translationText = verse . text || "" ;
const verseCounts = getTextCounts ( extractVerseCountText ( verse , source , displayPreferences , translationText ) ) ;
2026-03-14 00:45:15 -07:00
const isHighlighted = options . highlight !== false && isHighlightedVerse ( verse ) ;
2026-03-09 23:27:03 -07:00
const article = document . createElement ( "article" ) ;
article . className = "alpha-text-verse" ;
2026-03-14 00:45:15 -07:00
article . classList . toggle ( "is-highlighted" , isHighlighted ) ;
2026-03-09 23:27:03 -07:00
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 } ` : "" ) ;
2026-03-12 04:18:26 -07:00
const stats = document . createElement ( "span" ) ;
stats . className = "alpha-text-verse-counts" ;
stats . textContent = formatCountSummary ( verseCounts ) ;
head . append ( reference , stats ) ;
2026-03-10 14:55:01 -07:00
article . append ( head ) ;
2026-03-14 00:45:15 -07:00
appendVerseTextLines ( article , verse , source , displayPreferences , translationText , isHighlighted ? state . searchQuery : "" ) ;
2026-03-09 23:27:03 -07:00
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 ( ) ;
}
2026-03-14 00:45:15 -07:00
function appendVerseTextLines ( target , verse , source , displayPreferences , translationText , highlightQuery = "" ) {
2026-03-10 14:55:01 -07:00
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 } ` ;
2026-03-14 00:45:15 -07:00
appendHighlightedText ( text , line . text , highlightQuery ) ;
2026-03-10 14:55:01 -07:00
target . appendChild ( text ) ;
} ) ;
}
2026-03-14 00:45:15 -07:00
function createTokenVerse ( verse , lexiconId , displayPreferences , source , options = { } ) {
2026-03-12 04:18:26 -07:00
const translationText = buildTokenTranslationText ( verse ? . tokens , verse ? . text ) ;
const verseCounts = getTextCounts ( extractVerseCountText ( verse , source , displayPreferences , translationText ) ) ;
2026-03-14 00:45:15 -07:00
const isHighlighted = options . highlight !== false && isHighlightedVerse ( verse ) ;
2026-03-09 23:27:03 -07:00
const article = document . createElement ( "article" ) ;
2026-03-10 14:55:01 -07:00
article . className = "alpha-text-verse" ;
article . classList . toggle ( "alpha-text-verse--interlinear" , Boolean ( displayPreferences ? . showInterlinear ) ) ;
2026-03-14 00:45:15 -07:00
article . classList . toggle ( "is-highlighted" , isHighlighted ) ;
2026-03-09 23:27:03 -07:00
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 } ` : "" ) ;
2026-03-12 04:18:26 -07:00
const stats = document . createElement ( "span" ) ;
stats . className = "alpha-text-verse-counts" ;
stats . textContent = formatCountSummary ( verseCounts ) ;
2026-03-09 23:27:03 -07:00
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 ) ;
} ) ;
2026-03-12 04:18:26 -07:00
head . append ( reference , stats ) ;
2026-03-10 14:55:01 -07:00
article . append ( head ) ;
2026-03-14 00:45:15 -07:00
appendVerseTextLines ( article , verse , source , displayPreferences , translationText , isHighlighted ? state . searchQuery : "" ) ;
2026-03-10 14:55:01 -07:00
if ( displayPreferences ? . showInterlinear ) {
article . appendChild ( tokenGrid ) ;
}
2026-03-09 23:27:03 -07:00
return article ;
}
2026-03-14 00:45:15 -07:00
function createReaderNavigation ( passage ) {
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 ) ;
}
return navigation . childElementCount ? navigation : null ;
}
function createReaderCard ( passage , options = { } ) {
2026-03-09 23:27:03 -07:00
const source = passage ? . source || getSelectedSource ( ) ;
2026-03-10 14:55:01 -07:00
const displayPreferences = getSourceDisplayPreferences ( source , passage ) ;
2026-03-14 00:45:15 -07:00
const card = createCard ( options . title || getPassageLocationLabel ( passage ) ) ;
2026-03-10 14:55:01 -07:00
card . classList . add ( "alpha-text-reader-card" ) ;
2026-03-14 00:45:15 -07:00
if ( options . compare ) {
card . classList . add ( "alpha-text-reader-card--compare" ) ;
}
2026-03-09 23:27:03 -07:00
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
2026-03-14 00:45:15 -07:00
? createTokenVerse ( verse , source . features . lexiconIds ? . [ 0 ] || "" , displayPreferences , source , options )
: createPlainVerse ( verse , source , displayPreferences , options ) ;
2026-03-09 23:27:03 -07:00
reader . appendChild ( verseEl ) ;
} ) ;
card . appendChild ( reader ) ;
2026-03-10 14:55:01 -07:00
2026-03-14 00:45:15 -07:00
const navigation = options . showNavigation === false ? null : createReaderNavigation ( passage ) ;
if ( navigation ) {
card . appendChild ( navigation ) ;
2026-03-10 14:55:01 -07:00
}
2026-03-14 00:45:15 -07:00
return card ;
}
2026-03-10 14:55:01 -07:00
2026-03-14 00:45:15 -07:00
function createCompareReaderGrid ( primaryPassage , comparePassage ) {
const wrapper = document . createElement ( "div" ) ;
wrapper . className = "alpha-text-reader-compare" ;
wrapper . appendChild ( createReaderCard ( primaryPassage , {
title : buildCompareCardTitle ( primaryPassage ) ,
showNavigation : false
} ) ) ;
if ( comparePassage ) {
wrapper . appendChild ( createReaderCard ( comparePassage , {
title : buildCompareCardTitle ( comparePassage ) ,
compare : true ,
highlight : false ,
showNavigation : false
} ) ) ;
2026-03-10 14:55:01 -07:00
}
2026-03-14 00:45:15 -07:00
return wrapper ;
2026-03-09 23:27:03 -07:00
}
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 ( ) {
2026-03-12 04:37:26 -07:00
return ( state . activeSearchScope === "global" || state . activeSearchScope === "source" )
2026-03-09 23:27:03 -07:00
&& Boolean ( state . searchQuery )
&& ! state . highlightedVerseId ;
}
function renderDetail ( ) {
const source = getSelectedSource ( ) ;
const work = getSelectedWork ( source ) ;
const section = getSelectedSection ( source , work ) ;
2026-03-14 00:45:15 -07:00
const compareEnabled = isCompareModeEnabled ( getSelectedSourceGroup ( ) ) ;
2026-03-09 23:27:03 -07:00
const globalSearchOnlyMode = isGlobalSearchOnlyMode ( ) ;
2026-03-12 04:37:26 -07:00
setGlobalSearchHeadingMode ( globalSearchOnlyMode ) ;
2026-03-09 23:27:03 -07:00
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"
2026-03-14 00:45:15 -07:00
: buildSourceDetailSubtitle ( source , work ) ;
2026-03-09 23:27:03 -07:00
}
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 ) ) ;
2026-03-14 00:45:15 -07:00
if ( compareEnabled && state . comparePassage ) {
detailBodyEl . appendChild ( createCompareReaderGrid ( state . currentPassage , state . comparePassage ) ) ;
const compareNavigation = createReaderNavigation ( state . currentPassage ) ;
if ( compareNavigation ) {
detailBodyEl . appendChild ( compareNavigation ) ;
}
} else {
detailBodyEl . appendChild ( createReaderCard ( state . currentPassage ) ) ;
}
2026-03-09 23:27:03 -07:00
renderLexiconPopup ( ) ;
}
2026-03-14 00:45:15 -07:00
function getComparableWork ( source , work ) {
const works = Array . isArray ( source ? . works ) ? source . works : [ ] ;
return findById ( works , work ? . id )
|| works . find ( ( entry ) => normalizeId ( entry ? . title ) === normalizeId ( work ? . title ) )
|| works [ 0 ]
|| null ;
}
function getComparableSection ( work , section ) {
const sections = Array . isArray ( work ? . sections ) ? work . sections : [ ] ;
return findById ( sections , section ? . id )
|| sections . find ( ( entry ) => Number ( entry ? . number || 0 ) === Number ( section ? . number || 0 ) )
|| sections . find ( ( entry ) => normalizeId ( entry ? . title ) === normalizeId ( section ? . title ) )
|| sections . find ( ( entry ) => normalizeId ( entry ? . label ) === normalizeId ( section ? . label ) )
|| sections [ 0 ]
|| null ;
}
function buildPassageLoadError ( source , work , section , message ) {
return {
source ,
work ,
section ,
verses : [ ] ,
errorMessage : message
} ;
}
async function loadComparablePassage ( compareSource , currentWork , currentSection ) {
const compareWork = getComparableWork ( compareSource , currentWork ) ;
const compareSection = getComparableSection ( compareWork , currentSection ) ;
if ( ! compareWork || ! compareSection ) {
return buildPassageLoadError ( compareSource , compareWork , compareSection , "Unable to align this comparison section." ) ;
}
try {
return await dataService . loadTextSection ? . ( compareSource . id , compareWork . id , compareSection . id ) ;
} catch ( error ) {
return buildPassageLoadError ( compareSource , compareWork , compareSection , error ? . message || "Unable to load the comparison translation." ) ;
}
}
2026-03-09 23:27:03 -07:00
async function loadSelectedPassage ( ) {
const source = getSelectedSource ( ) ;
const work = getSelectedWork ( source ) ;
const section = getSelectedSection ( source , work ) ;
2026-03-14 00:45:15 -07:00
const compareSource = isCompareModeEnabled ( getSelectedSourceGroup ( ) ) ? getCompareSource ( ) : null ;
2026-03-09 23:27:03 -07:00
if ( ! source || ! work || ! section ) {
state . currentPassage = null ;
2026-03-14 00:45:15 -07:00
state . comparePassage = null ;
2026-03-09 23:27:03 -07:00
renderDetail ( ) ;
return ;
}
state . currentPassage = null ;
2026-03-14 00:45:15 -07:00
state . comparePassage = null ;
2026-03-09 23:27:03 -07:00
renderDetail ( ) ;
2026-03-14 00:45:15 -07:00
const [ primaryResult , compareResult ] = await Promise . allSettled ( [
dataService . loadTextSection ? . ( source . id , work . id , section . id ) ,
compareSource ? loadComparablePassage ( compareSource , work , section ) : Promise . resolve ( null )
] ) ;
if ( primaryResult . status === "fulfilled" ) {
state . currentPassage = primaryResult . value ;
} else {
state . currentPassage = buildPassageLoadError ( source , work , section , primaryResult . reason ? . message || "Unable to load this section." ) ;
}
if ( compareResult . status === "fulfilled" ) {
state . comparePassage = compareResult . value ;
} else if ( compareSource ) {
const compareWork = getComparableWork ( compareSource , work ) ;
const compareSection = getComparableSection ( compareWork , section ) ;
state . comparePassage = buildPassageLoadError ( compareSource , compareWork , compareSection , compareResult . reason ? . message || "Unable to load the comparison translation." ) ;
}
renderDetail ( ) ;
if ( state . highlightedVerseId ) {
requestAnimationFrame ( scrollHighlightedVerseIntoView ) ;
2026-03-09 23:27:03 -07:00
}
}
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 ;
}
2026-03-12 04:37:26 -07:00
if ( normalizedScope === "global" || normalizedScope === "source" ) {
showDetailOnlyMode ( ) ;
}
2026-03-09 23:27:03 -07:00
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 ;
}
2026-03-14 00:45:15 -07:00
const sourceGroup = findSourceGroupBySourceId ( result . sourceId ) ;
state . selectedSourceGroupId = sourceGroup ? . id || "" ;
2026-03-09 23:27:03 -07:00
state . selectedSourceId = result . sourceId ;
2026-03-14 00:45:15 -07:00
rememberSelectedSource ( sourceGroup , result . sourceId ) ;
2026-03-09 23:27:03 -07:00
state . selectedWorkId = result . workId ;
state . selectedSectionId = result . sectionId ;
state . highlightedVerseId = result . verseId ;
dismissLexiconEntry ( { restoreFocus : false } ) ;
syncSelectionForSource ( getSelectedSource ( ) ) ;
renderSourceList ( ) ;
renderSelectors ( ) ;
2026-03-12 02:35:02 -07:00
showDetailOnlyMode ( ) ;
2026-03-09 23:27:03 -07:00
await loadSelectedPassage ( ) ;
2026-03-10 14:55:01 -07:00
clearActiveSearchUi ( { preserveHighlight : true } ) ;
renderDetail ( ) ;
2026-03-09 23:27:03 -07:00
}
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 ( ) ;
}
} ) ;
}
2026-03-14 00:45:15 -07:00
if ( translationSelectEl instanceof HTMLSelectElement ) {
translationSelectEl . addEventListener ( "change" , ( ) => {
const sourceGroup = getSelectedSourceGroup ( ) ;
state . selectedSourceId = String ( translationSelectEl . value || "" ) ;
rememberSelectedSource ( sourceGroup , state . selectedSourceId ) ;
syncSelectionForSource ( getSelectedSource ( ) ) ;
state . currentPassage = null ;
state . comparePassage = null ;
state . lexiconEntry = null ;
state . highlightedVerseId = "" ;
syncCompareSelection ( sourceGroup ) ;
renderSourceList ( ) ;
renderSelectors ( ) ;
showDetailOnlyMode ( ) ;
if ( state . searchQuery && state . activeSearchScope === "source" ) {
void Promise . all ( [ loadSelectedPassage ( ) , runSearch ( "source" ) ] ) ;
return ;
}
void loadSelectedPassage ( ) ;
} ) ;
}
if ( compareToggleEl instanceof HTMLButtonElement ) {
compareToggleEl . addEventListener ( "click" , ( ) => {
const sourceGroup = getSelectedSourceGroup ( ) ;
setCompareModeEnabled ( sourceGroup , ! isCompareModeEnabled ( sourceGroup ) ) ;
syncCompareSelection ( sourceGroup ) ;
state . comparePassage = null ;
renderSelectors ( ) ;
void loadSelectedPassage ( ) ;
} ) ;
}
if ( compareSelectEl instanceof HTMLSelectElement ) {
compareSelectEl . addEventListener ( "change" , ( ) => {
const sourceGroup = getSelectedSourceGroup ( ) ;
rememberCompareSource ( sourceGroup , String ( compareSelectEl . value || "" ) ) ;
state . comparePassage = null ;
renderSelectors ( ) ;
void loadSelectedPassage ( ) ;
} ) ;
}
2026-03-09 23:27:03 -07:00
if ( workSelectEl ) {
workSelectEl . addEventListener ( "change" , ( ) => {
state . selectedWorkId = String ( workSelectEl . value || "" ) ;
const source = getSelectedSource ( ) ;
syncSelectionForSource ( source ) ;
state . currentPassage = null ;
2026-03-14 00:45:15 -07:00
state . comparePassage = null ;
2026-03-09 23:27:03 -07:00
state . lexiconEntry = null ;
state . highlightedVerseId = "" ;
renderSelectors ( ) ;
void loadSelectedPassage ( ) ;
} ) ;
}
if ( sectionSelectEl ) {
sectionSelectEl . addEventListener ( "change" , ( ) => {
state . selectedSectionId = String ( sectionSelectEl . value || "" ) ;
state . currentPassage = null ;
2026-03-14 00:45:15 -07:00
state . comparePassage = null ;
2026-03-09 23:27:03 -07:00
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 ( ) ;
2026-03-12 02:35:02 -07:00
window . TarotChromeUi ? . initializeSidebarPopouts ? . ( ) ;
window . TarotChromeUi ? . initializeDetailPopouts ? . ( ) ;
2026-03-09 23:27:03 -07:00
if ( ! sourceListEl || ! detailBodyEl ) {
return ;
}
await ensureCatalogLoaded ( ) ;
renderSourceList ( ) ;
renderSelectors ( ) ;
updateSearchControls ( ) ;
if ( ! state . currentPassage ) {
await loadSelectedPassage ( ) ;
return ;
}
renderDetail ( ) ;
}
function resetState ( ) {
state . catalog = null ;
state . currentPassage = null ;
2026-03-14 00:45:15 -07:00
state . comparePassage = null ;
2026-03-09 23:27:03 -07:00
state . lexiconEntry = null ;
2026-03-14 00:45:15 -07:00
state . selectedSourceGroupId = "" ;
2026-03-09 23:27:03 -07:00
state . selectedSourceId = "" ;
2026-03-14 00:45:15 -07:00
state . selectedSourceIdByGroup = { } ;
state . compareSourceIdByGroup = { } ;
state . compareModeByGroup = { } ;
2026-03-09 23:27:03 -07:00
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
} ;
} ) ( ) ;