434 lines
14 KiB
JavaScript
434 lines
14 KiB
JavaScript
/* ui-holidays.js - Standalone holiday repository browser */
|
|
(function () {
|
|
"use strict";
|
|
|
|
const { getTarotCardDisplayName, getTarotCardSearchAliases } = window.TarotCardImages || {};
|
|
const holidayDataUi = window.HolidayDataUi || {};
|
|
const holidayRenderUi = window.HolidayRenderUi || {};
|
|
|
|
if (
|
|
typeof holidayDataUi.buildAllHolidays !== "function"
|
|
|| typeof holidayDataUi.buildCalendarData !== "function"
|
|
|| typeof holidayDataUi.buildGodsMap !== "function"
|
|
|| typeof holidayDataUi.buildHebrewMap !== "function"
|
|
|| typeof holidayDataUi.buildPlanetMap !== "function"
|
|
|| typeof holidayDataUi.buildSignsMap !== "function"
|
|
|| typeof holidayDataUi.calendarLabel !== "function"
|
|
|| typeof holidayDataUi.formatCalendarDateFromGregorian !== "function"
|
|
|| typeof holidayDataUi.formatGregorianReferenceDate !== "function"
|
|
|| typeof holidayDataUi.monthLabelForCalendar !== "function"
|
|
|| typeof holidayDataUi.normalizeSourceFilter !== "function"
|
|
|| typeof holidayDataUi.resolveHolidayGregorianDate !== "function"
|
|
|| typeof holidayRenderUi.holidaySearchText !== "function"
|
|
|| typeof holidayRenderUi.renderList !== "function"
|
|
|| typeof holidayRenderUi.renderHolidayDetail !== "function"
|
|
) {
|
|
throw new Error("HolidayDataUi and HolidayRenderUi modules must load before ui-holidays.js");
|
|
}
|
|
|
|
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
|
|
};
|
|
|
|
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 getRenderContext(elements = getElements()) {
|
|
return {
|
|
elements,
|
|
state,
|
|
cap,
|
|
normalizeSearchValue,
|
|
getDisplayTarotName,
|
|
resolveTarotTrumpNumber,
|
|
getTarotCardSearchAliases,
|
|
calendarLabel: holidayDataUi.calendarLabel,
|
|
monthLabelForCalendar: (calendarId, monthId) => holidayDataUi.monthLabelForCalendar(state.calendarData, calendarId, monthId),
|
|
normalizeSourceFilter: holidayDataUi.normalizeSourceFilter,
|
|
filterBySource,
|
|
resolveHolidayGregorianDate: (holiday) => holidayDataUi.resolveHolidayGregorianDate(holiday, {
|
|
selectedYear: state.selectedYear,
|
|
calendarData: state.calendarData
|
|
}),
|
|
formatGregorianReferenceDate: holidayDataUi.formatGregorianReferenceDate,
|
|
formatCalendarDateFromGregorian: holidayDataUi.formatCalendarDateFromGregorian,
|
|
selectByHolidayId
|
|
};
|
|
}
|
|
|
|
function getSelectedHoliday() {
|
|
return state.holidays.find((holiday) => holiday.id === state.selectedHolidayId) || null;
|
|
}
|
|
|
|
function filterBySource(holidays) {
|
|
const source = holidayDataUi.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 = holidayDataUi.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) {
|
|
holidayRenderUi.renderList(getRenderContext(elements));
|
|
}
|
|
|
|
function renderHolidayDetail(holiday) {
|
|
return holidayRenderUi.renderHolidayDetail(holiday, getRenderContext());
|
|
}
|
|
|
|
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 = `${holidayDataUi.calendarLabel(holiday?.calendarId)} - ${holidayDataUi.monthLabelForCalendar(state.calendarData, 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) => holidayRenderUi.holidaySearchText(holiday, getRenderContext()).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 = holidayDataUi.normalizeSourceFilter(state.selectedSource);
|
|
if (activeFilter !== "all" && activeFilter !== targetCalendar) {
|
|
state.selectedSource = targetCalendar || "all";
|
|
}
|
|
|
|
if (state.searchQuery && !holidayRenderUi.holidaySearchText(target, getRenderContext()).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 = holidayDataUi.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 = holidayDataUi.buildPlanetMap(referenceData.planets);
|
|
state.signsById = holidayDataUi.buildSignsMap(referenceData.signs);
|
|
state.godsById = holidayDataUi.buildGodsMap(state.magickDataset);
|
|
state.hebrewById = holidayDataUi.buildHebrewMap(state.magickDataset);
|
|
|
|
state.calendarData = holidayDataUi.buildCalendarData(referenceData);
|
|
state.holidays = holidayDataUi.buildAllHolidays(state.referenceData);
|
|
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
|
|
};
|
|
})();
|