update ui and add new audio components

This commit is contained in:
2026-03-14 00:45:15 -07:00
parent aa3f23c92c
commit 843c2fe96f
13 changed files with 2458 additions and 155 deletions

View File

@@ -6,10 +6,15 @@
const state = {
initialized: false,
catalog: null,
selectedSourceGroupId: "",
selectedSourceId: "",
selectedSourceIdByGroup: {},
compareSourceIdByGroup: {},
compareModeByGroup: {},
selectedWorkId: "",
selectedSectionId: "",
currentPassage: null,
comparePassage: null,
lexiconEntry: null,
lexiconRequestId: 0,
lexiconOccurrenceResults: null,
@@ -35,6 +40,12 @@
let globalSearchInputEl;
let localSearchFormEl;
let localSearchInputEl;
let translationSelectEl;
let translationControlEl;
let compareSelectEl;
let compareControlEl;
let compareToggleEl;
let compareToggleControlEl;
let workSelectEl;
let sectionSelectEl;
let detailHeadingEl;
@@ -63,6 +74,12 @@
globalSearchInputEl = document.getElementById("alpha-text-global-search-input");
localSearchFormEl = document.getElementById("alpha-text-local-search-form");
localSearchInputEl = document.getElementById("alpha-text-local-search-input");
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");
workSelectEl = document.getElementById("alpha-text-work-select");
sectionSelectEl = document.getElementById("alpha-text-section-select");
detailHeadingEl = document.querySelector("#alphabet-text-section .alpha-text-detail-heading");
@@ -167,13 +184,128 @@
return Array.isArray(state.catalog?.sources) ? state.catalog.sources : [];
}
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 : [];
}
function findById(entries, value) {
const needle = normalizeId(value);
return (Array.isArray(entries) ? entries : []).find((entry) => normalizeId(entry?.id) === needle) || null;
}
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);
}
}
function getSelectedSource() {
return findById(getSources(), state.selectedSourceId);
return getSourceForGroup(getSelectedSourceGroup(), state.selectedSourceId)
|| findById(getSources(), state.selectedSourceId);
}
function getSelectedWork(source = getSelectedSource()) {
@@ -188,6 +320,114 @@
return String(value || "").trim();
}
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 || "--"}`;
}
function extractVerseCountText(verse, source, displayPreferences, translationText = "") {
const mode = displayPreferences?.textMode || "translation";
const originalText = normalizeTextValue(verse?.originalText);
@@ -627,6 +867,28 @@
}
}
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);
}
async function ensureCatalogLoaded(forceRefresh = false) {
if (!forceRefresh && state.catalog) {
return state.catalog;
@@ -637,11 +899,17 @@
? payload
: { meta: {}, sources: [], lexicons: [] };
if (!state.selectedSourceId) {
state.selectedSourceId = getSources()[0]?.id || "";
state.catalog.sourceGroups = buildSourceGroups(getSources());
if (!state.selectedSourceGroupId && state.selectedSourceId) {
state.selectedSourceGroupId = findSourceGroupBySourceId(state.selectedSourceId)?.id || "";
}
syncSelectionForSource(getSelectedSource());
if (!state.selectedSourceGroupId) {
state.selectedSourceGroupId = getSourceGroups()[0]?.id || "";
}
syncSelectionForGroup(getSelectedSourceGroup());
return state.catalog;
}
@@ -668,39 +936,39 @@
}
sourceListEl.replaceChildren();
const sources = getSources();
sources.forEach((source) => {
const sourceGroups = getSourceGroups();
sourceGroups.forEach((group) => {
const source = getSourceForGroup(group);
const button = document.createElement("button");
button.type = "button";
button.className = "planet-list-item alpha-text-source-btn";
button.dataset.sourceId = source.id;
button.dataset.sourceGroupId = group.id;
button.setAttribute("role", "option");
const isSelected = normalizeId(source.id) === normalizeId(state.selectedSourceId);
const isSelected = normalizeId(group.id) === normalizeId(state.selectedSourceGroupId);
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;
name.textContent = group.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`;
meta.textContent = buildSourceGroupListMeta(group);
button.append(name, meta);
button.addEventListener("click", () => {
if (normalizeId(source.id) === normalizeId(state.selectedSourceId)) {
if (normalizeId(group.id) === normalizeId(state.selectedSourceGroupId)) {
showDetailOnlyMode();
return;
}
state.selectedSourceId = source.id;
state.selectedSourceGroupId = group.id;
state.currentPassage = null;
state.lexiconEntry = null;
state.highlightedVerseId = "";
syncSelectionForSource(getSelectedSource());
syncSelectionForGroup(group);
renderSourceList();
renderSelectors();
showDetailOnlyMode();
@@ -716,23 +984,56 @@
sourceListEl.appendChild(button);
});
if (!sources.length) {
if (!sourceGroups.length) {
sourceListEl.appendChild(createEmptyMessage("No text sources are available."));
}
if (sourceCountEl) {
sourceCountEl.textContent = `${sources.length} sources`;
sourceCountEl.textContent = `${sourceGroups.length} sources`;
}
}
function renderSelectors() {
const group = getSelectedSourceGroup();
const source = getSelectedSource();
const work = getSelectedWork(source);
const variants = getSourceVariants(group);
const compareCandidates = getCompareCandidates(group);
const compareSource = getCompareSource(group);
const compareEnabled = isCompareModeEnabled(group);
const works = Array.isArray(source?.works) ? source.works : [];
const sections = Array.isArray(work?.sections) ? work.sections : [];
fillSelect(translationSelectEl, variants, state.selectedSourceId, (entry) => buildTranslationOptionLabel(entry));
fillSelect(compareSelectEl, compareCandidates, compareSource?.id || "", (entry) => buildTranslationOptionLabel(entry));
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`);
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;
}
}
function closeLexiconEntry() {
@@ -1044,9 +1345,14 @@
}
function createMetaGrid(passage) {
const sourceGroup = getSelectedSourceGroup();
const source = passage?.source || getSelectedSource();
const work = passage?.work || getSelectedWork(source);
const section = passage?.section || getSelectedSection(source, work);
const metadata = getSourceMetadata(source);
const version = normalizeTextValue(metadata.versionLabel || metadata.version);
const translator = normalizeTextValue(metadata.translator);
const compareSource = getCompareSource(sourceGroup);
const displayPreferences = getSourceDisplayPreferences(source, passage);
const metaGrid = document.createElement("div");
metaGrid.className = "alpha-text-meta-grid";
@@ -1055,6 +1361,10 @@
overviewCard.innerHTML += `
<dl class="alpha-dl">
<dt>Source</dt><dd>${source?.title || "--"}</dd>
${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>` : ""}
<dt>Tradition</dt><dd>${source?.tradition || "--"}</dd>
<dt>Language</dt><dd>${source?.language || "--"}</dd>
<dt>Script</dt><dd>${source?.script || "--"}</dd>
@@ -1148,14 +1458,13 @@
return metaGrid;
}
function createPlainVerse(verse) {
const source = getSelectedSource();
const displayPreferences = getSourceDisplayPreferences(source, state.currentPassage);
function createPlainVerse(verse, source, displayPreferences, options = {}) {
const translationText = verse.text || "";
const verseCounts = getTextCounts(extractVerseCountText(verse, source, displayPreferences, translationText));
const isHighlighted = options.highlight !== false && isHighlightedVerse(verse);
const article = document.createElement("article");
article.className = "alpha-text-verse";
article.classList.toggle("is-highlighted", isHighlightedVerse(verse));
article.classList.toggle("is-highlighted", isHighlighted);
const head = document.createElement("div");
head.className = "alpha-text-verse-head";
@@ -1170,7 +1479,7 @@
head.append(reference, stats);
article.append(head);
appendVerseTextLines(article, verse, source, displayPreferences, translationText);
appendVerseTextLines(article, verse, source, displayPreferences, translationText, isHighlighted ? state.searchQuery : "");
return article;
}
@@ -1185,7 +1494,7 @@
return glossText || String(fallbackText || "").trim();
}
function appendVerseTextLines(target, verse, source, displayPreferences, translationText) {
function appendVerseTextLines(target, verse, source, displayPreferences, translationText, highlightQuery = "") {
if (!(target instanceof HTMLElement)) {
return;
}
@@ -1222,18 +1531,19 @@
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 : "");
appendHighlightedText(text, line.text, highlightQuery);
target.appendChild(text);
});
}
function createTokenVerse(verse, lexiconId, displayPreferences, source) {
function createTokenVerse(verse, lexiconId, displayPreferences, source, options = {}) {
const translationText = buildTokenTranslationText(verse?.tokens, verse?.text);
const verseCounts = getTextCounts(extractVerseCountText(verse, source, displayPreferences, translationText));
const isHighlighted = options.highlight !== false && isHighlightedVerse(verse);
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));
article.classList.toggle("is-highlighted", isHighlighted);
const head = document.createElement("div");
head.className = "alpha-text-verse-head";
@@ -1282,43 +1592,14 @@
head.append(reference, stats);
article.append(head);
appendVerseTextLines(article, verse, source, displayPreferences, translationText);
appendVerseTextLines(article, verse, source, displayPreferences, translationText, isHighlighted ? state.searchQuery : "");
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);
function createReaderNavigation(passage) {
const navigation = document.createElement("div");
navigation.className = "alpha-text-reader-navigation";
@@ -1344,13 +1625,70 @@
navigation.appendChild(nextButton);
}
if (navigation.childElementCount) {
return navigation.childElementCount ? navigation : null;
}
function createReaderCard(passage, options = {}) {
const source = passage?.source || getSelectedSource();
const displayPreferences = getSourceDisplayPreferences(source, passage);
const card = createCard(options.title || getPassageLocationLabel(passage));
card.classList.add("alpha-text-reader-card");
if (options.compare) {
card.classList.add("alpha-text-reader-card--compare");
}
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, options)
: createPlainVerse(verse, source, displayPreferences, options);
reader.appendChild(verseEl);
});
card.appendChild(reader);
const navigation = options.showNavigation === false ? null : createReaderNavigation(passage);
if (navigation) {
card.appendChild(navigation);
}
return card;
}
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
}));
}
return wrapper;
}
function createSearchCard() {
const hasSearchState = state.searchLoading || state.searchError || state.searchResults || state.searchQuery;
if (!hasSearchState) {
@@ -1443,6 +1781,7 @@
const source = getSelectedSource();
const work = getSelectedWork(source);
const section = getSelectedSection(source, work);
const compareEnabled = isCompareModeEnabled(getSelectedSourceGroup());
const globalSearchOnlyMode = isGlobalSearchOnlyMode();
setGlobalSearchHeadingMode(globalSearchOnlyMode);
@@ -1460,7 +1799,7 @@
if (detailSubEl) {
detailSubEl.textContent = globalSearchOnlyMode
? "All text sources"
: `${source.title} · ${work.title}`;
: buildSourceDetailSubtitle(source, work);
}
if (!detailBodyEl) {
return;
@@ -1487,38 +1826,99 @@
}
detailBodyEl.appendChild(createMetaGrid(state.currentPassage));
detailBodyEl.appendChild(createReaderCard(state.currentPassage));
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));
}
renderLexiconPopup();
}
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.");
}
}
async function loadSelectedPassage() {
const source = getSelectedSource();
const work = getSelectedWork(source);
const section = getSelectedSection(source, work);
const compareSource = isCompareModeEnabled(getSelectedSourceGroup()) ? getCompareSource() : null;
if (!source || !work || !section) {
state.currentPassage = null;
state.comparePassage = null;
renderDetail();
return;
}
state.currentPassage = null;
state.comparePassage = 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();
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);
}
}
@@ -1586,7 +1986,10 @@
return;
}
const sourceGroup = findSourceGroupBySourceId(result.sourceId);
state.selectedSourceGroupId = sourceGroup?.id || "";
state.selectedSourceId = result.sourceId;
rememberSelectedSource(sourceGroup, result.sourceId);
state.selectedWorkId = result.workId;
state.selectedSectionId = result.sectionId;
state.highlightedVerseId = result.verseId;
@@ -1641,12 +2044,58 @@
});
}
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();
});
}
if (workSelectEl) {
workSelectEl.addEventListener("change", () => {
state.selectedWorkId = String(workSelectEl.value || "");
const source = getSelectedSource();
syncSelectionForSource(source);
state.currentPassage = null;
state.comparePassage = null;
state.lexiconEntry = null;
state.highlightedVerseId = "";
renderSelectors();
@@ -1658,6 +2107,7 @@
sectionSelectEl.addEventListener("change", () => {
state.selectedSectionId = String(sectionSelectEl.value || "");
state.currentPassage = null;
state.comparePassage = null;
state.lexiconEntry = null;
state.highlightedVerseId = "";
void loadSelectedPassage();
@@ -1699,8 +2149,13 @@
function resetState() {
state.catalog = null;
state.currentPassage = null;
state.comparePassage = null;
state.lexiconEntry = null;
state.selectedSourceGroupId = "";
state.selectedSourceId = "";
state.selectedSourceIdByGroup = {};
state.compareSourceIdByGroup = {};
state.compareModeByGroup = {};
state.selectedWorkId = "";
state.selectedSectionId = "";
state.lexiconRequestId = 0;