513 lines
16 KiB
JavaScript
513 lines
16 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
const SIDEBAR_COLLAPSE_STORAGE_PREFIX = "tarot-sidebar-collapsed:v2:";
|
|
const DETAIL_COLLAPSE_STORAGE_PREFIX = "tarot-detail-collapsed:v2:";
|
|
const DEFAULT_DATASET_ENTRY_COLLAPSED = false;
|
|
const DEFAULT_DATASET_DETAIL_COLLAPSED = true;
|
|
const sidebarControllers = new WeakMap();
|
|
const detailControllers = new WeakMap();
|
|
const AUTO_COLLAPSE_ENTRY_SELECTOR = [
|
|
".planet-list-item",
|
|
".tarot-list-item",
|
|
"[role='option']",
|
|
".kab-node[data-sephira]",
|
|
".kab-path-hit[data-path]",
|
|
".kab-path-tarot[data-path]",
|
|
".kab-rose-petal[data-path]",
|
|
".cube-face[role='button']",
|
|
".cube-edge-line[role='button']",
|
|
".cube-direction[role='button']",
|
|
".cube-connector[role='button']",
|
|
".cube-center[role='button']",
|
|
".kab-chip[data-path]"
|
|
].join(", ");
|
|
const AUTO_COLLAPSE_IGNORE_SELECTOR = [
|
|
".sidebar-toggle-inline",
|
|
".sidebar-popout-open",
|
|
"input",
|
|
"select",
|
|
"textarea",
|
|
"label",
|
|
"form",
|
|
".dataset-search-wrap",
|
|
".alpha-text-search-controls",
|
|
".cube-rotation-controls",
|
|
".cube-rotation-btn",
|
|
".tarot-house-action-btn"
|
|
].join(", ");
|
|
|
|
function loadSidebarCollapsedState(storageKey) {
|
|
try {
|
|
const raw = window.localStorage?.getItem(storageKey);
|
|
if (raw === "1") {
|
|
return true;
|
|
}
|
|
if (raw === "0") {
|
|
return false;
|
|
}
|
|
return null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function saveSidebarCollapsedState(storageKey, collapsed) {
|
|
try {
|
|
window.localStorage?.setItem(storageKey, collapsed ? "1" : "0");
|
|
} catch {
|
|
// Ignore storage failures silently.
|
|
}
|
|
}
|
|
|
|
function resolveLayoutTarget(target) {
|
|
if (target instanceof HTMLElement) {
|
|
if (target.matches(".planet-layout, .tarot-layout, .kab-layout")) {
|
|
return target;
|
|
}
|
|
|
|
return target.closest(".planet-layout, .tarot-layout, .kab-layout");
|
|
}
|
|
|
|
if (typeof target === "string" && target) {
|
|
const element = document.getElementById(target);
|
|
if (element instanceof HTMLElement) {
|
|
if (element.matches(".planet-layout, .tarot-layout, .kab-layout")) {
|
|
return element;
|
|
}
|
|
|
|
return element.querySelector(".planet-layout, .tarot-layout, .kab-layout")
|
|
|| element.closest(".planet-layout, .tarot-layout, .kab-layout");
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function setSidebarCollapsed(target, collapsed, persist = true) {
|
|
const layout = resolveLayoutTarget(target);
|
|
const controller = layout ? sidebarControllers.get(layout) : null;
|
|
if (!controller) {
|
|
return false;
|
|
}
|
|
|
|
controller.applyCollapsedState(Boolean(collapsed), persist);
|
|
return true;
|
|
}
|
|
|
|
function setDetailCollapsed(target, collapsed, persist = true) {
|
|
const layout = resolveLayoutTarget(target);
|
|
const controller = layout ? detailControllers.get(layout) : null;
|
|
if (!controller) {
|
|
return false;
|
|
}
|
|
|
|
controller.applyCollapsedState(Boolean(collapsed), persist);
|
|
return true;
|
|
}
|
|
|
|
function showDetailOnly(target, persist = true) {
|
|
const layout = resolveLayoutTarget(target);
|
|
if (!layout) {
|
|
return false;
|
|
}
|
|
|
|
const detailChanged = setDetailCollapsed(layout, false, persist);
|
|
const sidebarChanged = setSidebarCollapsed(layout, true, persist);
|
|
return detailChanged || sidebarChanged;
|
|
}
|
|
|
|
function showSidebarOnly(target, persist = true) {
|
|
const layout = resolveLayoutTarget(target);
|
|
if (!layout) {
|
|
return false;
|
|
}
|
|
|
|
const detailChanged = setDetailCollapsed(layout, true, persist);
|
|
const sidebarChanged = setSidebarCollapsed(layout, false, persist);
|
|
return detailChanged || sidebarChanged;
|
|
}
|
|
|
|
function shouldAutoCollapseFromEvent(panel, target) {
|
|
if (!(panel instanceof HTMLElement) || !(target instanceof Element) || !panel.contains(target)) {
|
|
return false;
|
|
}
|
|
|
|
if (target.closest(AUTO_COLLAPSE_IGNORE_SELECTOR)) {
|
|
return false;
|
|
}
|
|
|
|
return Boolean(target.closest(AUTO_COLLAPSE_ENTRY_SELECTOR));
|
|
}
|
|
|
|
function scheduleAutoCollapse(layout) {
|
|
if (!(layout instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
|
|
window.requestAnimationFrame(() => {
|
|
showDetailOnly(layout);
|
|
});
|
|
}
|
|
|
|
function initializeSidebarAutoCollapse() {
|
|
const layouts = document.querySelectorAll(".planet-layout, .tarot-layout, .kab-layout");
|
|
|
|
layouts.forEach((layout) => {
|
|
if (!(layout instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
|
|
const panel = Array.from(layout.children).find((child) => (
|
|
child instanceof HTMLElement
|
|
&& child.matches("aside.planet-list-panel, aside.tarot-list-panel, aside.kab-tree-panel")
|
|
));
|
|
|
|
if (!(panel instanceof HTMLElement) || panel.dataset.sidebarAutoCollapseReady === "1") {
|
|
return;
|
|
}
|
|
|
|
panel.dataset.sidebarAutoCollapseReady = "1";
|
|
|
|
panel.addEventListener("click", (event) => {
|
|
const target = event.target instanceof Element ? event.target : null;
|
|
if (!shouldAutoCollapseFromEvent(panel, target)) {
|
|
return;
|
|
}
|
|
|
|
scheduleAutoCollapse(layout);
|
|
});
|
|
|
|
panel.addEventListener("keydown", (event) => {
|
|
if (event.key !== "Enter" && event.key !== " ") {
|
|
return;
|
|
}
|
|
|
|
const target = event.target instanceof Element ? event.target : null;
|
|
if (!shouldAutoCollapseFromEvent(panel, target)) {
|
|
return;
|
|
}
|
|
|
|
scheduleAutoCollapse(layout);
|
|
});
|
|
});
|
|
}
|
|
|
|
function initializeSidebarPopouts() {
|
|
const layouts = document.querySelectorAll(".planet-layout, .tarot-layout, .kab-layout");
|
|
|
|
layouts.forEach((layout, index) => {
|
|
if (!(layout instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
|
|
const panel = Array.from(layout.children).find((child) => (
|
|
child instanceof HTMLElement
|
|
&& child.matches("aside.planet-list-panel, aside.tarot-list-panel, aside.kab-tree-panel")
|
|
));
|
|
|
|
if (!(panel instanceof HTMLElement) || panel.dataset.sidebarPopoutReady === "1") {
|
|
return;
|
|
}
|
|
|
|
const header = panel.querySelector(".planet-list-header, .tarot-list-header");
|
|
if (!(header instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
|
|
panel.dataset.sidebarPopoutReady = "1";
|
|
|
|
const sectionId = layout.closest("section")?.id || `layout-${index + 1}`;
|
|
const panelId = panel.id || `${sectionId}-entry-panel`;
|
|
panel.id = panelId;
|
|
|
|
const storageKey = `${SIDEBAR_COLLAPSE_STORAGE_PREFIX}${sectionId}`;
|
|
|
|
const collapseBtn = document.createElement("button");
|
|
collapseBtn.type = "button";
|
|
collapseBtn.className = "sidebar-toggle-inline";
|
|
collapseBtn.textContent = "Hide Panel";
|
|
collapseBtn.setAttribute("aria-label", "Hide entry panel");
|
|
collapseBtn.setAttribute("aria-controls", panelId);
|
|
header.appendChild(collapseBtn);
|
|
|
|
const openBtn = document.createElement("button");
|
|
openBtn.type = "button";
|
|
openBtn.className = "sidebar-popout-open";
|
|
openBtn.textContent = "Show Panel";
|
|
openBtn.setAttribute("aria-label", "Show entry panel");
|
|
openBtn.setAttribute("aria-controls", panelId);
|
|
openBtn.hidden = true;
|
|
layout.appendChild(openBtn);
|
|
|
|
const applyCollapsedState = (collapsed, persist = true) => {
|
|
layout.classList.toggle("layout-sidebar-collapsed", collapsed);
|
|
collapseBtn.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
|
openBtn.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
|
openBtn.hidden = !collapsed;
|
|
|
|
if (persist) {
|
|
saveSidebarCollapsedState(storageKey, collapsed);
|
|
}
|
|
};
|
|
|
|
sidebarControllers.set(layout, {
|
|
applyCollapsedState,
|
|
panel,
|
|
collapseBtn,
|
|
openBtn,
|
|
storageKey
|
|
});
|
|
|
|
collapseBtn.addEventListener("click", () => {
|
|
showDetailOnly(layout);
|
|
});
|
|
|
|
openBtn.addEventListener("click", () => {
|
|
showSidebarOnly(layout);
|
|
});
|
|
|
|
const storedCollapsed = loadSidebarCollapsedState(storageKey);
|
|
applyCollapsedState(storedCollapsed == null ? DEFAULT_DATASET_ENTRY_COLLAPSED : storedCollapsed, false);
|
|
});
|
|
}
|
|
|
|
function initializeDetailPopouts() {
|
|
const layouts = document.querySelectorAll(".planet-layout, .tarot-layout, .kab-layout");
|
|
|
|
layouts.forEach((layout, index) => {
|
|
if (!(layout instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
|
|
const detailPanel = Array.from(layout.children).find((child) => (
|
|
child instanceof HTMLElement
|
|
&& child.matches("section.planet-detail-panel, section.tarot-detail-panel, section.kab-detail-panel")
|
|
));
|
|
|
|
if (!(detailPanel instanceof HTMLElement) || detailPanel.dataset.detailPopoutReady === "1") {
|
|
return;
|
|
}
|
|
|
|
detailPanel.dataset.detailPopoutReady = "1";
|
|
|
|
const sectionId = layout.closest("section")?.id || `layout-${index + 1}`;
|
|
const panelId = detailPanel.id || `${sectionId}-detail-panel`;
|
|
detailPanel.id = panelId;
|
|
|
|
const detailStorageKey = `${DETAIL_COLLAPSE_STORAGE_PREFIX}${sectionId}`;
|
|
const sidebarStorageKey = `${SIDEBAR_COLLAPSE_STORAGE_PREFIX}${sectionId}`;
|
|
|
|
const applyCollapsedState = (collapsed, persist = true) => {
|
|
if (collapsed && layout.classList.contains("layout-sidebar-collapsed")) {
|
|
const sidebarController = sidebarControllers.get(layout);
|
|
if (sidebarController) {
|
|
sidebarController.applyCollapsedState(false, persist);
|
|
} else {
|
|
layout.classList.remove("layout-sidebar-collapsed");
|
|
saveSidebarCollapsedState(sidebarStorageKey, false);
|
|
}
|
|
}
|
|
|
|
layout.classList.toggle("layout-detail-collapsed", collapsed);
|
|
detailPanel.setAttribute("aria-hidden", collapsed ? "true" : "false");
|
|
|
|
if (persist) {
|
|
saveSidebarCollapsedState(detailStorageKey, collapsed);
|
|
}
|
|
};
|
|
|
|
detailControllers.set(layout, {
|
|
applyCollapsedState,
|
|
detailPanel,
|
|
detailStorageKey
|
|
});
|
|
|
|
const storedCollapsed = loadSidebarCollapsedState(detailStorageKey);
|
|
const initialCollapsed = storedCollapsed == null ? DEFAULT_DATASET_DETAIL_COLLAPSED : storedCollapsed;
|
|
applyCollapsedState(initialCollapsed, false);
|
|
});
|
|
}
|
|
|
|
function setTopbarDropdownOpen(dropdownEl, isOpen) {
|
|
if (!(dropdownEl instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
|
|
dropdownEl.classList.toggle("is-open", Boolean(isOpen));
|
|
const trigger = dropdownEl.querySelector("button[aria-haspopup='menu']");
|
|
if (trigger) {
|
|
trigger.setAttribute("aria-expanded", isOpen ? "true" : "false");
|
|
}
|
|
}
|
|
|
|
function getTopbarElements() {
|
|
const topbarEl = document.querySelector(".topbar");
|
|
const actionsEl = document.getElementById("topbar-actions");
|
|
const menuToggleEl = document.getElementById("topbar-menu-toggle");
|
|
|
|
return {
|
|
topbarEl: topbarEl instanceof HTMLElement ? topbarEl : null,
|
|
actionsEl: actionsEl instanceof HTMLElement ? actionsEl : null,
|
|
menuToggleEl: menuToggleEl instanceof HTMLButtonElement ? menuToggleEl : null
|
|
};
|
|
}
|
|
|
|
function setTopbarMenuOpen(isOpen) {
|
|
const { topbarEl, menuToggleEl } = getTopbarElements();
|
|
if (!(topbarEl instanceof HTMLElement) || !(menuToggleEl instanceof HTMLButtonElement)) {
|
|
return;
|
|
}
|
|
|
|
const nextOpen = Boolean(isOpen);
|
|
topbarEl.classList.toggle("is-menu-open", nextOpen);
|
|
menuToggleEl.setAttribute("aria-expanded", nextOpen ? "true" : "false");
|
|
menuToggleEl.textContent = nextOpen ? "Close" : "Menu";
|
|
menuToggleEl.setAttribute("aria-label", nextOpen ? "Close navigation menu" : "Open navigation menu");
|
|
|
|
if (!nextOpen) {
|
|
closeTopbarDropdowns();
|
|
}
|
|
}
|
|
|
|
function bindTopbarMobileMenu() {
|
|
const { topbarEl, actionsEl, menuToggleEl } = getTopbarElements();
|
|
if (!(topbarEl instanceof HTMLElement) || !(actionsEl instanceof HTMLElement) || !(menuToggleEl instanceof HTMLButtonElement)) {
|
|
return;
|
|
}
|
|
|
|
if (menuToggleEl.dataset.mobileMenuReady === "1") {
|
|
return;
|
|
}
|
|
|
|
menuToggleEl.dataset.mobileMenuReady = "1";
|
|
setTopbarMenuOpen(false);
|
|
|
|
menuToggleEl.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
const nextOpen = !topbarEl.classList.contains("is-menu-open");
|
|
setTopbarMenuOpen(nextOpen);
|
|
});
|
|
|
|
actionsEl.addEventListener("click", (event) => {
|
|
const button = event.target instanceof Element
|
|
? event.target.closest("button")
|
|
: null;
|
|
if (!(button instanceof HTMLButtonElement)) {
|
|
return;
|
|
}
|
|
|
|
const isDropdownTrigger = button.getAttribute("aria-haspopup") === "menu";
|
|
const isMenuItem = button.getAttribute("role") === "menuitem";
|
|
|
|
if (!isDropdownTrigger || isMenuItem) {
|
|
window.requestAnimationFrame(() => {
|
|
setTopbarMenuOpen(false);
|
|
});
|
|
}
|
|
});
|
|
|
|
document.addEventListener("click", (event) => {
|
|
const clickTarget = event.target;
|
|
if (clickTarget instanceof Node && topbarEl.contains(clickTarget)) {
|
|
return;
|
|
}
|
|
|
|
setTopbarMenuOpen(false);
|
|
});
|
|
|
|
document.addEventListener("keydown", (event) => {
|
|
if (event.key === "Escape") {
|
|
setTopbarMenuOpen(false);
|
|
}
|
|
});
|
|
}
|
|
|
|
function closeTopbarDropdowns(exceptEl = null) {
|
|
const topbarDropdownEls = Array.from(document.querySelectorAll(".topbar-dropdown"));
|
|
topbarDropdownEls.forEach((dropdownEl) => {
|
|
if (exceptEl && dropdownEl === exceptEl) {
|
|
return;
|
|
}
|
|
setTopbarDropdownOpen(dropdownEl, false);
|
|
});
|
|
}
|
|
|
|
function bindTopbarDropdownInteractions() {
|
|
const topbarDropdownEls = Array.from(document.querySelectorAll(".topbar-dropdown"));
|
|
if (!topbarDropdownEls.length) {
|
|
return;
|
|
}
|
|
|
|
topbarDropdownEls.forEach((dropdownEl) => {
|
|
const trigger = dropdownEl.querySelector("button[aria-haspopup='menu']");
|
|
if (!(trigger instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
|
|
setTopbarDropdownOpen(dropdownEl, false);
|
|
|
|
dropdownEl.addEventListener("focusout", (event) => {
|
|
const nextTarget = event.relatedTarget;
|
|
if (!(nextTarget instanceof Node) || !dropdownEl.contains(nextTarget)) {
|
|
setTopbarDropdownOpen(dropdownEl, false);
|
|
}
|
|
});
|
|
|
|
trigger.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
const nextOpen = !dropdownEl.classList.contains("is-open");
|
|
closeTopbarDropdowns(dropdownEl);
|
|
setTopbarDropdownOpen(dropdownEl, nextOpen);
|
|
});
|
|
|
|
const menuItems = dropdownEl.querySelectorAll(".topbar-dropdown-menu [role='menuitem']");
|
|
menuItems.forEach((menuItem) => {
|
|
menuItem.addEventListener("click", () => {
|
|
closeTopbarDropdowns();
|
|
});
|
|
});
|
|
});
|
|
|
|
document.addEventListener("click", (event) => {
|
|
const clickTarget = event.target;
|
|
if (clickTarget instanceof Node && topbarDropdownEls.some((dropdownEl) => dropdownEl.contains(clickTarget))) {
|
|
return;
|
|
}
|
|
|
|
closeTopbarDropdowns();
|
|
});
|
|
|
|
document.addEventListener("keydown", (event) => {
|
|
if (event.key === "Escape") {
|
|
closeTopbarDropdowns();
|
|
}
|
|
});
|
|
}
|
|
|
|
function init() {
|
|
initializeSidebarPopouts();
|
|
initializeDetailPopouts();
|
|
initializeSidebarAutoCollapse();
|
|
bindTopbarMobileMenu();
|
|
bindTopbarDropdownInteractions();
|
|
}
|
|
|
|
window.TarotChromeUi = {
|
|
...(window.TarotChromeUi || {}),
|
|
init,
|
|
initializeSidebarPopouts,
|
|
initializeDetailPopouts,
|
|
initializeSidebarAutoCollapse,
|
|
bindTopbarMobileMenu,
|
|
setSidebarCollapsed,
|
|
setDetailCollapsed,
|
|
showDetailOnly,
|
|
showSidebarOnly,
|
|
setTopbarMenuOpen,
|
|
setTopbarDropdownOpen,
|
|
closeTopbarDropdowns,
|
|
bindTopbarDropdownInteractions
|
|
};
|
|
})();
|