refactoring
This commit is contained in:
425
app/ui-tarot-spread.js
Normal file
425
app/ui-tarot-spread.js
Normal file
@@ -0,0 +1,425 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
let initialized = false;
|
||||
let activeTarotSpread = null;
|
||||
let activeTarotSpreadDraw = [];
|
||||
let config = {
|
||||
ensureTarotSection: null,
|
||||
getReferenceData: () => null,
|
||||
getMagickDataset: () => null,
|
||||
getActiveSection: () => "home",
|
||||
setActiveSection: null
|
||||
};
|
||||
|
||||
const THREE_CARD_POSITIONS = [
|
||||
{ pos: "past", label: "Past" },
|
||||
{ pos: "present", label: "Present" },
|
||||
{ pos: "future", label: "Future" }
|
||||
];
|
||||
|
||||
const CELTIC_CROSS_POSITIONS = [
|
||||
{ pos: "crown", label: "Crown" },
|
||||
{ pos: "out", label: "Outcome" },
|
||||
{ pos: "past", label: "Recent Past" },
|
||||
{ pos: "present", label: "Present" },
|
||||
{ pos: "near-fut", label: "Near Future" },
|
||||
{ pos: "hope", label: "Hopes & Fears" },
|
||||
{ pos: "chall", label: "Challenge" },
|
||||
{ pos: "env", label: "Environment" },
|
||||
{ pos: "found", label: "Foundation" },
|
||||
{ pos: "self", label: "Self" }
|
||||
];
|
||||
|
||||
function getElements() {
|
||||
return {
|
||||
openTarotCardsEl: document.getElementById("open-tarot-cards"),
|
||||
openTarotSpreadEl: document.getElementById("open-tarot-spread"),
|
||||
tarotBrowseViewEl: document.getElementById("tarot-browse-view"),
|
||||
tarotSpreadViewEl: document.getElementById("tarot-spread-view"),
|
||||
tarotSpreadBackEl: document.getElementById("tarot-spread-back"),
|
||||
tarotSpreadBtnThreeEl: document.getElementById("tarot-spread-btn-three"),
|
||||
tarotSpreadBtnCelticEl: document.getElementById("tarot-spread-btn-celtic"),
|
||||
tarotSpreadRevealAllEl: document.getElementById("tarot-spread-reveal-all"),
|
||||
tarotSpreadRedrawEl: document.getElementById("tarot-spread-redraw"),
|
||||
tarotSpreadMeaningsEl: document.getElementById("tarot-spread-meanings"),
|
||||
tarotSpreadBoardEl: document.getElementById("tarot-spread-board")
|
||||
};
|
||||
}
|
||||
|
||||
function ensureTarotBrowseData() {
|
||||
const referenceData = typeof config.getReferenceData === "function" ? config.getReferenceData() : null;
|
||||
const magickDataset = typeof config.getMagickDataset === "function" ? config.getMagickDataset() : null;
|
||||
if (typeof config.ensureTarotSection === "function" && referenceData) {
|
||||
config.ensureTarotSection(referenceData, magickDataset);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeTarotSpread(value) {
|
||||
return value === "celtic-cross" ? "celtic-cross" : "three-card";
|
||||
}
|
||||
|
||||
function drawNFromDeck(n) {
|
||||
const allCards = window.TarotSectionUi?.getCards?.() || [];
|
||||
if (!allCards.length) return [];
|
||||
|
||||
const shuffled = [...allCards];
|
||||
for (let index = shuffled.length - 1; index > 0; index -= 1) {
|
||||
const swapIndex = Math.floor(Math.random() * (index + 1));
|
||||
[shuffled[index], shuffled[swapIndex]] = [shuffled[swapIndex], shuffled[index]];
|
||||
}
|
||||
|
||||
return shuffled.slice(0, n).map((card) => ({
|
||||
...card,
|
||||
reversed: Math.random() < 0.3
|
||||
}));
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/\"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function getSpreadPositions(spreadId) {
|
||||
return spreadId === "celtic-cross" ? CELTIC_CROSS_POSITIONS : THREE_CARD_POSITIONS;
|
||||
}
|
||||
|
||||
function regenerateTarotSpreadDraw() {
|
||||
const normalizedSpread = normalizeTarotSpread(activeTarotSpread);
|
||||
const positions = getSpreadPositions(normalizedSpread);
|
||||
const cards = drawNFromDeck(positions.length);
|
||||
activeTarotSpreadDraw = positions.map((position, index) => ({
|
||||
position,
|
||||
card: cards[index] || null,
|
||||
revealed: false
|
||||
}));
|
||||
}
|
||||
|
||||
function renderTarotSpreadMeanings() {
|
||||
const { tarotSpreadMeaningsEl } = getElements();
|
||||
if (!tarotSpreadMeaningsEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!activeTarotSpreadDraw.length || activeTarotSpreadDraw.some((entry) => !entry.card)) {
|
||||
tarotSpreadMeaningsEl.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const revealedEntries = activeTarotSpreadDraw.filter((entry) => entry.card && entry.revealed);
|
||||
if (!revealedEntries.length) {
|
||||
tarotSpreadMeaningsEl.innerHTML = '<div class="tarot-spread-meanings-empty">Cards are face down. Click a card to reveal its meaning.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const hiddenCount = activeTarotSpreadDraw.length - revealedEntries.length;
|
||||
const hiddenHintMarkup = hiddenCount > 0
|
||||
? `<div class="tarot-spread-meanings-empty">${hiddenCount} card${hiddenCount === 1 ? "" : "s"} still face down.</div>`
|
||||
: "";
|
||||
|
||||
tarotSpreadMeaningsEl.innerHTML = revealedEntries.map((entry) => {
|
||||
const positionLabel = escapeHtml(entry.position.label).toUpperCase();
|
||||
const card = entry.card;
|
||||
const cardName = escapeHtml(card.name || "Unknown Card");
|
||||
const meaningText = escapeHtml(card.reversed ? (card.meanings?.reversed || card.summary || "--") : (card.meanings?.upright || card.summary || "--"));
|
||||
const keywords = Array.isArray(card.keywords)
|
||||
? card.keywords.map((keyword) => String(keyword || "").trim()).filter(Boolean)
|
||||
: [];
|
||||
const keywordMarkup = keywords.length
|
||||
? `<div class="tarot-spread-meaning-keywords">Keywords: ${escapeHtml(keywords.join(", "))}</div>`
|
||||
: "";
|
||||
const orientationMarkup = card.reversed
|
||||
? ' <span class="tarot-spread-meaning-orientation">(Reversed)</span>'
|
||||
: "";
|
||||
|
||||
return `<div class="tarot-spread-meaning-item">`
|
||||
+ `<div class="tarot-spread-meaning-head">${positionLabel}: <span class="tarot-spread-meaning-card">${cardName}</span>${orientationMarkup}</div>`
|
||||
+ `<div class="tarot-spread-meaning-text">${meaningText}</div>`
|
||||
+ keywordMarkup
|
||||
+ `</div>`;
|
||||
}).join("") + hiddenHintMarkup;
|
||||
}
|
||||
|
||||
function renderTarotSpread() {
|
||||
const { tarotSpreadBoardEl, tarotSpreadMeaningsEl, tarotSpreadRevealAllEl } = getElements();
|
||||
if (!tarotSpreadBoardEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedSpread = normalizeTarotSpread(activeTarotSpread);
|
||||
const isCeltic = normalizedSpread === "celtic-cross";
|
||||
const cardBackImageSrc = String(window.TarotCardImages?.resolveTarotCardBackImage?.() || "").trim();
|
||||
|
||||
if (!activeTarotSpreadDraw.length) {
|
||||
regenerateTarotSpreadDraw();
|
||||
}
|
||||
|
||||
tarotSpreadBoardEl.className = `tarot-spread-board tarot-spread-board--${isCeltic ? "celtic" : "three"}`;
|
||||
|
||||
if (!activeTarotSpreadDraw.length || activeTarotSpreadDraw.some((entry) => !entry.card)) {
|
||||
tarotSpreadBoardEl.innerHTML = '<div class="spread-empty">Tarot deck not loaded yet - open Cards first, then return to Spread.</div>';
|
||||
if (tarotSpreadMeaningsEl) {
|
||||
tarotSpreadMeaningsEl.innerHTML = "";
|
||||
}
|
||||
if (tarotSpreadRevealAllEl) {
|
||||
tarotSpreadRevealAllEl.disabled = true;
|
||||
tarotSpreadRevealAllEl.textContent = "Reveal All";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (tarotSpreadRevealAllEl) {
|
||||
const totalCards = activeTarotSpreadDraw.length;
|
||||
const revealedCount = activeTarotSpreadDraw.reduce((count, entry) => (
|
||||
count + (entry?.card && entry.revealed ? 1 : 0)
|
||||
), 0);
|
||||
tarotSpreadRevealAllEl.disabled = revealedCount >= totalCards;
|
||||
tarotSpreadRevealAllEl.textContent = revealedCount >= totalCards
|
||||
? "All Revealed"
|
||||
: `Reveal All (${totalCards - revealedCount})`;
|
||||
}
|
||||
|
||||
renderTarotSpreadMeanings();
|
||||
|
||||
tarotSpreadBoardEl.innerHTML = activeTarotSpreadDraw.map((entry, index) => {
|
||||
const position = entry.position;
|
||||
const card = entry.card;
|
||||
const imgSrc = window.TarotCardImages?.resolveTarotCardImage?.(card.name);
|
||||
const isRevealed = Boolean(entry.revealed);
|
||||
const cardBackAttr = cardBackImageSrc
|
||||
? ` data-card-back-src="${escapeHtml(cardBackImageSrc)}"`
|
||||
: "";
|
||||
const reversed = card.reversed;
|
||||
const wrapClass = [
|
||||
"spread-card-wrap",
|
||||
isRevealed ? "is-revealed" : "is-facedown",
|
||||
(isRevealed && reversed) ? "is-reversed" : ""
|
||||
].filter(Boolean).join(" ");
|
||||
|
||||
let faceMarkup = "";
|
||||
if (isRevealed) {
|
||||
faceMarkup = imgSrc
|
||||
? `<img class="spread-card-img" src="${imgSrc}" alt="${escapeHtml(card.name)}" loading="lazy">`
|
||||
: `<div class="spread-card-placeholder">${escapeHtml(card.name)}</div>`;
|
||||
} else if (cardBackImageSrc) {
|
||||
faceMarkup = '<img class="spread-card-back-img" src="' + cardBackImageSrc + '" alt="Face-down tarot card" loading="lazy">';
|
||||
} else {
|
||||
faceMarkup = '<div class="spread-card-back-fallback">CARD BACK</div>';
|
||||
}
|
||||
|
||||
const reversedTag = isRevealed && reversed
|
||||
? '<span class="spread-reversed-tag">Reversed</span>'
|
||||
: "";
|
||||
const buttonAriaLabel = isRevealed
|
||||
? `Open ${escapeHtml(card.name)} for ${escapeHtml(position.label)} in fullscreen`
|
||||
: `Reveal ${escapeHtml(position.label)} card`;
|
||||
|
||||
return `<div class="spread-position" data-pos="${position.pos}">`
|
||||
+ `<div class="spread-pos-label">${escapeHtml(position.label)}</div>`
|
||||
+ `<button type="button" class="${wrapClass}" data-spread-index="${index}" aria-label="${buttonAriaLabel}"${cardBackAttr}>${faceMarkup}</button>`
|
||||
+ (reversedTag ? `<div class="spread-card-name">${reversedTag}</div>` : "")
|
||||
+ `</div>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function applyViewState() {
|
||||
const {
|
||||
openTarotCardsEl,
|
||||
openTarotSpreadEl,
|
||||
tarotBrowseViewEl,
|
||||
tarotSpreadViewEl,
|
||||
tarotSpreadBtnThreeEl,
|
||||
tarotSpreadBtnCelticEl
|
||||
} = getElements();
|
||||
const isSpreadOpen = activeTarotSpread !== null;
|
||||
const isCeltic = activeTarotSpread === "celtic-cross";
|
||||
const isTarotActive = typeof config.getActiveSection === "function" && config.getActiveSection() === "tarot";
|
||||
|
||||
if (tarotBrowseViewEl) tarotBrowseViewEl.hidden = isSpreadOpen;
|
||||
if (tarotSpreadViewEl) tarotSpreadViewEl.hidden = !isSpreadOpen;
|
||||
|
||||
if (tarotSpreadBtnThreeEl) tarotSpreadBtnThreeEl.classList.toggle("is-active", isSpreadOpen && !isCeltic);
|
||||
if (tarotSpreadBtnCelticEl) tarotSpreadBtnCelticEl.classList.toggle("is-active", isSpreadOpen && isCeltic);
|
||||
|
||||
if (openTarotCardsEl) openTarotCardsEl.classList.toggle("is-active", isTarotActive && !isSpreadOpen);
|
||||
if (openTarotSpreadEl) openTarotSpreadEl.classList.toggle("is-active", isTarotActive && isSpreadOpen);
|
||||
}
|
||||
|
||||
function showCardsView() {
|
||||
activeTarotSpread = null;
|
||||
activeTarotSpreadDraw = [];
|
||||
applyViewState();
|
||||
ensureTarotBrowseData();
|
||||
const detailPanelEl = document.querySelector("#tarot-browse-view .tarot-detail-panel");
|
||||
if (detailPanelEl instanceof HTMLElement) {
|
||||
detailPanelEl.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function showTarotSpreadView(spreadId = "three-card") {
|
||||
activeTarotSpread = normalizeTarotSpread(spreadId);
|
||||
regenerateTarotSpreadDraw();
|
||||
applyViewState();
|
||||
ensureTarotBrowseData();
|
||||
renderTarotSpread();
|
||||
}
|
||||
|
||||
function setSpread(spreadId, openTarotSection = false) {
|
||||
if (openTarotSection && typeof config.setActiveSection === "function") {
|
||||
config.setActiveSection("tarot");
|
||||
}
|
||||
showTarotSpreadView(spreadId);
|
||||
}
|
||||
|
||||
function revealAll() {
|
||||
if (!activeTarotSpreadDraw.length) {
|
||||
regenerateTarotSpreadDraw();
|
||||
}
|
||||
|
||||
activeTarotSpreadDraw.forEach((entry) => {
|
||||
if (entry?.card) {
|
||||
entry.revealed = true;
|
||||
}
|
||||
});
|
||||
|
||||
renderTarotSpread();
|
||||
}
|
||||
|
||||
function handleBoardClick(event) {
|
||||
const target = event.target;
|
||||
if (!(target instanceof Node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const button = target instanceof Element
|
||||
? target.closest(".spread-card-wrap[data-spread-index]")
|
||||
: null;
|
||||
if (!(button instanceof HTMLButtonElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const spreadIndex = Number(button.dataset.spreadIndex);
|
||||
if (!Number.isInteger(spreadIndex) || spreadIndex < 0 || spreadIndex >= activeTarotSpreadDraw.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const spreadEntry = activeTarotSpreadDraw[spreadIndex];
|
||||
if (!spreadEntry?.card) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!spreadEntry.revealed) {
|
||||
spreadEntry.revealed = true;
|
||||
renderTarotSpread();
|
||||
return;
|
||||
}
|
||||
|
||||
const imageSrc = window.TarotCardImages?.resolveTarotCardImage?.(spreadEntry.card.name);
|
||||
if (imageSrc) {
|
||||
window.TarotUiLightbox?.open?.(imageSrc, `${spreadEntry.card.name} (${spreadEntry.position?.label || "Spread"})`);
|
||||
}
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
const {
|
||||
openTarotCardsEl,
|
||||
openTarotSpreadEl,
|
||||
tarotSpreadBackEl,
|
||||
tarotSpreadBtnThreeEl,
|
||||
tarotSpreadBtnCelticEl,
|
||||
tarotSpreadRevealAllEl,
|
||||
tarotSpreadRedrawEl,
|
||||
tarotSpreadBoardEl
|
||||
} = getElements();
|
||||
|
||||
if (openTarotCardsEl) {
|
||||
openTarotCardsEl.addEventListener("click", () => {
|
||||
if (typeof config.setActiveSection === "function") {
|
||||
config.setActiveSection("tarot");
|
||||
}
|
||||
showCardsView();
|
||||
});
|
||||
}
|
||||
|
||||
if (openTarotSpreadEl) {
|
||||
openTarotSpreadEl.addEventListener("click", () => {
|
||||
setSpread("three-card", true);
|
||||
});
|
||||
}
|
||||
|
||||
if (tarotSpreadBackEl) {
|
||||
tarotSpreadBackEl.addEventListener("click", () => {
|
||||
showCardsView();
|
||||
});
|
||||
}
|
||||
|
||||
if (tarotSpreadBtnThreeEl) {
|
||||
tarotSpreadBtnThreeEl.addEventListener("click", () => {
|
||||
showTarotSpreadView("three-card");
|
||||
});
|
||||
}
|
||||
|
||||
if (tarotSpreadBtnCelticEl) {
|
||||
tarotSpreadBtnCelticEl.addEventListener("click", () => {
|
||||
showTarotSpreadView("celtic-cross");
|
||||
});
|
||||
}
|
||||
|
||||
if (tarotSpreadRedrawEl) {
|
||||
tarotSpreadRedrawEl.addEventListener("click", () => {
|
||||
regenerateTarotSpreadDraw();
|
||||
renderTarotSpread();
|
||||
});
|
||||
}
|
||||
|
||||
if (tarotSpreadRevealAllEl) {
|
||||
tarotSpreadRevealAllEl.addEventListener("click", revealAll);
|
||||
}
|
||||
|
||||
if (tarotSpreadBoardEl) {
|
||||
tarotSpreadBoardEl.addEventListener("click", handleBoardClick);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSectionActivated() {
|
||||
ensureTarotBrowseData();
|
||||
applyViewState();
|
||||
if (activeTarotSpread !== null) {
|
||||
renderTarotSpread();
|
||||
}
|
||||
}
|
||||
|
||||
function init(nextConfig = {}) {
|
||||
config = {
|
||||
...config,
|
||||
...nextConfig
|
||||
};
|
||||
|
||||
if (initialized) {
|
||||
applyViewState();
|
||||
return;
|
||||
}
|
||||
|
||||
bindEvents();
|
||||
applyViewState();
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
window.TarotSpreadUi = {
|
||||
...(window.TarotSpreadUi || {}),
|
||||
init,
|
||||
applyViewState,
|
||||
showCardsView,
|
||||
showTarotSpreadView,
|
||||
setSpread,
|
||||
handleSectionActivated,
|
||||
renderTarotSpread,
|
||||
isSpreadOpen() {
|
||||
return activeTarotSpread !== null;
|
||||
}
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user