Template
Firmendaten durch Platzhalter ersetzt (Mustermann Haustechnik GmbH, Musterstraße 1, 12345 Musterstadt, 01234 567890, example.com), Design und Funktionen unverändert. - Feiertage im Öffnungsstatus per BUNDESLAND (alle 16 Länder) statt fest Bayern - Datenschutz: Aufsichtsbehörde als TODO je Bundesland - Skript-Namensraum olz -> hsk, Logo-SVG und OG-Bild neu - .webklar-preview.json von Olzinger entfernt (sonst Deploy auf olzinger) - .gitea/template: neue Repos aus der Vorlage erben die Preview-Datei nicht - README: Anleitung für neue HSK-Seiten, Platzhalter-Tabelle, Checkliste Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
203 lines
8.4 KiB
JavaScript
203 lines
8.4 KiB
JavaScript
/* ==========================================================================
|
||
HSK-Vorlage – Hauptskript
|
||
Navigation, mobiles Menü, dezente Einblendungen, Leistungs-Karussell (mobil)
|
||
und Kartenstapel (Desktop). Bewusst ohne Bibliotheken.
|
||
========================================================================== */
|
||
(function (w, d) {
|
||
'use strict';
|
||
|
||
var html = d.documentElement;
|
||
var $ = function (s, c) { return (c || d).querySelector(s); };
|
||
var $$ = function (s, c) { return Array.prototype.slice.call((c || d).querySelectorAll(s)); };
|
||
|
||
var reduce = w.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||
var hasIO = 'IntersectionObserver' in w;
|
||
w.__hskReady = true;
|
||
|
||
/* ---------- Dynamische Jahreszahlen ---------- */
|
||
var nowYear = new Date().getFullYear();
|
||
$$('[data-current-year]').forEach(function (el) { el.textContent = nowYear; });
|
||
$$('[data-years]').forEach(function (el) { el.textContent = nowYear - 1990; });
|
||
|
||
/* ---------- Wärmebild (ruhiger Hintergrund im Hero) ---------- */
|
||
var canvas = $('[data-thermal]');
|
||
if (canvas && w.HskThermal) {
|
||
var thermal = w.HskThermal.init(canvas, {
|
||
hero: $('[data-hero]'),
|
||
calm: $('.hero__aside'),
|
||
reduceMotion: reduce
|
||
});
|
||
if (thermal) thermal.heatUp();
|
||
} else if (canvas) {
|
||
html.classList.add('no-webgl');
|
||
}
|
||
|
||
/* ---------- Mobiles Menü ---------- */
|
||
var menu = $('[data-menu]');
|
||
var toggle = $('[data-menu-toggle]');
|
||
var menuOpen = false;
|
||
|
||
if (menu) { menu.hidden = false; menu.setAttribute('inert', ''); menu.setAttribute('aria-hidden', 'true'); }
|
||
|
||
function setMenu(open) {
|
||
if (!menu || !toggle || open === menuOpen) return;
|
||
menuOpen = open;
|
||
menu.classList.toggle('is-open', open);
|
||
if (open) menu.removeAttribute('inert'); else menu.setAttribute('inert', '');
|
||
menu.setAttribute('aria-hidden', open ? 'false' : 'true');
|
||
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||
toggle.querySelector('.sr-only').textContent = open ? 'Menü schließen' : 'Menü öffnen';
|
||
html.classList.toggle('menu-open', open);
|
||
// Inhalt hinter dem Menü für Tastatur und Screenreader sperren
|
||
[d.getElementById('main'), $('.footer')].forEach(function (el) { if (el) el.inert = open; });
|
||
}
|
||
if (toggle) toggle.addEventListener('click', function () { setMenu(!menuOpen); });
|
||
d.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Escape' && menuOpen) { setMenu(false); toggle.focus(); }
|
||
});
|
||
// Wird das Fenster breit genug für die Desktop-Navigation (z. B. Tablet drehen), Menü schließen
|
||
var deskMQ = w.matchMedia('(min-width: 1181px)');
|
||
var onDesk = function (e) { if (e.matches) setMenu(false); };
|
||
if (deskMQ.addEventListener) deskMQ.addEventListener('change', onDesk); else if (deskMQ.addListener) deskMQ.addListener(onDesk);
|
||
|
||
/* ---------- Anker-Links: Menü schließen, Thema ans Formular geben ---------- */
|
||
d.addEventListener('click', function (e) {
|
||
var a = e.target.closest('a[href*="#"]');
|
||
if (!a) return;
|
||
if (a.dataset.topic) w.dispatchEvent(new CustomEvent('hsk:topic', { detail: a.dataset.topic }));
|
||
if (menuOpen) setMenu(false);
|
||
});
|
||
|
||
/* ---------- Navigation: Hintergrund ab Scroll, beim Runterscrollen ausblenden ---------- */
|
||
var nav = $('[data-nav]');
|
||
if (nav) {
|
||
var lastY = w.scrollY, ticking = false;
|
||
var onScroll = function () {
|
||
ticking = false;
|
||
var y = w.scrollY;
|
||
nav.classList.toggle('is-scrolled', y > 40);
|
||
if (!menuOpen) {
|
||
if (y > 420 && y > lastY + 4) nav.classList.add('is-hidden');
|
||
else if (y < lastY - 4 || y <= 420) nav.classList.remove('is-hidden');
|
||
}
|
||
lastY = y;
|
||
};
|
||
w.addEventListener('scroll', function () {
|
||
if (!ticking) { ticking = true; w.requestAnimationFrame(onScroll); }
|
||
}, { passive: true });
|
||
nav.addEventListener('focusin', function () { nav.classList.remove('is-hidden'); });
|
||
onScroll();
|
||
}
|
||
|
||
// Aktiven Menüpunkt markieren
|
||
var navLinks = $$('[data-nav-link]');
|
||
if (hasIO && navLinks.length) {
|
||
var linkFor = {};
|
||
navLinks.forEach(function (l) { linkFor[l.getAttribute('href').slice(1)] = l; });
|
||
var activeIO = new IntersectionObserver(function (entries) {
|
||
entries.forEach(function (en) {
|
||
var link = linkFor[en.target.id];
|
||
if (link) link.classList.toggle('is-active', en.isIntersecting);
|
||
});
|
||
}, { rootMargin: '-45% 0px -54% 0px' });
|
||
Object.keys(linkFor).forEach(function (id) {
|
||
var sec = d.getElementById(id);
|
||
if (sec) activeIO.observe(sec);
|
||
});
|
||
}
|
||
|
||
/* ---------- Dezente Einblendungen ---------- */
|
||
var reveals = $$('[data-reveal]');
|
||
if (hasIO && !reduce) {
|
||
var revealIO = new IntersectionObserver(function (entries) {
|
||
entries.forEach(function (en) {
|
||
if (!en.isIntersecting) return;
|
||
en.target.classList.add('is-in');
|
||
revealIO.unobserve(en.target);
|
||
});
|
||
}, { rootMargin: '0px 0px -8% 0px' });
|
||
reveals.forEach(function (el) { revealIO.observe(el); });
|
||
} else {
|
||
reveals.forEach(function (el) { el.classList.add('is-in'); });
|
||
}
|
||
|
||
/* ---------- Leistungen: Karussell (mobil) bzw. Kartenstapel (Desktop) ---------- */
|
||
var stack = $('[data-stack]');
|
||
var cards = $$('.svc');
|
||
var stackNav = $('[data-stack-nav]');
|
||
var stackMQ = w.matchMedia('(min-width: 900px) and (min-height: 660px)');
|
||
|
||
if (stack && stackNav) {
|
||
var prevBtn = $('[data-stack-prev]', stackNav);
|
||
var nextBtn = $('[data-stack-next]', stackNav);
|
||
var currentEl = $('[data-stack-current]', stackNav);
|
||
var current = 0;
|
||
|
||
var isCarousel = function () { return stack.scrollWidth > stack.clientWidth + 2; };
|
||
var cardLeft = function (c) { return c.offsetLeft - cards[0].offsetLeft; };
|
||
var cardIndex = function () {
|
||
var x = stack.scrollLeft, best = 0, bestDist = Infinity;
|
||
if (x >= stack.scrollWidth - stack.clientWidth - 2) return cards.length - 1;
|
||
cards.forEach(function (c, i) {
|
||
var dist = Math.abs(cardLeft(c) - x);
|
||
if (dist < bestDist) { bestDist = dist; best = i; }
|
||
});
|
||
return best;
|
||
};
|
||
var updateNav = function () {
|
||
stackNav.hidden = !isCarousel();
|
||
current = cardIndex();
|
||
currentEl.textContent = current + 1;
|
||
prevBtn.disabled = current === 0;
|
||
nextBtn.disabled = current === cards.length - 1;
|
||
};
|
||
// Schritt relativ zur echten Scrollposition (bei zwei Karten nebeneinander
|
||
// liegen die letzten Karten jenseits der maximalen Scrollweite)
|
||
var step = function (dir) {
|
||
var x = stack.scrollLeft, i;
|
||
if (dir > 0) { for (i = 0; i < cards.length - 1; i++) if (cardLeft(cards[i]) > x + 2) break; }
|
||
else { for (i = cards.length - 1; i > 0; i--) if (cardLeft(cards[i]) < x - 2) break; }
|
||
stack.scrollTo({ left: Math.min(cardLeft(cards[i]), stack.scrollWidth - stack.clientWidth), behavior: reduce ? 'auto' : 'smooth' });
|
||
};
|
||
|
||
var navTick = false;
|
||
stack.addEventListener('scroll', function () {
|
||
if (!navTick) { navTick = true; w.requestAnimationFrame(function () { navTick = false; updateNav(); }); }
|
||
}, { passive: true });
|
||
prevBtn.addEventListener('click', function () { step(-1); });
|
||
nextBtn.addEventListener('click', function () { step(1); });
|
||
w.addEventListener('resize', updateNav);
|
||
updateNav();
|
||
}
|
||
|
||
// Desktop: hintere Karten treten beim Überdecken leicht zurück
|
||
if (stack && !reduce) {
|
||
var inners = cards.map(function (c) { return $('.svc__inner', c); });
|
||
var stackTick = false;
|
||
var paintStack = function () {
|
||
stackTick = false;
|
||
if (!stackMQ.matches) {
|
||
inners.forEach(function (el) { el.style.transform = ''; el.style.removeProperty('--shade'); });
|
||
return;
|
||
}
|
||
var vh = w.innerHeight;
|
||
cards.forEach(function (card, i) {
|
||
var next = cards[i + 1];
|
||
if (!next) return;
|
||
var stickyTop = parseFloat(getComputedStyle(next).top) || 100;
|
||
var start = vh * 0.85;
|
||
var p = (start - next.getBoundingClientRect().top) / (start - stickyTop);
|
||
p = Math.max(0, Math.min(1, p));
|
||
inners[i].style.transform = p ? 'scale(' + (1 - p * (0.1 - i * 0.012)) + ')' : '';
|
||
inners[i].style.setProperty('--shade', (p * 0.5).toFixed(3));
|
||
});
|
||
};
|
||
var requestPaint = function () {
|
||
if (!stackTick) { stackTick = true; w.requestAnimationFrame(paintStack); }
|
||
};
|
||
w.addEventListener('scroll', requestPaint, { passive: true });
|
||
w.addEventListener('resize', requestPaint);
|
||
paintStack();
|
||
}
|
||
})(window, document);
|