455 lines
14 KiB
JavaScript
455 lines
14 KiB
JavaScript
(function () {
|
||
"use strict";
|
||
|
||
const { getTarotCardSearchAliases } = window.TarotCardImages || {};
|
||
|
||
const CLASSICAL_ELEMENT_IDS = ["fire", "water", "air", "earth"];
|
||
|
||
const ACE_BY_ELEMENT_ID = {
|
||
water: "Ace of Cups",
|
||
fire: "Ace of Wands",
|
||
air: "Ace of Swords",
|
||
earth: "Ace of Disks"
|
||
};
|
||
|
||
const HEBREW_LETTER_NAME_BY_ELEMENT_ID = {
|
||
fire: "Yod",
|
||
water: "Heh",
|
||
air: "Vav",
|
||
earth: "Heh"
|
||
};
|
||
|
||
const HEBREW_LETTER_CHAR_BY_ELEMENT_ID = {
|
||
fire: "י",
|
||
water: "ה",
|
||
air: "ו",
|
||
earth: "ה"
|
||
};
|
||
|
||
const COURT_RANK_BY_ELEMENT_ID = {
|
||
fire: "Knight",
|
||
water: "Queen",
|
||
air: "Prince",
|
||
earth: "Princess"
|
||
};
|
||
|
||
const COURT_SUITS = ["Wands", "Cups", "Swords", "Disks"];
|
||
|
||
const SUIT_BY_ELEMENT_ID = {
|
||
fire: "Wands",
|
||
water: "Cups",
|
||
air: "Swords",
|
||
earth: "Disks"
|
||
};
|
||
|
||
const SMALL_CARD_GROUPS = [
|
||
{ label: "2–4", modality: "Cardinal", numbers: [2, 3, 4] },
|
||
{ label: "5–7", modality: "Fixed", numbers: [5, 6, 7] },
|
||
{ label: "8–10", modality: "Mutable", numbers: [8, 9, 10] }
|
||
];
|
||
|
||
const SIGN_BY_ELEMENT_AND_MODALITY = {
|
||
fire: { cardinal: "aries", fixed: "leo", mutable: "sagittarius" },
|
||
water: { cardinal: "cancer", fixed: "scorpio", mutable: "pisces" },
|
||
air: { cardinal: "libra", fixed: "aquarius", mutable: "gemini" },
|
||
earth: { cardinal: "capricorn", fixed: "taurus", mutable: "virgo" }
|
||
};
|
||
|
||
const state = {
|
||
initialized: false,
|
||
entries: [],
|
||
filteredEntries: [],
|
||
selectedId: "",
|
||
searchQuery: ""
|
||
};
|
||
|
||
function getElements() {
|
||
return {
|
||
listEl: document.getElementById("elements-list"),
|
||
countEl: document.getElementById("elements-count"),
|
||
searchEl: document.getElementById("elements-search-input"),
|
||
searchClearEl: document.getElementById("elements-search-clear"),
|
||
detailNameEl: document.getElementById("elements-detail-name"),
|
||
detailSubEl: document.getElementById("elements-detail-sub"),
|
||
detailBodyEl: document.getElementById("elements-detail-body")
|
||
};
|
||
}
|
||
|
||
function normalize(value) {
|
||
return String(value || "")
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/\s+/g, " ");
|
||
}
|
||
|
||
function titleCase(value) {
|
||
return String(value || "")
|
||
.split(" ")
|
||
.filter(Boolean)
|
||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||
.join(" ");
|
||
}
|
||
|
||
function buildTarotAliasText(cardNames) {
|
||
if (typeof getTarotCardSearchAliases !== "function") {
|
||
return Array.isArray(cardNames) ? cardNames.join(" ") : "";
|
||
}
|
||
|
||
const aliases = new Set();
|
||
(Array.isArray(cardNames) ? cardNames : []).forEach((cardName) => {
|
||
getTarotCardSearchAliases(cardName).forEach((alias) => aliases.add(alias));
|
||
});
|
||
return Array.from(aliases).join(" ");
|
||
}
|
||
|
||
function buildSmallCardGroupsForElement(elementId) {
|
||
const suit = SUIT_BY_ELEMENT_ID[elementId] || "";
|
||
const signByModality = SIGN_BY_ELEMENT_AND_MODALITY[elementId] || {};
|
||
|
||
return SMALL_CARD_GROUPS.map((group) => {
|
||
const signId = String(signByModality[String(group.modality || "").toLowerCase()] || "").trim();
|
||
const signName = titleCase(signId);
|
||
const cardNames = group.numbers.map((number) => `${number} of ${suit}`);
|
||
|
||
return {
|
||
rangeLabel: group.label,
|
||
modality: group.modality,
|
||
signId,
|
||
signName,
|
||
cardNames
|
||
};
|
||
});
|
||
}
|
||
|
||
function buildEntries(magickDataset) {
|
||
const source = magickDataset?.grouped?.alchemy?.elements;
|
||
if (!source || typeof source !== "object") {
|
||
return [];
|
||
}
|
||
|
||
return CLASSICAL_ELEMENT_IDS
|
||
.map((id) => {
|
||
const item = source[id];
|
||
if (!item || typeof item !== "object") {
|
||
return null;
|
||
}
|
||
|
||
const name = String(item?.name?.en || item?.name || titleCase(id)).trim() || titleCase(id);
|
||
const symbol = String(item?.symbol || "").trim();
|
||
const aceCardName = ACE_BY_ELEMENT_ID[id] || "";
|
||
const hebrewLetter = HEBREW_LETTER_CHAR_BY_ELEMENT_ID[id] || "";
|
||
const hebrewLetterName = HEBREW_LETTER_NAME_BY_ELEMENT_ID[id] || "";
|
||
const courtRank = COURT_RANK_BY_ELEMENT_ID[id] || "";
|
||
const courtCardNames = courtRank
|
||
? COURT_SUITS.map((suit) => `${courtRank} of ${suit}`)
|
||
: [];
|
||
const smallCardGroups = buildSmallCardGroupsForElement(id);
|
||
const smallCardNames = smallCardGroups.flatMap((group) => group.cardNames || []);
|
||
const tarotAliasText = buildTarotAliasText([aceCardName, ...courtCardNames, ...smallCardNames]);
|
||
|
||
return {
|
||
id,
|
||
name,
|
||
symbol,
|
||
elementalId: String(item?.elementalId || "").trim(),
|
||
aceCardName,
|
||
hebrewLetter,
|
||
hebrewLetterName,
|
||
courtRank,
|
||
courtCardNames,
|
||
smallCardGroups,
|
||
searchText: normalize(`${id} ${name} ${symbol} ${aceCardName} ${hebrewLetter} ${hebrewLetterName} ${courtRank} ${courtCardNames.join(" ")} ${smallCardGroups.map((group) => `${group.modality} ${group.signName} ${group.cardNames.join(" ")}`).join(" ")} ${tarotAliasText}`)
|
||
};
|
||
})
|
||
.filter(Boolean);
|
||
}
|
||
|
||
function findEntryById(id) {
|
||
const normalizedId = normalize(id);
|
||
return state.entries.find((entry) => entry.id === normalizedId) || null;
|
||
}
|
||
|
||
function renderList(elements) {
|
||
if (!elements?.listEl) {
|
||
return;
|
||
}
|
||
|
||
elements.listEl.replaceChildren();
|
||
|
||
state.filteredEntries.forEach((entry) => {
|
||
const button = document.createElement("button");
|
||
button.type = "button";
|
||
button.className = "planet-list-item";
|
||
button.dataset.elementId = entry.id;
|
||
button.setAttribute("role", "option");
|
||
|
||
const isSelected = entry.id === state.selectedId;
|
||
button.classList.toggle("is-selected", isSelected);
|
||
button.setAttribute("aria-selected", isSelected ? "true" : "false");
|
||
|
||
const name = document.createElement("span");
|
||
name.className = "planet-list-name";
|
||
name.textContent = `${entry.symbol} ${entry.name}`.trim();
|
||
|
||
const meta = document.createElement("span");
|
||
meta.className = "planet-list-meta";
|
||
meta.textContent = `Letter: ${entry.hebrewLetter || "--"} · Ace: ${entry.aceCardName || "--"} · Court: ${entry.courtRank || "--"}`;
|
||
|
||
button.append(name, meta);
|
||
elements.listEl.appendChild(button);
|
||
});
|
||
|
||
if (elements.countEl) {
|
||
elements.countEl.textContent = `${state.filteredEntries.length} elements`;
|
||
}
|
||
|
||
if (!state.filteredEntries.length) {
|
||
const empty = document.createElement("div");
|
||
empty.className = "planet-text";
|
||
empty.style.padding = "16px";
|
||
empty.style.color = "#71717a";
|
||
empty.textContent = "No elements match your search.";
|
||
elements.listEl.appendChild(empty);
|
||
}
|
||
}
|
||
|
||
function renderDetail(elements) {
|
||
if (!elements?.detailNameEl || !elements.detailSubEl || !elements.detailBodyEl) {
|
||
return;
|
||
}
|
||
|
||
const entry = findEntryById(state.selectedId);
|
||
elements.detailBodyEl.replaceChildren();
|
||
|
||
if (!entry) {
|
||
elements.detailNameEl.textContent = "--";
|
||
elements.detailSubEl.textContent = "Select an element to explore";
|
||
return;
|
||
}
|
||
|
||
elements.detailNameEl.textContent = `${entry.symbol} ${entry.name}`.trim();
|
||
elements.detailSubEl.textContent = "Classical Element";
|
||
|
||
const grid = document.createElement("div");
|
||
grid.className = "planet-meta-grid";
|
||
|
||
const detailsCard = document.createElement("div");
|
||
detailsCard.className = "planet-meta-card";
|
||
detailsCard.innerHTML = `
|
||
<strong>Element Details</strong>
|
||
<dl class="alpha-dl">
|
||
<dt>Name</dt><dd>${entry.name}</dd>
|
||
<dt>Symbol</dt><dd>${entry.symbol || "--"}</dd>
|
||
<dt>Hebrew Letter</dt><dd>${entry.hebrewLetter || "--"}</dd>
|
||
<dt>Court Rank</dt><dd>${entry.courtRank || "--"}</dd>
|
||
<dt>ID</dt><dd>${entry.id}</dd>
|
||
</dl>
|
||
`;
|
||
|
||
const tarotCard = document.createElement("div");
|
||
tarotCard.className = "planet-meta-card";
|
||
|
||
const tarotTitle = document.createElement("strong");
|
||
tarotTitle.textContent = "Tarot Correspondence";
|
||
|
||
const tarotText = document.createElement("div");
|
||
tarotText.className = "planet-text";
|
||
tarotText.textContent = [
|
||
entry.aceCardName ? `Ace: ${entry.aceCardName}` : "",
|
||
entry.courtRank ? `Court Rank: ${entry.courtRank} (all suits)` : ""
|
||
].filter(Boolean).join(" · ") || "--";
|
||
|
||
tarotCard.append(tarotTitle, tarotText);
|
||
|
||
if (entry.aceCardName || entry.courtCardNames.length) {
|
||
const navWrap = document.createElement("div");
|
||
navWrap.className = "alpha-nav-btns";
|
||
|
||
if (entry.aceCardName) {
|
||
const tarotBtn = document.createElement("button");
|
||
tarotBtn.type = "button";
|
||
tarotBtn.className = "alpha-nav-btn";
|
||
tarotBtn.textContent = `Open ${entry.aceCardName} ↗`;
|
||
tarotBtn.addEventListener("click", () => {
|
||
document.dispatchEvent(new CustomEvent("nav:tarot-trump", {
|
||
detail: { cardName: entry.aceCardName }
|
||
}));
|
||
});
|
||
|
||
navWrap.appendChild(tarotBtn);
|
||
}
|
||
|
||
entry.courtCardNames.forEach((cardName) => {
|
||
const courtBtn = document.createElement("button");
|
||
courtBtn.type = "button";
|
||
courtBtn.className = "alpha-nav-btn";
|
||
courtBtn.textContent = `Open ${cardName} ↗`;
|
||
courtBtn.addEventListener("click", () => {
|
||
document.dispatchEvent(new CustomEvent("nav:tarot-trump", {
|
||
detail: { cardName }
|
||
}));
|
||
});
|
||
navWrap.appendChild(courtBtn);
|
||
});
|
||
|
||
tarotCard.appendChild(navWrap);
|
||
}
|
||
|
||
const smallCardCard = document.createElement("div");
|
||
smallCardCard.className = "planet-meta-card";
|
||
|
||
const smallCardTitle = document.createElement("strong");
|
||
smallCardTitle.textContent = "Small Card Sign Types";
|
||
smallCardCard.appendChild(smallCardTitle);
|
||
|
||
const smallCardStack = document.createElement("div");
|
||
smallCardStack.className = "cal-item-stack";
|
||
|
||
(entry.smallCardGroups || []).forEach((group) => {
|
||
const row = document.createElement("div");
|
||
row.className = "cal-item-row";
|
||
|
||
const head = document.createElement("div");
|
||
head.className = "cal-item-head";
|
||
head.innerHTML = `
|
||
<span class="cal-item-name">${group.rangeLabel} · ${group.modality}</span>
|
||
<span class="planet-list-meta">${group.signName || "--"}</span>
|
||
`;
|
||
row.appendChild(head);
|
||
|
||
const navWrap = document.createElement("div");
|
||
navWrap.className = "alpha-nav-btns";
|
||
|
||
if (group.signId) {
|
||
const signBtn = document.createElement("button");
|
||
signBtn.type = "button";
|
||
signBtn.className = "alpha-nav-btn";
|
||
signBtn.textContent = `Open ${group.signName} ↗`;
|
||
signBtn.addEventListener("click", () => {
|
||
document.dispatchEvent(new CustomEvent("nav:zodiac", {
|
||
detail: { signId: group.signId }
|
||
}));
|
||
});
|
||
navWrap.appendChild(signBtn);
|
||
}
|
||
|
||
(group.cardNames || []).forEach((cardName) => {
|
||
const cardBtn = document.createElement("button");
|
||
cardBtn.type = "button";
|
||
cardBtn.className = "alpha-nav-btn";
|
||
cardBtn.textContent = `Open ${cardName} ↗`;
|
||
cardBtn.addEventListener("click", () => {
|
||
document.dispatchEvent(new CustomEvent("nav:tarot-trump", {
|
||
detail: { cardName }
|
||
}));
|
||
});
|
||
navWrap.appendChild(cardBtn);
|
||
});
|
||
|
||
row.appendChild(navWrap);
|
||
smallCardStack.appendChild(row);
|
||
});
|
||
|
||
smallCardCard.appendChild(smallCardStack);
|
||
|
||
grid.append(detailsCard, tarotCard, smallCardCard);
|
||
elements.detailBodyEl.appendChild(grid);
|
||
}
|
||
|
||
function applyFilter(elements) {
|
||
const query = normalize(state.searchQuery);
|
||
state.filteredEntries = query
|
||
? state.entries.filter((entry) => entry.searchText.includes(query))
|
||
: [...state.entries];
|
||
|
||
if (elements?.searchClearEl) {
|
||
elements.searchClearEl.disabled = !query;
|
||
}
|
||
|
||
if (!state.filteredEntries.some((entry) => entry.id === state.selectedId)) {
|
||
state.selectedId = state.filteredEntries[0]?.id || "";
|
||
}
|
||
|
||
renderList(elements);
|
||
renderDetail(elements);
|
||
}
|
||
|
||
function selectByElementId(elementId) {
|
||
const target = findEntryById(elementId);
|
||
if (!target) {
|
||
return false;
|
||
}
|
||
|
||
const elements = getElements();
|
||
state.selectedId = target.id;
|
||
renderList(elements);
|
||
renderDetail(elements);
|
||
|
||
const listItem = elements.listEl?.querySelector(`[data-element-id="${target.id}"]`);
|
||
listItem?.scrollIntoView({ block: "nearest" });
|
||
|
||
return true;
|
||
}
|
||
|
||
function ensureElementsSection(magickDataset) {
|
||
const elements = getElements();
|
||
if (!elements.listEl || !elements.detailBodyEl) {
|
||
return;
|
||
}
|
||
|
||
state.entries = buildEntries(magickDataset);
|
||
|
||
if (!state.selectedId && state.entries.length) {
|
||
state.selectedId = state.entries[0].id;
|
||
}
|
||
|
||
applyFilter(elements);
|
||
|
||
if (state.initialized) {
|
||
return;
|
||
}
|
||
|
||
elements.listEl.addEventListener("click", (event) => {
|
||
const target = event.target instanceof Element
|
||
? event.target.closest(".planet-list-item")
|
||
: null;
|
||
|
||
if (!(target instanceof HTMLButtonElement)) {
|
||
return;
|
||
}
|
||
|
||
const elementId = target.dataset.elementId;
|
||
if (!elementId) {
|
||
return;
|
||
}
|
||
|
||
state.selectedId = elementId;
|
||
renderList(elements);
|
||
renderDetail(elements);
|
||
});
|
||
|
||
if (elements.searchEl) {
|
||
elements.searchEl.addEventListener("input", () => {
|
||
state.searchQuery = elements.searchEl.value || "";
|
||
applyFilter(elements);
|
||
});
|
||
}
|
||
|
||
if (elements.searchClearEl && elements.searchEl) {
|
||
elements.searchClearEl.addEventListener("click", () => {
|
||
state.searchQuery = "";
|
||
elements.searchEl.value = "";
|
||
applyFilter(elements);
|
||
elements.searchEl.focus();
|
||
});
|
||
}
|
||
|
||
state.initialized = true;
|
||
}
|
||
|
||
window.ElementsSectionUi = {
|
||
ensureElementsSection,
|
||
selectByElementId
|
||
};
|
||
})();
|