/** * Screenshot Gallery / Lightbox functionality — the website's ONE picture viewer (2026-10-08). * * A set is DECLARED in the page, never guessed: every picture that opens the viewer is a real link to its full-size * file, ``, and a set is all links with the same name, in page order. * An app card's further pictures are hidden links in the same card. Nothing is probed, nothing outside the page is * requested. With JavaScript off the link simply opens the image, so the page keeps working. * * Closes on: the close button, Esc, a click beside the picture, and a click or tap ON the picture. A swipe changes the * picture and does not close. Labels follow . */ (function () { 'use strict'; // Debug mode - set to true for console logging const DEBUG = false; function log(...args) { if (DEBUG) console.log('[Gallery]', ...args); } const LABELS = { hu: { close: 'Bezárás', prev: 'Előző', next: 'Következő', loading: 'Betöltés...', go: n => `Ugrás ${n}. képhez`, dialog: 'Képnézegető' }, en: { close: 'Close', prev: 'Previous', next: 'Next', loading: 'Loading...', go: n => `Go to image ${n}`, dialog: 'Picture viewer' }, }; const lang = (document.documentElement.lang || 'hu').slice(0, 2); const L = LABELS[lang] || LABELS.hu; // The markup, built once. const box = document.createElement('div'); box.className = 'lightbox-overlay'; box.setAttribute('role', 'dialog'); box.setAttribute('aria-modal', 'true'); box.setAttribute('aria-label', L.dialog); box.tabIndex = -1; box.innerHTML = '' + '' + '' + ''; const closeBtn = box.querySelector('.lightbox-close'); const prevBtn = box.querySelector('.lightbox-prev'); const nextBtn = box.querySelector('.lightbox-next'); const img = box.querySelector('.lightbox-image'); const counter = box.querySelector('.lightbox-counter'); const dots = box.querySelector('.lightbox-dots'); closeBtn.setAttribute('aria-label', L.close); closeBtn.innerHTML = '×'; prevBtn.setAttribute('aria-label', L.prev); nextBtn.setAttribute('aria-label', L.next); const state = { items: [], index: 0, opener: null, swiped: false }; function altOf(a, fallback) { const i = a.querySelector('img'); return (i && i.getAttribute('alt')) || a.getAttribute('data-alt') || fallback || ''; } function show() { const it = state.items[state.index]; counter.textContent = L.loading; img.onload = () => update(); img.src = it.src; img.alt = it.alt; update(); } function update() { const n = state.items.length; counter.textContent = `${state.index + 1} / ${n}`; dots.querySelectorAll('.lightbox-dot').forEach((d, i) => d.classList.toggle('active', i === state.index)); prevBtn.disabled = state.index === 0; nextBtn.disabled = state.index === n - 1; const single = n <= 1; prevBtn.style.display = single ? 'none' : 'flex'; nextBtn.style.display = single ? 'none' : 'flex'; dots.style.display = single ? 'none' : 'flex'; counter.style.display = single ? 'none' : 'block'; // a button that just turned off would drop the focus to the page behind the viewer if (document.activeElement && document.activeElement.disabled) closeBtn.focus(); } function go(i) { if (i >= 0 && i < state.items.length && i !== state.index) { state.index = i; show(); } } function open(link) { const name = link.getAttribute('data-gallery'); const links = Array.from(document.querySelectorAll('a[data-gallery]')).filter(a => a.getAttribute('data-gallery') === name); const clickedAlt = altOf(link); state.items = links.map(a => ({ src: a.getAttribute('href'), alt: a === link ? clickedAlt : altOf(a, clickedAlt) })); state.index = Math.max(0, links.indexOf(link)); state.opener = link; log('open', name, state.items.length, 'pictures'); dots.innerHTML = ''; state.items.forEach((_, i) => { const d = document.createElement('button'); d.type = 'button'; d.className = 'lightbox-dot'; d.setAttribute('aria-label', L.go(i + 1)); d.addEventListener('click', e => { e.stopPropagation(); go(i); }); dots.appendChild(d); }); if (!box.parentNode) document.body.appendChild(box); box.classList.add('active'); document.body.style.overflow = 'hidden'; show(); closeBtn.focus(); } function close() { if (!box.classList.contains('active')) return; box.classList.remove('active'); document.body.style.overflow = ''; img.removeAttribute('src'); const back = state.opener; state.items = []; state.index = 0; state.opener = null; if (back) back.focus(); log('closed'); } // Openers: a plain click opens the viewer; a modified click (new tab, etc.) is left to the browser. document.addEventListener('click', e => { const a = e.target.closest('a[data-gallery]'); if (!a || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; e.preventDefault(); open(a); }); closeBtn.addEventListener('click', e => { e.stopPropagation(); close(); }); prevBtn.addEventListener('click', e => { e.stopPropagation(); go(state.index - 1); }); nextBtn.addEventListener('click', e => { e.stopPropagation(); go(state.index + 1); }); // A click anywhere else closes: beside the picture, and on the picture itself — unless it ended a swipe. box.addEventListener('click', e => { if (state.swiped) { state.swiped = false; return; } if (e.target === box || e.target.classList.contains('lightbox-content') || e.target === img) close(); }); document.addEventListener('keydown', e => { if (!box.classList.contains('active')) return; if (e.key === 'Escape') close(); else if (e.key === 'ArrowLeft') go(state.index - 1); else if (e.key === 'ArrowRight') go(state.index + 1); }); // Swipe on a phone: changes the picture, never closes (the click that follows a swipe is swallowed above). let startX = 0, startY = 0; box.addEventListener('touchstart', e => { startX = e.changedTouches[0].screenX; startY = e.changedTouches[0].screenY; state.swiped = false; }, { passive: true }); box.addEventListener('touchend', e => { const dx = startX - e.changedTouches[0].screenX; const dy = startY - e.changedTouches[0].screenY; if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy)) { state.swiped = true; if (dx > 0) go(state.index + 1); else go(state.index - 1); } }, { passive: true }); log('Gallery initialized'); })();