1108 lines
34 KiB
JavaScript
1108 lines
34 KiB
JavaScript
|
|
/* ui-holidays.js - Standalone holiday repository browser */
|
||
|
|
(function () {
|
||
|
|
"use strict";
|
||
|
|
|
||
|
|
const { getTarotCardDisplayName, getTarotCardSearchAliases } = window.TarotCardImages || {};
|
||
|
|
|
||
|
|
const state = {
|
||
|
|
initialized: false,
|
||
|
|
referenceData: null,
|
||
|
|
magickDataset: null,
|
||
|
|
selectedYear: new Date().getFullYear(),
|
||
|
|
selectedSource: "all",
|
||
|
|
searchQuery: "",
|
||
|
|
holidays: [],
|
||
|
|
filteredHolidays: [],
|
||
|
|
selectedHolidayId: null,
|
||
|
|
planetsById: new Map(),
|
||
|
|
signsById: new Map(),
|
||
|
|
godsById: new Map(),
|
||
|
|
hebrewById: new Map(),
|
||
|
|
calendarData: {}
|
||
|
|
};
|
||
|
|
|
||
|
|
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 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 {
|
||
|
|
sourceSelectEl: document.getElementById("holiday-source-select"),
|
||
|
|
yearInputEl: document.getElementById("holiday-year-input"),
|
||
|
|
searchInputEl: document.getElementById("holiday-search-input"),
|
||
|
|
searchClearEl: document.getElementById("holiday-search-clear"),
|
||
|
|
countEl: document.getElementById("holiday-count"),
|
||
|
|
listEl: document.getElementById("holiday-list"),
|
||
|
|
detailNameEl: document.getElementById("holiday-detail-name"),
|
||
|
|
detailSubEl: document.getElementById("holiday-detail-sub"),
|
||
|
|
detailBodyEl: document.getElementById("holiday-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 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 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 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 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 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 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 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 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 calendarLabel(calendarId) {
|
||
|
|
const key = String(calendarId || "").trim().toLowerCase();
|
||
|
|
if (key === "hebrew") return "Hebrew";
|
||
|
|
if (key === "islamic") return "Islamic";
|
||
|
|
if (key === "wheel-of-year") return "Wheel of the Year";
|
||
|
|
return "Gregorian";
|
||
|
|
}
|
||
|
|
|
||
|
|
function monthLabelForCalendar(calendarId, monthId) {
|
||
|
|
const months = state.calendarData?.[calendarId];
|
||
|
|
if (!Array.isArray(months)) {
|
||
|
|
return monthId || "--";
|
||
|
|
}
|
||
|
|
|
||
|
|
const month = months.find((entry) => String(entry?.id || "").toLowerCase() === String(monthId || "").toLowerCase());
|
||
|
|
return month?.name || monthId || "--";
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizeSourceFilter(value) {
|
||
|
|
const key = String(value || "").trim().toLowerCase();
|
||
|
|
if (key === "gregorian" || key === "hebrew" || key === "islamic" || key === "wheel-of-year") {
|
||
|
|
return key;
|
||
|
|
}
|
||
|
|
return "all";
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildAllHolidays() {
|
||
|
|
if (Array.isArray(state.referenceData?.calendarHolidays) && state.referenceData.calendarHolidays.length) {
|
||
|
|
return [...state.referenceData.calendarHolidays].sort((left, right) => {
|
||
|
|
const calCmp = calendarLabel(left?.calendarId).localeCompare(calendarLabel(right?.calendarId));
|
||
|
|
if (calCmp !== 0) return calCmp;
|
||
|
|
|
||
|
|
const leftDay = Number(left?.day);
|
||
|
|
const rightDay = Number(right?.day);
|
||
|
|
if (Number.isFinite(leftDay) && Number.isFinite(rightDay) && leftDay !== rightDay) {
|
||
|
|
return leftDay - rightDay;
|
||
|
|
}
|
||
|
|
|
||
|
|
return String(left?.name || "").localeCompare(String(right?.name || ""));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const legacy = Array.isArray(state.referenceData?.celestialHolidays) ? state.referenceData.celestialHolidays : [];
|
||
|
|
return legacy.map((holiday) => ({
|
||
|
|
...holiday,
|
||
|
|
calendarId: "gregorian",
|
||
|
|
dateText: holiday?.date || holiday?.dateRange || ""
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
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 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 getSelectedHoliday() {
|
||
|
|
return state.holidays.find((holiday) => holiday.id === state.selectedHolidayId) || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function filterBySource(holidays) {
|
||
|
|
const source = normalizeSourceFilter(state.selectedSource);
|
||
|
|
if (source === "all") {
|
||
|
|
return [...holidays];
|
||
|
|
}
|
||
|
|
return holidays.filter((holiday) => String(holiday?.calendarId || "").trim().toLowerCase() === source);
|
||
|
|
}
|
||
|
|
|
||
|
|
function syncControls(elements) {
|
||
|
|
if (elements.sourceSelectEl) {
|
||
|
|
elements.sourceSelectEl.value = normalizeSourceFilter(state.selectedSource);
|
||
|
|
}
|
||
|
|
if (elements.yearInputEl) {
|
||
|
|
elements.yearInputEl.value = String(state.selectedYear);
|
||
|
|
}
|
||
|
|
if (elements.searchInputEl) {
|
||
|
|
elements.searchInputEl.value = state.searchQuery;
|
||
|
|
}
|
||
|
|
if (elements.searchClearEl) {
|
||
|
|
elements.searchClearEl.disabled = !state.searchQuery;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderList(elements) {
|
||
|
|
const { listEl, countEl } = elements;
|
||
|
|
if (!listEl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
listEl.innerHTML = "";
|
||
|
|
|
||
|
|
state.filteredHolidays.forEach((holiday) => {
|
||
|
|
const isSelected = holiday.id === state.selectedHolidayId;
|
||
|
|
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.holidayId = holiday.id;
|
||
|
|
|
||
|
|
const sourceCalendar = calendarLabel(holiday.calendarId);
|
||
|
|
const sourceMonth = monthLabelForCalendar(holiday.calendarId, holiday.monthId);
|
||
|
|
const sourceDate = holiday?.dateText || holiday?.date || holiday?.dateRange || "--";
|
||
|
|
|
||
|
|
itemEl.innerHTML = `
|
||
|
|
<div class="planet-list-name">${holiday?.name || holiday?.id || "Holiday"}</div>
|
||
|
|
<div class="planet-list-meta">${sourceCalendar} - ${sourceMonth} - ${sourceDate}</div>
|
||
|
|
`;
|
||
|
|
|
||
|
|
itemEl.addEventListener("click", () => {
|
||
|
|
selectByHolidayId(holiday.id, elements);
|
||
|
|
});
|
||
|
|
|
||
|
|
listEl.appendChild(itemEl);
|
||
|
|
});
|
||
|
|
|
||
|
|
if (countEl) {
|
||
|
|
const sourceFiltered = filterBySource(state.holidays);
|
||
|
|
const activeFilter = normalizeSourceFilter(state.selectedSource);
|
||
|
|
const sourceLabel = activeFilter === "all"
|
||
|
|
? ""
|
||
|
|
: ` (${calendarLabel(activeFilter)})`;
|
||
|
|
countEl.textContent = state.searchQuery
|
||
|
|
? `${state.filteredHolidays.length} of ${sourceFiltered.length} holidays${sourceLabel}`
|
||
|
|
: `${sourceFiltered.length} holidays${sourceLabel}`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderHolidayDetail(holiday) {
|
||
|
|
const gregorianDate = resolveHolidayGregorianDate(holiday);
|
||
|
|
const gregorianRef = formatGregorianReferenceDate(gregorianDate);
|
||
|
|
const hebrewRef = formatCalendarDateFromGregorian(gregorianDate, "hebrew");
|
||
|
|
const islamicRef = formatCalendarDateFromGregorian(gregorianDate, "islamic");
|
||
|
|
const confidence = String(holiday?.conversionConfidence || holiday?.datePrecision || "approximate").toLowerCase();
|
||
|
|
const confidenceLabel = (!(gregorianDate instanceof Date) || Number.isNaN(gregorianDate.getTime()))
|
||
|
|
? "unresolved"
|
||
|
|
: (confidence === "exact" ? "exact" : "approximate");
|
||
|
|
const monthName = monthLabelForCalendar(holiday?.calendarId, holiday?.monthId);
|
||
|
|
const holidayDate = holiday?.dateText || holiday?.date || holiday?.dateRange || "--";
|
||
|
|
const sourceMonthLink = holiday?.monthId
|
||
|
|
? `<div class="alpha-nav-btns"><button class="alpha-nav-btn" data-nav="calendar-month" data-calendar-id="${holiday.calendarId || ""}" data-month-id="${holiday.monthId}">Open ${calendarLabel(holiday?.calendarId)} ${monthName} -></button></div>`
|
||
|
|
: "";
|
||
|
|
|
||
|
|
return `
|
||
|
|
<div class="planet-meta-grid">
|
||
|
|
<div class="planet-meta-card">
|
||
|
|
<strong>Holiday Facts</strong>
|
||
|
|
<div class="planet-text">
|
||
|
|
<dl class="alpha-dl">
|
||
|
|
<dt>Source Calendar</dt><dd>${calendarLabel(holiday?.calendarId)}</dd>
|
||
|
|
<dt>Source Month</dt><dd>${monthName}</dd>
|
||
|
|
<dt>Source Date</dt><dd>${holidayDate}</dd>
|
||
|
|
<dt>Reference Year</dt><dd>${state.selectedYear}</dd>
|
||
|
|
<dt>Conversion</dt><dd>${confidenceLabel}</dd>
|
||
|
|
</dl>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="planet-meta-card">
|
||
|
|
<strong>Cross-Calendar Dates</strong>
|
||
|
|
<div class="planet-text">
|
||
|
|
<dl class="alpha-dl">
|
||
|
|
<dt>Gregorian</dt><dd>${gregorianRef}</dd>
|
||
|
|
<dt>Hebrew</dt><dd>${hebrewRef}</dd>
|
||
|
|
<dt>Islamic</dt><dd>${islamicRef}</dd>
|
||
|
|
</dl>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="planet-meta-card">
|
||
|
|
<strong>Description</strong>
|
||
|
|
<div class="planet-text">${holiday?.description || "--"}</div>
|
||
|
|
${sourceMonthLink}
|
||
|
|
</div>
|
||
|
|
<div class="planet-meta-card">
|
||
|
|
<strong>Associations</strong>
|
||
|
|
${buildAssociationButtons(holiday?.associations)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderDetail(elements) {
|
||
|
|
const { detailNameEl, detailSubEl, detailBodyEl } = elements;
|
||
|
|
if (!detailNameEl || !detailSubEl || !detailBodyEl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const holiday = getSelectedHoliday();
|
||
|
|
if (!holiday) {
|
||
|
|
detailNameEl.textContent = "--";
|
||
|
|
detailSubEl.textContent = "Select a holiday to explore";
|
||
|
|
detailBodyEl.innerHTML = "";
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
detailNameEl.textContent = holiday?.name || holiday?.id || "Holiday";
|
||
|
|
detailSubEl.textContent = `${calendarLabel(holiday?.calendarId)} - ${monthLabelForCalendar(holiday?.calendarId, holiday?.monthId)}`;
|
||
|
|
detailBodyEl.innerHTML = renderHolidayDetail(holiday);
|
||
|
|
attachNavHandlers(detailBodyEl);
|
||
|
|
}
|
||
|
|
|
||
|
|
function applyFilters(elements) {
|
||
|
|
const sourceFiltered = filterBySource(state.holidays);
|
||
|
|
state.filteredHolidays = state.searchQuery
|
||
|
|
? sourceFiltered.filter((holiday) => holidaySearchText(holiday).includes(state.searchQuery))
|
||
|
|
: sourceFiltered;
|
||
|
|
|
||
|
|
if (!state.filteredHolidays.some((holiday) => holiday.id === state.selectedHolidayId)) {
|
||
|
|
state.selectedHolidayId = state.filteredHolidays[0]?.id || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
syncControls(elements);
|
||
|
|
renderList(elements);
|
||
|
|
renderDetail(elements);
|
||
|
|
}
|
||
|
|
|
||
|
|
function selectByHolidayId(holidayId, elements = getElements()) {
|
||
|
|
const target = state.holidays.find((holiday) => holiday.id === holidayId);
|
||
|
|
if (!target) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
const targetCalendar = String(target.calendarId || "").trim().toLowerCase();
|
||
|
|
const activeFilter = normalizeSourceFilter(state.selectedSource);
|
||
|
|
if (activeFilter !== "all" && activeFilter !== targetCalendar) {
|
||
|
|
state.selectedSource = targetCalendar || "all";
|
||
|
|
}
|
||
|
|
|
||
|
|
if (state.searchQuery && !holidaySearchText(target).includes(state.searchQuery)) {
|
||
|
|
state.searchQuery = "";
|
||
|
|
}
|
||
|
|
|
||
|
|
state.selectedHolidayId = target.id;
|
||
|
|
applyFilters(elements);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
function bindControls(elements) {
|
||
|
|
if (elements.sourceSelectEl) {
|
||
|
|
elements.sourceSelectEl.addEventListener("change", () => {
|
||
|
|
state.selectedSource = normalizeSourceFilter(elements.sourceSelectEl.value);
|
||
|
|
applyFilters(elements);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (elements.yearInputEl) {
|
||
|
|
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);
|
||
|
|
renderDetail(elements);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (elements.searchInputEl) {
|
||
|
|
elements.searchInputEl.addEventListener("input", () => {
|
||
|
|
state.searchQuery = normalizeSearchValue(elements.searchInputEl.value);
|
||
|
|
applyFilters(elements);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (elements.searchClearEl && elements.searchInputEl) {
|
||
|
|
elements.searchClearEl.addEventListener("click", () => {
|
||
|
|
state.searchQuery = "";
|
||
|
|
elements.searchInputEl.value = "";
|
||
|
|
applyFilters(elements);
|
||
|
|
elements.searchInputEl.focus();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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
|
||
|
|
}
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function ensureHolidaySection(referenceData, magickDataset) {
|
||
|
|
if (!referenceData) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
state.referenceData = referenceData;
|
||
|
|
state.magickDataset = magickDataset || null;
|
||
|
|
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 : []
|
||
|
|
};
|
||
|
|
|
||
|
|
state.holidays = buildAllHolidays();
|
||
|
|
if (!state.selectedHolidayId || !state.holidays.some((holiday) => holiday.id === state.selectedHolidayId)) {
|
||
|
|
state.selectedHolidayId = state.holidays[0]?.id || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const elements = getElements();
|
||
|
|
|
||
|
|
if (!state.initialized) {
|
||
|
|
state.initialized = true;
|
||
|
|
bindControls(elements);
|
||
|
|
}
|
||
|
|
|
||
|
|
applyFilters(elements);
|
||
|
|
}
|
||
|
|
|
||
|
|
window.HolidaySectionUi = {
|
||
|
|
ensureHolidaySection,
|
||
|
|
selectByHolidayId
|
||
|
|
};
|
||
|
|
})();
|