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
|
|
|
|
|
|
};
|
|
|
|
|
|
})();
|