Initial commit
This commit is contained in:
882
app/ui-iching.js
Normal file
882
app/ui-iching.js
Normal file
@@ -0,0 +1,882 @@
|
||||
(function () {
|
||||
const { getTarotCardSearchAliases } = window.TarotCardImages || {};
|
||||
|
||||
const state = {
|
||||
initialized: false,
|
||||
hexagrams: [],
|
||||
filteredHexagrams: [],
|
||||
trigramsByName: {},
|
||||
tarotByTrigramName: {},
|
||||
monthRefsByHexagramNumber: new Map(),
|
||||
searchQuery: "",
|
||||
selectedNumber: null
|
||||
};
|
||||
|
||||
const ICHING_PLANET_BY_PLANET_ID = {
|
||||
sol: "Sun",
|
||||
luna: "Moon",
|
||||
mercury: "Mercury",
|
||||
venus: "Venus",
|
||||
mars: "Mars",
|
||||
jupiter: "Jupiter",
|
||||
saturn: "Saturn",
|
||||
earth: "Earth",
|
||||
uranus: "Uranus",
|
||||
neptune: "Neptune",
|
||||
pluto: "Pluto"
|
||||
};
|
||||
|
||||
function getElements() {
|
||||
return {
|
||||
ichingCardListEl: document.getElementById("iching-card-list"),
|
||||
ichingSearchInputEl: document.getElementById("iching-search-input"),
|
||||
ichingSearchClearEl: document.getElementById("iching-search-clear"),
|
||||
ichingCountEl: document.getElementById("iching-card-count"),
|
||||
ichingDetailNameEl: document.getElementById("iching-detail-name"),
|
||||
ichingDetailTypeEl: document.getElementById("iching-detail-type"),
|
||||
ichingDetailSummaryEl: document.getElementById("iching-detail-summary"),
|
||||
ichingDetailJudgementEl: document.getElementById("iching-detail-judgement"),
|
||||
ichingDetailImageEl: document.getElementById("iching-detail-image"),
|
||||
ichingDetailBinaryEl: document.getElementById("iching-detail-binary"),
|
||||
ichingDetailLineEl: document.getElementById("iching-detail-line"),
|
||||
ichingDetailKeywordsEl: document.getElementById("iching-detail-keywords"),
|
||||
ichingDetailTrigramsEl: document.getElementById("iching-detail-trigrams"),
|
||||
ichingDetailPlanetEl: document.getElementById("iching-detail-planet"),
|
||||
ichingDetailTarotEl: document.getElementById("iching-detail-tarot"),
|
||||
ichingDetailCalendarEl: document.getElementById("iching-detail-calendar")
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSearchValue(value) {
|
||||
return String(value || "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function clearChildren(element) {
|
||||
if (element) {
|
||||
element.replaceChildren();
|
||||
}
|
||||
}
|
||||
|
||||
function getTrigramByName(name) {
|
||||
const key = normalizeSearchValue(name);
|
||||
return key ? state.trigramsByName[key] || null : null;
|
||||
}
|
||||
|
||||
function normalizePlanetInfluence(value) {
|
||||
return String(value || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z]/g, "");
|
||||
}
|
||||
|
||||
function resolveAssociationPlanetInfluence(associations) {
|
||||
if (!associations || typeof associations !== "object") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const explicit = normalizePlanetInfluence(associations.iChingPlanetaryInfluence || associations.planetaryInfluence);
|
||||
if (explicit) {
|
||||
return explicit;
|
||||
}
|
||||
|
||||
const planetId = normalizePlanetInfluence(associations.planetId);
|
||||
if (!planetId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return normalizePlanetInfluence(ICHING_PLANET_BY_PLANET_ID[planetId]);
|
||||
}
|
||||
|
||||
function buildMonthReferencesByHexagram(referenceData, hexagrams) {
|
||||
const map = new Map();
|
||||
const months = Array.isArray(referenceData?.calendarMonths) ? referenceData.calendarMonths : [];
|
||||
const holidays = Array.isArray(referenceData?.celestialHolidays) ? referenceData.celestialHolidays : [];
|
||||
const monthById = new Map(months.map((month) => [month.id, month]));
|
||||
|
||||
function parseMonthDayToken(value) {
|
||||
const text = String(value || "").trim();
|
||||
const match = text.match(/^(\d{1,2})-(\d{1,2})$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const monthNo = Number(match[1]);
|
||||
const dayNo = Number(match[2]);
|
||||
if (!Number.isInteger(monthNo) || !Number.isInteger(dayNo) || monthNo < 1 || monthNo > 12 || dayNo < 1 || dayNo > 31) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { month: monthNo, day: dayNo };
|
||||
}
|
||||
|
||||
function parseMonthDayTokensFromText(value) {
|
||||
const text = String(value || "");
|
||||
const matches = [...text.matchAll(/(\d{1,2})-(\d{1,2})/g)];
|
||||
return matches
|
||||
.map((match) => ({ month: Number(match[1]), day: Number(match[2]) }))
|
||||
.filter((token) => Number.isInteger(token.month) && Number.isInteger(token.day) && token.month >= 1 && token.month <= 12 && token.day >= 1 && token.day <= 31);
|
||||
}
|
||||
|
||||
function toDateToken(token, year) {
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
return new Date(year, token.month - 1, token.day, 12, 0, 0, 0);
|
||||
}
|
||||
|
||||
function splitMonthDayRangeByMonth(startToken, endToken) {
|
||||
const startDate = toDateToken(startToken, 2025);
|
||||
const endBase = toDateToken(endToken, 2025);
|
||||
if (!startDate || !endBase) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const wrapsYear = endBase.getTime() < startDate.getTime();
|
||||
const endDate = wrapsYear ? toDateToken(endToken, 2026) : endBase;
|
||||
if (!endDate) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const segments = [];
|
||||
let cursor = new Date(startDate);
|
||||
while (cursor.getTime() <= endDate.getTime()) {
|
||||
const monthEnd = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0, 12, 0, 0, 0);
|
||||
const segmentEnd = monthEnd.getTime() < endDate.getTime() ? monthEnd : endDate;
|
||||
|
||||
segments.push({
|
||||
monthNo: cursor.getMonth() + 1,
|
||||
startDay: cursor.getDate(),
|
||||
endDay: segmentEnd.getDate()
|
||||
});
|
||||
|
||||
cursor = new Date(segmentEnd.getFullYear(), segmentEnd.getMonth(), segmentEnd.getDate() + 1, 12, 0, 0, 0);
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
function tokenToString(monthNo, dayNo) {
|
||||
return `${String(monthNo).padStart(2, "0")}-${String(dayNo).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function formatRangeLabel(monthName, startDay, endDay) {
|
||||
if (!Number.isFinite(startDay) || !Number.isFinite(endDay)) {
|
||||
return monthName;
|
||||
}
|
||||
if (startDay === endDay) {
|
||||
return `${monthName} ${startDay}`;
|
||||
}
|
||||
return `${monthName} ${startDay}-${endDay}`;
|
||||
}
|
||||
|
||||
function resolveRangeForMonth(month, options = {}) {
|
||||
const monthOrder = Number(month?.order);
|
||||
const monthStart = parseMonthDayToken(month?.start);
|
||||
const monthEnd = parseMonthDayToken(month?.end);
|
||||
if (!Number.isFinite(monthOrder) || !monthStart || !monthEnd) {
|
||||
return {
|
||||
startToken: String(month?.start || "").trim() || null,
|
||||
endToken: String(month?.end || "").trim() || null,
|
||||
label: month?.name || month?.id || "",
|
||||
isFullMonth: true
|
||||
};
|
||||
}
|
||||
|
||||
let startToken = parseMonthDayToken(options.startToken);
|
||||
let endToken = parseMonthDayToken(options.endToken);
|
||||
|
||||
if (!startToken || !endToken) {
|
||||
const tokens = parseMonthDayTokensFromText(options.rawDateText);
|
||||
if (tokens.length >= 2) {
|
||||
startToken = tokens[0];
|
||||
endToken = tokens[1];
|
||||
} else if (tokens.length === 1) {
|
||||
startToken = tokens[0];
|
||||
endToken = tokens[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (!startToken || !endToken) {
|
||||
startToken = monthStart;
|
||||
endToken = monthEnd;
|
||||
}
|
||||
|
||||
const segments = splitMonthDayRangeByMonth(startToken, endToken);
|
||||
const segment = segments.find((entry) => entry.monthNo === monthOrder) || null;
|
||||
|
||||
const useStart = segment ? { month: monthOrder, day: segment.startDay } : startToken;
|
||||
const useEnd = segment ? { month: monthOrder, day: segment.endDay } : endToken;
|
||||
const startText = tokenToString(useStart.month, useStart.day);
|
||||
const endText = tokenToString(useEnd.month, useEnd.day);
|
||||
const isFullMonth = startText === month.start && endText === month.end;
|
||||
|
||||
return {
|
||||
startToken: startText,
|
||||
endToken: endText,
|
||||
label: isFullMonth
|
||||
? (month.name || month.id)
|
||||
: formatRangeLabel(month.name || month.id, useStart.day, useEnd.day),
|
||||
isFullMonth
|
||||
};
|
||||
}
|
||||
|
||||
function pushRef(hexagramNumber, month, options = {}) {
|
||||
if (!Number.isFinite(hexagramNumber) || !month?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!map.has(hexagramNumber)) {
|
||||
map.set(hexagramNumber, []);
|
||||
}
|
||||
|
||||
const rows = map.get(hexagramNumber);
|
||||
const range = resolveRangeForMonth(month, options);
|
||||
const rowKey = `${month.id}|${range.startToken || ""}|${range.endToken || ""}`;
|
||||
if (rows.some((entry) => entry.key === rowKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
rows.push({
|
||||
id: month.id,
|
||||
name: month.name || month.id,
|
||||
order: Number.isFinite(Number(month.order)) ? Number(month.order) : 999,
|
||||
label: range.label,
|
||||
startToken: range.startToken,
|
||||
endToken: range.endToken,
|
||||
isFullMonth: range.isFullMonth,
|
||||
key: rowKey
|
||||
});
|
||||
}
|
||||
|
||||
function collectRefs(associations, month, options = {}) {
|
||||
const associationInfluence = resolveAssociationPlanetInfluence(associations);
|
||||
if (!associationInfluence) {
|
||||
return;
|
||||
}
|
||||
|
||||
hexagrams.forEach((hexagram) => {
|
||||
const hexagramInfluence = normalizePlanetInfluence(hexagram?.planetaryInfluence);
|
||||
if (hexagramInfluence && hexagramInfluence === associationInfluence) {
|
||||
pushRef(hexagram.number, month, options);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
months.forEach((month) => {
|
||||
collectRefs(month?.associations, month);
|
||||
const events = Array.isArray(month?.events) ? month.events : [];
|
||||
events.forEach((event) => {
|
||||
collectRefs(event?.associations, month, {
|
||||
rawDateText: event?.dateRange || event?.date || ""
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
holidays.forEach((holiday) => {
|
||||
const month = monthById.get(holiday?.monthId);
|
||||
if (!month) {
|
||||
return;
|
||||
}
|
||||
collectRefs(holiday?.associations, month, {
|
||||
rawDateText: holiday?.dateRange || holiday?.date || ""
|
||||
});
|
||||
});
|
||||
|
||||
map.forEach((rows, key) => {
|
||||
const preciseMonthIds = new Set(
|
||||
rows
|
||||
.filter((entry) => !entry.isFullMonth)
|
||||
.map((entry) => entry.id)
|
||||
);
|
||||
|
||||
const filtered = rows.filter((entry) => {
|
||||
if (!entry.isFullMonth) {
|
||||
return true;
|
||||
}
|
||||
return !preciseMonthIds.has(entry.id);
|
||||
});
|
||||
|
||||
filtered.sort((left, right) => {
|
||||
if (left.order !== right.order) {
|
||||
return left.order - right.order;
|
||||
}
|
||||
|
||||
const startLeft = parseMonthDayToken(left.startToken);
|
||||
const startRight = parseMonthDayToken(right.startToken);
|
||||
const dayLeft = startLeft ? startLeft.day : 999;
|
||||
const dayRight = startRight ? startRight.day : 999;
|
||||
if (dayLeft !== dayRight) {
|
||||
return dayLeft - dayRight;
|
||||
}
|
||||
|
||||
return String(left.label || left.name || "").localeCompare(String(right.label || right.name || ""));
|
||||
});
|
||||
|
||||
map.set(key, filtered);
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
function getBinaryPattern(value, expectedLength = 0) {
|
||||
const raw = String(value || "").trim();
|
||||
if (!raw) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (/^[01]+$/.test(raw)) {
|
||||
if (!expectedLength || raw.length === expectedLength) {
|
||||
return raw;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
if (/^[|:]+$/.test(raw)) {
|
||||
const mapped = raw
|
||||
.split("")
|
||||
.map((char) => (char === "|" ? "1" : "0"))
|
||||
.join("");
|
||||
if (!expectedLength || mapped.length === expectedLength) {
|
||||
return mapped;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function createLineStack(binaryPattern, variant = "trigram") {
|
||||
const container = document.createElement("div");
|
||||
container.className = `iching-lines iching-lines-${variant}`;
|
||||
|
||||
binaryPattern.split("").forEach((bit) => {
|
||||
const line = document.createElement("div");
|
||||
line.className = bit === "1" ? "iching-line is-yang" : "iching-line is-yin";
|
||||
container.appendChild(line);
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
function buildHexagramSearchText(entry) {
|
||||
const upper = getTrigramByName(entry?.upperTrigram);
|
||||
const lower = getTrigramByName(entry?.lowerTrigram);
|
||||
const upperCards = upper ? state.tarotByTrigramName[normalizeSearchValue(upper.name)] || [] : [];
|
||||
const lowerCards = lower ? state.tarotByTrigramName[normalizeSearchValue(lower.name)] || [] : [];
|
||||
const tarotAliasText = typeof getTarotCardSearchAliases === "function"
|
||||
? [...upperCards, ...lowerCards]
|
||||
.flatMap((cardName) => getTarotCardSearchAliases(cardName))
|
||||
.join(" ")
|
||||
: [...upperCards, ...lowerCards].join(" ");
|
||||
const parts = [
|
||||
entry?.number,
|
||||
entry?.name,
|
||||
entry?.chineseName,
|
||||
entry?.pinyin,
|
||||
entry?.judgement,
|
||||
entry?.image,
|
||||
entry?.upperTrigram,
|
||||
entry?.lowerTrigram,
|
||||
entry?.planetaryInfluence,
|
||||
entry?.binary,
|
||||
entry?.lineDiagram,
|
||||
...(Array.isArray(entry?.keywords) ? entry.keywords : []),
|
||||
upper?.name,
|
||||
upper?.chineseName,
|
||||
upper?.pinyin,
|
||||
upper?.element,
|
||||
upper?.attribute,
|
||||
upper?.binary,
|
||||
upper?.description,
|
||||
lower?.name,
|
||||
lower?.chineseName,
|
||||
lower?.pinyin,
|
||||
lower?.element,
|
||||
lower?.attribute,
|
||||
lower?.binary,
|
||||
lower?.description,
|
||||
upperCards.join(" "),
|
||||
lowerCards.join(" "),
|
||||
tarotAliasText
|
||||
];
|
||||
|
||||
return normalizeSearchValue(parts.filter((part) => part !== null && part !== undefined).join(" "));
|
||||
}
|
||||
|
||||
function updateSelection(elements) {
|
||||
if (!elements?.ichingCardListEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const buttons = elements.ichingCardListEl.querySelectorAll(".planet-list-item");
|
||||
buttons.forEach((button) => {
|
||||
const isSelected = Number(button.dataset.hexagramNumber) === state.selectedNumber;
|
||||
button.classList.toggle("is-selected", isSelected);
|
||||
button.setAttribute("aria-selected", isSelected ? "true" : "false");
|
||||
});
|
||||
}
|
||||
|
||||
function renderEmptyDetail(elements, detailName = "No hexagrams found") {
|
||||
if (!elements) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (elements.ichingDetailNameEl) {
|
||||
elements.ichingDetailNameEl.textContent = detailName;
|
||||
}
|
||||
if (elements.ichingDetailTypeEl) {
|
||||
elements.ichingDetailTypeEl.textContent = "--";
|
||||
}
|
||||
if (elements.ichingDetailSummaryEl) {
|
||||
elements.ichingDetailSummaryEl.textContent = "--";
|
||||
}
|
||||
if (elements.ichingDetailJudgementEl) {
|
||||
elements.ichingDetailJudgementEl.textContent = "--";
|
||||
}
|
||||
if (elements.ichingDetailImageEl) {
|
||||
elements.ichingDetailImageEl.textContent = "--";
|
||||
}
|
||||
if (elements.ichingDetailBinaryEl) {
|
||||
elements.ichingDetailBinaryEl.textContent = "--";
|
||||
}
|
||||
if (elements.ichingDetailLineEl) {
|
||||
clearChildren(elements.ichingDetailLineEl);
|
||||
elements.ichingDetailLineEl.textContent = "--";
|
||||
}
|
||||
if (elements.ichingDetailPlanetEl) {
|
||||
elements.ichingDetailPlanetEl.textContent = "--";
|
||||
}
|
||||
if (elements.ichingDetailTarotEl) {
|
||||
elements.ichingDetailTarotEl.textContent = "--";
|
||||
}
|
||||
if (elements.ichingDetailCalendarEl) {
|
||||
clearChildren(elements.ichingDetailCalendarEl);
|
||||
elements.ichingDetailCalendarEl.textContent = "--";
|
||||
}
|
||||
|
||||
clearChildren(elements.ichingDetailKeywordsEl);
|
||||
clearChildren(elements.ichingDetailTrigramsEl);
|
||||
}
|
||||
|
||||
function renderKeywords(entry, elements) {
|
||||
clearChildren(elements.ichingDetailKeywordsEl);
|
||||
const keywords = Array.isArray(entry?.keywords) ? entry.keywords : [];
|
||||
|
||||
if (!keywords.length) {
|
||||
if (elements.ichingDetailKeywordsEl) {
|
||||
elements.ichingDetailKeywordsEl.textContent = "--";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
keywords.forEach((keyword) => {
|
||||
const chip = document.createElement("span");
|
||||
chip.className = "tarot-keyword-chip";
|
||||
chip.textContent = keyword;
|
||||
elements.ichingDetailKeywordsEl?.appendChild(chip);
|
||||
});
|
||||
}
|
||||
|
||||
function createTrigramCard(label, trigram) {
|
||||
const card = document.createElement("div");
|
||||
card.className = "iching-trigram-card";
|
||||
|
||||
if (!trigram) {
|
||||
card.textContent = `${label}: --`;
|
||||
return card;
|
||||
}
|
||||
|
||||
const title = document.createElement("div");
|
||||
title.className = "iching-trigram-title";
|
||||
const chinese = trigram.chineseName ? ` (${trigram.chineseName})` : "";
|
||||
title.textContent = `${label}: ${trigram.name || "--"}${chinese}`;
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "iching-trigram-meta";
|
||||
const attribute = trigram.attribute || "--";
|
||||
const element = trigram.element || "--";
|
||||
meta.textContent = `${attribute} · ${element}`;
|
||||
|
||||
const diagram = document.createElement("div");
|
||||
diagram.className = "iching-trigram-diagram";
|
||||
const binaryPattern = getBinaryPattern(trigram.binary || trigram.lineDiagram, 3);
|
||||
|
||||
if (binaryPattern) {
|
||||
const binaryLabel = document.createElement("div");
|
||||
binaryLabel.className = "iching-line-label";
|
||||
binaryLabel.textContent = binaryPattern;
|
||||
diagram.append(binaryLabel, createLineStack(binaryPattern, "trigram"));
|
||||
} else {
|
||||
diagram.textContent = "--";
|
||||
}
|
||||
|
||||
card.append(title, meta, diagram);
|
||||
|
||||
if (trigram.description) {
|
||||
const description = document.createElement("div");
|
||||
description.className = "planet-text";
|
||||
description.textContent = trigram.description;
|
||||
card.appendChild(description);
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
function renderTrigrams(entry, elements) {
|
||||
clearChildren(elements.ichingDetailTrigramsEl);
|
||||
|
||||
const upper = getTrigramByName(entry?.upperTrigram);
|
||||
const lower = getTrigramByName(entry?.lowerTrigram);
|
||||
|
||||
elements.ichingDetailTrigramsEl?.append(
|
||||
createTrigramCard("Upper", upper),
|
||||
createTrigramCard("Lower", lower)
|
||||
);
|
||||
}
|
||||
|
||||
function renderTarotCorrespondences(entry, elements) {
|
||||
if (!elements?.ichingDetailTarotEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const upperKey = normalizeSearchValue(entry?.upperTrigram);
|
||||
const lowerKey = normalizeSearchValue(entry?.lowerTrigram);
|
||||
const upperCards = upperKey ? state.tarotByTrigramName[upperKey] || [] : [];
|
||||
const lowerCards = lowerKey ? state.tarotByTrigramName[lowerKey] || [] : [];
|
||||
|
||||
const upperTrigram = upperKey ? state.trigramsByName[upperKey] : null;
|
||||
const lowerTrigram = lowerKey ? state.trigramsByName[lowerKey] : null;
|
||||
const upperLabel = upperTrigram?.element || entry?.upperTrigram || "--";
|
||||
const lowerLabel = lowerTrigram?.element || entry?.lowerTrigram || "--";
|
||||
|
||||
const lines = [];
|
||||
if (upperKey) {
|
||||
lines.push(`Upper (${upperLabel}): ${upperCards.length ? upperCards.join(", ") : "--"}`);
|
||||
}
|
||||
if (lowerKey) {
|
||||
lines.push(`Lower (${lowerLabel}): ${lowerCards.length ? lowerCards.join(", ") : "--"}`);
|
||||
}
|
||||
|
||||
elements.ichingDetailTarotEl.textContent = lines.length ? lines.join("\n\n") : "--";
|
||||
}
|
||||
|
||||
function renderCalendarMonths(entry, elements) {
|
||||
if (!elements?.ichingDetailCalendarEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearChildren(elements.ichingDetailCalendarEl);
|
||||
const rows = state.monthRefsByHexagramNumber.get(entry?.number) || [];
|
||||
|
||||
if (!rows.length) {
|
||||
elements.ichingDetailCalendarEl.textContent = "--";
|
||||
return;
|
||||
}
|
||||
|
||||
rows.forEach((month) => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "alpha-nav-btn";
|
||||
button.textContent = `${month.label || month.name} ↗`;
|
||||
button.addEventListener("click", () => {
|
||||
document.dispatchEvent(new CustomEvent("nav:calendar-month", {
|
||||
detail: { monthId: month.id }
|
||||
}));
|
||||
});
|
||||
elements.ichingDetailCalendarEl.appendChild(button);
|
||||
});
|
||||
}
|
||||
|
||||
function renderDetail(entry, elements) {
|
||||
if (!entry || !elements) {
|
||||
return;
|
||||
}
|
||||
|
||||
const number = Number.isFinite(entry.number) ? entry.number : "--";
|
||||
if (elements.ichingDetailNameEl) {
|
||||
elements.ichingDetailNameEl.textContent = `${number} · ${entry.name || "--"}`;
|
||||
}
|
||||
|
||||
if (elements.ichingDetailTypeEl) {
|
||||
const chinese = entry.chineseName || "--";
|
||||
const pinyin = entry.pinyin || "--";
|
||||
const upper = entry.upperTrigram || "--";
|
||||
const lower = entry.lowerTrigram || "--";
|
||||
elements.ichingDetailTypeEl.textContent = `Hexagram · ${chinese} · ${pinyin} · ${upper}/${lower}`;
|
||||
}
|
||||
|
||||
if (elements.ichingDetailSummaryEl) {
|
||||
elements.ichingDetailSummaryEl.textContent = entry.judgement || "--";
|
||||
}
|
||||
|
||||
if (elements.ichingDetailJudgementEl) {
|
||||
elements.ichingDetailJudgementEl.textContent = entry.judgement || "--";
|
||||
}
|
||||
|
||||
if (elements.ichingDetailImageEl) {
|
||||
elements.ichingDetailImageEl.textContent = entry.image || "--";
|
||||
}
|
||||
|
||||
if (elements.ichingDetailBinaryEl) {
|
||||
const binary = entry.binary || "--";
|
||||
elements.ichingDetailBinaryEl.textContent = `Binary: ${binary}`;
|
||||
}
|
||||
|
||||
if (elements.ichingDetailLineEl) {
|
||||
clearChildren(elements.ichingDetailLineEl);
|
||||
const linePattern = getBinaryPattern(entry.binary, 6) || getBinaryPattern(entry.lineDiagram, 6);
|
||||
if (linePattern) {
|
||||
elements.ichingDetailLineEl.appendChild(createLineStack(linePattern, "hexagram"));
|
||||
} else {
|
||||
elements.ichingDetailLineEl.textContent = "--";
|
||||
}
|
||||
}
|
||||
|
||||
if (elements.ichingDetailPlanetEl) {
|
||||
elements.ichingDetailPlanetEl.textContent = entry.planetaryInfluence || "--";
|
||||
}
|
||||
|
||||
renderKeywords(entry, elements);
|
||||
renderTrigrams(entry, elements);
|
||||
renderTarotCorrespondences(entry, elements);
|
||||
renderCalendarMonths(entry, elements);
|
||||
}
|
||||
|
||||
function selectByNumber(number, elements) {
|
||||
const numeric = Number(number);
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = state.hexagrams.find((hexagram) => hexagram.number === numeric);
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.selectedNumber = entry.number;
|
||||
updateSelection(elements);
|
||||
renderDetail(entry, elements);
|
||||
}
|
||||
|
||||
function renderList(elements) {
|
||||
if (!elements?.ichingCardListEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearChildren(elements.ichingCardListEl);
|
||||
|
||||
state.filteredHexagrams.forEach((entry) => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "planet-list-item";
|
||||
button.dataset.hexagramNumber = String(entry.number);
|
||||
button.setAttribute("role", "option");
|
||||
|
||||
const nameEl = document.createElement("span");
|
||||
nameEl.className = "planet-list-name";
|
||||
const number = Number.isFinite(entry.number) ? `#${entry.number} ` : "";
|
||||
nameEl.textContent = `${number}${entry.name || "--"}`;
|
||||
|
||||
const metaEl = document.createElement("span");
|
||||
metaEl.className = "planet-list-meta";
|
||||
const upper = entry.upperTrigram || "--";
|
||||
const lower = entry.lowerTrigram || "--";
|
||||
metaEl.textContent = `${upper}/${lower} · ${entry.planetaryInfluence || "--"}`;
|
||||
|
||||
button.append(nameEl, metaEl);
|
||||
elements.ichingCardListEl.appendChild(button);
|
||||
});
|
||||
|
||||
if (elements.ichingCountEl) {
|
||||
elements.ichingCountEl.textContent = state.searchQuery
|
||||
? `${state.filteredHexagrams.length} of ${state.hexagrams.length} hexagrams`
|
||||
: `${state.hexagrams.length} hexagrams`;
|
||||
}
|
||||
}
|
||||
|
||||
function applySearchFilter(elements) {
|
||||
const query = normalizeSearchValue(state.searchQuery);
|
||||
state.filteredHexagrams = query
|
||||
? state.hexagrams.filter((entry) => buildHexagramSearchText(entry).includes(query))
|
||||
: [...state.hexagrams];
|
||||
|
||||
if (elements?.ichingSearchClearEl) {
|
||||
elements.ichingSearchClearEl.disabled = !query;
|
||||
}
|
||||
|
||||
renderList(elements);
|
||||
|
||||
if (!state.filteredHexagrams.some((entry) => entry.number === state.selectedNumber)) {
|
||||
if (state.filteredHexagrams.length > 0) {
|
||||
selectByNumber(state.filteredHexagrams[0].number, elements);
|
||||
} else {
|
||||
state.selectedNumber = null;
|
||||
updateSelection(elements);
|
||||
renderEmptyDetail(elements);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
updateSelection(elements);
|
||||
}
|
||||
|
||||
function ensureIChingSection(referenceData) {
|
||||
const elements = getElements();
|
||||
|
||||
if (state.initialized) {
|
||||
state.monthRefsByHexagramNumber = buildMonthReferencesByHexagram(referenceData, state.hexagrams);
|
||||
const selected = state.hexagrams.find((hexagram) => hexagram.number === state.selectedNumber);
|
||||
if (selected) {
|
||||
renderDetail(selected, elements);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!elements.ichingCardListEl || !elements.ichingDetailNameEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const iChing = referenceData?.iChing;
|
||||
const trigrams = Array.isArray(iChing?.trigrams) ? iChing.trigrams : [];
|
||||
const hexagrams = Array.isArray(iChing?.hexagrams) ? iChing.hexagrams : [];
|
||||
const correspondences = iChing?.correspondences;
|
||||
|
||||
if (!hexagrams.length) {
|
||||
renderEmptyDetail(elements, "I Ching data unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
state.trigramsByName = trigrams.reduce((acc, trigram) => {
|
||||
const key = normalizeSearchValue(trigram?.name);
|
||||
if (key) {
|
||||
acc[key] = trigram;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const tarotToTrigram = Array.isArray(correspondences?.tarotToTrigram)
|
||||
? correspondences.tarotToTrigram
|
||||
: [];
|
||||
|
||||
state.tarotByTrigramName = tarotToTrigram.reduce((acc, row) => {
|
||||
const key = normalizeSearchValue(row?.trigram);
|
||||
const tarotCard = String(row?.tarot || "").trim();
|
||||
if (!key || !tarotCard) {
|
||||
return acc;
|
||||
}
|
||||
if (!Array.isArray(acc[key])) {
|
||||
acc[key] = [];
|
||||
}
|
||||
if (!acc[key].includes(tarotCard)) {
|
||||
acc[key].push(tarotCard);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
state.hexagrams = [...hexagrams]
|
||||
.map((entry) => ({
|
||||
...entry,
|
||||
number: Number(entry?.number)
|
||||
}))
|
||||
.filter((entry) => Number.isFinite(entry.number))
|
||||
.sort((a, b) => a.number - b.number);
|
||||
|
||||
state.monthRefsByHexagramNumber = buildMonthReferencesByHexagram(referenceData, state.hexagrams);
|
||||
|
||||
state.filteredHexagrams = [...state.hexagrams];
|
||||
renderList(elements);
|
||||
|
||||
if (state.hexagrams.length > 0) {
|
||||
selectByNumber(state.hexagrams[0].number, elements);
|
||||
}
|
||||
|
||||
elements.ichingCardListEl.addEventListener("click", (event) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof Node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const button = target instanceof Element
|
||||
? target.closest(".planet-list-item")
|
||||
: null;
|
||||
|
||||
if (!(button instanceof HTMLButtonElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedNumber = button.dataset.hexagramNumber;
|
||||
if (!selectedNumber) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectByNumber(selectedNumber, elements);
|
||||
});
|
||||
|
||||
if (elements.ichingSearchInputEl) {
|
||||
elements.ichingSearchInputEl.addEventListener("input", () => {
|
||||
state.searchQuery = elements.ichingSearchInputEl.value || "";
|
||||
applySearchFilter(elements);
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.ichingSearchClearEl && elements.ichingSearchInputEl) {
|
||||
elements.ichingSearchClearEl.addEventListener("click", () => {
|
||||
elements.ichingSearchInputEl.value = "";
|
||||
state.searchQuery = "";
|
||||
applySearchFilter(elements);
|
||||
elements.ichingSearchInputEl.focus();
|
||||
});
|
||||
}
|
||||
|
||||
state.initialized = true;
|
||||
}
|
||||
|
||||
function selectByHexagramNumber(number) {
|
||||
if (!state.initialized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const elements = getElements();
|
||||
const numeric = Number(number);
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const entry = state.hexagrams.find((hexagram) => hexagram.number === numeric);
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
selectByNumber(entry.number, elements);
|
||||
elements.ichingCardListEl
|
||||
?.querySelector(`[data-hexagram-number="${entry.number}"]`)
|
||||
?.scrollIntoView({ block: "nearest" });
|
||||
return true;
|
||||
}
|
||||
|
||||
function selectByPlanetaryInfluence(planetaryInfluence) {
|
||||
if (!state.initialized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetInfluence = normalizePlanetInfluence(planetaryInfluence);
|
||||
if (!targetInfluence) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const entry = state.hexagrams.find((hexagram) =>
|
||||
normalizePlanetInfluence(hexagram?.planetaryInfluence) === targetInfluence
|
||||
);
|
||||
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return selectByHexagramNumber(entry.number);
|
||||
}
|
||||
|
||||
window.IChingSectionUi = {
|
||||
ensureIChingSection,
|
||||
selectByHexagramNumber,
|
||||
selectByPlanetaryInfluence
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user