Initial commit
This commit is contained in:
459
app/ui-enochian.js
Normal file
459
app/ui-enochian.js
Normal file
@@ -0,0 +1,459 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const TABLET_META = {
|
||||
union: { label: "Enochian Tablet of Union", element: "Spirit", order: 0 },
|
||||
spirit: { label: "Enochian Tablet of Union", element: "Spirit", order: 0 },
|
||||
earth: { label: "Enochian Tablet of Earth", element: "Earth", order: 1 },
|
||||
air: { label: "Enochian Tablet of Air", element: "Air", order: 2 },
|
||||
water: { label: "Enochian Tablet of Water", element: "Water", order: 3 },
|
||||
fire: { label: "Enochian Tablet of Fire", element: "Fire", order: 4 }
|
||||
};
|
||||
|
||||
const TAROT_NAME_ALIASES = {
|
||||
juggler: "magus",
|
||||
magician: "magus",
|
||||
strength: "lust",
|
||||
temperance: "art",
|
||||
judgement: "aeon",
|
||||
judgment: "aeon",
|
||||
charit: "chariot"
|
||||
};
|
||||
|
||||
const state = {
|
||||
initialized: false,
|
||||
entries: [],
|
||||
filteredEntries: [],
|
||||
selectedId: "",
|
||||
selectedCell: null,
|
||||
searchQuery: "",
|
||||
lettersById: new Map()
|
||||
};
|
||||
|
||||
function getElements() {
|
||||
return {
|
||||
listEl: document.getElementById("enochian-list"),
|
||||
countEl: document.getElementById("enochian-count"),
|
||||
searchEl: document.getElementById("enochian-search-input"),
|
||||
searchClearEl: document.getElementById("enochian-search-clear"),
|
||||
detailNameEl: document.getElementById("enochian-detail-name"),
|
||||
detailSubEl: document.getElementById("enochian-detail-sub"),
|
||||
detailBodyEl: document.getElementById("enochian-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 getTabletMeta(id) {
|
||||
const normalizedId = normalize(id);
|
||||
return TABLET_META[normalizedId] || {
|
||||
label: `Enochian Tablet of ${titleCase(normalizedId || "Unknown")}`,
|
||||
element: titleCase(normalizedId || "Unknown"),
|
||||
order: 99
|
||||
};
|
||||
}
|
||||
|
||||
function buildSearchText(entry) {
|
||||
return normalize([
|
||||
entry.id,
|
||||
entry.label,
|
||||
entry.element,
|
||||
entry.rowCount,
|
||||
entry.colCount,
|
||||
...entry.uniqueLetters
|
||||
].join(" "));
|
||||
}
|
||||
|
||||
function buildEntries(magickDataset) {
|
||||
const tablets = magickDataset?.grouped?.enochian?.tablets;
|
||||
if (!tablets || typeof tablets !== "object") {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(tablets)
|
||||
.map(([key, value]) => {
|
||||
const id = normalize(value?.id || key);
|
||||
const grid = Array.isArray(value?.grid)
|
||||
? value.grid.map((row) => (Array.isArray(row)
|
||||
? row.map((cell) => String(cell || "").trim())
|
||||
: []))
|
||||
: [];
|
||||
|
||||
const rowCount = grid.length;
|
||||
const colCount = grid.reduce((max, row) => Math.max(max, row.length), 0);
|
||||
const uniqueLetters = [...new Set(
|
||||
grid
|
||||
.flat()
|
||||
.map((cell) => String(cell || "").trim().toUpperCase())
|
||||
.filter(Boolean)
|
||||
)].sort((left, right) => left.localeCompare(right));
|
||||
|
||||
const meta = getTabletMeta(id);
|
||||
|
||||
return {
|
||||
id,
|
||||
grid,
|
||||
rowCount,
|
||||
colCount,
|
||||
uniqueLetters,
|
||||
element: meta.element,
|
||||
label: meta.label,
|
||||
order: Number(meta.order)
|
||||
};
|
||||
})
|
||||
.sort((left, right) => left.order - right.order || left.label.localeCompare(right.label));
|
||||
}
|
||||
|
||||
function buildLetterMap(magickDataset) {
|
||||
const letters = magickDataset?.grouped?.enochian?.letters;
|
||||
if (!letters || typeof letters !== "object") {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
return new Map(
|
||||
Object.entries(letters)
|
||||
.map(([key, value]) => [String(key || "").trim().toUpperCase(), value])
|
||||
.filter(([key]) => Boolean(key))
|
||||
);
|
||||
}
|
||||
|
||||
function findEntryById(id) {
|
||||
return state.entries.find((entry) => entry.id === id) || null;
|
||||
}
|
||||
|
||||
function getDefaultCell(entry) {
|
||||
if (!entry || !Array.isArray(entry.grid)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (let rowIndex = 0; rowIndex < entry.grid.length; rowIndex += 1) {
|
||||
const row = entry.grid[rowIndex];
|
||||
for (let colIndex = 0; colIndex < row.length; colIndex += 1) {
|
||||
const value = String(row[colIndex] || "").trim();
|
||||
if (value) {
|
||||
return { rowIndex, colIndex, value };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 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 = "enoch-list-item";
|
||||
button.dataset.tabletId = 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 = "enoch-list-name";
|
||||
name.textContent = entry.label;
|
||||
|
||||
const meta = document.createElement("span");
|
||||
meta.className = "enoch-list-meta";
|
||||
meta.textContent = `${entry.element} · ${entry.rowCount}×${entry.colCount} · ${entry.uniqueLetters.length} letters`;
|
||||
|
||||
button.append(name, meta);
|
||||
elements.listEl.appendChild(button);
|
||||
});
|
||||
|
||||
if (elements.countEl) {
|
||||
elements.countEl.textContent = `${state.filteredEntries.length} tablets`;
|
||||
}
|
||||
|
||||
if (!state.filteredEntries.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "planet-text";
|
||||
empty.style.padding = "16px";
|
||||
empty.style.color = "#71717a";
|
||||
empty.textContent = "No Enochian tablets match your search.";
|
||||
elements.listEl.appendChild(empty);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveTarotCardName(value) {
|
||||
const normalized = normalize(value);
|
||||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return TAROT_NAME_ALIASES[normalized] || normalized;
|
||||
}
|
||||
|
||||
function renderDetail(elements) {
|
||||
if (!elements?.detailBodyEl || !elements.detailNameEl || !elements.detailSubEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = findEntryById(state.selectedId);
|
||||
elements.detailBodyEl.replaceChildren();
|
||||
|
||||
if (!entry) {
|
||||
elements.detailNameEl.textContent = "--";
|
||||
elements.detailSubEl.textContent = "Select a tablet to explore";
|
||||
return;
|
||||
}
|
||||
|
||||
elements.detailNameEl.textContent = entry.label;
|
||||
elements.detailSubEl.textContent = `${entry.element} Tablet · ${entry.rowCount} rows × ${entry.colCount} columns`;
|
||||
|
||||
if (!state.selectedCell) {
|
||||
state.selectedCell = getDefaultCell(entry);
|
||||
}
|
||||
|
||||
const detailGrid = document.createElement("div");
|
||||
detailGrid.className = "planet-meta-grid";
|
||||
|
||||
const summaryCard = document.createElement("div");
|
||||
summaryCard.className = "planet-meta-card";
|
||||
summaryCard.innerHTML = `
|
||||
<strong>Tablet Overview</strong>
|
||||
<div class="enoch-letter-meta">
|
||||
<div>${entry.label}</div>
|
||||
<div>${entry.rowCount} rows × ${entry.colCount} columns</div>
|
||||
<div>Unique letters: ${entry.uniqueLetters.length}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const gridCard = document.createElement("div");
|
||||
gridCard.className = "planet-meta-card";
|
||||
const gridTitle = document.createElement("strong");
|
||||
gridTitle.textContent = "Tablet Grid";
|
||||
const gridEl = document.createElement("div");
|
||||
gridEl.className = "enoch-grid";
|
||||
|
||||
entry.grid.forEach((row, rowIndex) => {
|
||||
const rowEl = document.createElement("div");
|
||||
rowEl.className = "enoch-grid-row";
|
||||
row.forEach((cell, colIndex) => {
|
||||
const value = String(cell || "").trim();
|
||||
const cellBtn = document.createElement("button");
|
||||
cellBtn.type = "button";
|
||||
cellBtn.className = "enoch-grid-cell";
|
||||
cellBtn.textContent = value || "·";
|
||||
|
||||
const isSelectedCell = state.selectedCell
|
||||
&& state.selectedCell.rowIndex === rowIndex
|
||||
&& state.selectedCell.colIndex === colIndex;
|
||||
cellBtn.classList.toggle("is-selected", Boolean(isSelectedCell));
|
||||
|
||||
cellBtn.addEventListener("click", () => {
|
||||
state.selectedCell = { rowIndex, colIndex, value };
|
||||
renderDetail(elements);
|
||||
});
|
||||
|
||||
rowEl.appendChild(cellBtn);
|
||||
});
|
||||
gridEl.appendChild(rowEl);
|
||||
});
|
||||
|
||||
gridCard.append(gridTitle, gridEl);
|
||||
|
||||
const letterCard = document.createElement("div");
|
||||
letterCard.className = "planet-meta-card";
|
||||
const letterTitle = document.createElement("strong");
|
||||
letterTitle.textContent = "Selected Letter";
|
||||
|
||||
const selectedLetter = String(state.selectedCell?.value || "").trim().toUpperCase();
|
||||
const letterData = selectedLetter ? state.lettersById.get(selectedLetter) : null;
|
||||
|
||||
const letterContent = document.createElement("div");
|
||||
letterContent.className = "enoch-letter-meta";
|
||||
|
||||
if (!selectedLetter) {
|
||||
letterContent.textContent = "Select any grid cell to inspect its correspondence data.";
|
||||
} else {
|
||||
const firstRow = document.createElement("div");
|
||||
firstRow.className = "enoch-letter-row";
|
||||
const chip = document.createElement("span");
|
||||
chip.className = "enoch-letter-chip";
|
||||
chip.textContent = selectedLetter;
|
||||
firstRow.appendChild(chip);
|
||||
|
||||
const title = document.createElement("span");
|
||||
title.textContent = letterData?.title
|
||||
? `${letterData.title}${letterData.english ? ` · ${letterData.english}` : ""}`
|
||||
: "No letter metadata yet";
|
||||
firstRow.appendChild(title);
|
||||
letterContent.appendChild(firstRow);
|
||||
|
||||
if (letterData) {
|
||||
const detailRows = [
|
||||
["Pronunciation", letterData.pronounciation],
|
||||
["Planet / Element", letterData["planet/element"]],
|
||||
["Tarot", letterData.tarot],
|
||||
["Gematria", letterData.gematria]
|
||||
];
|
||||
|
||||
detailRows.forEach(([label, value]) => {
|
||||
if (value === undefined || value === null || String(value).trim() === "") {
|
||||
return;
|
||||
}
|
||||
const row = document.createElement("div");
|
||||
row.className = "enoch-letter-row";
|
||||
row.innerHTML = `<span style="color:#a1a1aa">${label}:</span><span>${value}</span>`;
|
||||
letterContent.appendChild(row);
|
||||
});
|
||||
|
||||
const navRow = document.createElement("div");
|
||||
navRow.className = "enoch-letter-row";
|
||||
|
||||
const tarotCardName = resolveTarotCardName(letterData.tarot);
|
||||
if (tarotCardName) {
|
||||
const tarotBtn = document.createElement("button");
|
||||
tarotBtn.type = "button";
|
||||
tarotBtn.className = "enoch-nav-btn";
|
||||
tarotBtn.textContent = `Open Tarot (${titleCase(tarotCardName)}) ↗`;
|
||||
tarotBtn.addEventListener("click", () => {
|
||||
document.dispatchEvent(new CustomEvent("nav:tarot-trump", {
|
||||
detail: { cardName: tarotCardName }
|
||||
}));
|
||||
});
|
||||
navRow.appendChild(tarotBtn);
|
||||
}
|
||||
|
||||
const alphabetBtn = document.createElement("button");
|
||||
alphabetBtn.type = "button";
|
||||
alphabetBtn.className = "enoch-nav-btn";
|
||||
alphabetBtn.textContent = "Open Alphabet ↗";
|
||||
alphabetBtn.addEventListener("click", () => {
|
||||
document.dispatchEvent(new CustomEvent("nav:alphabet", {
|
||||
detail: {
|
||||
alphabet: "english",
|
||||
englishLetter: selectedLetter
|
||||
}
|
||||
}));
|
||||
});
|
||||
navRow.appendChild(alphabetBtn);
|
||||
|
||||
letterContent.appendChild(navRow);
|
||||
}
|
||||
}
|
||||
|
||||
letterCard.append(letterTitle, letterContent);
|
||||
|
||||
detailGrid.append(summaryCard, letterCard, gridCard);
|
||||
elements.detailBodyEl.appendChild(detailGrid);
|
||||
}
|
||||
|
||||
function applyFilter(elements) {
|
||||
const query = normalize(state.searchQuery);
|
||||
state.filteredEntries = query
|
||||
? state.entries.filter((entry) => buildSearchText(entry).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 || "";
|
||||
state.selectedCell = state.selectedId ? getDefaultCell(findEntryById(state.selectedId)) : null;
|
||||
}
|
||||
|
||||
renderList(elements);
|
||||
renderDetail(elements);
|
||||
}
|
||||
|
||||
function selectByTabletId(tabletId) {
|
||||
const elements = getElements();
|
||||
const target = findEntryById(normalize(tabletId));
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state.selectedId = target.id;
|
||||
state.selectedCell = getDefaultCell(target);
|
||||
renderList(elements);
|
||||
renderDetail(elements);
|
||||
return true;
|
||||
}
|
||||
|
||||
function ensureEnochianSection(magickDataset) {
|
||||
const elements = getElements();
|
||||
if (!elements.listEl || !elements.detailBodyEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.entries = buildEntries(magickDataset);
|
||||
state.lettersById = buildLetterMap(magickDataset);
|
||||
|
||||
if (!state.selectedId && state.entries.length) {
|
||||
state.selectedId = state.entries[0].id;
|
||||
state.selectedCell = getDefaultCell(state.entries[0]);
|
||||
}
|
||||
|
||||
applyFilter(elements);
|
||||
|
||||
if (state.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
elements.listEl.addEventListener("click", (event) => {
|
||||
const target = event.target instanceof Element
|
||||
? event.target.closest(".enoch-list-item")
|
||||
: null;
|
||||
if (!(target instanceof HTMLButtonElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabletId = target.dataset.tabletId;
|
||||
if (!tabletId) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.selectedId = tabletId;
|
||||
state.selectedCell = getDefaultCell(findEntryById(tabletId));
|
||||
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.EnochianSectionUi = {
|
||||
ensureEnochianSection,
|
||||
selectByTabletId
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user