1048 lines
33 KiB
JavaScript
1048 lines
33 KiB
JavaScript
(function () {
|
||
const dataService = window.TarotDataService || {};
|
||
const {
|
||
resolveTarotCardImage,
|
||
resolveTarotCardThumbnail,
|
||
getTarotCardDisplayName,
|
||
getTarotCardSearchAliases,
|
||
getDeckOptions,
|
||
getActiveDeck
|
||
} = 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: "",
|
||
houseFocusMode: false,
|
||
houseTopCardsVisible: true,
|
||
houseTopInfoModes: {
|
||
hebrew: true,
|
||
planet: true,
|
||
zodiac: true,
|
||
trump: true,
|
||
path: true
|
||
},
|
||
houseBottomCardsVisible: true,
|
||
houseBottomInfoModes: {
|
||
zodiac: true,
|
||
decan: true,
|
||
month: true,
|
||
ruler: true,
|
||
date: false
|
||
},
|
||
houseExportInProgress: false,
|
||
houseExportFormat: "png",
|
||
magickDataset: null,
|
||
referenceData: null,
|
||
monthRefsByCardId: new Map(),
|
||
courtCardByDecanId: new Map(),
|
||
loadingPromise: null
|
||
};
|
||
|
||
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"
|
||
}
|
||
};
|
||
|
||
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 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"),
|
||
tarotMetaIChingCardEl: document.getElementById("tarot-meta-iching-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"),
|
||
tarotDetailIChingEl: document.getElementById("tarot-detail-iching"),
|
||
tarotDetailCalendarEl: document.getElementById("tarot-detail-calendar"),
|
||
tarotKabPathEl: document.getElementById("tarot-kab-path"),
|
||
tarotHouseOfCardsEl: document.getElementById("tarot-house-of-cards"),
|
||
tarotBrowseViewEl: document.getElementById("tarot-browse-view"),
|
||
tarotHouseTopCardsVisibleEl: document.getElementById("tarot-house-top-cards-visible"),
|
||
tarotHouseTopInfoHebrewEl: document.getElementById("tarot-house-top-info-hebrew"),
|
||
tarotHouseTopInfoPlanetEl: document.getElementById("tarot-house-top-info-planet"),
|
||
tarotHouseTopInfoZodiacEl: document.getElementById("tarot-house-top-info-zodiac"),
|
||
tarotHouseTopInfoTrumpEl: document.getElementById("tarot-house-top-info-trump"),
|
||
tarotHouseTopInfoPathEl: document.getElementById("tarot-house-top-info-path"),
|
||
tarotHouseBottomCardsVisibleEl: document.getElementById("tarot-house-bottom-cards-visible"),
|
||
tarotHouseBottomInfoZodiacEl: document.getElementById("tarot-house-bottom-info-zodiac"),
|
||
tarotHouseBottomInfoDecanEl: document.getElementById("tarot-house-bottom-info-decan"),
|
||
tarotHouseBottomInfoMonthEl: document.getElementById("tarot-house-bottom-info-month"),
|
||
tarotHouseBottomInfoRulerEl: document.getElementById("tarot-house-bottom-info-ruler"),
|
||
tarotHouseBottomInfoDateEl: document.getElementById("tarot-house-bottom-info-date"),
|
||
tarotHouseFocusToggleEl: document.getElementById("tarot-house-focus-toggle"),
|
||
tarotHouseExportEl: document.getElementById("tarot-house-export"),
|
||
tarotHouseExportWebpEl: document.getElementById("tarot-house-export-webp")
|
||
};
|
||
}
|
||
|
||
function setHouseBottomInfoCheckboxState(checkbox, enabled) {
|
||
if (!checkbox) {
|
||
return;
|
||
}
|
||
checkbox.checked = Boolean(enabled);
|
||
checkbox.disabled = Boolean(state.houseExportInProgress);
|
||
}
|
||
|
||
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,
|
||
resolveTarotCardThumbnail,
|
||
getDisplayCardName,
|
||
buildTypeLabel,
|
||
clearChildren,
|
||
normalizeRelationObject,
|
||
buildElementRelationsForCard,
|
||
buildTetragrammatonRelationsForCard,
|
||
buildSmallCardRulershipRelation,
|
||
buildSmallCardCourtLinkRelations,
|
||
buildCubeRelationsForCard,
|
||
buildIChingRelationsForCard,
|
||
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 syncHouseControls(elements) {
|
||
if (elements?.tarotBrowseViewEl) {
|
||
elements.tarotBrowseViewEl.classList.toggle("is-house-focus", Boolean(state.houseFocusMode));
|
||
}
|
||
|
||
if (elements?.tarotHouseTopCardsVisibleEl) {
|
||
elements.tarotHouseTopCardsVisibleEl.checked = Boolean(state.houseTopCardsVisible);
|
||
elements.tarotHouseTopCardsVisibleEl.disabled = Boolean(state.houseExportInProgress);
|
||
}
|
||
|
||
setHouseBottomInfoCheckboxState(elements?.tarotHouseTopInfoHebrewEl, state.houseTopInfoModes.hebrew);
|
||
setHouseBottomInfoCheckboxState(elements?.tarotHouseTopInfoPlanetEl, state.houseTopInfoModes.planet);
|
||
setHouseBottomInfoCheckboxState(elements?.tarotHouseTopInfoZodiacEl, state.houseTopInfoModes.zodiac);
|
||
setHouseBottomInfoCheckboxState(elements?.tarotHouseTopInfoTrumpEl, state.houseTopInfoModes.trump);
|
||
setHouseBottomInfoCheckboxState(elements?.tarotHouseTopInfoPathEl, state.houseTopInfoModes.path);
|
||
|
||
if (elements?.tarotHouseBottomCardsVisibleEl) {
|
||
elements.tarotHouseBottomCardsVisibleEl.checked = Boolean(state.houseBottomCardsVisible);
|
||
elements.tarotHouseBottomCardsVisibleEl.disabled = Boolean(state.houseExportInProgress);
|
||
}
|
||
|
||
setHouseBottomInfoCheckboxState(elements?.tarotHouseBottomInfoZodiacEl, state.houseBottomInfoModes.zodiac);
|
||
setHouseBottomInfoCheckboxState(elements?.tarotHouseBottomInfoDecanEl, state.houseBottomInfoModes.decan);
|
||
setHouseBottomInfoCheckboxState(elements?.tarotHouseBottomInfoMonthEl, state.houseBottomInfoModes.month);
|
||
setHouseBottomInfoCheckboxState(elements?.tarotHouseBottomInfoRulerEl, state.houseBottomInfoModes.ruler);
|
||
setHouseBottomInfoCheckboxState(elements?.tarotHouseBottomInfoDateEl, state.houseBottomInfoModes.date);
|
||
|
||
if (elements?.tarotHouseFocusToggleEl) {
|
||
elements.tarotHouseFocusToggleEl.setAttribute("aria-pressed", state.houseFocusMode ? "true" : "false");
|
||
elements.tarotHouseFocusToggleEl.textContent = state.houseFocusMode ? "Show Full Tarot" : "Focus House";
|
||
}
|
||
|
||
if (elements?.tarotHouseExportEl) {
|
||
elements.tarotHouseExportEl.disabled = Boolean(state.houseExportInProgress);
|
||
elements.tarotHouseExportEl.textContent = state.houseExportInProgress ? "Exporting..." : "Export PNG";
|
||
}
|
||
|
||
if (elements?.tarotHouseExportWebpEl) {
|
||
const supportsWebp = tarotHouseUi.isExportFormatSupported?.("webp") === true;
|
||
elements.tarotHouseExportWebpEl.disabled = Boolean(state.houseExportInProgress) || !supportsWebp;
|
||
elements.tarotHouseExportWebpEl.hidden = !supportsWebp;
|
||
elements.tarotHouseExportWebpEl.textContent = state.houseExportInProgress && state.houseExportFormat === "webp"
|
||
? "Exporting..."
|
||
: "Export WebP";
|
||
if (supportsWebp) {
|
||
elements.tarotHouseExportWebpEl.title = "Smaller file size, but not guaranteed lossless like PNG.";
|
||
}
|
||
}
|
||
}
|
||
|
||
async function exportHouseOfCards(elements, format = "png") {
|
||
if (state.houseExportInProgress) {
|
||
return;
|
||
}
|
||
|
||
state.houseExportInProgress = true;
|
||
state.houseExportFormat = format;
|
||
syncHouseControls(elements);
|
||
|
||
try {
|
||
await tarotHouseUi.exportImage?.(format);
|
||
} catch (error) {
|
||
window.alert(error instanceof Error ? error.message : "Unable to export the House of Cards image.");
|
||
} finally {
|
||
state.houseExportInProgress = false;
|
||
state.houseExportFormat = "png";
|
||
syncHouseControls(elements);
|
||
}
|
||
}
|
||
|
||
function buildTypeLabel(card) {
|
||
return tarotCardDerivationsUi.buildTypeLabel(card);
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
function buildIChingRelationsForCard(card) {
|
||
if (typeof tarotRelationsUi.buildIChingRelationsForCard !== "function") {
|
||
return [];
|
||
}
|
||
return tarotRelationsUi.buildIChingRelationsForCard(card, state.referenceData);
|
||
}
|
||
|
||
// 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 scrollCardIntoView(cardIdToReveal, elements) {
|
||
elements?.tarotCardListEl
|
||
?.querySelector(`[data-card-id="${cardIdToReveal}"]`)
|
||
?.scrollIntoView({ block: "nearest" });
|
||
}
|
||
|
||
function getRegisteredDeckOptionMap() {
|
||
const entries = typeof getDeckOptions === "function" ? getDeckOptions() : [];
|
||
return new Map(
|
||
(Array.isArray(entries) ? entries : [])
|
||
.map((entry) => ({
|
||
id: String(entry?.id || "").trim(),
|
||
label: String(entry?.label || entry?.id || "").trim()
|
||
}))
|
||
.filter((entry) => entry.id)
|
||
.map((entry) => [entry.id, entry])
|
||
);
|
||
}
|
||
|
||
function getRegisteredDeckList() {
|
||
return Array.from(getRegisteredDeckOptionMap().values());
|
||
}
|
||
|
||
function buildDeckLightboxCardRequest(cardIdToResolve, deckIdToResolve = "") {
|
||
const card = state.cards.find((entry) => entry.id === cardIdToResolve);
|
||
if (!card) {
|
||
return null;
|
||
}
|
||
|
||
const resolvedDeckId = String(deckIdToResolve || getActiveDeck?.() || "").trim();
|
||
const trumpNumber = Number.isFinite(Number(card?.number)) ? Number(card.number) : undefined;
|
||
const deckOptions = resolvedDeckId ? { deckId: resolvedDeckId, trumpNumber } : { trumpNumber };
|
||
const src = typeof resolveTarotCardImage === "function"
|
||
? resolveTarotCardImage(card.name, deckOptions)
|
||
: "";
|
||
const deckMeta = resolvedDeckId ? getRegisteredDeckOptionMap().get(resolvedDeckId) : null;
|
||
const label = (typeof getTarotCardDisplayName === "function"
|
||
? getTarotCardDisplayName(card.name, deckOptions)
|
||
: "") || getDisplayCardName(card) || card.name || "Tarot card enlarged image";
|
||
|
||
return {
|
||
src,
|
||
altText: label,
|
||
label,
|
||
cardId: card.id,
|
||
deckId: resolvedDeckId,
|
||
deckLabel: deckMeta?.label || resolvedDeckId,
|
||
compareDetails: tarotDetailRenderer.buildCompareDetails?.(card) || []
|
||
};
|
||
}
|
||
|
||
function buildLightboxCardRequestById(cardIdToResolve) {
|
||
const request = buildDeckLightboxCardRequest(cardIdToResolve, getActiveDeck?.() || "");
|
||
if (!request?.src) {
|
||
return null;
|
||
}
|
||
|
||
return request;
|
||
}
|
||
|
||
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`;
|
||
}
|
||
}
|
||
|
||
async function loadCards(referenceData, magickDataset) {
|
||
const payload = await dataService.loadTarotCards?.();
|
||
const cards = Array.isArray(payload?.cards)
|
||
? payload.cards
|
||
: (Array.isArray(payload) ? payload : []);
|
||
|
||
return cards.map((card) => ({
|
||
...card,
|
||
id: String(card?.id || "").trim() || cardId(card)
|
||
}));
|
||
}
|
||
|
||
async function ensureTarotSection(referenceData, magickDataset = null) {
|
||
state.referenceData = referenceData || state.referenceData;
|
||
|
||
if (magickDataset) {
|
||
state.magickDataset = magickDataset;
|
||
}
|
||
|
||
tarotHouseUi.init?.({
|
||
resolveTarotCardImage,
|
||
resolveTarotCardThumbnail,
|
||
getDisplayCardName,
|
||
clearChildren,
|
||
normalizeTarotCardLookupName,
|
||
selectCardById,
|
||
openCardLightbox: (src, altText, options = {}) => {
|
||
const cardId = String(options?.cardId || "").trim();
|
||
const primaryCardRequest = cardId ? buildLightboxCardRequestById(cardId) : null;
|
||
const activeDeckId = String(getActiveDeck?.() || primaryCardRequest?.deckId || "").trim();
|
||
const availableCompareDecks = getRegisteredDeckList().filter((deck) => deck.id && deck.id !== activeDeckId);
|
||
window.TarotUiLightbox?.open?.({
|
||
src: primaryCardRequest?.src || src,
|
||
altText: primaryCardRequest?.altText || altText || "Tarot card enlarged image",
|
||
label: primaryCardRequest?.label || altText || "Tarot card enlarged image",
|
||
cardId: primaryCardRequest?.cardId || cardId,
|
||
deckId: primaryCardRequest?.deckId || activeDeckId,
|
||
deckLabel: primaryCardRequest?.deckLabel || "",
|
||
compareDetails: primaryCardRequest?.compareDetails || [],
|
||
allowOverlayCompare: true,
|
||
allowDeckCompare: Boolean(cardId),
|
||
activeDeckId,
|
||
activeDeckLabel: primaryCardRequest?.deckLabel || "",
|
||
availableCompareDecks,
|
||
maxCompareDecks: 2,
|
||
sequenceIds: state.cards.map((card) => card.id),
|
||
resolveCardById: buildLightboxCardRequestById,
|
||
resolveDeckCardById: buildDeckLightboxCardRequest,
|
||
onSelectCardId: (nextCardId) => {
|
||
const latestElements = getElements();
|
||
selectCardById(nextCardId, latestElements);
|
||
scrollCardIntoView(nextCardId, latestElements);
|
||
}
|
||
});
|
||
},
|
||
isHouseFocusMode: () => state.houseFocusMode,
|
||
getCards: () => state.cards,
|
||
getSelectedCardId: () => state.selectedCardId,
|
||
getHouseTopCardsVisible: () => state.houseTopCardsVisible,
|
||
getHouseTopInfoModes: () => ({ ...state.houseTopInfoModes }),
|
||
getHouseBottomCardsVisible: () => state.houseBottomCardsVisible,
|
||
getHouseBottomInfoModes: () => ({ ...state.houseBottomInfoModes })
|
||
});
|
||
|
||
const elements = getElements();
|
||
|
||
if (state.initialized) {
|
||
state.monthRefsByCardId = buildMonthReferencesByCard(referenceData, state.cards);
|
||
state.courtCardByDecanId = buildCourtCardByDecanId(state.cards);
|
||
renderHouseOfCards(elements);
|
||
syncHouseControls(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;
|
||
}
|
||
|
||
if (state.loadingPromise) {
|
||
await state.loadingPromise;
|
||
return;
|
||
}
|
||
|
||
state.loadingPromise = (async () => {
|
||
const cards = await loadCards(referenceData, magickDataset);
|
||
|
||
state.cards = cards;
|
||
state.monthRefsByCardId = buildMonthReferencesByCard(referenceData, cards);
|
||
state.courtCardByDecanId = buildCourtCardByDecanId(cards);
|
||
state.filteredCards = [...cards];
|
||
renderList(elements);
|
||
renderHouseOfCards(elements);
|
||
syncHouseControls(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.tarotHouseFocusToggleEl) {
|
||
elements.tarotHouseFocusToggleEl.addEventListener("click", () => {
|
||
state.houseFocusMode = !state.houseFocusMode;
|
||
syncHouseControls(elements);
|
||
});
|
||
}
|
||
|
||
if (elements.tarotHouseTopCardsVisibleEl) {
|
||
elements.tarotHouseTopCardsVisibleEl.addEventListener("change", () => {
|
||
state.houseTopCardsVisible = Boolean(elements.tarotHouseTopCardsVisibleEl.checked);
|
||
renderHouseOfCards(elements);
|
||
syncHouseControls(elements);
|
||
});
|
||
}
|
||
|
||
[
|
||
[elements.tarotHouseTopInfoHebrewEl, "hebrew"],
|
||
[elements.tarotHouseTopInfoPlanetEl, "planet"],
|
||
[elements.tarotHouseTopInfoZodiacEl, "zodiac"],
|
||
[elements.tarotHouseTopInfoTrumpEl, "trump"],
|
||
[elements.tarotHouseTopInfoPathEl, "path"]
|
||
].forEach(([checkbox, key]) => {
|
||
if (!checkbox) {
|
||
return;
|
||
}
|
||
checkbox.addEventListener("change", () => {
|
||
state.houseTopInfoModes[key] = Boolean(checkbox.checked);
|
||
renderHouseOfCards(elements);
|
||
syncHouseControls(elements);
|
||
});
|
||
});
|
||
|
||
if (elements.tarotHouseBottomCardsVisibleEl) {
|
||
elements.tarotHouseBottomCardsVisibleEl.addEventListener("change", () => {
|
||
state.houseBottomCardsVisible = Boolean(elements.tarotHouseBottomCardsVisibleEl.checked);
|
||
renderHouseOfCards(elements);
|
||
syncHouseControls(elements);
|
||
});
|
||
}
|
||
|
||
[
|
||
[elements.tarotHouseBottomInfoZodiacEl, "zodiac"],
|
||
[elements.tarotHouseBottomInfoDecanEl, "decan"],
|
||
[elements.tarotHouseBottomInfoMonthEl, "month"],
|
||
[elements.tarotHouseBottomInfoRulerEl, "ruler"],
|
||
[elements.tarotHouseBottomInfoDateEl, "date"]
|
||
].forEach(([checkbox, key]) => {
|
||
if (!checkbox) {
|
||
return;
|
||
}
|
||
checkbox.addEventListener("change", () => {
|
||
state.houseBottomInfoModes[key] = Boolean(checkbox.checked);
|
||
renderHouseOfCards(elements);
|
||
syncHouseControls(elements);
|
||
});
|
||
});
|
||
|
||
if (elements.tarotHouseExportEl) {
|
||
elements.tarotHouseExportEl.addEventListener("click", () => {
|
||
exportHouseOfCards(elements, "png");
|
||
});
|
||
}
|
||
|
||
if (elements.tarotHouseExportWebpEl) {
|
||
elements.tarotHouseExportWebpEl.addEventListener("click", () => {
|
||
exportHouseOfCards(elements, "webp");
|
||
});
|
||
}
|
||
|
||
if (elements.tarotDetailImageEl) {
|
||
elements.tarotDetailImageEl.addEventListener("click", () => {
|
||
if (elements.tarotDetailImageEl.style.display === "none" || !state.selectedCardId) {
|
||
return;
|
||
}
|
||
|
||
const request = buildLightboxCardRequestById(state.selectedCardId);
|
||
if (!request?.src) {
|
||
return;
|
||
}
|
||
|
||
window.TarotUiLightbox?.open?.(request);
|
||
});
|
||
}
|
||
|
||
state.initialized = true;
|
||
})();
|
||
|
||
try {
|
||
await state.loadingPromise;
|
||
} finally {
|
||
state.loadingPromise = null;
|
||
}
|
||
}
|
||
|
||
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);
|
||
scrollCardIntoView(card.id, el);
|
||
}
|
||
|
||
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);
|
||
scrollCardIntoView(card.id, el);
|
||
}
|
||
|
||
window.TarotSectionUi = {
|
||
ensureTarotSection,
|
||
selectCardByTrump,
|
||
selectCardByName,
|
||
getCards: () => state.cards
|
||
};
|
||
})();
|