Initial commit
This commit is contained in:
454
app/ui-elements.js
Normal file
454
app/ui-elements.js
Normal file
@@ -0,0 +1,454 @@
|
||||
(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
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user