Files
homepage/custom.js
T
2026-03-21 19:28:12 +01:00

435 lines
10 KiB
JavaScript
Executable File

/* ============================================================================
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 },
);
}