2529 lines
79 KiB
JavaScript
2529 lines
79 KiB
JavaScript
/* ui-calendar.js — Month and celestial holiday browser */
|
||
(function () {
|
||
"use strict";
|
||
|
||
const { getTarotCardDisplayName, getTarotCardSearchAliases } = window.TarotCardImages || {};
|
||
|
||
const state = {
|
||
initialized: false,
|
||
referenceData: null,
|
||
magickDataset: null,
|
||
selectedCalendar: "gregorian",
|
||
calendarData: {},
|
||
months: [],
|
||
filteredMonths: [],
|
||
holidays: [],
|
||
calendarHolidays: [],
|
||
selectedMonthId: null,
|
||
searchQuery: "",
|
||
selectedYear: new Date().getFullYear(),
|
||
selectedDayMonthId: null,
|
||
selectedDayCalendarId: null,
|
||
selectedDayEntries: [],
|
||
planetsById: new Map(),
|
||
signsById: new Map(),
|
||
godsById: new Map(),
|
||
hebrewById: new Map(),
|
||
dayLinksCache: new Map()
|
||
};
|
||
|
||
const TAROT_TRUMP_NUMBER_BY_NAME = {
|
||
"the fool": 0,
|
||
fool: 0,
|
||
"the magus": 1,
|
||
magus: 1,
|
||
magician: 1,
|
||
"the high priestess": 2,
|
||
"high priestess": 2,
|
||
"the empress": 3,
|
||
empress: 3,
|
||
"the emperor": 4,
|
||
emperor: 4,
|
||
"the hierophant": 5,
|
||
hierophant: 5,
|
||
"the lovers": 6,
|
||
lovers: 6,
|
||
"the chariot": 7,
|
||
chariot: 7,
|
||
strength: 8,
|
||
lust: 8,
|
||
"the hermit": 9,
|
||
hermit: 9,
|
||
fortune: 10,
|
||
"wheel of fortune": 10,
|
||
justice: 11,
|
||
"the hanged man": 12,
|
||
"hanged man": 12,
|
||
death: 13,
|
||
temperance: 14,
|
||
art: 14,
|
||
"the devil": 15,
|
||
devil: 15,
|
||
"the tower": 16,
|
||
tower: 16,
|
||
"the star": 17,
|
||
star: 17,
|
||
"the moon": 18,
|
||
moon: 18,
|
||
"the sun": 19,
|
||
sun: 19,
|
||
aeon: 20,
|
||
judgement: 20,
|
||
judgment: 20,
|
||
universe: 21,
|
||
world: 21,
|
||
"the world": 21
|
||
};
|
||
|
||
const MINOR_NUMBER_WORD = {
|
||
1: "Ace",
|
||
2: "Two",
|
||
3: "Three",
|
||
4: "Four",
|
||
5: "Five",
|
||
6: "Six",
|
||
7: "Seven",
|
||
8: "Eight",
|
||
9: "Nine",
|
||
10: "Ten"
|
||
};
|
||
|
||
const HEBREW_MONTH_ALIAS_BY_ID = {
|
||
nisan: ["nisan"],
|
||
iyar: ["iyar"],
|
||
sivan: ["sivan"],
|
||
tammuz: ["tamuz", "tammuz"],
|
||
av: ["av"],
|
||
elul: ["elul"],
|
||
tishrei: ["tishri", "tishrei"],
|
||
cheshvan: ["heshvan", "cheshvan", "marcheshvan"],
|
||
kislev: ["kislev"],
|
||
tevet: ["tevet"],
|
||
shvat: ["shevat", "shvat"],
|
||
adar: ["adar", "adar i", "adar 1"],
|
||
"adar-ii": ["adar ii", "adar 2"]
|
||
};
|
||
|
||
const MONTH_NAME_TO_INDEX = {
|
||
january: 0,
|
||
february: 1,
|
||
march: 2,
|
||
april: 3,
|
||
may: 4,
|
||
june: 5,
|
||
july: 6,
|
||
august: 7,
|
||
september: 8,
|
||
october: 9,
|
||
november: 10,
|
||
december: 11
|
||
};
|
||
|
||
const GREGORIAN_MONTH_ID_TO_ORDER = {
|
||
january: 1,
|
||
february: 2,
|
||
march: 3,
|
||
april: 4,
|
||
may: 5,
|
||
june: 6,
|
||
july: 7,
|
||
august: 8,
|
||
september: 9,
|
||
october: 10,
|
||
november: 11,
|
||
december: 12
|
||
};
|
||
|
||
function getElements() {
|
||
return {
|
||
monthListEl: document.getElementById("calendar-month-list"),
|
||
listTitleEl: document.getElementById("calendar-list-title"),
|
||
monthCountEl: document.getElementById("calendar-month-count"),
|
||
yearInputEl: document.getElementById("calendar-year-input"),
|
||
calendarYearWrapEl: document.getElementById("calendar-year-wrap"),
|
||
calendarTypeEl: document.getElementById("calendar-type-select"),
|
||
searchInputEl: document.getElementById("calendar-search-input"),
|
||
searchClearEl: document.getElementById("calendar-search-clear"),
|
||
detailNameEl: document.getElementById("calendar-detail-name"),
|
||
detailSubEl: document.getElementById("calendar-detail-sub"),
|
||
detailBodyEl: document.getElementById("calendar-detail-body")
|
||
};
|
||
}
|
||
|
||
function normalizeText(value) {
|
||
return String(value || "").trim();
|
||
}
|
||
|
||
function normalizeSearchValue(value) {
|
||
return String(value || "").trim().toLowerCase();
|
||
}
|
||
|
||
function cap(value) {
|
||
const text = normalizeText(value);
|
||
return text ? text.charAt(0).toUpperCase() + text.slice(1) : text;
|
||
}
|
||
|
||
function normalizeTarotName(value) {
|
||
return String(value || "")
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/\s+/g, " ");
|
||
}
|
||
|
||
function resolveTarotTrumpNumber(cardName) {
|
||
const key = normalizeTarotName(cardName);
|
||
if (!key) {
|
||
return null;
|
||
}
|
||
if (Object.prototype.hasOwnProperty.call(TAROT_TRUMP_NUMBER_BY_NAME, key)) {
|
||
return TAROT_TRUMP_NUMBER_BY_NAME[key];
|
||
}
|
||
const withoutLeadingThe = key.replace(/^the\s+/, "");
|
||
if (Object.prototype.hasOwnProperty.call(TAROT_TRUMP_NUMBER_BY_NAME, withoutLeadingThe)) {
|
||
return TAROT_TRUMP_NUMBER_BY_NAME[withoutLeadingThe];
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function getDisplayTarotName(cardName, trumpNumber) {
|
||
if (!cardName) {
|
||
return "";
|
||
}
|
||
|
||
if (typeof getTarotCardDisplayName !== "function") {
|
||
return cardName;
|
||
}
|
||
|
||
if (Number.isFinite(Number(trumpNumber))) {
|
||
return getTarotCardDisplayName(cardName, { trumpNumber: Number(trumpNumber) }) || cardName;
|
||
}
|
||
|
||
return getTarotCardDisplayName(cardName) || cardName;
|
||
}
|
||
|
||
function normalizeMinorTarotCardName(cardName) {
|
||
const text = String(cardName || "").trim();
|
||
if (!text) {
|
||
return "";
|
||
}
|
||
|
||
const match = text.match(/^(\d{1,2})\s+of\s+(.+)$/i);
|
||
if (!match) {
|
||
return text.replace(/\b(pentacles?|coins?)\b/i, "Disks");
|
||
}
|
||
|
||
const numeric = Number(match[1]);
|
||
const suitRaw = String(match[2] || "").trim();
|
||
const rank = MINOR_NUMBER_WORD[numeric] || String(numeric);
|
||
const suit = suitRaw.replace(/\b(pentacles?|coins?)\b/i, "Disks");
|
||
return `${rank} of ${suit}`;
|
||
}
|
||
|
||
function parseMonthDayToken(token) {
|
||
const [month, day] = String(token || "").split("-").map((part) => Number(part));
|
||
if (!Number.isFinite(month) || !Number.isFinite(day)) {
|
||
return null;
|
||
}
|
||
return { month, day };
|
||
}
|
||
|
||
function monthDayDate(monthDay, year) {
|
||
const parsed = parseMonthDayToken(monthDay);
|
||
if (!parsed) {
|
||
return null;
|
||
}
|
||
return new Date(year, parsed.month - 1, parsed.day);
|
||
}
|
||
|
||
function buildSignDateBounds(sign) {
|
||
const start = monthDayDate(sign?.start, 2025);
|
||
const endBase = monthDayDate(sign?.end, 2025);
|
||
if (!start || !endBase) {
|
||
return null;
|
||
}
|
||
|
||
const wrapsYear = endBase.getTime() < start.getTime();
|
||
const end = wrapsYear ? monthDayDate(sign?.end, 2026) : endBase;
|
||
if (!end) {
|
||
return null;
|
||
}
|
||
|
||
return { start, end };
|
||
}
|
||
|
||
function addDays(date, days) {
|
||
const next = new Date(date);
|
||
next.setDate(next.getDate() + days);
|
||
return next;
|
||
}
|
||
|
||
function formatDateLabel(date) {
|
||
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
||
}
|
||
|
||
function monthDayOrdinal(month, day) {
|
||
if (!Number.isFinite(month) || !Number.isFinite(day)) {
|
||
return null;
|
||
}
|
||
const base = new Date(2025, Math.trunc(month) - 1, Math.trunc(day), 12, 0, 0, 0);
|
||
if (Number.isNaN(base.getTime())) {
|
||
return null;
|
||
}
|
||
const start = new Date(2025, 0, 1, 12, 0, 0, 0);
|
||
const diff = base.getTime() - start.getTime();
|
||
return Math.floor(diff / (24 * 60 * 60 * 1000)) + 1;
|
||
}
|
||
|
||
function isMonthDayInRange(targetMonth, targetDay, startMonth, startDay, endMonth, endDay) {
|
||
const target = monthDayOrdinal(targetMonth, targetDay);
|
||
const start = monthDayOrdinal(startMonth, startDay);
|
||
const end = monthDayOrdinal(endMonth, endDay);
|
||
if (!Number.isFinite(target) || !Number.isFinite(start) || !Number.isFinite(end)) {
|
||
return false;
|
||
}
|
||
|
||
if (end >= start) {
|
||
return target >= start && target <= end;
|
||
}
|
||
return target >= start || target <= end;
|
||
}
|
||
|
||
function parseMonthDayTokensFromText(value) {
|
||
const text = String(value || "");
|
||
const matches = [...text.matchAll(/(\d{2})-(\d{2})/g)];
|
||
return matches
|
||
.map((match) => ({ month: Number(match[1]), day: Number(match[2]) }))
|
||
.filter((token) => Number.isFinite(token.month) && Number.isFinite(token.day));
|
||
}
|
||
|
||
function parseDayRangeFromText(value) {
|
||
const text = String(value || "");
|
||
const range = text.match(/\b(\d{1,2})\s*[–-]\s*(\d{1,2})\b/);
|
||
if (!range) {
|
||
return null;
|
||
}
|
||
|
||
const startDay = Number(range[1]);
|
||
const endDay = Number(range[2]);
|
||
if (!Number.isFinite(startDay) || !Number.isFinite(endDay)) {
|
||
return null;
|
||
}
|
||
|
||
return { startDay, endDay };
|
||
}
|
||
|
||
function isoToDateAtNoon(iso) {
|
||
const text = String(iso || "").trim();
|
||
if (!text) {
|
||
return null;
|
||
}
|
||
const parsed = new Date(`${text}T12:00:00`);
|
||
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
||
}
|
||
|
||
function normalizeDayFilterEntry(dayNumber, gregorianIso) {
|
||
const day = Math.trunc(Number(dayNumber));
|
||
if (!Number.isFinite(day) || day <= 0) {
|
||
return null;
|
||
}
|
||
|
||
const iso = String(gregorianIso || "").trim();
|
||
if (!iso) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
dayNumber: day,
|
||
gregorianIso: iso
|
||
};
|
||
}
|
||
|
||
function sortDayFilterEntries(entries) {
|
||
return [...entries].sort((left, right) => left.dayNumber - right.dayNumber || left.gregorianIso.localeCompare(right.gregorianIso));
|
||
}
|
||
|
||
function ensureDayFilterContext(month) {
|
||
if (!month) {
|
||
return;
|
||
}
|
||
|
||
const sameContext = state.selectedDayMonthId === month.id
|
||
&& state.selectedDayCalendarId === state.selectedCalendar;
|
||
|
||
if (!sameContext) {
|
||
state.selectedDayMonthId = month.id;
|
||
state.selectedDayCalendarId = state.selectedCalendar;
|
||
state.selectedDayEntries = [];
|
||
}
|
||
}
|
||
|
||
function clearSelectedDayFilter() {
|
||
state.selectedDayMonthId = null;
|
||
state.selectedDayCalendarId = null;
|
||
state.selectedDayEntries = [];
|
||
}
|
||
|
||
function toggleDayFilterEntry(month, dayNumber, gregorianIso) {
|
||
ensureDayFilterContext(month);
|
||
|
||
const next = normalizeDayFilterEntry(dayNumber, gregorianIso);
|
||
if (!next) {
|
||
return;
|
||
}
|
||
|
||
const entries = state.selectedDayEntries;
|
||
const existingIndex = entries.findIndex((entry) => entry.dayNumber === next.dayNumber);
|
||
if (existingIndex >= 0) {
|
||
entries.splice(existingIndex, 1);
|
||
} else {
|
||
entries.push(next);
|
||
}
|
||
|
||
state.selectedDayEntries = sortDayFilterEntries(entries);
|
||
}
|
||
|
||
function toggleDayRangeFilter(month, startDay, endDay) {
|
||
ensureDayFilterContext(month);
|
||
|
||
const start = Math.trunc(Number(startDay));
|
||
const end = Math.trunc(Number(endDay));
|
||
if (!Number.isFinite(start) || !Number.isFinite(end) || start <= 0 || end <= 0) {
|
||
return;
|
||
}
|
||
|
||
const minDay = Math.min(start, end);
|
||
const maxDay = Math.max(start, end);
|
||
const rows = getMonthDayLinkRows(month)
|
||
.filter((row) => row.isResolved && row.day >= minDay && row.day <= maxDay)
|
||
.map((row) => normalizeDayFilterEntry(row.day, row.gregorianDate))
|
||
.filter(Boolean);
|
||
|
||
if (!rows.length) {
|
||
return;
|
||
}
|
||
|
||
const existingSet = new Set(state.selectedDayEntries.map((entry) => entry.dayNumber));
|
||
const allSelected = rows.every((row) => existingSet.has(row.dayNumber));
|
||
|
||
if (allSelected) {
|
||
const removeSet = new Set(rows.map((row) => row.dayNumber));
|
||
state.selectedDayEntries = state.selectedDayEntries.filter((entry) => !removeSet.has(entry.dayNumber));
|
||
return;
|
||
}
|
||
|
||
rows.forEach((row) => {
|
||
if (!existingSet.has(row.dayNumber)) {
|
||
state.selectedDayEntries.push(row);
|
||
}
|
||
});
|
||
|
||
state.selectedDayEntries = sortDayFilterEntries(state.selectedDayEntries);
|
||
}
|
||
|
||
function getSelectedDayFilterContext(month) {
|
||
if (!month) {
|
||
return null;
|
||
}
|
||
if (state.selectedDayMonthId !== month.id) {
|
||
return null;
|
||
}
|
||
if (state.selectedDayCalendarId !== state.selectedCalendar) {
|
||
return null;
|
||
}
|
||
|
||
if (!Array.isArray(state.selectedDayEntries) || !state.selectedDayEntries.length) {
|
||
return null;
|
||
}
|
||
|
||
const entries = state.selectedDayEntries
|
||
.map((entry) => {
|
||
const normalized = normalizeDayFilterEntry(entry.dayNumber, entry.gregorianIso);
|
||
if (!normalized) {
|
||
return null;
|
||
}
|
||
return {
|
||
...normalized,
|
||
gregorianDate: isoToDateAtNoon(normalized.gregorianIso)
|
||
};
|
||
})
|
||
.filter(Boolean);
|
||
|
||
if (!entries.length) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
entries,
|
||
dayNumbers: new Set(entries.map((entry) => entry.dayNumber))
|
||
};
|
||
}
|
||
|
||
function buildDecanWindow(sign, decanIndex) {
|
||
const bounds = buildSignDateBounds(sign);
|
||
const index = Number(decanIndex);
|
||
if (!bounds || !Number.isFinite(index)) {
|
||
return null;
|
||
}
|
||
|
||
const start = addDays(bounds.start, (index - 1) * 10);
|
||
const nominalEnd = addDays(start, 9);
|
||
const end = nominalEnd.getTime() > bounds.end.getTime() ? bounds.end : nominalEnd;
|
||
|
||
return {
|
||
start,
|
||
end,
|
||
label: `${formatDateLabel(start)}–${formatDateLabel(end)}`
|
||
};
|
||
}
|
||
|
||
function listMonthNumbersBetween(start, end) {
|
||
const result = [];
|
||
const seen = new Set();
|
||
const cursor = new Date(start.getFullYear(), start.getMonth(), 1);
|
||
const limit = new Date(end.getFullYear(), end.getMonth(), 1);
|
||
|
||
while (cursor.getTime() <= limit.getTime()) {
|
||
const monthNo = cursor.getMonth() + 1;
|
||
if (!seen.has(monthNo)) {
|
||
seen.add(monthNo);
|
||
result.push(monthNo);
|
||
}
|
||
cursor.setMonth(cursor.getMonth() + 1);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
function buildDecanTarotRowsForMonth(month) {
|
||
const monthOrder = Number(month?.order);
|
||
if (!Number.isFinite(monthOrder)) {
|
||
return [];
|
||
}
|
||
|
||
const rows = [];
|
||
const seen = new Set();
|
||
const decansBySign = state.referenceData?.decansBySign || {};
|
||
|
||
Object.entries(decansBySign).forEach(([signId, decans]) => {
|
||
const sign = state.signsById.get(signId);
|
||
if (!sign || !Array.isArray(decans)) {
|
||
return;
|
||
}
|
||
|
||
decans.forEach((decan) => {
|
||
const window = buildDecanWindow(sign, decan?.index);
|
||
if (!window) {
|
||
return;
|
||
}
|
||
|
||
const monthsTouched = listMonthNumbersBetween(window.start, window.end);
|
||
if (!monthsTouched.includes(monthOrder)) {
|
||
return;
|
||
}
|
||
|
||
const cardName = normalizeMinorTarotCardName(decan?.tarotMinorArcana);
|
||
if (!cardName) {
|
||
return;
|
||
}
|
||
|
||
const key = `${cardName}|${signId}|${decan.index}`;
|
||
if (seen.has(key)) {
|
||
return;
|
||
}
|
||
seen.add(key);
|
||
|
||
const startDegree = (Number(decan.index) - 1) * 10;
|
||
const endDegree = startDegree + 10;
|
||
const signName = sign?.name?.en || sign?.name || signId;
|
||
|
||
rows.push({
|
||
cardName,
|
||
signId,
|
||
signName,
|
||
signSymbol: sign?.symbol || "",
|
||
decanIndex: Number(decan.index),
|
||
startDegree,
|
||
endDegree,
|
||
startTime: window.start.getTime(),
|
||
endTime: window.end.getTime(),
|
||
startMonth: window.start.getMonth() + 1,
|
||
startDay: window.start.getDate(),
|
||
endMonth: window.end.getMonth() + 1,
|
||
endDay: window.end.getDate(),
|
||
dateRange: window.label
|
||
});
|
||
});
|
||
});
|
||
|
||
rows.sort((left, right) => {
|
||
if (left.startTime !== right.startTime) {
|
||
return left.startTime - right.startTime;
|
||
}
|
||
if (left.decanIndex !== right.decanIndex) {
|
||
return left.decanIndex - right.decanIndex;
|
||
}
|
||
return left.cardName.localeCompare(right.cardName);
|
||
});
|
||
|
||
return rows;
|
||
}
|
||
|
||
function buildPlanetMap(planetsObj) {
|
||
const map = new Map();
|
||
if (!planetsObj || typeof planetsObj !== "object") {
|
||
return map;
|
||
}
|
||
|
||
Object.values(planetsObj).forEach((planet) => {
|
||
if (!planet?.id) {
|
||
return;
|
||
}
|
||
map.set(planet.id, planet);
|
||
});
|
||
|
||
return map;
|
||
}
|
||
|
||
function buildSignsMap(signs) {
|
||
const map = new Map();
|
||
if (!Array.isArray(signs)) {
|
||
return map;
|
||
}
|
||
|
||
signs.forEach((sign) => {
|
||
if (!sign?.id) {
|
||
return;
|
||
}
|
||
map.set(sign.id, sign);
|
||
});
|
||
|
||
return map;
|
||
}
|
||
|
||
function buildGodsMap(magickDataset) {
|
||
const gods = magickDataset?.grouped?.gods?.gods;
|
||
const map = new Map();
|
||
|
||
if (!Array.isArray(gods)) {
|
||
return map;
|
||
}
|
||
|
||
gods.forEach((god) => {
|
||
if (!god?.id) {
|
||
return;
|
||
}
|
||
map.set(god.id, god);
|
||
});
|
||
|
||
return map;
|
||
}
|
||
|
||
function findGodIdByName(name) {
|
||
if (!name || !state.godsById) return null;
|
||
const normalized = String(name).trim().toLowerCase().replace(/^the\s+/, "");
|
||
for (const [id, god] of state.godsById) {
|
||
const godName = String(god.name || "").trim().toLowerCase().replace(/^the\s+/, "");
|
||
if (godName === normalized || id.toLowerCase() === normalized) return id;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function buildHebrewMap(magickDataset) {
|
||
const map = new Map();
|
||
const letters = magickDataset?.grouped?.alphabets?.hebrew;
|
||
if (!Array.isArray(letters)) {
|
||
return map;
|
||
}
|
||
|
||
letters.forEach((letter) => {
|
||
if (!letter?.hebrewLetterId) {
|
||
return;
|
||
}
|
||
map.set(letter.hebrewLetterId, letter);
|
||
});
|
||
|
||
return map;
|
||
}
|
||
|
||
function sortMonths(months) {
|
||
return [...months].sort((left, right) => Number(left?.order || 0) - Number(right?.order || 0));
|
||
}
|
||
|
||
function getSelectedMonth() {
|
||
return state.months.find((month) => month.id === state.selectedMonthId) || null;
|
||
}
|
||
|
||
function getDaysInMonth(year, monthOrder) {
|
||
if (!Number.isFinite(year) || !Number.isFinite(monthOrder)) {
|
||
return null;
|
||
}
|
||
return new Date(year, monthOrder, 0).getDate();
|
||
}
|
||
|
||
function getMonthStartWeekday(year, monthOrder) {
|
||
const date = new Date(year, monthOrder - 1, 1);
|
||
return date.toLocaleDateString(undefined, { weekday: "long" });
|
||
}
|
||
|
||
function parseMonthRange(month) {
|
||
const startText = normalizeText(month?.start);
|
||
const endText = normalizeText(month?.end);
|
||
if (!startText || !endText) {
|
||
return "--";
|
||
}
|
||
return `${startText} to ${endText}`;
|
||
}
|
||
|
||
function getGregorianMonthOrderFromId(monthId) {
|
||
if (!monthId) {
|
||
return null;
|
||
}
|
||
const key = String(monthId).trim().toLowerCase();
|
||
const value = GREGORIAN_MONTH_ID_TO_ORDER[key];
|
||
return Number.isFinite(value) ? value : null;
|
||
}
|
||
|
||
function normalizeCalendarText(value) {
|
||
return String(value || "")
|
||
.normalize("NFKD")
|
||
.replace(/[\u0300-\u036f]/g, "")
|
||
.replace(/['`´ʻ’]/g, "")
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9]+/g, " ")
|
||
.trim();
|
||
}
|
||
|
||
function readNumericPart(parts, partType) {
|
||
const raw = parts.find((part) => part.type === partType)?.value;
|
||
if (!raw) {
|
||
return null;
|
||
}
|
||
|
||
const digits = String(raw).replace(/[^0-9]/g, "");
|
||
if (!digits) {
|
||
return null;
|
||
}
|
||
|
||
const parsed = Number(digits);
|
||
return Number.isFinite(parsed) ? parsed : null;
|
||
}
|
||
|
||
function formatGregorianReferenceDate(date) {
|
||
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
||
return "--";
|
||
}
|
||
|
||
return date.toLocaleDateString(undefined, {
|
||
weekday: "long",
|
||
year: "numeric",
|
||
month: "long",
|
||
day: "numeric"
|
||
});
|
||
}
|
||
|
||
function formatCalendarDateFromGregorian(date, calendarId) {
|
||
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
||
return "--";
|
||
}
|
||
|
||
const locale = calendarId === "hebrew"
|
||
? "en-u-ca-hebrew"
|
||
: (calendarId === "islamic" ? "en-u-ca-islamic" : "en");
|
||
|
||
return new Intl.DateTimeFormat(locale, {
|
||
weekday: "long",
|
||
year: "numeric",
|
||
month: "long",
|
||
day: "numeric"
|
||
}).format(date);
|
||
}
|
||
|
||
function getGregorianMonthStartDate(monthOrder, year = state.selectedYear) {
|
||
if (!Number.isFinite(monthOrder) || !Number.isFinite(year)) {
|
||
return null;
|
||
}
|
||
|
||
return new Date(Math.trunc(year), Math.trunc(monthOrder) - 1, 1, 12, 0, 0, 0);
|
||
}
|
||
|
||
function getHebrewMonthAliases(month) {
|
||
const aliases = [];
|
||
const idAliases = HEBREW_MONTH_ALIAS_BY_ID[String(month?.id || "").toLowerCase()] || [];
|
||
aliases.push(...idAliases);
|
||
|
||
const nameAlias = normalizeCalendarText(month?.name);
|
||
if (nameAlias) {
|
||
aliases.push(nameAlias);
|
||
}
|
||
|
||
return Array.from(new Set(aliases.map((alias) => normalizeCalendarText(alias)).filter(Boolean)));
|
||
}
|
||
|
||
function findHebrewMonthStartInGregorianYear(month, year) {
|
||
const aliases = getHebrewMonthAliases(month);
|
||
if (!aliases.length || !Number.isFinite(year)) {
|
||
return null;
|
||
}
|
||
|
||
const formatter = new Intl.DateTimeFormat("en-u-ca-hebrew", {
|
||
day: "numeric",
|
||
month: "long",
|
||
year: "numeric"
|
||
});
|
||
|
||
const cursor = new Date(Math.trunc(year), 0, 1, 12, 0, 0, 0);
|
||
const end = new Date(Math.trunc(year), 11, 31, 12, 0, 0, 0);
|
||
|
||
while (cursor.getTime() <= end.getTime()) {
|
||
const parts = formatter.formatToParts(cursor);
|
||
const day = readNumericPart(parts, "day");
|
||
const monthName = normalizeCalendarText(parts.find((part) => part.type === "month")?.value);
|
||
if (day === 1 && aliases.includes(monthName)) {
|
||
return new Date(cursor);
|
||
}
|
||
cursor.setDate(cursor.getDate() + 1);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function findIslamicMonthStartInGregorianYear(month, year) {
|
||
const targetOrder = Number(month?.order);
|
||
if (!Number.isFinite(targetOrder) || !Number.isFinite(year)) {
|
||
return null;
|
||
}
|
||
|
||
const formatter = new Intl.DateTimeFormat("en-u-ca-islamic", {
|
||
day: "numeric",
|
||
month: "numeric",
|
||
year: "numeric"
|
||
});
|
||
|
||
const cursor = new Date(Math.trunc(year), 0, 1, 12, 0, 0, 0);
|
||
const end = new Date(Math.trunc(year), 11, 31, 12, 0, 0, 0);
|
||
|
||
while (cursor.getTime() <= end.getTime()) {
|
||
const parts = formatter.formatToParts(cursor);
|
||
const day = readNumericPart(parts, "day");
|
||
const monthNo = readNumericPart(parts, "month");
|
||
if (day === 1 && monthNo === Math.trunc(targetOrder)) {
|
||
return new Date(cursor);
|
||
}
|
||
cursor.setDate(cursor.getDate() + 1);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function parseFirstMonthDayFromText(dateText) {
|
||
const text = String(dateText || "").replace(/~/g, " ");
|
||
const firstSegment = text.split("/")[0] || text;
|
||
const match = firstSegment.match(/(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2})/i);
|
||
if (!match) {
|
||
return null;
|
||
}
|
||
|
||
const monthIndex = MONTH_NAME_TO_INDEX[String(match[1]).toLowerCase()];
|
||
const day = Number(match[2]);
|
||
if (!Number.isFinite(monthIndex) || !Number.isFinite(day)) {
|
||
return null;
|
||
}
|
||
|
||
return { monthIndex, day };
|
||
}
|
||
|
||
function parseMonthDayStartToken(token) {
|
||
const match = String(token || "").match(/(\d{2})-(\d{2})/);
|
||
if (!match) {
|
||
return null;
|
||
}
|
||
|
||
const month = Number(match[1]);
|
||
const day = Number(match[2]);
|
||
if (!Number.isFinite(month) || !Number.isFinite(day)) {
|
||
return null;
|
||
}
|
||
|
||
return { month, day };
|
||
}
|
||
|
||
function createDateAtNoon(year, monthIndex, dayOfMonth) {
|
||
return new Date(Math.trunc(year), monthIndex, Math.trunc(dayOfMonth), 12, 0, 0, 0);
|
||
}
|
||
|
||
function computeWesternEasterDate(year) {
|
||
const y = Math.trunc(Number(year));
|
||
if (!Number.isFinite(y)) {
|
||
return null;
|
||
}
|
||
|
||
// Meeus/Jones/Butcher Gregorian algorithm.
|
||
const a = y % 19;
|
||
const b = Math.floor(y / 100);
|
||
const c = y % 100;
|
||
const d = Math.floor(b / 4);
|
||
const e = b % 4;
|
||
const f = Math.floor((b + 8) / 25);
|
||
const g = Math.floor((b - f + 1) / 3);
|
||
const h = (19 * a + b - d - g + 15) % 30;
|
||
const i = Math.floor(c / 4);
|
||
const k = c % 4;
|
||
const l = (32 + 2 * e + 2 * i - h - k) % 7;
|
||
const m = Math.floor((a + 11 * h + 22 * l) / 451);
|
||
const month = Math.floor((h + l - 7 * m + 114) / 31);
|
||
const day = ((h + l - 7 * m + 114) % 31) + 1;
|
||
return createDateAtNoon(y, month - 1, day);
|
||
}
|
||
|
||
function computeNthWeekdayOfMonth(year, monthIndex, weekday, ordinal) {
|
||
const y = Math.trunc(Number(year));
|
||
if (!Number.isFinite(y)) {
|
||
return null;
|
||
}
|
||
|
||
const first = createDateAtNoon(y, monthIndex, 1);
|
||
const firstWeekday = first.getDay();
|
||
const offset = (weekday - firstWeekday + 7) % 7;
|
||
const dayOfMonth = 1 + offset + (Math.trunc(ordinal) - 1) * 7;
|
||
const daysInMonth = new Date(y, monthIndex + 1, 0).getDate();
|
||
if (dayOfMonth > daysInMonth) {
|
||
return null;
|
||
}
|
||
return createDateAtNoon(y, monthIndex, dayOfMonth);
|
||
}
|
||
|
||
function resolveGregorianDateRule(rule) {
|
||
const key = String(rule || "").trim().toLowerCase();
|
||
if (!key) {
|
||
return null;
|
||
}
|
||
|
||
if (key === "gregorian-easter-sunday") {
|
||
return computeWesternEasterDate(state.selectedYear);
|
||
}
|
||
|
||
if (key === "gregorian-good-friday") {
|
||
const easter = computeWesternEasterDate(state.selectedYear);
|
||
if (!(easter instanceof Date) || Number.isNaN(easter.getTime())) {
|
||
return null;
|
||
}
|
||
return createDateAtNoon(easter.getFullYear(), easter.getMonth(), easter.getDate() - 2);
|
||
}
|
||
|
||
if (key === "gregorian-thanksgiving-us") {
|
||
// US Thanksgiving: 4th Thursday of November.
|
||
return computeNthWeekdayOfMonth(state.selectedYear, 10, 4, 4);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function findHebrewMonthDayInGregorianYear(monthId, day, year) {
|
||
const aliases = HEBREW_MONTH_ALIAS_BY_ID[String(monthId || "").toLowerCase()] || [];
|
||
const targetDay = Number(day);
|
||
if (!aliases.length || !Number.isFinite(targetDay) || !Number.isFinite(year)) {
|
||
return null;
|
||
}
|
||
|
||
const normalizedAliases = aliases.map((alias) => normalizeCalendarText(alias)).filter(Boolean);
|
||
const formatter = new Intl.DateTimeFormat("en-u-ca-hebrew", {
|
||
day: "numeric",
|
||
month: "long",
|
||
year: "numeric"
|
||
});
|
||
|
||
const cursor = new Date(Math.trunc(year), 0, 1, 12, 0, 0, 0);
|
||
const end = new Date(Math.trunc(year), 11, 31, 12, 0, 0, 0);
|
||
|
||
while (cursor.getTime() <= end.getTime()) {
|
||
const parts = formatter.formatToParts(cursor);
|
||
const currentDay = readNumericPart(parts, "day");
|
||
const monthName = normalizeCalendarText(parts.find((part) => part.type === "month")?.value);
|
||
if (currentDay === Math.trunc(targetDay) && normalizedAliases.includes(monthName)) {
|
||
return new Date(cursor);
|
||
}
|
||
cursor.setDate(cursor.getDate() + 1);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function getIslamicMonthOrderById(monthId) {
|
||
const month = (state.calendarData?.islamic || []).find((item) => item?.id === monthId);
|
||
const order = Number(month?.order);
|
||
return Number.isFinite(order) ? Math.trunc(order) : null;
|
||
}
|
||
|
||
function findIslamicMonthDayInGregorianYear(monthId, day, year) {
|
||
const monthOrder = getIslamicMonthOrderById(monthId);
|
||
const targetDay = Number(day);
|
||
if (!Number.isFinite(monthOrder) || !Number.isFinite(targetDay) || !Number.isFinite(year)) {
|
||
return null;
|
||
}
|
||
|
||
const formatter = new Intl.DateTimeFormat("en-u-ca-islamic", {
|
||
day: "numeric",
|
||
month: "numeric",
|
||
year: "numeric"
|
||
});
|
||
|
||
const cursor = new Date(Math.trunc(year), 0, 1, 12, 0, 0, 0);
|
||
const end = new Date(Math.trunc(year), 11, 31, 12, 0, 0, 0);
|
||
|
||
while (cursor.getTime() <= end.getTime()) {
|
||
const parts = formatter.formatToParts(cursor);
|
||
const currentDay = readNumericPart(parts, "day");
|
||
const currentMonth = readNumericPart(parts, "month");
|
||
if (currentDay === Math.trunc(targetDay) && currentMonth === monthOrder) {
|
||
return new Date(cursor);
|
||
}
|
||
cursor.setDate(cursor.getDate() + 1);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function resolveHolidayGregorianDate(holiday) {
|
||
if (!holiday || typeof holiday !== "object") {
|
||
return null;
|
||
}
|
||
|
||
const calendarId = String(holiday.calendarId || "").trim().toLowerCase();
|
||
const monthId = String(holiday.monthId || "").trim().toLowerCase();
|
||
const day = Number(holiday.day);
|
||
|
||
if (calendarId === "gregorian") {
|
||
if (holiday?.dateRule) {
|
||
const ruledDate = resolveGregorianDateRule(holiday.dateRule);
|
||
if (ruledDate) {
|
||
return ruledDate;
|
||
}
|
||
}
|
||
|
||
const monthDay = parseMonthDayStartToken(holiday.monthDayStart) || parseMonthDayStartToken(holiday.dateText);
|
||
if (monthDay) {
|
||
return new Date(state.selectedYear, monthDay.month - 1, monthDay.day, 12, 0, 0, 0);
|
||
}
|
||
const order = getGregorianMonthOrderFromId(monthId);
|
||
if (Number.isFinite(order) && Number.isFinite(day)) {
|
||
return new Date(state.selectedYear, order - 1, Math.trunc(day), 12, 0, 0, 0);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
if (calendarId === "hebrew") {
|
||
return findHebrewMonthDayInGregorianYear(monthId, day, state.selectedYear);
|
||
}
|
||
|
||
if (calendarId === "islamic") {
|
||
return findIslamicMonthDayInGregorianYear(monthId, day, state.selectedYear);
|
||
}
|
||
|
||
if (calendarId === "wheel-of-year") {
|
||
const monthDay = parseMonthDayStartToken(holiday.monthDayStart) || parseFirstMonthDayFromText(holiday.dateText);
|
||
if (monthDay?.month && monthDay?.day) {
|
||
return new Date(state.selectedYear, monthDay.month - 1, monthDay.day, 12, 0, 0, 0);
|
||
}
|
||
if (monthDay?.monthIndex != null && monthDay?.day) {
|
||
return new Date(state.selectedYear, monthDay.monthIndex, monthDay.day, 12, 0, 0, 0);
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function findWheelMonthStartInGregorianYear(month, year) {
|
||
const parsed = parseFirstMonthDayFromText(month?.date);
|
||
if (!parsed || !Number.isFinite(year)) {
|
||
return null;
|
||
}
|
||
|
||
return new Date(Math.trunc(year), parsed.monthIndex, parsed.day, 12, 0, 0, 0);
|
||
}
|
||
|
||
function getGregorianReferenceDateForCalendarMonth(month) {
|
||
const calId = state.selectedCalendar;
|
||
if (calId === "gregorian") {
|
||
return getGregorianMonthStartDate(Number(month?.order));
|
||
}
|
||
if (calId === "hebrew") {
|
||
return findHebrewMonthStartInGregorianYear(month, state.selectedYear);
|
||
}
|
||
if (calId === "islamic") {
|
||
return findIslamicMonthStartInGregorianYear(month, state.selectedYear);
|
||
}
|
||
if (calId === "wheel-of-year") {
|
||
return findWheelMonthStartInGregorianYear(month, state.selectedYear);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function getMonthSubtitle(month) {
|
||
const calId = state.selectedCalendar;
|
||
if (calId === "hebrew" || calId === "islamic") {
|
||
const native = month.nativeName ? ` · ${month.nativeName}` : "";
|
||
const days = month.days ? ` · ${month.days} days` : "";
|
||
return `${month.season || ""}${native}${days}`;
|
||
}
|
||
if (calId === "wheel-of-year") {
|
||
return [month.date, month.type, month.season].filter(Boolean).join(" · ");
|
||
}
|
||
return parseMonthRange(month);
|
||
}
|
||
|
||
function formatIsoDate(date) {
|
||
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
||
return "";
|
||
}
|
||
|
||
const year = date.getFullYear();
|
||
const month = `${date.getMonth() + 1}`.padStart(2, "0");
|
||
const day = `${date.getDate()}`.padStart(2, "0");
|
||
return `${year}-${month}-${day}`;
|
||
}
|
||
|
||
function resolveCalendarDayToGregorian(month, dayNumber) {
|
||
const calId = state.selectedCalendar;
|
||
const day = Math.trunc(Number(dayNumber));
|
||
if (!Number.isFinite(day) || day <= 0) {
|
||
return null;
|
||
}
|
||
|
||
if (calId === "gregorian") {
|
||
const monthOrder = Number(month?.order);
|
||
if (!Number.isFinite(monthOrder)) {
|
||
return null;
|
||
}
|
||
return new Date(state.selectedYear, monthOrder - 1, day, 12, 0, 0, 0);
|
||
}
|
||
|
||
if (calId === "hebrew") {
|
||
return findHebrewMonthDayInGregorianYear(month?.id, day, state.selectedYear);
|
||
}
|
||
|
||
if (calId === "islamic") {
|
||
return findIslamicMonthDayInGregorianYear(month?.id, day, state.selectedYear);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function getMonthDayLinkRows(month) {
|
||
const cacheKey = `${state.selectedCalendar}|${state.selectedYear}|${month?.id || ""}`;
|
||
if (state.dayLinksCache.has(cacheKey)) {
|
||
return state.dayLinksCache.get(cacheKey);
|
||
}
|
||
|
||
let dayCount = null;
|
||
if (state.selectedCalendar === "gregorian") {
|
||
dayCount = getDaysInMonth(state.selectedYear, Number(month?.order));
|
||
} else if (state.selectedCalendar === "hebrew" || state.selectedCalendar === "islamic") {
|
||
const baseDays = Number(month?.days);
|
||
const variantDays = Number(month?.daysVariant);
|
||
if (Number.isFinite(baseDays) && Number.isFinite(variantDays)) {
|
||
dayCount = Math.max(Math.trunc(baseDays), Math.trunc(variantDays));
|
||
} else if (Number.isFinite(baseDays)) {
|
||
dayCount = Math.trunc(baseDays);
|
||
} else if (Number.isFinite(variantDays)) {
|
||
dayCount = Math.trunc(variantDays);
|
||
}
|
||
}
|
||
|
||
if (!Number.isFinite(dayCount) || dayCount <= 0) {
|
||
state.dayLinksCache.set(cacheKey, []);
|
||
return [];
|
||
}
|
||
|
||
const rows = [];
|
||
for (let day = 1; day <= dayCount; day += 1) {
|
||
const gregorianDate = resolveCalendarDayToGregorian(month, day);
|
||
rows.push({
|
||
day,
|
||
gregorianDate: formatIsoDate(gregorianDate),
|
||
isResolved: Boolean(gregorianDate && !Number.isNaN(gregorianDate.getTime()))
|
||
});
|
||
}
|
||
|
||
state.dayLinksCache.set(cacheKey, rows);
|
||
return rows;
|
||
}
|
||
|
||
function renderDayLinksCard(month) {
|
||
const rows = getMonthDayLinkRows(month);
|
||
if (!rows.length) {
|
||
return "";
|
||
}
|
||
|
||
const selectedContext = getSelectedDayFilterContext(month);
|
||
const selectedDaySet = selectedContext?.dayNumbers || new Set();
|
||
const selectedDays = selectedContext?.entries?.map((entry) => entry.dayNumber) || [];
|
||
const selectedSummary = selectedDays.length
|
||
? selectedDays.join(", ")
|
||
: "";
|
||
|
||
const links = rows.map((row) => {
|
||
if (!row.isResolved) {
|
||
return `<span class="planet-list-meta">${row.day}</span>`;
|
||
}
|
||
|
||
const isSelected = selectedDaySet.has(Number(row.day));
|
||
return `<button class="alpha-nav-btn${isSelected ? " is-selected" : ""}" data-nav="calendar-day" data-day-number="${row.day}" data-gregorian-date="${row.gregorianDate}" aria-pressed="${isSelected ? "true" : "false"}" title="Filter this month by day ${row.day}">${row.day}</button>`;
|
||
}).join("");
|
||
|
||
const clearButton = selectedContext
|
||
? `<button class="alpha-nav-btn" data-nav="calendar-day-clear" type="button">Show All Days</button>`
|
||
: "";
|
||
|
||
const helperText = selectedContext
|
||
? `<div class="planet-list-meta">Filtered to days: ${selectedSummary}</div>`
|
||
: "";
|
||
|
||
return `
|
||
<div class="planet-meta-card">
|
||
<strong>Day Links</strong>
|
||
<div class="planet-text">Filter this month to events, holidays, and data connected to a specific day.</div>
|
||
${helperText}
|
||
<div class="alpha-nav-btns">${links}</div>
|
||
${clearButton ? `<div class="alpha-nav-btns">${clearButton}</div>` : ""}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderList(elements) {
|
||
const { monthListEl, monthCountEl, listTitleEl } = elements;
|
||
if (!monthListEl) {
|
||
return;
|
||
}
|
||
|
||
monthListEl.innerHTML = "";
|
||
|
||
state.filteredMonths.forEach((month) => {
|
||
const isSelected = month.id === state.selectedMonthId;
|
||
const itemEl = document.createElement("div");
|
||
itemEl.className = `planet-list-item${isSelected ? " is-selected" : ""}`;
|
||
itemEl.setAttribute("role", "option");
|
||
itemEl.setAttribute("aria-selected", isSelected ? "true" : "false");
|
||
itemEl.dataset.monthId = month.id;
|
||
|
||
itemEl.innerHTML = `
|
||
<div class="planet-list-name">${month.name || month.id}</div>
|
||
<div class="planet-list-meta">${getMonthSubtitle(month)}</div>
|
||
`;
|
||
|
||
itemEl.addEventListener("click", () => {
|
||
selectByMonthId(month.id, elements);
|
||
});
|
||
|
||
monthListEl.appendChild(itemEl);
|
||
});
|
||
|
||
if (monthCountEl) {
|
||
monthCountEl.textContent = state.searchQuery
|
||
? `${state.filteredMonths.length} of ${state.months.length} months`
|
||
: `${state.months.length} months`;
|
||
}
|
||
|
||
if (listTitleEl) {
|
||
listTitleEl.textContent = "Calendar > Months";
|
||
}
|
||
}
|
||
|
||
function planetLabel(planetId) {
|
||
if (!planetId) {
|
||
return "Planet";
|
||
}
|
||
|
||
const planet = state.planetsById.get(planetId);
|
||
if (!planet) {
|
||
return cap(planetId);
|
||
}
|
||
|
||
return `${planet.symbol || ""} ${planet.name || cap(planetId)}`.trim();
|
||
}
|
||
|
||
function zodiacLabel(signId) {
|
||
if (!signId) {
|
||
return "Zodiac";
|
||
}
|
||
|
||
const sign = state.signsById.get(signId);
|
||
if (!sign) {
|
||
return cap(signId);
|
||
}
|
||
|
||
return `${sign.symbol || ""} ${sign.name || cap(signId)}`.trim();
|
||
}
|
||
|
||
function godLabel(godId, godName) {
|
||
if (godName) {
|
||
return godName;
|
||
}
|
||
|
||
if (!godId) {
|
||
return "Deity";
|
||
}
|
||
|
||
const god = state.godsById.get(godId);
|
||
return god?.name || cap(godId);
|
||
}
|
||
|
||
function hebrewLabel(hebrewLetterId) {
|
||
if (!hebrewLetterId) {
|
||
return "Hebrew Letter";
|
||
}
|
||
|
||
const letter = state.hebrewById.get(hebrewLetterId);
|
||
if (!letter) {
|
||
return cap(hebrewLetterId);
|
||
}
|
||
|
||
return `${letter.char || ""} ${letter.name || cap(hebrewLetterId)}`.trim();
|
||
}
|
||
|
||
function computeDigitalRoot(value) {
|
||
let current = Math.abs(Math.trunc(Number(value)));
|
||
if (!Number.isFinite(current)) {
|
||
return null;
|
||
}
|
||
|
||
while (current >= 10) {
|
||
current = String(current)
|
||
.split("")
|
||
.reduce((sum, digit) => sum + Number(digit), 0);
|
||
}
|
||
|
||
return current;
|
||
}
|
||
|
||
function buildAssociationButtons(associations) {
|
||
if (!associations || typeof associations !== "object") {
|
||
return "<div class=\"planet-text\">--</div>";
|
||
}
|
||
|
||
const buttons = [];
|
||
|
||
if (associations.planetId) {
|
||
buttons.push(
|
||
`<button class="alpha-nav-btn" data-nav="planet" data-planet-id="${associations.planetId}">${planetLabel(associations.planetId)} ↗</button>`
|
||
);
|
||
}
|
||
|
||
if (associations.zodiacSignId) {
|
||
buttons.push(
|
||
`<button class="alpha-nav-btn" data-nav="zodiac" data-sign-id="${associations.zodiacSignId}">${zodiacLabel(associations.zodiacSignId)} ↗</button>`
|
||
);
|
||
}
|
||
|
||
if (Number.isFinite(Number(associations.numberValue))) {
|
||
const rawNumber = Math.trunc(Number(associations.numberValue));
|
||
if (rawNumber >= 0) {
|
||
const numberValue = computeDigitalRoot(rawNumber);
|
||
if (numberValue != null) {
|
||
const label = rawNumber === numberValue
|
||
? `Number ${numberValue}`
|
||
: `Number ${numberValue} (from ${rawNumber})`;
|
||
buttons.push(
|
||
`<button class="alpha-nav-btn" data-nav="number" data-number-value="${numberValue}">${label} ↗</button>`
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (associations.tarotCard) {
|
||
const trumpNumber = resolveTarotTrumpNumber(associations.tarotCard);
|
||
const explicitTrumpNumber = Number(associations.tarotTrumpNumber);
|
||
const tarotTrumpNumber = Number.isFinite(explicitTrumpNumber) ? explicitTrumpNumber : trumpNumber;
|
||
const tarotLabel = getDisplayTarotName(associations.tarotCard, tarotTrumpNumber);
|
||
buttons.push(
|
||
`<button class="alpha-nav-btn" data-nav="tarot-card" data-card-name="${associations.tarotCard}" data-trump-number="${tarotTrumpNumber ?? ""}">${tarotLabel} ↗</button>`
|
||
);
|
||
}
|
||
|
||
if (associations.godId || associations.godName) {
|
||
const label = godLabel(associations.godId, associations.godName);
|
||
buttons.push(
|
||
`<button class="alpha-nav-btn" data-nav="god" data-god-id="${associations.godId || ""}" data-god-name="${associations.godName || label}">${label} ↗</button>`
|
||
);
|
||
}
|
||
|
||
if (associations.hebrewLetterId) {
|
||
buttons.push(
|
||
`<button class="alpha-nav-btn" data-nav="alphabet" data-alphabet="hebrew" data-hebrew-letter-id="${associations.hebrewLetterId}">${hebrewLabel(associations.hebrewLetterId)} ↗</button>`
|
||
);
|
||
}
|
||
|
||
if (associations.kabbalahPathNumber != null) {
|
||
buttons.push(
|
||
`<button class="alpha-nav-btn" data-nav="kabbalah" data-path-no="${associations.kabbalahPathNumber}">Path ${associations.kabbalahPathNumber} ↗</button>`
|
||
);
|
||
}
|
||
|
||
if (associations.iChingPlanetaryInfluence) {
|
||
buttons.push(
|
||
`<button class="alpha-nav-btn" data-nav="iching" data-planetary-influence="${associations.iChingPlanetaryInfluence}">I Ching · ${associations.iChingPlanetaryInfluence} ↗</button>`
|
||
);
|
||
}
|
||
|
||
if (!buttons.length) {
|
||
return "<div class=\"planet-text\">--</div>";
|
||
}
|
||
|
||
return `<div class="alpha-nav-btns">${buttons.join("")}</div>`;
|
||
}
|
||
|
||
function associationSearchText(associations) {
|
||
if (!associations || typeof associations !== "object") {
|
||
return "";
|
||
}
|
||
|
||
const tarotAliases = associations.tarotCard && typeof getTarotCardSearchAliases === "function"
|
||
? getTarotCardSearchAliases(associations.tarotCard, { trumpNumber: associations.tarotTrumpNumber })
|
||
: [];
|
||
|
||
return [
|
||
associations.planetId,
|
||
associations.zodiacSignId,
|
||
associations.numberValue,
|
||
associations.tarotCard,
|
||
associations.tarotTrumpNumber,
|
||
...tarotAliases,
|
||
associations.godId,
|
||
associations.godName,
|
||
associations.hebrewLetterId,
|
||
associations.kabbalahPathNumber,
|
||
associations.iChingPlanetaryInfluence
|
||
].filter(Boolean).join(" ");
|
||
}
|
||
|
||
function eventSearchText(event) {
|
||
return normalizeSearchValue([
|
||
event?.name,
|
||
event?.date,
|
||
event?.dateRange,
|
||
event?.description,
|
||
associationSearchText(event?.associations)
|
||
].filter(Boolean).join(" "));
|
||
}
|
||
|
||
function holidaySearchText(holiday) {
|
||
return normalizeSearchValue([
|
||
holiday?.name,
|
||
holiday?.kind,
|
||
holiday?.date,
|
||
holiday?.dateRange,
|
||
holiday?.dateText,
|
||
holiday?.monthDayStart,
|
||
holiday?.calendarId,
|
||
holiday?.description,
|
||
associationSearchText(holiday?.associations)
|
||
].filter(Boolean).join(" "));
|
||
}
|
||
|
||
function buildHolidayList(month) {
|
||
const calendarId = state.selectedCalendar;
|
||
const monthOrder = Number(month?.order);
|
||
|
||
const fromRepo = state.calendarHolidays.filter((holiday) => {
|
||
const holidayCalendarId = String(holiday?.calendarId || "").trim().toLowerCase();
|
||
if (holidayCalendarId !== calendarId) {
|
||
return false;
|
||
}
|
||
|
||
const isDirectMonthMatch = String(holiday?.monthId || "").trim().toLowerCase() === String(month?.id || "").trim().toLowerCase();
|
||
if (isDirectMonthMatch) {
|
||
return true;
|
||
}
|
||
|
||
// For movable Gregorian holidays, place the holiday under the computed month for the selected year.
|
||
if (calendarId === "gregorian" && holiday?.dateRule && Number.isFinite(monthOrder)) {
|
||
const computedDate = resolveHolidayGregorianDate(holiday);
|
||
return computedDate instanceof Date
|
||
&& !Number.isNaN(computedDate.getTime())
|
||
&& (computedDate.getMonth() + 1) === Math.trunc(monthOrder);
|
||
}
|
||
|
||
return false;
|
||
});
|
||
|
||
if (fromRepo.length) {
|
||
return [...fromRepo].sort((left, right) => {
|
||
const leftDate = resolveHolidayGregorianDate(left);
|
||
const rightDate = resolveHolidayGregorianDate(right);
|
||
const leftDay = Number.isFinite(Number(left?.day))
|
||
? Number(left.day)
|
||
: ((leftDate instanceof Date && !Number.isNaN(leftDate.getTime())) ? leftDate.getDate() : NaN);
|
||
const rightDay = Number.isFinite(Number(right?.day))
|
||
? Number(right.day)
|
||
: ((rightDate instanceof Date && !Number.isNaN(rightDate.getTime())) ? rightDate.getDate() : NaN);
|
||
if (Number.isFinite(leftDay) && Number.isFinite(rightDay) && leftDay !== rightDay) {
|
||
return leftDay - rightDay;
|
||
}
|
||
return String(left?.name || "").localeCompare(String(right?.name || ""));
|
||
});
|
||
}
|
||
|
||
// Legacy fallback for old Gregorian-only holiday structure.
|
||
const seen = new Set();
|
||
const ordered = [];
|
||
|
||
(month.holidayIds || []).forEach((holidayId) => {
|
||
const holiday = state.holidays.find((item) => item.id === holidayId);
|
||
if (!holiday || seen.has(holiday.id)) {
|
||
return;
|
||
}
|
||
seen.add(holiday.id);
|
||
ordered.push(holiday);
|
||
});
|
||
|
||
state.holidays.forEach((holiday) => {
|
||
if (holiday?.monthId !== month.id || seen.has(holiday.id)) {
|
||
return;
|
||
}
|
||
seen.add(holiday.id);
|
||
ordered.push(holiday);
|
||
});
|
||
|
||
return ordered;
|
||
}
|
||
|
||
function buildMonthSearchText(month) {
|
||
const calId = state.selectedCalendar;
|
||
const monthHolidays = buildHolidayList(month);
|
||
const holidayText = monthHolidays.map((holiday) => holidaySearchText(holiday)).join(" ");
|
||
|
||
if (calId === "gregorian") {
|
||
const events = Array.isArray(month?.events) ? month.events : [];
|
||
|
||
const searchable = [
|
||
month?.name,
|
||
month?.id,
|
||
month?.start,
|
||
month?.end,
|
||
month?.coreTheme,
|
||
month?.seasonNorth,
|
||
month?.seasonSouth,
|
||
associationSearchText(month?.associations),
|
||
events.map((event) => eventSearchText(event)).join(" "),
|
||
holidayText
|
||
];
|
||
|
||
return normalizeSearchValue(searchable.filter(Boolean).join(" "));
|
||
}
|
||
|
||
const wheelAssocText = month?.associations
|
||
? [
|
||
Array.isArray(month.associations.themes) ? month.associations.themes.join(" ") : "",
|
||
Array.isArray(month.associations.deities) ? month.associations.deities.join(" ") : "",
|
||
month.associations.element,
|
||
month.associations.direction
|
||
].filter(Boolean).join(" ")
|
||
: "";
|
||
|
||
const searchable = [
|
||
month?.name,
|
||
month?.id,
|
||
month?.nativeName,
|
||
month?.meaning,
|
||
month?.season,
|
||
month?.description,
|
||
month?.zodiacSign,
|
||
month?.tribe,
|
||
month?.element,
|
||
month?.type,
|
||
month?.date,
|
||
month?.hebrewLetter,
|
||
holidayText,
|
||
wheelAssocText
|
||
];
|
||
|
||
return normalizeSearchValue(searchable.filter(Boolean).join(" "));
|
||
}
|
||
|
||
function matchesSearch(searchText) {
|
||
if (!state.searchQuery) {
|
||
return true;
|
||
}
|
||
return searchText.includes(state.searchQuery);
|
||
}
|
||
|
||
function syncSearchControls(elements) {
|
||
if (elements.searchInputEl) {
|
||
elements.searchInputEl.value = state.searchQuery;
|
||
}
|
||
if (elements.searchClearEl) {
|
||
elements.searchClearEl.disabled = !state.searchQuery;
|
||
}
|
||
}
|
||
|
||
function applySearchFilter(elements) {
|
||
state.filteredMonths = state.searchQuery
|
||
? state.months.filter((month) => matchesSearch(buildMonthSearchText(month)))
|
||
: [...state.months];
|
||
|
||
if (!state.filteredMonths.some((month) => month.id === state.selectedMonthId)) {
|
||
state.selectedMonthId = state.filteredMonths[0]?.id || null;
|
||
}
|
||
|
||
syncSearchControls(elements);
|
||
renderList(elements);
|
||
renderDetail(elements);
|
||
}
|
||
|
||
function renderFactsCard(month) {
|
||
const monthOrder = Number(month?.order);
|
||
const daysInMonth = getDaysInMonth(state.selectedYear, monthOrder);
|
||
const hoursInMonth = Number.isFinite(daysInMonth) ? daysInMonth * 24 : null;
|
||
const firstWeekday = Number.isFinite(monthOrder)
|
||
? getMonthStartWeekday(state.selectedYear, monthOrder)
|
||
: "--";
|
||
const gregorianStartDate = getGregorianMonthStartDate(monthOrder);
|
||
const hebrewStartReference = formatCalendarDateFromGregorian(gregorianStartDate, "hebrew");
|
||
const islamicStartReference = formatCalendarDateFromGregorian(gregorianStartDate, "islamic");
|
||
|
||
return `
|
||
<div class="planet-meta-card">
|
||
<strong>Month Facts</strong>
|
||
<div class="planet-text">
|
||
<dl class="alpha-dl">
|
||
<dt>Year</dt><dd>${state.selectedYear}</dd>
|
||
<dt>Start Date (Gregorian)</dt><dd>${formatGregorianReferenceDate(gregorianStartDate)}</dd>
|
||
<dt>Days</dt><dd>${daysInMonth ?? "--"}</dd>
|
||
<dt>Hours</dt><dd>${hoursInMonth ?? "--"}</dd>
|
||
<dt>Starts On</dt><dd>${firstWeekday}</dd>
|
||
<dt>Hebrew On 1st</dt><dd>${hebrewStartReference}</dd>
|
||
<dt>Islamic On 1st</dt><dd>${islamicStartReference}</dd>
|
||
<dt>North Season</dt><dd>${month.seasonNorth || "--"}</dd>
|
||
<dt>South Season</dt><dd>${month.seasonSouth || "--"}</dd>
|
||
</dl>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderAssociationsCard(month) {
|
||
const monthOrder = Number(month?.order);
|
||
const associations = {
|
||
...(month?.associations || {}),
|
||
...(Number.isFinite(monthOrder) ? { numberValue: Math.trunc(monthOrder) } : {})
|
||
};
|
||
|
||
return `
|
||
<div class="planet-meta-card">
|
||
<strong>Associations</strong>
|
||
<div class="planet-text">${month.coreTheme || "--"}</div>
|
||
${buildAssociationButtons(associations)}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderEventsCard(month) {
|
||
const allEvents = Array.isArray(month?.events) ? month.events : [];
|
||
if (!allEvents.length) {
|
||
return `
|
||
<div class="planet-meta-card">
|
||
<strong>Monthly Events</strong>
|
||
<div class="planet-text">No monthly events listed.</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
const selectedDay = getSelectedDayFilterContext(month);
|
||
|
||
function eventMatchesDay(event) {
|
||
if (!selectedDay) {
|
||
return true;
|
||
}
|
||
|
||
return selectedDay.entries.some((entry) => {
|
||
const targetDate = entry.gregorianDate;
|
||
const targetMonth = targetDate?.getMonth() + 1;
|
||
const targetDayNo = targetDate?.getDate();
|
||
|
||
const explicitDate = parseMonthDayToken(event?.date);
|
||
if (explicitDate && Number.isFinite(targetMonth) && Number.isFinite(targetDayNo)) {
|
||
return explicitDate.month === targetMonth && explicitDate.day === targetDayNo;
|
||
}
|
||
|
||
const rangeTokens = parseMonthDayTokensFromText(event?.dateRange || event?.dateText || "");
|
||
if (rangeTokens.length >= 2 && Number.isFinite(targetMonth) && Number.isFinite(targetDayNo)) {
|
||
const start = rangeTokens[0];
|
||
const end = rangeTokens[1];
|
||
return isMonthDayInRange(targetMonth, targetDayNo, start.month, start.day, end.month, end.day);
|
||
}
|
||
|
||
const dayRange = parseDayRangeFromText(event?.date || event?.dateRange || event?.dateText || "");
|
||
if (dayRange) {
|
||
return entry.dayNumber >= dayRange.startDay && entry.dayNumber <= dayRange.endDay;
|
||
}
|
||
|
||
return false;
|
||
});
|
||
}
|
||
|
||
const dayFiltered = allEvents.filter((event) => eventMatchesDay(event));
|
||
const events = state.searchQuery
|
||
? dayFiltered.filter((event) => matchesSearch(eventSearchText(event)))
|
||
: dayFiltered;
|
||
|
||
if (!events.length) {
|
||
return `
|
||
<div class="planet-meta-card">
|
||
<strong>Monthly Events</strong>
|
||
<div class="planet-text">No monthly events match current search.</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
const rows = events.map((event) => {
|
||
const dateText = event?.date || event?.dateRange || "--";
|
||
return `
|
||
<div class="cal-item-row">
|
||
<div class="cal-item-head">
|
||
<span class="cal-item-name">${event?.name || "Untitled"}</span>
|
||
<span class="planet-list-meta">${dateText}</span>
|
||
</div>
|
||
<div class="planet-text">${event?.description || ""}</div>
|
||
${buildAssociationButtons(event?.associations)}
|
||
</div>
|
||
`;
|
||
}).join("");
|
||
|
||
return `
|
||
<div class="planet-meta-card">
|
||
<strong>Monthly Events</strong>
|
||
<div class="cal-item-stack">${rows}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderHolidaysCard(month, title = "Holiday Repository") {
|
||
const allHolidays = buildHolidayList(month);
|
||
if (!allHolidays.length) {
|
||
return `
|
||
<div class="planet-meta-card">
|
||
<strong>${title}</strong>
|
||
<div class="planet-text">No holidays listed in the repository for this month.</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
const selectedDay = getSelectedDayFilterContext(month);
|
||
|
||
function holidayMatchesDay(holiday) {
|
||
if (!selectedDay) {
|
||
return true;
|
||
}
|
||
|
||
return selectedDay.entries.some((entry) => {
|
||
const targetDate = entry.gregorianDate;
|
||
const targetMonth = targetDate?.getMonth() + 1;
|
||
const targetDayNo = targetDate?.getDate();
|
||
|
||
const exactResolved = resolveHolidayGregorianDate(holiday);
|
||
if (exactResolved instanceof Date && !Number.isNaN(exactResolved.getTime()) && targetDate instanceof Date) {
|
||
return formatIsoDate(exactResolved) === formatIsoDate(targetDate);
|
||
}
|
||
|
||
if (state.selectedCalendar === "gregorian" && Number.isFinite(targetMonth) && Number.isFinite(targetDayNo)) {
|
||
const tokens = parseMonthDayTokensFromText(holiday?.dateText || holiday?.dateRange || "");
|
||
if (tokens.length >= 2) {
|
||
const start = tokens[0];
|
||
const end = tokens[1];
|
||
return isMonthDayInRange(targetMonth, targetDayNo, start.month, start.day, end.month, end.day);
|
||
}
|
||
|
||
if (tokens.length === 1) {
|
||
const single = tokens[0];
|
||
return single.month === targetMonth && single.day === targetDayNo;
|
||
}
|
||
|
||
const direct = parseMonthDayStartToken(holiday?.monthDayStart || holiday?.dateText || "");
|
||
if (direct) {
|
||
return direct.month === targetMonth && direct.day === targetDayNo;
|
||
}
|
||
|
||
if (Number.isFinite(Number(holiday?.day))) {
|
||
return Number(holiday.day) === entry.dayNumber;
|
||
}
|
||
}
|
||
|
||
const localRange = parseDayRangeFromText(holiday?.dateText || holiday?.dateRange || "");
|
||
if (localRange) {
|
||
return entry.dayNumber >= localRange.startDay && entry.dayNumber <= localRange.endDay;
|
||
}
|
||
|
||
if (Number.isFinite(Number(holiday?.day))) {
|
||
return Number(holiday.day) === entry.dayNumber;
|
||
}
|
||
|
||
return false;
|
||
});
|
||
}
|
||
|
||
const dayFiltered = allHolidays.filter((holiday) => holidayMatchesDay(holiday));
|
||
const holidays = state.searchQuery
|
||
? dayFiltered.filter((holiday) => matchesSearch(holidaySearchText(holiday)))
|
||
: dayFiltered;
|
||
|
||
if (!holidays.length) {
|
||
return `
|
||
<div class="planet-meta-card">
|
||
<strong>${title}</strong>
|
||
<div class="planet-text">No holidays match current search.</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
const rows = holidays.map((holiday) => {
|
||
const dateText = holiday?.dateText || holiday?.date || holiday?.dateRange || "--";
|
||
const gregorianDate = resolveHolidayGregorianDate(holiday);
|
||
const gregorianRef = formatGregorianReferenceDate(gregorianDate);
|
||
const hebrewRef = formatCalendarDateFromGregorian(gregorianDate, "hebrew");
|
||
const islamicRef = formatCalendarDateFromGregorian(gregorianDate, "islamic");
|
||
const conversionConfidence = String(holiday?.conversionConfidence || holiday?.datePrecision || "approximate").toLowerCase();
|
||
const conversionLabel = (!(gregorianDate instanceof Date) || Number.isNaN(gregorianDate.getTime()))
|
||
? "Conversion: unresolved"
|
||
: (conversionConfidence === "exact" ? "Conversion: exact" : "Conversion: approximate");
|
||
return `
|
||
<div class="cal-item-row">
|
||
<div class="cal-item-head">
|
||
<span class="cal-item-name">${holiday?.name || "Untitled"}</span>
|
||
<span class="planet-list-meta">${dateText}</span>
|
||
</div>
|
||
<div class="planet-list-meta">${cap(holiday?.kind || holiday?.calendarId || "observance")}</div>
|
||
<div class="planet-list-meta">${conversionLabel}</div>
|
||
<div class="planet-text"><strong>Gregorian:</strong> ${gregorianRef}</div>
|
||
<div class="planet-text"><strong>Hebrew:</strong> ${hebrewRef}</div>
|
||
<div class="planet-text"><strong>Islamic:</strong> ${islamicRef}</div>
|
||
<div class="planet-text">${holiday?.description || ""}</div>
|
||
${buildAssociationButtons(holiday?.associations)}
|
||
</div>
|
||
`;
|
||
}).join("");
|
||
|
||
return `
|
||
<div class="planet-meta-card">
|
||
<strong>${title}</strong>
|
||
<div class="cal-item-stack">${rows}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function findSignIdByAstrologyName(name) {
|
||
const token = normalizeCalendarText(name);
|
||
if (!token) {
|
||
return null;
|
||
}
|
||
|
||
for (const [signId, sign] of state.signsById) {
|
||
const idToken = normalizeCalendarText(signId);
|
||
const nameToken = normalizeCalendarText(sign?.name?.en || sign?.name || "");
|
||
if (token === idToken || token === nameToken) {
|
||
return signId;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function intersectDateRanges(startA, endA, startB, endB) {
|
||
const start = startA.getTime() > startB.getTime() ? startA : startB;
|
||
const end = endA.getTime() < endB.getTime() ? endA : endB;
|
||
return start.getTime() <= end.getTime() ? { start, end } : null;
|
||
}
|
||
|
||
function buildMajorArcanaRowsForMonth(month) {
|
||
if (state.selectedCalendar !== "gregorian") {
|
||
return [];
|
||
}
|
||
|
||
const monthOrder = Number(month?.order);
|
||
if (!Number.isFinite(monthOrder)) {
|
||
return [];
|
||
}
|
||
|
||
const monthStart = new Date(state.selectedYear, monthOrder - 1, 1, 12, 0, 0, 0);
|
||
const monthEnd = new Date(state.selectedYear, monthOrder, 0, 12, 0, 0, 0);
|
||
const rows = [];
|
||
|
||
state.hebrewById.forEach((letter) => {
|
||
const astrologyType = normalizeCalendarText(letter?.astrology?.type);
|
||
if (astrologyType !== "zodiac") {
|
||
return;
|
||
}
|
||
|
||
const signId = findSignIdByAstrologyName(letter?.astrology?.name);
|
||
const sign = signId ? state.signsById.get(signId) : null;
|
||
if (!sign) {
|
||
return;
|
||
}
|
||
|
||
const startToken = parseMonthDayToken(sign?.start);
|
||
const endToken = parseMonthDayToken(sign?.end);
|
||
if (!startToken || !endToken) {
|
||
return;
|
||
}
|
||
|
||
const spanStart = new Date(state.selectedYear, startToken.month - 1, startToken.day, 12, 0, 0, 0);
|
||
const spanEnd = new Date(state.selectedYear, endToken.month - 1, endToken.day, 12, 0, 0, 0);
|
||
const wraps = spanEnd.getTime() < spanStart.getTime();
|
||
|
||
const segments = wraps
|
||
? [
|
||
{
|
||
start: spanStart,
|
||
end: new Date(state.selectedYear, 11, 31, 12, 0, 0, 0)
|
||
},
|
||
{
|
||
start: new Date(state.selectedYear, 0, 1, 12, 0, 0, 0),
|
||
end: spanEnd
|
||
}
|
||
]
|
||
: [{ start: spanStart, end: spanEnd }];
|
||
|
||
segments.forEach((segment) => {
|
||
const overlap = intersectDateRanges(segment.start, segment.end, monthStart, monthEnd);
|
||
if (!overlap) {
|
||
return;
|
||
}
|
||
|
||
const rangeStartDay = overlap.start.getDate();
|
||
const rangeEndDay = overlap.end.getDate();
|
||
const cardName = String(letter?.tarot?.card || "").trim();
|
||
const trumpNumber = Number(letter?.tarot?.trumpNumber);
|
||
if (!cardName) {
|
||
return;
|
||
}
|
||
|
||
rows.push({
|
||
id: `${signId}-${rangeStartDay}-${rangeEndDay}`,
|
||
signId,
|
||
signName: sign?.name?.en || sign?.name || signId,
|
||
signSymbol: sign?.symbol || "",
|
||
cardName,
|
||
trumpNumber: Number.isFinite(trumpNumber) ? Math.trunc(trumpNumber) : null,
|
||
hebrewLetterId: String(letter?.hebrewLetterId || "").trim(),
|
||
hebrewLetterName: String(letter?.name || "").trim(),
|
||
hebrewLetterChar: String(letter?.char || "").trim(),
|
||
dayStart: rangeStartDay,
|
||
dayEnd: rangeEndDay,
|
||
rangeLabel: `${month?.name || "Month"} ${rangeStartDay}-${rangeEndDay}`
|
||
});
|
||
});
|
||
});
|
||
|
||
rows.sort((left, right) => {
|
||
if (left.dayStart !== right.dayStart) {
|
||
return left.dayStart - right.dayStart;
|
||
}
|
||
return left.cardName.localeCompare(right.cardName);
|
||
});
|
||
|
||
return rows;
|
||
}
|
||
|
||
function renderMajorArcanaCard(month) {
|
||
const selectedDay = getSelectedDayFilterContext(month);
|
||
const allRows = buildMajorArcanaRowsForMonth(month);
|
||
|
||
const rows = selectedDay
|
||
? allRows.filter((row) => selectedDay.entries.some((entry) => entry.dayNumber >= row.dayStart && entry.dayNumber <= row.dayEnd))
|
||
: allRows;
|
||
|
||
if (!rows.length) {
|
||
return `
|
||
<div class="planet-meta-card">
|
||
<strong>Major Arcana Windows</strong>
|
||
<div class="planet-text">No major arcana windows for this month.</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
const list = rows.map((row) => {
|
||
const hebrewLabel = row.hebrewLetterId
|
||
? `${row.hebrewLetterChar ? `${row.hebrewLetterChar} ` : ""}${row.hebrewLetterName || row.hebrewLetterId}`
|
||
: "--";
|
||
const displayCardName = getDisplayTarotName(row.cardName, row.trumpNumber);
|
||
|
||
return `
|
||
<div class="cal-item-row">
|
||
<div class="cal-item-head">
|
||
<span class="cal-item-name">${displayCardName}${row.trumpNumber != null ? ` · Trump ${row.trumpNumber}` : ""}</span>
|
||
<span class="planet-list-meta">${row.rangeLabel}</span>
|
||
</div>
|
||
<div class="planet-list-meta">${row.signSymbol} ${row.signName} · Hebrew: ${hebrewLabel}</div>
|
||
<div class="alpha-nav-btns">
|
||
<button class="alpha-nav-btn" data-nav="calendar-day-range" data-range-start="${row.dayStart}" data-range-end="${row.dayEnd}">${row.rangeLabel} ↗</button>
|
||
<button class="alpha-nav-btn" data-nav="tarot-card" data-card-name="${row.cardName}" data-trump-number="${row.trumpNumber ?? ""}">${displayCardName} ↗</button>
|
||
${row.hebrewLetterId ? `<button class="alpha-nav-btn" data-nav="alphabet" data-alphabet="hebrew" data-hebrew-letter-id="${row.hebrewLetterId}">${hebrewLabel} ↗</button>` : ""}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join("");
|
||
|
||
return `
|
||
<div class="planet-meta-card">
|
||
<strong>Major Arcana Windows</strong>
|
||
<div class="cal-item-stack">${list}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderDecanTarotCard(month) {
|
||
const selectedDay = getSelectedDayFilterContext(month);
|
||
const allRows = buildDecanTarotRowsForMonth(month);
|
||
const rows = selectedDay
|
||
? allRows.filter((row) => selectedDay.entries.some((entry) => {
|
||
const targetDate = entry.gregorianDate;
|
||
if (!(targetDate instanceof Date) || Number.isNaN(targetDate.getTime())) {
|
||
return false;
|
||
}
|
||
|
||
const targetMonth = targetDate.getMonth() + 1;
|
||
const targetDayNo = targetDate.getDate();
|
||
return isMonthDayInRange(
|
||
targetMonth,
|
||
targetDayNo,
|
||
row.startMonth,
|
||
row.startDay,
|
||
row.endMonth,
|
||
row.endDay
|
||
);
|
||
}))
|
||
: allRows;
|
||
|
||
if (!rows.length) {
|
||
return `
|
||
<div class="planet-meta-card">
|
||
<strong>Decan Tarot Windows</strong>
|
||
<div class="planet-text">No decan tarot windows for this month.</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
const list = rows.map((row) => {
|
||
const displayCardName = getDisplayTarotName(row.cardName);
|
||
return `
|
||
<div class="cal-item-row">
|
||
<div class="cal-item-head">
|
||
<span class="cal-item-name">${row.signSymbol} ${row.signName} · Decan ${row.decanIndex}</span>
|
||
<span class="planet-list-meta">${row.startDegree}°–${row.endDegree}° · ${row.dateRange}</span>
|
||
</div>
|
||
<div class="alpha-nav-btns">
|
||
<button class="alpha-nav-btn" data-nav="tarot-card" data-card-name="${row.cardName}">${displayCardName} ↗</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join("");
|
||
|
||
return `
|
||
<div class="planet-meta-card">
|
||
<strong>Decan Tarot Windows</strong>
|
||
<div class="cal-item-stack">${list}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function attachNavHandlers(detailBodyEl) {
|
||
if (!detailBodyEl) {
|
||
return;
|
||
}
|
||
|
||
detailBodyEl.querySelectorAll("[data-nav]").forEach((button) => {
|
||
button.addEventListener("click", () => {
|
||
const navType = button.dataset.nav;
|
||
|
||
if (navType === "planet" && button.dataset.planetId) {
|
||
document.dispatchEvent(new CustomEvent("nav:planet", {
|
||
detail: { planetId: button.dataset.planetId }
|
||
}));
|
||
return;
|
||
}
|
||
|
||
if (navType === "zodiac" && button.dataset.signId) {
|
||
document.dispatchEvent(new CustomEvent("nav:zodiac", {
|
||
detail: { signId: button.dataset.signId }
|
||
}));
|
||
return;
|
||
}
|
||
|
||
if (navType === "number" && button.dataset.numberValue) {
|
||
document.dispatchEvent(new CustomEvent("nav:number", {
|
||
detail: { value: Number(button.dataset.numberValue) }
|
||
}));
|
||
return;
|
||
}
|
||
|
||
if (navType === "tarot-card" && button.dataset.cardName) {
|
||
const trumpNumber = Number(button.dataset.trumpNumber);
|
||
document.dispatchEvent(new CustomEvent("nav:tarot-trump", {
|
||
detail: {
|
||
cardName: button.dataset.cardName,
|
||
trumpNumber: Number.isFinite(trumpNumber) ? trumpNumber : undefined
|
||
}
|
||
}));
|
||
return;
|
||
}
|
||
|
||
if (navType === "god") {
|
||
document.dispatchEvent(new CustomEvent("nav:gods", {
|
||
detail: {
|
||
godId: button.dataset.godId || undefined,
|
||
godName: button.dataset.godName || undefined
|
||
}
|
||
}));
|
||
return;
|
||
}
|
||
|
||
if (navType === "alphabet" && button.dataset.hebrewLetterId) {
|
||
document.dispatchEvent(new CustomEvent("nav:alphabet", {
|
||
detail: {
|
||
alphabet: "hebrew",
|
||
hebrewLetterId: button.dataset.hebrewLetterId
|
||
}
|
||
}));
|
||
return;
|
||
}
|
||
|
||
if (navType === "kabbalah" && button.dataset.pathNo) {
|
||
document.dispatchEvent(new CustomEvent("nav:kabbalah-path", {
|
||
detail: { pathNo: Number(button.dataset.pathNo) }
|
||
}));
|
||
return;
|
||
}
|
||
|
||
if (navType === "iching" && button.dataset.planetaryInfluence) {
|
||
document.dispatchEvent(new CustomEvent("nav:iching", {
|
||
detail: {
|
||
planetaryInfluence: button.dataset.planetaryInfluence
|
||
}
|
||
}));
|
||
return;
|
||
}
|
||
|
||
if (navType === "calendar-month" && button.dataset.monthId) {
|
||
document.dispatchEvent(new CustomEvent("nav:calendar-month", {
|
||
detail: {
|
||
calendarId: button.dataset.calendarId || undefined,
|
||
monthId: button.dataset.monthId
|
||
}
|
||
}));
|
||
return;
|
||
}
|
||
|
||
if (navType === "calendar-day" && button.dataset.dayNumber) {
|
||
const month = getSelectedMonth();
|
||
const dayNumber = Number(button.dataset.dayNumber);
|
||
if (!month || !Number.isFinite(dayNumber)) {
|
||
return;
|
||
}
|
||
|
||
toggleDayFilterEntry(month, dayNumber, button.dataset.gregorianDate);
|
||
renderDetail(getElements());
|
||
return;
|
||
}
|
||
|
||
if (navType === "calendar-day-range" && button.dataset.rangeStart && button.dataset.rangeEnd) {
|
||
const month = getSelectedMonth();
|
||
if (!month) {
|
||
return;
|
||
}
|
||
|
||
toggleDayRangeFilter(month, Number(button.dataset.rangeStart), Number(button.dataset.rangeEnd));
|
||
renderDetail(getElements());
|
||
return;
|
||
}
|
||
|
||
if (navType === "calendar-day-clear") {
|
||
clearSelectedDayFilter();
|
||
renderDetail(getElements());
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function renderHebrewMonthDetail(month) {
|
||
const gregorianStartDate = getGregorianReferenceDateForCalendarMonth(month);
|
||
const factsRows = [
|
||
["Hebrew Name", month.nativeName || "--"],
|
||
["Month Order", month.leapYearOnly ? `${month.order} (leap year only)` : String(month.order)],
|
||
["Gregorian Reference Year", String(state.selectedYear)],
|
||
["Month Start (Gregorian)", formatGregorianReferenceDate(gregorianStartDate)],
|
||
["Days", month.daysVariant ? `${month.days}–${month.daysVariant} (varies)` : String(month.days || "--")],
|
||
["Season", month.season || "--"],
|
||
["Zodiac Sign", cap(month.zodiacSign) || "--"],
|
||
["Tribe of Israel", month.tribe || "--"],
|
||
["Sense", month.sense || "--"],
|
||
["Hebrew Letter", month.hebrewLetter || "--"]
|
||
].map(([dt, dd]) => `<dt>${dt}</dt><dd>${dd}</dd>`).join("");
|
||
|
||
const monthOrder = Number(month?.order);
|
||
const navButtons = buildAssociationButtons({
|
||
...(month?.associations || {}),
|
||
...(Number.isFinite(monthOrder) ? { numberValue: Math.trunc(monthOrder) } : {})
|
||
});
|
||
const connectionsCard = navButtons
|
||
? `<div class="planet-meta-card"><strong>Connections</strong>${navButtons}</div>`
|
||
: "";
|
||
|
||
return `
|
||
<div class="planet-meta-grid">
|
||
<div class="planet-meta-card">
|
||
<strong>Month Facts</strong>
|
||
<div class="planet-text">
|
||
<dl class="alpha-dl">${factsRows}</dl>
|
||
</div>
|
||
</div>
|
||
${connectionsCard}
|
||
<div class="planet-meta-card">
|
||
<strong>About ${month.name}</strong>
|
||
<div class="planet-text">${month.description || "--"}</div>
|
||
</div>
|
||
${renderDayLinksCard(month)}
|
||
${renderHolidaysCard(month, "Holiday Repository")}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderIslamicMonthDetail(month) {
|
||
const gregorianStartDate = getGregorianReferenceDateForCalendarMonth(month);
|
||
const factsRows = [
|
||
["Arabic Name", month.nativeName || "--"],
|
||
["Month Order", String(month.order)],
|
||
["Gregorian Reference Year", String(state.selectedYear)],
|
||
["Month Start (Gregorian)", formatGregorianReferenceDate(gregorianStartDate)],
|
||
["Meaning", month.meaning || "--"],
|
||
["Days", month.daysVariant ? `${month.days}–${month.daysVariant} (varies)` : String(month.days || "--")],
|
||
["Sacred Month", month.sacred ? "Yes — warfare prohibited" : "No"]
|
||
].map(([dt, dd]) => `<dt>${dt}</dt><dd>${dd}</dd>`).join("");
|
||
|
||
const monthOrder = Number(month?.order);
|
||
const hasNumberLink = Number.isFinite(monthOrder) && monthOrder >= 0;
|
||
const navButtons = hasNumberLink
|
||
? buildAssociationButtons({ numberValue: Math.trunc(monthOrder) })
|
||
: "";
|
||
const connectionsCard = hasNumberLink
|
||
? `<div class="planet-meta-card"><strong>Connections</strong>${navButtons}</div>`
|
||
: "";
|
||
|
||
return `
|
||
<div class="planet-meta-grid">
|
||
<div class="planet-meta-card">
|
||
<strong>Month Facts</strong>
|
||
<div class="planet-text">
|
||
<dl class="alpha-dl">${factsRows}</dl>
|
||
</div>
|
||
</div>
|
||
${connectionsCard}
|
||
<div class="planet-meta-card">
|
||
<strong>About ${month.name}</strong>
|
||
<div class="planet-text">${month.description || "--"}</div>
|
||
</div>
|
||
${renderDayLinksCard(month)}
|
||
${renderHolidaysCard(month, "Holiday Repository")}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function buildWheelDeityButtons(deities) {
|
||
const buttons = [];
|
||
(Array.isArray(deities) ? deities : []).forEach((rawName) => {
|
||
// Strip qualifiers like "(early)" or "/ Father Christmas" before matching
|
||
const cleanName = String(rawName || "").replace(/\s*\/.*$/, "").replace(/\s*\(.*\)$/, "").trim();
|
||
const godId = findGodIdByName(cleanName) || findGodIdByName(rawName);
|
||
if (!godId) return;
|
||
const god = state.godsById.get(godId);
|
||
const label = god?.name || cleanName;
|
||
buttons.push(`<button class="alpha-nav-btn" data-nav="god" data-god-id="${godId}" data-god-name="${label}">${label} \u2197</button>`);
|
||
});
|
||
return buttons;
|
||
}
|
||
|
||
function renderWheelMonthDetail(month) {
|
||
const gregorianStartDate = getGregorianReferenceDateForCalendarMonth(month);
|
||
const assoc = month?.associations;
|
||
const themes = Array.isArray(assoc?.themes) ? assoc.themes.join(", ") : "--";
|
||
const deities = Array.isArray(assoc?.deities) ? assoc.deities.join(", ") : "--";
|
||
const colors = Array.isArray(assoc?.colors) ? assoc.colors.join(", ") : "--";
|
||
const herbs = Array.isArray(assoc?.herbs) ? assoc.herbs.join(", ") : "--";
|
||
|
||
const factsRows = [
|
||
["Date", month.date || "--"],
|
||
["Type", cap(month.type) || "--"],
|
||
["Gregorian Reference Year", String(state.selectedYear)],
|
||
["Start (Gregorian)", formatGregorianReferenceDate(gregorianStartDate)],
|
||
["Season", month.season || "--"],
|
||
["Element", cap(month.element) || "--"],
|
||
["Direction", assoc?.direction || "--"]
|
||
].map(([dt, dd]) => `<dt>${dt}</dt><dd>${dd}</dd>`).join("");
|
||
|
||
const assocRows = [
|
||
["Themes", themes],
|
||
["Deities", deities],
|
||
["Colors", colors],
|
||
["Herbs", herbs]
|
||
].map(([dt, dd]) => `<dt>${dt}</dt><dd class="planet-text">${dd}</dd>`).join("");
|
||
|
||
const deityButtons = buildWheelDeityButtons(assoc?.deities);
|
||
const deityLinksCard = deityButtons.length
|
||
? `<div class="planet-meta-card"><strong>Linked Deities</strong><div class="alpha-nav-btns">${deityButtons.join("")}</div></div>`
|
||
: "";
|
||
|
||
const monthOrder = Number(month?.order);
|
||
const hasNumberLink = Number.isFinite(monthOrder) && monthOrder >= 0;
|
||
const numberButtons = hasNumberLink
|
||
? buildAssociationButtons({ numberValue: Math.trunc(monthOrder) })
|
||
: "";
|
||
const numberLinksCard = hasNumberLink
|
||
? `<div class="planet-meta-card"><strong>Connections</strong>${numberButtons}</div>`
|
||
: "";
|
||
|
||
return `
|
||
<div class="planet-meta-grid">
|
||
<div class="planet-meta-card">
|
||
<strong>Sabbat Facts</strong>
|
||
<div class="planet-text">
|
||
<dl class="alpha-dl">${factsRows}</dl>
|
||
</div>
|
||
</div>
|
||
<div class="planet-meta-card">
|
||
<strong>About ${month.name}</strong>
|
||
<div class="planet-text">${month.description || "--"}</div>
|
||
</div>
|
||
<div class="planet-meta-card">
|
||
<strong>Associations</strong>
|
||
<div class="planet-text">
|
||
<dl class="alpha-dl">${assocRows}</dl>
|
||
</div>
|
||
</div>
|
||
${renderDayLinksCard(month)}
|
||
${numberLinksCard}
|
||
${deityLinksCard}
|
||
${renderHolidaysCard(month, "Holiday Repository")}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderDetail(elements) {
|
||
const { detailNameEl, detailSubEl, detailBodyEl } = elements;
|
||
if (!detailBodyEl || !detailNameEl || !detailSubEl) {
|
||
return;
|
||
}
|
||
|
||
const month = getSelectedMonth();
|
||
if (!month) {
|
||
detailNameEl.textContent = "--";
|
||
detailSubEl.textContent = "Select a month to explore";
|
||
detailBodyEl.innerHTML = "";
|
||
return;
|
||
}
|
||
|
||
detailNameEl.textContent = month.name || month.id;
|
||
|
||
const calId = state.selectedCalendar;
|
||
|
||
if (calId === "gregorian") {
|
||
detailSubEl.textContent = `${parseMonthRange(month)} · ${month.coreTheme || "Month correspondences"}`;
|
||
detailBodyEl.innerHTML = `
|
||
<div class="planet-meta-grid">
|
||
${renderFactsCard(month)}
|
||
${renderDayLinksCard(month)}
|
||
${renderAssociationsCard(month)}
|
||
${renderMajorArcanaCard(month)}
|
||
${renderDecanTarotCard(month)}
|
||
${renderEventsCard(month)}
|
||
${renderHolidaysCard(month, "Holiday Repository")}
|
||
</div>
|
||
`;
|
||
} else if (calId === "hebrew") {
|
||
detailSubEl.textContent = getMonthSubtitle(month);
|
||
detailBodyEl.innerHTML = renderHebrewMonthDetail(month);
|
||
} else if (calId === "islamic") {
|
||
detailSubEl.textContent = getMonthSubtitle(month);
|
||
detailBodyEl.innerHTML = renderIslamicMonthDetail(month);
|
||
} else {
|
||
detailSubEl.textContent = getMonthSubtitle(month);
|
||
detailBodyEl.innerHTML = renderWheelMonthDetail(month);
|
||
}
|
||
|
||
attachNavHandlers(detailBodyEl);
|
||
}
|
||
|
||
function selectByMonthId(monthId, elements = getElements()) {
|
||
const target = state.months.find((month) => month.id === monthId);
|
||
if (!target) {
|
||
return false;
|
||
}
|
||
|
||
if (state.searchQuery && !state.filteredMonths.some((month) => month.id === target.id)) {
|
||
state.searchQuery = "";
|
||
state.filteredMonths = [...state.months];
|
||
}
|
||
|
||
if (state.selectedMonthId !== target.id) {
|
||
clearSelectedDayFilter();
|
||
}
|
||
|
||
state.selectedMonthId = target.id;
|
||
syncSearchControls(elements);
|
||
renderList(elements);
|
||
renderDetail(elements);
|
||
return true;
|
||
}
|
||
|
||
function selectCalendarType(calendarId, elements = getElements()) {
|
||
if (!state.calendarData || !Array.isArray(state.calendarData[calendarId])) {
|
||
return false;
|
||
}
|
||
|
||
if (elements.calendarTypeEl) {
|
||
elements.calendarTypeEl.value = calendarId;
|
||
}
|
||
|
||
loadCalendarType(calendarId, elements);
|
||
return true;
|
||
}
|
||
|
||
function bindYearInput(elements) {
|
||
if (!elements.yearInputEl) {
|
||
return;
|
||
}
|
||
|
||
elements.yearInputEl.value = String(state.selectedYear);
|
||
elements.yearInputEl.addEventListener("change", () => {
|
||
const nextYear = Number(elements.yearInputEl.value);
|
||
if (!Number.isFinite(nextYear)) {
|
||
elements.yearInputEl.value = String(state.selectedYear);
|
||
return;
|
||
}
|
||
|
||
state.selectedYear = Math.min(2500, Math.max(1900, Math.round(nextYear)));
|
||
elements.yearInputEl.value = String(state.selectedYear);
|
||
state.dayLinksCache = new Map();
|
||
clearSelectedDayFilter();
|
||
renderDetail(elements);
|
||
});
|
||
}
|
||
|
||
function bindSearchInput(elements) {
|
||
if (elements.searchInputEl) {
|
||
elements.searchInputEl.addEventListener("input", () => {
|
||
state.searchQuery = normalizeSearchValue(elements.searchInputEl.value);
|
||
applySearchFilter(elements);
|
||
});
|
||
}
|
||
|
||
if (elements.searchClearEl && elements.searchInputEl) {
|
||
elements.searchClearEl.addEventListener("click", () => {
|
||
state.searchQuery = "";
|
||
elements.searchInputEl.value = "";
|
||
applySearchFilter(elements);
|
||
elements.searchInputEl.focus();
|
||
});
|
||
}
|
||
}
|
||
|
||
function loadCalendarType(calendarId, elements) {
|
||
const months = state.calendarData[calendarId];
|
||
if (!Array.isArray(months)) {
|
||
return;
|
||
}
|
||
|
||
state.selectedCalendar = calendarId;
|
||
state.dayLinksCache = new Map();
|
||
clearSelectedDayFilter();
|
||
state.months = sortMonths(months);
|
||
state.filteredMonths = [...state.months];
|
||
state.selectedMonthId = state.months[0]?.id || null;
|
||
state.searchQuery = "";
|
||
|
||
if (elements.calendarYearWrapEl) {
|
||
elements.calendarYearWrapEl.hidden = false;
|
||
}
|
||
|
||
syncSearchControls(elements);
|
||
applySearchFilter(elements);
|
||
}
|
||
|
||
function bindCalendarTypeSelect(elements) {
|
||
if (!elements.calendarTypeEl) {
|
||
return;
|
||
}
|
||
|
||
elements.calendarTypeEl.value = state.selectedCalendar;
|
||
elements.calendarTypeEl.addEventListener("change", () => {
|
||
const calId = String(elements.calendarTypeEl.value || "gregorian");
|
||
loadCalendarType(calId, elements);
|
||
});
|
||
}
|
||
|
||
function ensureCalendarSection(referenceData, magickDataset) {
|
||
if (!referenceData) {
|
||
return;
|
||
}
|
||
|
||
state.referenceData = referenceData;
|
||
state.magickDataset = magickDataset || null;
|
||
state.dayLinksCache = new Map();
|
||
clearSelectedDayFilter();
|
||
state.holidays = Array.isArray(referenceData.celestialHolidays) ? referenceData.celestialHolidays : [];
|
||
state.calendarHolidays = Array.isArray(referenceData.calendarHolidays) ? referenceData.calendarHolidays : [];
|
||
state.planetsById = buildPlanetMap(referenceData.planets);
|
||
state.signsById = buildSignsMap(referenceData.signs);
|
||
state.godsById = buildGodsMap(state.magickDataset);
|
||
state.hebrewById = buildHebrewMap(state.magickDataset);
|
||
|
||
state.calendarData = {
|
||
gregorian: Array.isArray(referenceData.calendarMonths) ? referenceData.calendarMonths : [],
|
||
hebrew: Array.isArray(referenceData.hebrewCalendar?.months) ? referenceData.hebrewCalendar.months : [],
|
||
islamic: Array.isArray(referenceData.islamicCalendar?.months) ? referenceData.islamicCalendar.months : [],
|
||
"wheel-of-year": Array.isArray(referenceData.wheelOfYear?.months) ? referenceData.wheelOfYear.months : []
|
||
};
|
||
|
||
const currentCalMonths = state.calendarData[state.selectedCalendar] || state.calendarData.gregorian || [];
|
||
state.months = sortMonths(currentCalMonths);
|
||
state.filteredMonths = [...state.months];
|
||
|
||
const elements = getElements();
|
||
|
||
if (elements.calendarYearWrapEl) {
|
||
elements.calendarYearWrapEl.hidden = false;
|
||
}
|
||
|
||
if (!state.months.length) {
|
||
if (elements.detailNameEl) {
|
||
elements.detailNameEl.textContent = "Calendar";
|
||
}
|
||
if (elements.detailSubEl) {
|
||
elements.detailSubEl.textContent = "No month data available.";
|
||
}
|
||
if (elements.detailBodyEl) {
|
||
elements.detailBodyEl.innerHTML = "";
|
||
}
|
||
if (elements.monthListEl) {
|
||
elements.monthListEl.innerHTML = "";
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (!state.selectedMonthId || !state.months.some((month) => month.id === state.selectedMonthId)) {
|
||
state.selectedMonthId = state.months[0].id;
|
||
}
|
||
|
||
if (!state.initialized) {
|
||
state.initialized = true;
|
||
bindYearInput(elements);
|
||
bindSearchInput(elements);
|
||
bindCalendarTypeSelect(elements);
|
||
}
|
||
|
||
applySearchFilter(elements);
|
||
}
|
||
|
||
window.CalendarSectionUi = {
|
||
ensureCalendarSection,
|
||
selectByMonthId,
|
||
selectCalendarType
|
||
};
|
||
})();
|