Files
TaroTime/app/ui-calendar.js

1013 lines
29 KiB
JavaScript
Raw Normal View History

2026-03-07 01:09:00 -08:00
/* ui-calendar.js — Month and celestial holiday browser */
(function () {
"use strict";
const { getTarotCardDisplayName, getTarotCardSearchAliases } = window.TarotCardImages || {};
2026-03-07 05:17:50 -08:00
const calendarDatesUi = window.TarotCalendarDates || {};
const calendarDetailUi = window.TarotCalendarDetail || {};
const {
addDays,
buildSignDateBounds,
formatCalendarDateFromGregorian,
formatDateLabel,
formatGregorianReferenceDate,
formatIsoDate,
getDaysInMonth,
getGregorianMonthStartDate,
getGregorianReferenceDateForCalendarMonth,
getMonthStartWeekday,
intersectDateRanges,
isMonthDayInRange,
isoToDateAtNoon,
normalizeCalendarText,
parseDayRangeFromText,
parseMonthDayStartToken,
parseMonthDayToken,
parseMonthDayTokensFromText,
parseMonthRange,
resolveCalendarDayToGregorian,
resolveHolidayGregorianDate
} = calendarDatesUi;
2026-03-07 01:09:00 -08:00
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
};
function normalizeText(value) {
return String(value || "").trim();
}
function normalizeSearchValue(value) {
2026-03-07 05:17:50 -08:00
return normalizeText(value).toLowerCase();
2026-03-07 01:09:00 -08:00
}
function cap(value) {
const text = normalizeText(value);
return text ? text.charAt(0).toUpperCase() + text.slice(1) : text;
}
function normalizeTarotName(value) {
2026-03-07 05:17:50 -08:00
return normalizeText(value)
2026-03-07 01:09:00 -08:00
.toLowerCase()
.replace(/\s+/g, " ");
}
function resolveTarotTrumpNumber(cardName) {
const key = normalizeTarotName(cardName);
if (!key) {
return null;
}
2026-03-07 05:17:50 -08:00
2026-03-07 01:09:00 -08:00
if (Object.prototype.hasOwnProperty.call(TAROT_TRUMP_NUMBER_BY_NAME, key)) {
return TAROT_TRUMP_NUMBER_BY_NAME[key];
}
2026-03-07 05:17:50 -08:00
2026-03-07 01:09:00 -08:00
const withoutLeadingThe = key.replace(/^the\s+/, "");
if (Object.prototype.hasOwnProperty.call(TAROT_TRUMP_NUMBER_BY_NAME, withoutLeadingThe)) {
return TAROT_TRUMP_NUMBER_BY_NAME[withoutLeadingThe];
}
2026-03-07 05:17:50 -08:00
2026-03-07 01:09:00 -08:00
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;
}
2026-03-07 05:17:50 -08:00
function normalizeMinorTarotCardName(value) {
return normalizeTarotName(value)
.split(" ")
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(" ");
2026-03-07 01:09:00 -08:00
}
2026-03-07 05:17:50 -08:00
function normalizeDayFilterEntry(dayNumber, gregorianIso) {
const nextDayNumber = Math.trunc(Number(dayNumber));
const nextIso = normalizeText(gregorianIso);
if (!Number.isFinite(nextDayNumber) || nextDayNumber <= 0 || !nextIso) {
2026-03-07 01:09:00 -08:00
return null;
}
2026-03-07 05:17:50 -08:00
return {
dayNumber: nextDayNumber,
gregorianIso: nextIso
};
2026-03-07 01:09:00 -08:00
}
2026-03-07 05:17:50 -08:00
function sortDayFilterEntries(entries) {
const deduped = new Map();
(Array.isArray(entries) ? entries : []).forEach((entry) => {
const normalized = normalizeDayFilterEntry(entry?.dayNumber, entry?.gregorianIso);
if (normalized) {
deduped.set(normalized.dayNumber, normalized);
}
});
2026-03-07 01:09:00 -08:00
2026-03-07 05:17:50 -08:00
return [...deduped.values()].sort((left, right) => left.dayNumber - right.dayNumber);
2026-03-07 01:09:00 -08:00
}
2026-03-07 05:17:50 -08:00
function getElements() {
2026-03-07 01:09:00 -08:00
return {
2026-03-07 05:17:50 -08:00
monthListEl: document.getElementById("calendar-month-list"),
monthCountEl: document.getElementById("calendar-month-count"),
listTitleEl: document.getElementById("calendar-list-title"),
calendarTypeEl: document.getElementById("calendar-type-select"),
calendarYearWrapEl: document.getElementById("calendar-year-wrap"),
yearInputEl: document.getElementById("calendar-year-input"),
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")
2026-03-07 01:09:00 -08:00
};
}
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;
}
2026-03-07 05:17:50 -08:00
const entries = [...state.selectedDayEntries];
2026-03-07 01:09:00 -08:00
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) => {
2026-03-07 05:17:50 -08:00
if (planet?.id) {
map.set(planet.id, planet);
2026-03-07 01:09:00 -08:00
}
});
return map;
}
function buildSignsMap(signs) {
const map = new Map();
if (!Array.isArray(signs)) {
return map;
}
signs.forEach((sign) => {
2026-03-07 05:17:50 -08:00
if (sign?.id) {
map.set(sign.id, sign);
2026-03-07 01:09:00 -08:00
}
});
return map;
}
function buildGodsMap(magickDataset) {
const gods = magickDataset?.grouped?.gods?.gods;
const map = new Map();
if (!Array.isArray(gods)) {
return map;
}
gods.forEach((god) => {
2026-03-07 05:17:50 -08:00
if (god?.id) {
map.set(god.id, god);
2026-03-07 01:09:00 -08:00
}
});
return map;
}
function findGodIdByName(name) {
2026-03-07 05:17:50 -08:00
if (!name) {
return null;
}
const normalized = normalizeText(name).toLowerCase().replace(/^the\s+/, "");
2026-03-07 01:09:00 -08:00
for (const [id, god] of state.godsById) {
2026-03-07 05:17:50 -08:00
const godName = normalizeText(god?.name).toLowerCase().replace(/^the\s+/, "");
if (godName === normalized || id.toLowerCase() === normalized) {
return id;
}
2026-03-07 01:09:00 -08:00
}
2026-03-07 05:17:50 -08:00
2026-03-07 01:09:00 -08:00
return null;
}
function buildHebrewMap(magickDataset) {
const map = new Map();
const letters = magickDataset?.grouped?.alphabets?.hebrew;
if (!Array.isArray(letters)) {
return map;
}
letters.forEach((letter) => {
2026-03-07 05:17:50 -08:00
if (letter?.hebrewLetterId) {
map.set(letter.hebrewLetterId, letter);
2026-03-07 01:09:00 -08:00
}
});
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;
}
2026-03-07 05:17:50 -08:00
function getMonthSubtitle(month) {
if (state.selectedCalendar === "hebrew" || state.selectedCalendar === "islamic") {
const native = month.nativeName ? ` · ${month.nativeName}` : "";
const days = month.days ? ` · ${month.days} days` : "";
return `${month.season || ""}${native}${days}`;
2026-03-07 01:09:00 -08:00
}
2026-03-07 05:17:50 -08:00
if (state.selectedCalendar === "wheel-of-year") {
return [month.date, month.type, month.season].filter(Boolean).join(" · ");
2026-03-07 01:09:00 -08:00
}
2026-03-07 05:17:50 -08:00
return parseMonthRange(month);
2026-03-07 01:09:00 -08:00
}
2026-03-07 05:17:50 -08:00
function getMonthDayLinkRows(month) {
const cacheKey = `${state.selectedCalendar}|${state.selectedYear}|${month?.id || ""}`;
if (state.dayLinksCache.has(cacheKey)) {
return state.dayLinksCache.get(cacheKey);
2026-03-07 01:09:00 -08:00
}
2026-03-07 05:17:50 -08:00
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);
}
2026-03-07 01:09:00 -08:00
}
2026-03-07 05:17:50 -08:00
if (!Number.isFinite(dayCount) || dayCount <= 0) {
state.dayLinksCache.set(cacheKey, []);
return [];
2026-03-07 01:09:00 -08:00
}
2026-03-07 05:17:50 -08:00
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()))
});
2026-03-07 01:09:00 -08:00
}
2026-03-07 05:17:50 -08:00
state.dayLinksCache.set(cacheKey, rows);
return rows;
2026-03-07 01:09:00 -08:00
}
2026-03-07 05:17:50 -08:00
function renderList(elements) {
const { monthListEl, monthCountEl, listTitleEl } = elements;
if (!monthListEl) {
return;
2026-03-07 01:09:00 -08:00
}
2026-03-07 05:17:50 -08:00
monthListEl.innerHTML = "";
2026-03-07 01:09:00 -08:00
2026-03-07 05:17:50 -08:00
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);
});
2026-03-07 01:09:00 -08:00
2026-03-07 05:17:50 -08:00
if (monthCountEl) {
monthCountEl.textContent = state.searchQuery
? `${state.filteredMonths.length} of ${state.months.length} months`
: `${state.months.length} months`;
2026-03-07 01:09:00 -08:00
}
2026-03-07 05:17:50 -08:00
if (listTitleEl) {
listTitleEl.textContent = "Calendar > Months";
2026-03-07 01:09:00 -08:00
}
}
2026-03-07 05:17:50 -08:00
function associationSearchText(associations) {
if (!associations || typeof associations !== "object") {
return "";
2026-03-07 01:09:00 -08:00
}
2026-03-07 05:17:50 -08:00
const tarotAliases = associations.tarotCard && typeof getTarotCardSearchAliases === "function"
? getTarotCardSearchAliases(associations.tarotCard, { trumpNumber: associations.tarotTrumpNumber })
: [];
2026-03-07 01:09:00 -08:00
2026-03-07 05:17:50 -08:00
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(" ");
2026-03-07 01:09:00 -08:00
}
2026-03-07 05:17:50 -08:00
function eventSearchText(event) {
return normalizeSearchValue([
event?.name,
event?.date,
event?.dateRange,
event?.description,
associationSearchText(event?.associations)
].filter(Boolean).join(" "));
}
2026-03-07 01:09:00 -08:00
2026-03-07 05:17:50 -08:00
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(" "));
}
2026-03-07 01:09:00 -08:00
2026-03-07 05:17:50 -08:00
function buildHolidayList(month) {
const calendarId = state.selectedCalendar;
const monthOrder = Number(month?.order);
2026-03-07 01:09:00 -08:00
2026-03-07 05:17:50 -08:00
const fromRepo = state.calendarHolidays.filter((holiday) => {
const holidayCalendarId = normalizeText(holiday?.calendarId).toLowerCase();
if (holidayCalendarId !== calendarId) {
return false;
2026-03-07 01:09:00 -08:00
}
2026-03-07 05:17:50 -08:00
const isDirectMonthMatch = normalizeText(holiday?.monthId).toLowerCase() === normalizeText(month?.id).toLowerCase();
if (isDirectMonthMatch) {
return true;
}
2026-03-07 01:09:00 -08:00
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;
}
2026-03-07 05:17:50 -08:00
return normalizeText(left?.name).localeCompare(normalizeText(right?.name));
2026-03-07 01:09:00 -08:00
});
}
const seen = new Set();
const ordered = [];
2026-03-07 05:17:50 -08:00
(month?.holidayIds || []).forEach((holidayId) => {
2026-03-07 01:09:00 -08:00
const holiday = state.holidays.find((item) => item.id === holidayId);
2026-03-07 05:17:50 -08:00
if (holiday && !seen.has(holiday.id)) {
seen.add(holiday.id);
ordered.push(holiday);
2026-03-07 01:09:00 -08:00
}
});
state.holidays.forEach((holiday) => {
2026-03-07 05:17:50 -08:00
if (holiday?.monthId === month.id && !seen.has(holiday.id)) {
seen.add(holiday.id);
ordered.push(holiday);
2026-03-07 01:09:00 -08:00
}
});
return ordered;
}
function buildMonthSearchText(month) {
const monthHolidays = buildHolidayList(month);
const holidayText = monthHolidays.map((holiday) => holidaySearchText(holiday)).join(" ");
2026-03-07 05:17:50 -08:00
if (state.selectedCalendar === "gregorian") {
2026-03-07 01:09:00 -08:00
const events = Array.isArray(month?.events) ? month.events : [];
2026-03-07 05:17:50 -08:00
return normalizeSearchValue([
2026-03-07 01:09:00 -08:00
month?.name,
month?.id,
month?.start,
month?.end,
month?.coreTheme,
month?.seasonNorth,
month?.seasonSouth,
associationSearchText(month?.associations),
events.map((event) => eventSearchText(event)).join(" "),
holidayText
2026-03-07 05:17:50 -08:00
].filter(Boolean).join(" "));
2026-03-07 01:09:00 -08:00
}
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(" ")
: "";
2026-03-07 05:17:50 -08:00
return normalizeSearchValue([
2026-03-07 01:09:00 -08:00
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
2026-03-07 05:17:50 -08:00
].filter(Boolean).join(" "));
2026-03-07 01:09:00 -08:00
}
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;
}
}
2026-03-07 05:17:50 -08:00
function renderDetail(elements) {
calendarDetailUi.renderDetail?.(elements);
}
2026-03-07 01:09:00 -08:00
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 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", () => {
2026-03-07 05:17:50 -08:00
loadCalendarType(String(elements.calendarTypeEl.value || "gregorian"), elements);
2026-03-07 01:09:00 -08:00
});
}
function ensureCalendarSection(referenceData, magickDataset) {
if (!referenceData) {
return;
}
2026-03-07 05:17:50 -08:00
calendarDatesUi.init?.({
getSelectedYear: () => state.selectedYear,
getSelectedCalendar: () => state.selectedCalendar,
getIslamicMonths: () => state.calendarData?.islamic || []
});
calendarDetailUi.init?.({
getState: () => state,
getElements,
getSelectedMonth,
getSelectedDayFilterContext,
clearSelectedDayFilter,
toggleDayFilterEntry,
toggleDayRangeFilter,
getMonthSubtitle,
getMonthDayLinkRows,
buildDecanTarotRowsForMonth,
buildHolidayList,
matchesSearch,
eventSearchText,
holidaySearchText,
getDisplayTarotName,
cap,
formatGregorianReferenceDate,
getDaysInMonth,
getMonthStartWeekday,
getGregorianMonthStartDate,
formatCalendarDateFromGregorian,
parseMonthDayToken,
parseMonthDayTokensFromText,
parseMonthDayStartToken,
parseDayRangeFromText,
parseMonthRange,
formatIsoDate,
resolveHolidayGregorianDate,
isMonthDayInRange,
intersectDateRanges,
getGregorianReferenceDateForCalendarMonth,
normalizeCalendarText,
findGodIdByName
});
2026-03-07 01:09:00 -08:00
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
};
})();