Files
TaroTime/app/ui-tarot.js
2026-03-07 13:38:13 -08:00

730 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function () {
const { resolveTarotCardImage, getTarotCardDisplayName, getTarotCardSearchAliases } = window.TarotCardImages || {};
const tarotHouseUi = window.TarotHouseUi || {};
const tarotRelationsUi = window.TarotRelationsUi || {};
const tarotCardDerivations = window.TarotCardDerivations || {};
const tarotDetailUi = window.TarotDetailUi || {};
const tarotRelationDisplay = window.TarotRelationDisplay || {};
const state = {
initialized: false,
cards: [],
filteredCards: [],
searchQuery: "",
selectedCardId: "",
magickDataset: null,
referenceData: null,
monthRefsByCardId: new Map(),
courtCardByDecanId: new Map()
};
const TAROT_TRUMP_NUMBER_BY_NAME = {
"the fool": 0,
fool: 0,
"the magus": 1,
magus: 1,
magician: 1,
"the high priestess": 2,
"high priestess": 2,
"the empress": 3,
empress: 3,
"the emperor": 4,
emperor: 4,
"the hierophant": 5,
hierophant: 5,
"the lovers": 6,
lovers: 6,
"the chariot": 7,
chariot: 7,
strength: 8,
lust: 8,
"the hermit": 9,
hermit: 9,
fortune: 10,
"wheel of fortune": 10,
justice: 11,
"the hanged man": 12,
"hanged man": 12,
death: 13,
temperance: 14,
art: 14,
"the devil": 15,
devil: 15,
"the tower": 16,
tower: 16,
"the star": 17,
star: 17,
"the moon": 18,
moon: 18,
"the sun": 19,
sun: 19,
aeon: 20,
judgement: 20,
judgment: 20,
universe: 21,
world: 21,
"the world": 21
};
const MINOR_NUMBER_WORD_BY_VALUE = {
1: "ace",
2: "two",
3: "three",
4: "four",
5: "five",
6: "six",
7: "seven",
8: "eight",
9: "nine",
10: "ten"
};
const HEBREW_LETTER_ALIASES = {
aleph: "alef",
alef: "alef",
heh: "he",
he: "he",
beth: "bet",
bet: "bet",
cheth: "het",
chet: "het",
kaph: "kaf",
kaf: "kaf",
peh: "pe",
tzaddi: "tsadi",
tzadi: "tsadi",
tsadi: "tsadi",
qoph: "qof",
qof: "qof",
taw: "tav",
tau: "tav"
};
const CUBE_MOTHER_CONNECTOR_BY_LETTER = {
alef: { connectorId: "above-below", connectorName: "Above ↔ Below" },
mem: { connectorId: "east-west", connectorName: "East ↔ West" },
shin: { connectorId: "south-north", connectorName: "South ↔ North" }
};
const ELEMENT_NAME_BY_ID = {
water: "Water",
fire: "Fire",
air: "Air",
earth: "Earth"
};
const ELEMENT_HEBREW_LETTER_BY_ID = {
fire: "Yod",
water: "Heh",
air: "Vav",
earth: "Heh"
};
const ELEMENT_HEBREW_CHAR_BY_ID = {
fire: "י",
water: "ה",
air: "ו",
earth: "ה"
};
const HEBREW_LETTER_ID_BY_TETRAGRAMMATON_LETTER = {
yod: "yod",
heh: "he",
vav: "vav"
};
const ACE_ELEMENT_BY_CARD_NAME = {
"ace of cups": "water",
"ace of wands": "fire",
"ace of swords": "air",
"ace of disks": "earth"
};
const COURT_ELEMENT_BY_RANK = {
knight: "fire",
queen: "water",
prince: "air",
princess: "earth"
};
const SUIT_ELEMENT_BY_SUIT = {
wands: "fire",
cups: "water",
swords: "air",
disks: "earth"
};
const MINOR_RANK_NUMBER_BY_NAME = {
ace: 1,
two: 2,
three: 3,
four: 4,
five: 5,
six: 6,
seven: 7,
eight: 8,
nine: 9,
ten: 10
};
const SMALL_CARD_SIGN_BY_MODALITY_AND_SUIT = {
cardinal: {
wands: "aries",
cups: "cancer",
swords: "libra",
disks: "capricorn"
},
fixed: {
wands: "leo",
cups: "scorpio",
swords: "aquarius",
disks: "taurus"
},
mutable: {
wands: "sagittarius",
cups: "pisces",
swords: "gemini",
disks: "virgo"
}
};
function slugify(value) {
return String(value || "")
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
}
function cardId(card) {
const suitPart = card.suit ? `-${slugify(card.suit)}` : "";
return `${slugify(card.arcana)}${suitPart}-${slugify(card.name)}`;
}
function getElements() {
return {
tarotCardListEl: document.getElementById("tarot-card-list"),
tarotSearchInputEl: document.getElementById("tarot-search-input"),
tarotSearchClearEl: document.getElementById("tarot-search-clear"),
tarotCountEl: document.getElementById("tarot-card-count"),
tarotDetailImageEl: document.getElementById("tarot-detail-image"),
tarotDetailNameEl: document.getElementById("tarot-detail-name"),
tarotDetailTypeEl: document.getElementById("tarot-detail-type"),
tarotDetailSummaryEl: document.getElementById("tarot-detail-summary"),
tarotDetailUprightEl: document.getElementById("tarot-detail-upright"),
tarotDetailReversedEl: document.getElementById("tarot-detail-reversed"),
tarotMetaMeaningCardEl: document.getElementById("tarot-meta-meaning-card"),
tarotDetailMeaningEl: document.getElementById("tarot-detail-meaning"),
tarotDetailKeywordsEl: document.getElementById("tarot-detail-keywords"),
tarotMetaPlanetCardEl: document.getElementById("tarot-meta-planet-card"),
tarotMetaElementCardEl: document.getElementById("tarot-meta-element-card"),
tarotMetaTetragrammatonCardEl: document.getElementById("tarot-meta-tetragrammaton-card"),
tarotMetaZodiacCardEl: document.getElementById("tarot-meta-zodiac-card"),
tarotMetaCourtDateCardEl: document.getElementById("tarot-meta-courtdate-card"),
tarotMetaHebrewCardEl: document.getElementById("tarot-meta-hebrew-card"),
tarotMetaCubeCardEl: document.getElementById("tarot-meta-cube-card"),
tarotMetaCalendarCardEl: document.getElementById("tarot-meta-calendar-card"),
tarotDetailPlanetEl: document.getElementById("tarot-detail-planet"),
tarotDetailElementEl: document.getElementById("tarot-detail-element"),
tarotDetailTetragrammatonEl: document.getElementById("tarot-detail-tetragrammaton"),
tarotDetailZodiacEl: document.getElementById("tarot-detail-zodiac"),
tarotDetailCourtDateEl: document.getElementById("tarot-detail-courtdate"),
tarotDetailHebrewEl: document.getElementById("tarot-detail-hebrew"),
tarotDetailCubeEl: document.getElementById("tarot-detail-cube"),
tarotDetailCalendarEl: document.getElementById("tarot-detail-calendar"),
tarotKabPathEl: document.getElementById("tarot-kab-path"),
tarotHouseOfCardsEl: document.getElementById("tarot-house-of-cards")
};
}
function normalizeRelationId(value) {
return String(value || "")
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
}
if (typeof tarotRelationDisplay.createTarotRelationDisplay !== "function") {
throw new Error("TarotRelationDisplay.createTarotRelationDisplay is unavailable. Ensure app/ui-tarot-relation-display.js loads before app/ui-tarot.js.");
}
if (typeof tarotCardDerivations.createTarotCardDerivations !== "function") {
throw new Error("TarotCardDerivations.createTarotCardDerivations is unavailable. Ensure app/ui-tarot-card-derivations.js loads before app/ui-tarot.js.");
}
if (typeof tarotDetailUi.createTarotDetailRenderer !== "function") {
throw new Error("TarotDetailUi.createTarotDetailRenderer is unavailable. Ensure app/ui-tarot-detail.js loads before app/ui-tarot.js.");
}
const tarotCardDerivationsUi = tarotCardDerivations.createTarotCardDerivations({
normalizeRelationId,
normalizeTarotCardLookupName,
toTitleCase,
getReferenceData: () => state.referenceData,
ELEMENT_NAME_BY_ID,
ELEMENT_HEBREW_LETTER_BY_ID,
ELEMENT_HEBREW_CHAR_BY_ID,
HEBREW_LETTER_ID_BY_TETRAGRAMMATON_LETTER,
ACE_ELEMENT_BY_CARD_NAME,
COURT_ELEMENT_BY_RANK,
MINOR_RANK_NUMBER_BY_NAME,
SMALL_CARD_SIGN_BY_MODALITY_AND_SUIT,
MINOR_PLURAL_BY_RANK
});
const tarotRelationDisplayUi = tarotRelationDisplay.createTarotRelationDisplay({
normalizeRelationId
});
const tarotDetailRenderer = tarotDetailUi.createTarotDetailRenderer({
getMonthRefsByCardId: () => state.monthRefsByCardId,
getMagickDataset: () => state.magickDataset,
resolveTarotCardImage,
getDisplayCardName,
buildTypeLabel,
clearChildren,
normalizeRelationObject,
buildElementRelationsForCard,
buildTetragrammatonRelationsForCard,
buildSmallCardRulershipRelation,
buildSmallCardCourtLinkRelations,
buildCubeRelationsForCard,
parseMonthDayToken,
createRelationListItem,
findSephirahForMinorCard
});
function normalizeSearchValue(value) {
return String(value || "").trim().toLowerCase();
}
function normalizeTarotName(value) {
return String(value || "")
.trim()
.toLowerCase()
.replace(/\s+/g, " ");
}
function normalizeTarotCardLookupName(value) {
const text = normalizeTarotName(value)
.replace(/\b(pentacles?|coins?)\b/g, "disks");
const match = text.match(/^(\d{1,2})\s+of\s+(.+)$/i);
if (!match) {
return text;
}
const numeric = Number(match[1]);
const suit = String(match[2] || "").trim();
const rankWord = MINOR_NUMBER_WORD_BY_VALUE[numeric];
if (!rankWord || !suit) {
return text;
}
return `${rankWord} of ${suit}`;
}
function getDisplayCardName(cardOrName, trumpNumber) {
const cardName = typeof cardOrName === "object"
? String(cardOrName?.name || "")
: String(cardOrName || "");
const resolvedTrumpNumber = typeof cardOrName === "object"
? cardOrName?.number
: trumpNumber;
if (typeof getTarotCardDisplayName === "function") {
const display = String(getTarotCardDisplayName(cardName, { trumpNumber: resolvedTrumpNumber }) || "").trim();
if (display) {
return display;
}
}
return cardName.trim();
}
function toTitleCase(value) {
return String(value || "")
.split(" ")
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(" ");
}
function buildElementRelationsForCard(card, baseElementRelations = []) {
return tarotCardDerivationsUi.buildElementRelationsForCard(card, baseElementRelations);
}
function buildTetragrammatonRelationsForCard(card) {
return tarotCardDerivationsUi.buildTetragrammatonRelationsForCard(card);
}
function buildSmallCardRulershipRelation(card) {
return tarotCardDerivationsUi.buildSmallCardRulershipRelation(card);
}
function buildCourtCardByDecanId(cards) {
if (typeof tarotRelationsUi.buildCourtCardByDecanId !== "function") {
return new Map();
}
return tarotRelationsUi.buildCourtCardByDecanId(cards);
}
function buildSmallCardCourtLinkRelations(card, relations) {
if (typeof tarotRelationsUi.buildSmallCardCourtLinkRelations !== "function") {
return [];
}
return tarotRelationsUi.buildSmallCardCourtLinkRelations(card, relations, state.courtCardByDecanId);
}
function parseMonthDayToken(value) {
if (typeof tarotRelationsUi.parseMonthDayToken !== "function") {
return null;
}
return tarotRelationsUi.parseMonthDayToken(value);
}
function buildMonthReferencesByCard(referenceData, cards) {
if (typeof tarotRelationsUi.buildMonthReferencesByCard !== "function") {
return new Map();
}
return tarotRelationsUi.buildMonthReferencesByCard(referenceData, cards);
}
function relationToSearchText(relation) {
if (!relation) {
return "";
}
if (typeof relation === "string") {
return relation;
}
const relationParts = [
relation.label,
relation.type,
relation.id,
relation.data && typeof relation.data === "object"
? Object.values(relation.data).join(" ")
: ""
];
return relationParts.filter(Boolean).join(" ");
}
function buildCardSearchText(card) {
const displayName = getDisplayCardName(card);
const tarotAliases = typeof getTarotCardSearchAliases === "function"
? getTarotCardSearchAliases(card?.name, { trumpNumber: card?.number })
: [];
const parts = [
card.name,
displayName,
card.arcana,
card.rank,
card.suit,
card.summary,
...tarotAliases,
...(Array.isArray(card.keywords) ? card.keywords : []),
...(Array.isArray(card.relations) ? card.relations.map(relationToSearchText) : [])
];
return normalizeSearchValue(parts.join(" "));
}
function applySearchFilter(elements) {
const query = normalizeSearchValue(state.searchQuery);
state.filteredCards = query
? state.cards.filter((card) => buildCardSearchText(card).includes(query))
: [...state.cards];
if (elements?.tarotSearchClearEl) {
elements.tarotSearchClearEl.disabled = !query;
}
renderList(elements);
if (!state.filteredCards.some((card) => card.id === state.selectedCardId)) {
if (state.filteredCards.length > 0) {
selectCardById(state.filteredCards[0].id, elements);
}
return;
}
updateListSelection(elements);
}
function clearChildren(element) {
if (element) {
element.replaceChildren();
}
}
function updateHouseSelection(elements) {
tarotHouseUi.updateSelection?.(elements);
}
function renderHouseOfCards(elements) {
tarotHouseUi.render?.(elements);
}
function buildTypeLabel(card) {
return tarotCardDerivationsUi.buildTypeLabel(card);
}
const MINOR_PLURAL_BY_RANK = {
ace: "aces",
two: "twos",
three: "threes",
four: "fours",
five: "fives",
six: "sixes",
seven: "sevens",
eight: "eights",
nine: "nines",
ten: "tens"
};
function findSephirahForMinorCard(card, kabTree) {
return tarotCardDerivationsUi.findSephirahForMinorCard(card, kabTree);
}
function formatRelation(relation) {
return tarotRelationDisplayUi.formatRelation(relation);
}
function relationKey(relation, index) {
return tarotRelationDisplayUi.relationKey(relation, index);
}
function normalizeRelationObject(relation, index) {
return tarotRelationDisplayUi.normalizeRelationObject(relation, index);
}
function formatRelationDataLines(relation) {
return tarotRelationDisplayUi.formatRelationDataLines(relation);
}
function buildCubeRelationsForCard(card) {
if (typeof tarotRelationsUi.buildCubeRelationsForCard !== "function") {
return [];
}
return tarotRelationsUi.buildCubeRelationsForCard(card, state.magickDataset);
}
// Returns nav dispatch config for relations that have a corresponding section,
// null for informational-only relations.
function getRelationNavTarget(relation) {
return tarotRelationDisplayUi.getRelationNavTarget(relation);
}
function createRelationListItem(relation) {
return tarotRelationDisplayUi.createRelationListItem(relation);
}
function renderStaticRelationGroup(targetEl, cardEl, relations) {
tarotDetailRenderer.renderStaticRelationGroup(targetEl, cardEl, relations);
}
function renderDetail(card, elements) {
tarotDetailRenderer.renderDetail(card, elements);
}
function updateListSelection(elements) {
if (!elements?.tarotCardListEl) {
return;
}
const buttons = elements.tarotCardListEl.querySelectorAll(".tarot-list-item");
buttons.forEach((button) => {
const isSelected = button.dataset.cardId === state.selectedCardId;
button.classList.toggle("is-selected", isSelected);
button.setAttribute("aria-selected", isSelected ? "true" : "false");
});
}
function selectCardById(cardIdToSelect, elements) {
const card = state.cards.find((entry) => entry.id === cardIdToSelect);
if (!card) {
return;
}
state.selectedCardId = card.id;
updateListSelection(elements);
updateHouseSelection(elements);
renderDetail(card, elements);
}
function renderList(elements) {
if (!elements?.tarotCardListEl) {
return;
}
clearChildren(elements.tarotCardListEl);
state.filteredCards.forEach((card) => {
const cardDisplayName = getDisplayCardName(card);
const button = document.createElement("button");
button.type = "button";
button.className = "tarot-list-item";
button.dataset.cardId = card.id;
button.setAttribute("role", "option");
const nameEl = document.createElement("span");
nameEl.className = "tarot-list-name";
nameEl.textContent = cardDisplayName || card.name;
const metaEl = document.createElement("span");
metaEl.className = "tarot-list-meta";
metaEl.textContent = buildTypeLabel(card);
button.append(nameEl, metaEl);
elements.tarotCardListEl.appendChild(button);
});
if (elements.tarotCountEl) {
elements.tarotCountEl.textContent = state.searchQuery
? `${state.filteredCards.length} of ${state.cards.length} cards`
: `${state.cards.length} cards`;
}
}
function ensureTarotSection(referenceData, magickDataset = null) {
state.referenceData = referenceData || state.referenceData;
if (magickDataset) {
state.magickDataset = magickDataset;
}
tarotHouseUi.init?.({
resolveTarotCardImage,
getDisplayCardName,
clearChildren,
normalizeTarotCardLookupName,
selectCardById,
getCards: () => state.cards,
getSelectedCardId: () => state.selectedCardId
});
const elements = getElements();
if (state.initialized) {
state.monthRefsByCardId = buildMonthReferencesByCard(referenceData, state.cards);
state.courtCardByDecanId = buildCourtCardByDecanId(state.cards);
renderHouseOfCards(elements);
if (state.selectedCardId) {
const selected = state.cards.find((card) => card.id === state.selectedCardId);
if (selected) {
renderDetail(selected, elements);
}
}
return;
}
if (!elements.tarotCardListEl || !elements.tarotDetailNameEl) {
return;
}
const databaseBuilder = window.TarotCardDatabase?.buildTarotDatabase;
if (typeof databaseBuilder !== "function") {
return;
}
const cards = databaseBuilder(referenceData, magickDataset).map((card) => ({
...card,
id: cardId(card)
}));
state.cards = cards;
state.monthRefsByCardId = buildMonthReferencesByCard(referenceData, cards);
state.courtCardByDecanId = buildCourtCardByDecanId(cards);
state.filteredCards = [...cards];
renderList(elements);
renderHouseOfCards(elements);
if (cards.length > 0) {
selectCardById(cards[0].id, elements);
}
elements.tarotCardListEl.addEventListener("click", (event) => {
const target = event.target;
if (!(target instanceof Node)) {
return;
}
const button = target instanceof Element
? target.closest(".tarot-list-item")
: null;
if (!(button instanceof HTMLButtonElement)) {
return;
}
const selectedId = button.dataset.cardId;
if (!selectedId) {
return;
}
selectCardById(selectedId, elements);
});
if (elements.tarotSearchInputEl) {
elements.tarotSearchInputEl.addEventListener("input", () => {
state.searchQuery = elements.tarotSearchInputEl.value || "";
applySearchFilter(elements);
});
}
if (elements.tarotSearchClearEl && elements.tarotSearchInputEl) {
elements.tarotSearchClearEl.addEventListener("click", () => {
elements.tarotSearchInputEl.value = "";
state.searchQuery = "";
applySearchFilter(elements);
elements.tarotSearchInputEl.focus();
});
}
if (elements.tarotDetailImageEl) {
elements.tarotDetailImageEl.addEventListener("click", () => {
const src = elements.tarotDetailImageEl.getAttribute("src") || "";
if (!src || elements.tarotDetailImageEl.style.display === "none") {
return;
}
window.TarotUiLightbox?.open?.(src, elements.tarotDetailImageEl.alt || "Tarot card enlarged image");
});
}
state.initialized = true;
}
function selectCardByTrump(trumpNumber) {
if (!state.initialized) return;
const el = getElements();
const card = state.cards.find(c => c.arcana === "Major" && c.number === trumpNumber);
if (!card) return;
selectCardById(card.id, el);
const listItem = el.tarotCardListEl?.querySelector(`[data-card-id="${card.id}"]`);
listItem?.scrollIntoView({ block: "nearest" });
}
function selectCardByName(name) {
if (!state.initialized) return;
const el = getElements();
const needle = normalizeTarotCardLookupName(name);
const card = state.cards.find((entry) => normalizeTarotCardLookupName(entry.name) === needle);
if (!card) return;
selectCardById(card.id, el);
el.tarotCardListEl
?.querySelector(`[data-card-id="${card.id}"]`)
?.scrollIntoView({ block: "nearest" });
}
window.TarotSectionUi = {
ensureTarotSection,
selectCardByTrump,
selectCardByName,
getCards: () => state.cards
};
})();