update ui and add new audio components
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user