/* ============================================================================ KalliLab — CUSTOM JAVASCRIPT Home / Media Tabs Reload-Button entfernen Aktiven Tab merken ============================================================================ */ const CONFIG = { STORAGE: { KEY: "kallilab-last-focused-tab-id", }, TIMING: { RETRY_DELAY: 500, STANDARD_REFRESH: 1800000, QUICK_REFRESH: 60000, RETRY_ON_ERROR: 30000, BATCH_DELAY: 100, }, SERVICES: { QUICK_REFRESH: [], }, }; const RELOAD_BUTTON_SELECTORS = [ "#revalidate", '[data-testid="revalidate"]', ".reload-button", 'button[aria-label="Reload"]', '[role="button"][aria-label="Reload"]', ]; const TAB_HASH_MAP = { "#home": "Home-tab", "#media": "Media-tab", "": "Home-tab", }; const state = { lastUpdate: new WeakMap(), currentFocusedTab: null, observers: { reloadButton: null, }, }; const domCache = { myTab: null, tabContents: null, activeTabContent: null, initCache() { this.myTab = document.getElementById("myTab"); this.tabContents = document.querySelectorAll(".tabcontent"); this.updateActiveTab(); }, updateActiveTab() { this.activeTabContent = document.querySelector(".tabcontent.active"); }, clear() { this.myTab = null; this.tabContents = null; this.activeTabContent = null; }, }; const storage = { save(tabId) { try { localStorage.setItem(CONFIG.STORAGE.KEY, tabId); } catch (error) { console.warn("Storage save failed:", error); } }, get() { try { return localStorage.getItem(CONFIG.STORAGE.KEY); } catch (error) { console.warn("Storage retrieval failed:", error); return null; } }, }; function debounce(func, wait) { let timeout; return function executedFunction(...args) { clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); }; } function throttle(func, limit) { let inThrottle; return function executedFunction(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => { inThrottle = false; }, limit); } }; } function removeReloadButton() { RELOAD_BUTTON_SELECTORS.forEach((selector) => { document.querySelectorAll(selector).forEach((element) => element.remove()); }); } function setupReloadButtonObserver() { if (state.observers.reloadButton) { return state.observers.reloadButton; } const observer = new MutationObserver( throttle((mutations) => { const hasAddedNodes = mutations.some((m) => m.addedNodes.length > 0); if (hasAddedNodes) { removeReloadButton(); } }, 100), ); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style"], }); state.observers.reloadButton = observer; return observer; } function setTabFocus(tab) { requestAnimationFrame(() => { if (state.currentFocusedTab) { state.currentFocusedTab.classList.remove("tab-focused"); } state.currentFocusedTab = tab; if (state.currentFocusedTab) { state.currentFocusedTab.classList.add("tab-focused"); } }); } function showTabContent(contentElement) { if (!contentElement || !domCache.tabContents) return; domCache.tabContents.forEach((content) => { content.classList.remove("active"); content.style.display = "none"; }); contentElement.classList.add("active"); contentElement.style.display = "block"; domCache.updateActiveTab(); } function resolveTabContentElement(tab) { if (!tab) return null; const ariaControls = tab.getAttribute("aria-controls"); if (ariaControls) { return document.getElementById(ariaControls); } const tabText = (tab.textContent || "").trim().toLowerCase(); const directMatch = Array.from(document.querySelectorAll(".tabcontent")).find((content) => { return content.id.toLowerCase() === tabText; }); return directMatch || document.querySelector(".tabcontent.active"); } function activateTab(tab, updateHash = true) { if (!tab) return; setTabFocus(tab); storage.save(tab.id); const contentToShow = resolveTabContentElement(tab); showTabContent(contentToShow); if (updateHash) { const normalized = (tab.textContent || "").trim().toLowerCase(); if (normalized === "home") { history.replaceState(null, "", "#home"); } else if (normalized === "media") { history.replaceState(null, "", "#media"); } } } function handleTabFocusFromURL() { const hash = window.location.hash.toLowerCase(); const tabId = TAB_HASH_MAP[hash] || TAB_HASH_MAP[""]; const tabToFocus = document.getElementById(tabId); if (tabToFocus) { activateTab(tabToFocus, false); return true; } return false; } function updateServiceCard(card, data) { requestAnimationFrame(() => { const titleElement = card.querySelector(".card-title"); const statusElement = card.querySelector(".card-status"); if (titleElement && data.title) { titleElement.textContent = data.title; } if (statusElement) { statusElement.textContent = Array.isArray(data) ? `${data.length} items` : (data.status ?? (typeof data === "object" ? "Data received" : "")); statusElement.style.color = ""; } }); } function updateServiceCardError(card, error) { requestAnimationFrame(() => { const statusElement = card.querySelector(".card-status"); if (statusElement) { statusElement.textContent = error.message.includes("404") ? "Service unavailable" : "Error loading data"; statusElement.style.color = "#ff7b7b"; } }); } async function refreshService(card) { const apiEndpoint = card.dataset.apiEndpoint; if (!apiEndpoint) return; const serviceId = card.id || apiEndpoint; const now = Date.now(); const lastUpdateTime = state.lastUpdate.get(card) || 0; const isQuickRefreshService = CONFIG.SERVICES.QUICK_REFRESH.includes(serviceId); const minInterval = isQuickRefreshService ? CONFIG.TIMING.QUICK_REFRESH : CONFIG.TIMING.STANDARD_REFRESH; if (now - lastUpdateTime < minInterval) return; try { card.classList.add("updating"); const response = await fetch(apiEndpoint, { signal: AbortSignal.timeout(10000), }).then((r) => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }); updateServiceCard(card, response); state.lastUpdate.set(card, now); } catch (error) { console.error(`${serviceId} refresh failed:`, error); updateServiceCardError(card, error); state.lastUpdate.set(card, now - (minInterval - CONFIG.TIMING.RETRY_ON_ERROR)); } finally { card.classList.remove("updating"); } } async function batchUpdateServiceCards(cards) { const batchSize = 3; const batches = []; for (let i = 0; i < cards.length; i += batchSize) { batches.push(cards.slice(i, i + batchSize)); } for (const batch of batches) { await Promise.all(batch.map(refreshService)); if (batches.length > 1) { await new Promise((resolve) => setTimeout(resolve, CONFIG.TIMING.BATCH_DELAY)); } } } async function preloadAllTabs() { const tabContents = document.querySelectorAll(".tabcontent, .tab-pane"); const serviceCards = new Set(); tabContents.forEach((tab) => { tab.querySelectorAll(".service-card[data-api-endpoint]").forEach((card) => { serviceCards.add(card); }); }); await batchUpdateServiceCards(Array.from(serviceCards)); } function setupPeriodicRefresh() { const debouncedRefresh = debounce(async () => { if (domCache.activeTabContent) { const cards = domCache.activeTabContent.querySelectorAll( ".service-card[data-api-endpoint]", ); await batchUpdateServiceCards(Array.from(cards)); } }, 250); setInterval( debouncedRefresh, Math.min(CONFIG.TIMING.QUICK_REFRESH, CONFIG.TIMING.STANDARD_REFRESH), ); document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") { debouncedRefresh(); } }); domCache.myTab?.addEventListener("click", (event) => { const tab = event.target.closest('[id$="-tab"]'); if (tab) { activateTab(tab); debouncedRefresh(); } }); } function initializeTabFocus() { const tabs = document.querySelectorAll('#myTab [id$="-tab"]'); if (!tabs.length) return; const handledByHash = handleTabFocusFromURL(); if (!handledByHash) { const savedTabId = storage.get(); const savedTab = savedTabId && document.getElementById(savedTabId); if (savedTab) { activateTab(savedTab, false); } else { const activeTab = document.querySelector('#myTab [aria-selected="true"]'); if (activeTab) { activateTab(activeTab, false); } else { activateTab(tabs[0], false); } } } tabs.forEach((tab) => { tab.addEventListener("click", () => { activateTab(tab); }); tab.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); activateTab(tab); } }); }); window.addEventListener("beforeunload", () => { if (state.currentFocusedTab) { storage.save(state.currentFocusedTab.id); } }); window.addEventListener("hashchange", () => { handleTabFocusFromURL(); }); } function initializeEverything() { removeReloadButton(); setupReloadButtonObserver(); [100, 500, 1000].forEach((delay) => { setTimeout(removeReloadButton, delay); }); const hasRequiredElements = document.querySelector("#myTab") && document.querySelector(".service-card"); if (hasRequiredElements) { domCache.initCache(); initializeTabFocus(); preloadAllTabs(); setupPeriodicRefresh(); } else { setTimeout(initializeEverything, CONFIG.TIMING.RETRY_DELAY); } } function cleanup() { if (state.observers.reloadButton) { state.observers.reloadButton.disconnect(); state.observers.reloadButton = null; } domCache.clear(); state.currentFocusedTab = null; } document.addEventListener("DOMContentLoaded", removeReloadButton); window.addEventListener("load", initializeEverything); window.addEventListener("unload", cleanup); if (typeof window.htmlLoaded === "function") { const originalHtmlLoaded = window.htmlLoaded; window.htmlLoaded = () => { originalHtmlLoaded(); initializeEverything(); }; } if ("ontouchstart" in window) { window.addEventListener( "touchend", () => { setTimeout(removeReloadButton, 100); }, { passive: true }, ); }