Files
felhom.eu/website/assets/gallery.js
T

173 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 = '&times;';
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');
})();