42519a776a
gates / gates (push) Successful in 4m29s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
173 lines
7.3 KiB
JavaScript
173 lines
7.3 KiB
JavaScript
/**
|
||
* 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, `<a href="/assets/…webp" data-gallery="<set>">`, 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 <html lang>.
|
||
*/
|
||
(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 =
|
||
'<button type="button" class="lightbox-close"></button>' +
|
||
'<div class="lightbox-content">' +
|
||
'<button type="button" class="lightbox-nav lightbox-prev">‹</button>' +
|
||
'<img class="lightbox-image" src="" alt="">' +
|
||
'<button type="button" class="lightbox-nav lightbox-next">›</button>' +
|
||
'</div>' +
|
||
'<div class="lightbox-counter"></div>' +
|
||
'<div class="lightbox-dots"></div>';
|
||
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');
|
||
})();
|