90 lines
3.3 KiB
JavaScript
90 lines
3.3 KiB
JavaScript
// Mobile Navigation und alle Scrollanimationen aus TriHub Design Original.
|
|
"use strict";
|
|
(() => {
|
|
const menu = document.querySelector(".menu-toggle"),
|
|
nav = document.querySelector("#mobile-nav");
|
|
menu.addEventListener("click", () => {
|
|
const open = menu.getAttribute("aria-expanded") !== "true";
|
|
menu.setAttribute("aria-expanded", String(open));
|
|
menu.setAttribute("aria-label", open ? "Menü schließen" : "Menü öffnen");
|
|
nav.hidden = !open;
|
|
});
|
|
nav.querySelectorAll("a").forEach((a) =>
|
|
a.addEventListener("click", () => {
|
|
nav.hidden = true;
|
|
menu.setAttribute("aria-expanded", "false");
|
|
menu.setAttribute("aria-label", "Menü öffnen");
|
|
}),
|
|
);
|
|
|
|
// Progressive enhancement: content stays visible without JS or animation support.
|
|
(() => {
|
|
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
if (!("IntersectionObserver" in window) || !Element.prototype.animate)
|
|
return;
|
|
const running = new Map();
|
|
const played = new WeakSet();
|
|
const delays = new WeakMap();
|
|
const targets = [
|
|
...document.querySelectorAll(
|
|
".hero-copy > *, .hero-visual, .section-heading, .benefit-grid > article, " +
|
|
".concept-note, .package-grid > article, .coaching-banner, .steps > li, .process-end, .detail-benefits > article, .detail-timeline > li, .price-panel",
|
|
),
|
|
];
|
|
document
|
|
.querySelectorAll(
|
|
".hero-copy, .benefit-grid, .package-grid, .steps, .detail-benefits, .detail-timeline",
|
|
)
|
|
.forEach((group) => {
|
|
[...group.children].forEach((child, index) =>
|
|
delays.set(child, Math.min(index * 80, 240)),
|
|
);
|
|
});
|
|
function reveal(element) {
|
|
if (played.has(element)) return;
|
|
played.add(element);
|
|
if (reducedMotion.matches) return;
|
|
const animation = element.animate(
|
|
[
|
|
{ opacity: 0, transform: "translateY(24px)" },
|
|
{ opacity: 1, transform: "translateY(0)" },
|
|
],
|
|
{
|
|
duration: 720,
|
|
delay: delays.get(element) || 0,
|
|
easing: "cubic-bezier(.22, 1, .36, 1)",
|
|
fill: "backwards",
|
|
},
|
|
);
|
|
running.set(element, animation);
|
|
const cleanup = () => {
|
|
if (running.get(element) === animation) running.delete(element);
|
|
};
|
|
animation.onfinish = cleanup;
|
|
animation.oncancel = cleanup;
|
|
}
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
if (!entry.isIntersecting) return;
|
|
observer.unobserve(entry.target);
|
|
// A direct anchor jump must not animate already-scrolled-past content.
|
|
if (entry.boundingClientRect.bottom > 0) reveal(entry.target);
|
|
});
|
|
},
|
|
{ threshold: 0, rootMargin: "0px 0px -24px 0px" },
|
|
);
|
|
targets.forEach((element) => observer.observe(element));
|
|
// Keyboard users never wait for a focused control to become visible.
|
|
document.addEventListener("focusin", (event) => {
|
|
running.forEach((animation, element) => {
|
|
if (element.contains(event.target)) animation.cancel();
|
|
});
|
|
});
|
|
reducedMotion.addEventListener("change", () => {
|
|
if (reducedMotion.matches)
|
|
running.forEach((animation) => animation.cancel());
|
|
});
|
|
})();
|
|
})();
|