/** * KUDLA.LT — front-end behaviour. * * Vanilla ES module, no dependencies. Progressive enhancement: every feature * degrades to a usable static state with JS off. */ const doc = document; const body = doc.body; /* -- loading page ------------------------------------------------------------ */ function initLoader() { const loader = doc.querySelector('.k-loader'); if (!loader) return; const done = () => body.classList.add('k-loaded'); if (doc.readyState === 'complete') { setTimeout(done, 240); return; } window.addEventListener('load', () => setTimeout(done, 240), { once: true }); // safety net setTimeout(done, 3500); } /* -- sticky header --------------------------------------------------------- */ function initHeader() { const header = doc.querySelector('[data-header]'); if (!header) return; const onScroll = () => header.classList.toggle('is-stuck', window.scrollY > 8); onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); } /* -- mega menu (desktop) ------------------------------------------------- */ function initMega() { const toggle = doc.querySelector('[data-mega-toggle]'); const mega = doc.querySelector('[data-mega]'); if (!toggle || !mega) return; const set = (open) => { mega.hidden = false; requestAnimationFrame(() => mega.classList.toggle('is-open', open)); toggle.setAttribute('aria-expanded', open ? 'true' : 'false'); if (!open) setTimeout(() => { if (!mega.classList.contains('is-open')) mega.hidden = true; }, 500); }; toggle.addEventListener('click', () => set(!mega.classList.contains('is-open'))); doc.addEventListener('click', (e) => { if (mega.classList.contains('is-open') && !mega.contains(e.target) && !toggle.contains(e.target)) set(false); }); doc.addEventListener('keydown', (e) => { if (e.key === 'Escape' && mega.classList.contains('is-open')) { set(false); toggle.focus(); } }); mega.querySelectorAll('a').forEach((a) => a.addEventListener('click', () => set(false))); // Hover intent on the header's right zone (desktop only). const zone = doc.querySelector('[data-mega-zone]'); const header = doc.querySelector('[data-header]'); if (zone && header && matchMedia('(hover: hover) and (min-width: 901px)').matches) { let t; zone.addEventListener('mouseenter', () => { clearTimeout(t); set(true); }); header.addEventListener('mouseleave', () => { t = setTimeout(() => set(false), 160); }); mega.addEventListener('mouseenter', () => clearTimeout(t)); mega.addEventListener('mouseleave', () => { t = setTimeout(() => set(false), 160); }); } } /* -- mobile drawer ------------------------------------------------------ */ function initDrawer() { const toggles = [...doc.querySelectorAll('[data-drawer-toggle]')]; const drawer = doc.querySelector('[data-drawer]'); if (!toggles.length || !drawer) return; const toggle = toggles[0]; const set = (open) => { drawer.hidden = !open ? true : false; if (open) drawer.hidden = false; body.classList.toggle('k-nav-open', open); toggles.forEach((t) => t.setAttribute('aria-expanded', open ? 'true' : 'false')); body.style.overflow = open ? 'hidden' : ''; if (!open) setTimeout(() => { if (!body.classList.contains('k-nav-open')) drawer.hidden = true; }, 500); }; toggles.forEach((t) => t.addEventListener('click', () => set(!body.classList.contains('k-nav-open')))); doc.addEventListener('keydown', (e) => { if (e.key === 'Escape' && body.classList.contains('k-nav-open')) { set(false); toggle.focus(); } }); drawer.querySelectorAll('a').forEach((a) => a.addEventListener('click', () => set(false))); } /* -- reveal on scroll ------------------------------------------------------ */ function initReveal() { const items = [...doc.querySelectorAll('[data-reveal]')]; if (!items.length) return; const show = (el) => { el.classList.remove('reveal-armed'); el.classList.add('is-in'); }; const disarmAll = () => items.forEach(show); if (!('IntersectionObserver' in window) || matchMedia('(prefers-reduced-motion: reduce)').matches) { return; // nothing armed -> everything already visible } // Arm ONLY the elements comfortably below the fold; the rest stay visible. const armLine = window.innerHeight * 1.15; items.forEach((el) => { if (el.getBoundingClientRect().top > armLine) el.classList.add('reveal-armed'); else el.classList.add('is-in'); }); const armed = items.filter((el) => el.classList.contains('reveal-armed')); if (!armed.length) return; const io = new IntersectionObserver((entries) => { entries.forEach((en) => { if (en.isIntersecting) { show(en.target); io.unobserve(en.target); } }); }, { rootMargin: '0px 0px -8% 0px', threshold: 0.04 }); armed.forEach((el) => io.observe(el)); // Safety nets: reveal on any scroll, and hard-reveal everything after a // short delay so nothing can stay hidden (frozen transition, no IO, …). const sweep = () => items.forEach((el) => { if (el.classList.contains('reveal-armed') && el.getBoundingClientRect().top < window.innerHeight * 0.98) show(el); }); window.addEventListener('scroll', sweep, { passive: true }); window.addEventListener('load', sweep); setTimeout(sweep, 300); setTimeout(disarmAll, 900); } /* -- home "Ar žinojai, kad…" fact rotator ------------------------------- */ function initKnow() { const root = doc.querySelector('[data-know]'); if (!root) return; const all = [...root.querySelectorAll('[data-know-fact]')]; const imgs = [...root.querySelectorAll('[data-know-img]')]; const prev = root.querySelector('[data-know-prev]'); const next = root.querySelector('[data-know-next]'); const chips = [...root.querySelectorAll('[data-chip]')]; if (all.length < 1) return; let pool = all; let idx = 0; const render = () => { all.forEach((f) => { f.hidden = true; }); const active = pool[idx] || null; if (active) active.hidden = false; const i = active ? active.dataset.knowI : '0'; imgs.forEach((fig) => { fig.hidden = fig.dataset.knowI !== i; }); const one = pool.length <= 1; if (prev) prev.disabled = one; if (next) next.disabled = one; }; const move = (d) => { if (pool.length) { idx = (idx + d + pool.length) % pool.length; render(); } }; prev && prev.addEventListener('click', () => move(-1)); next && next.addEventListener('click', () => move(1)); chips.forEach((chip) => { chip.addEventListener('click', () => { const on = chip.getAttribute('aria-pressed') === 'true'; chips.forEach((c) => c.setAttribute('aria-pressed', 'false')); if (on) { pool = all; } else { chip.setAttribute('aria-pressed', 'true'); const val = chip.dataset.chip; pool = all.filter((f) => (f.dataset.filterItem || '').split(/\s+/).includes(val)); if (!pool.length) pool = all; } idx = 0; render(); }); }); render(); } /* -- expanding image row (apie "2026 – KUDLA PRODUKTAI") ----------------- * Four images; hover one — or use the ← → arrows — to open it wide with its frosted note. Not a moving carousel; the open item just grows. */ function initCara() { const root = doc.querySelector('[data-cara]'); if (!root) return; const row = root.querySelector('[data-cara-row]'); const items = [...root.querySelectorAll('[data-cara-item]')]; if (!row || items.length < 2) return; const prev = root.querySelector('[data-cara-prev]'); const next = root.querySelector('[data-cara-next]'); let cur = Math.max(0, items.findIndex((i) => i.classList.contains('is-active'))); // Column template per open index — set inline so the change is always // picked up (attribute-selector recalcs can be flaky); CSS keeps matching // [data-active] rules as the no-JS / fallback path. const wide = items.length <= 3 ? 62 : 56; const narrow = (100 - wide) / (items.length - 1); const cols = items.map((_, k) => items.map((__, n) => (n === k ? wide : narrow) + 'fr').join(' ') ); const set = (i) => { cur = (i + items.length) % items.length; row.dataset.active = String(cur); row.style.gridTemplateColumns = cols[cur]; items.forEach((el, n) => { const on = n === cur; el.classList.toggle('is-active', on); el.setAttribute('aria-pressed', on ? 'true' : 'false'); }); }; items.forEach((el, n) => { el.addEventListener('mouseenter', () => set(n)); el.addEventListener('focus', () => set(n)); el.addEventListener('click', () => set(n)); }); prev && prev.addEventListener('click', () => set(cur - 1)); next && next.addEventListener('click', () => set(cur + 1)); set(cur); } /* -- simple carousels ----------------------------------------------------- */ function initCarousels() { doc.querySelectorAll('[data-carousel]').forEach((root) => { const track = root.querySelector('[data-carousel-track]'); const prev = root.querySelector('[data-carousel-prev]'); const next = root.querySelector('[data-carousel-next]'); if (!track) return; const step = () => { const first = track.children[0]; if (!first) return track.clientWidth * 0.8; const gap = parseFloat(getComputedStyle(track).columnGap || getComputedStyle(track).gap || '0') || 0; return first.getBoundingClientRect().width + gap; }; const sync = () => { if (prev) prev.disabled = track.scrollLeft < 4; if (next) next.disabled = track.scrollLeft + track.clientWidth >= track.scrollWidth - 4; }; prev && prev.addEventListener('click', () => track.scrollBy({ left: -step(), behavior: 'smooth' })); next && next.addEventListener('click', () => track.scrollBy({ left: step(), behavior: 'smooth' })); track.addEventListener('scroll', sync, { passive: true }); sync(); }); } /* -- filter chips (client-side filtering of [data-filter-item]) ---------- */ function initChips() { doc.querySelectorAll('[data-chips]').forEach((group) => { // The home "did you know" chips are driven by initKnow(), not here. if (group.closest('[data-know]')) return; const targets = doc.querySelectorAll(group.dataset.chips ? `[data-filter-scope="${group.dataset.chips}"] [data-filter-item]` : '[data-filter-item]'); group.querySelectorAll('[data-chip]').forEach((chip) => { chip.addEventListener('click', () => { group.querySelectorAll('[data-chip]').forEach((c) => c.setAttribute('aria-pressed', 'false')); chip.setAttribute('aria-pressed', 'true'); const val = chip.dataset.chip; targets.forEach((t) => { const tags = (t.dataset.filterItem || '').split(/\s+/); t.hidden = val !== '*' && !tags.includes(val); }); }); }); }); } /* -- responsive hero video -------------------------------------------------- One