be24904b78
Arrow keys and touch swipe now navigate between plates while the zoom is open, keeping the lightbox in sync underneath. goToZoomSlide clamps to list bounds and delegates to goToSlide for thumb/index sync. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
536 lines
20 KiB
JavaScript
536 lines
20 KiB
JavaScript
/* ROUX — app.js */
|
|
(function () {
|
|
'use strict';
|
|
|
|
// ── Data (injected by Hugo into a JSON script tag)
|
|
let POSTS = [];
|
|
try {
|
|
const el = document.getElementById('roux-data');
|
|
if (el) POSTS = JSON.parse(el.textContent) || [];
|
|
} catch (e) { console.warn('roux-data parse failed', e); }
|
|
|
|
const STOP = new Set(['the','a','an','of','and','in','on','at','by','with','is','to','for','from','as','into','onto','its','it','that','this','but','or','be','not','no','one','two','three']);
|
|
|
|
// ── Helpers
|
|
function esc(s) { return String(s).replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
|
|
|
function highlight(text, terms) {
|
|
if (!terms.length) return esc(text);
|
|
const re = new RegExp('(' + terms.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|') + ')', 'ig');
|
|
return esc(text).replace(re, '<mark class="hl">$1</mark>');
|
|
}
|
|
|
|
// ── Inverted search index
|
|
const INDEX = (() => {
|
|
const map = new Map();
|
|
function add(token, idx, weight) {
|
|
const t = token.toLowerCase();
|
|
if (!t || t.length < 2 || STOP.has(t)) return;
|
|
let b = map.get(t);
|
|
if (!b) { b = new Map(); map.set(t, b); }
|
|
b.set(idx, (b.get(idx) || 0) + weight);
|
|
}
|
|
function tok(s) { return (s || '').toLowerCase().split(/[^a-z0-9àâäéèêëîïôöùûüçœæñ]+/i).filter(Boolean); }
|
|
|
|
POSTS.forEach((p, i) => {
|
|
tok(p.title).forEach(t => add(t, i, 6));
|
|
(p.categories || []).forEach(c => tok(c).forEach(t => add(t, i, 4)));
|
|
(p.tags || []).forEach(tag => tok(tag).forEach(t => add(t, i, 5)));
|
|
tok(p.description).forEach(t => add(t, i, 1));
|
|
tok(p.id).forEach(t => add(t, i, 8));
|
|
});
|
|
|
|
return function search(q) {
|
|
const tokens = tok(q);
|
|
if (!tokens.length) return null;
|
|
const perToken = tokens.map(tok => {
|
|
const merged = new Map();
|
|
for (const [term, b] of map) {
|
|
if (term.startsWith(tok)) {
|
|
const factor = term === tok ? 1.0 : tok.length / term.length;
|
|
for (const [pi, w] of b) merged.set(pi, (merged.get(pi) || 0) + w * factor);
|
|
}
|
|
}
|
|
return merged;
|
|
});
|
|
const first = perToken[0];
|
|
const scores = new Map();
|
|
for (const [idx, w] of first) {
|
|
let total = w, ok = true;
|
|
for (let i = 1; i < perToken.length; i++) {
|
|
const v = perToken[i].get(idx);
|
|
if (!v) { ok = false; break; }
|
|
total += v;
|
|
}
|
|
if (ok) scores.set(idx, total);
|
|
}
|
|
return Array.from(scores.entries())
|
|
.sort((a, b) => b[1] - a[1])
|
|
.map(([i]) => POSTS[i]);
|
|
};
|
|
})();
|
|
|
|
// ── Masthead date
|
|
const mhDate = document.getElementById('mhDate');
|
|
if (mhDate) {
|
|
const d = new Date();
|
|
mhDate.textContent = d.toLocaleDateString('en-GB', { day:'numeric', month:'long', year:'numeric' });
|
|
}
|
|
|
|
// ── Count display
|
|
const countEl = document.getElementById('count');
|
|
function setCount(n) {
|
|
if (!countEl) return;
|
|
countEl.innerHTML = `<b>${String(n).padStart(3,'0')}</b> ${n === 1 ? 'post' : 'posts'}`;
|
|
}
|
|
setCount(POSTS.length);
|
|
|
|
// ── Tabs: sync active state with current URL
|
|
function syncTabs() {
|
|
const tabs = document.querySelectorAll('.tabs button[data-cat]');
|
|
const path = location.pathname;
|
|
tabs.forEach(btn => {
|
|
const cat = btn.dataset.cat;
|
|
const slug = cat.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
|
const active = (cat === 'All' && (path === '/' || path.startsWith('/issues') || path === '/posts/'))
|
|
|| (cat !== 'All' && path === `/categories/${slug}/`);
|
|
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|
});
|
|
}
|
|
syncTabs();
|
|
|
|
// Tab click → navigate to category page
|
|
document.addEventListener('click', e => {
|
|
const btn = e.target.closest('.tabs button[data-cat]');
|
|
if (!btn) return;
|
|
const cat = btn.dataset.cat;
|
|
if (cat === 'All') navigate('/');
|
|
else navigate('/categories/' + cat.toLowerCase().replace(/[\s,]+/g, '-').replace(/[^a-z0-9-]/g, '') + '/');
|
|
});
|
|
|
|
// ── Search popup
|
|
const searchInput = document.getElementById('searchInput');
|
|
const searchPop = document.getElementById('searchpop');
|
|
|
|
function allCats() {
|
|
const s = new Set();
|
|
POSTS.forEach(p => (p.categories || []).forEach(c => s.add(c)));
|
|
return Array.from(s).slice(0, 8);
|
|
}
|
|
function allTags() {
|
|
const freq = new Map();
|
|
POSTS.forEach(p => (p.tags || []).forEach(t => freq.set(t, (freq.get(t)||0)+1)));
|
|
return Array.from(freq.entries()).sort((a,b)=>b[1]-a[1]).slice(0,10).map(([t])=>t);
|
|
}
|
|
|
|
function renderSearchPop(q) {
|
|
if (!searchPop) return;
|
|
if (!q) {
|
|
const cats = allCats();
|
|
const tags = allTags();
|
|
searchPop.innerHTML = `
|
|
<div class="sp-section">
|
|
<div class="sp-label">Categories <small>${cats.length}</small></div>
|
|
<div class="sp-chips">
|
|
${cats.map(c => `<button class="sp-chip" data-jump="/categories/${encodeURIComponent(c.toLowerCase().replace(/[\s,]+/g,'-').replace(/[^a-z0-9-]/g,''))}/"> ${esc(c)}</button>`).join('')}
|
|
</div>
|
|
</div>
|
|
<div class="sp-section">
|
|
<div class="sp-label">Popular tags <small>${tags.length}</small></div>
|
|
<div class="sp-chips">
|
|
${tags.map(t => `<button class="sp-chip" data-jump="/tags/${encodeURIComponent(t.toLowerCase().replace(/[\s,]+/g,'-').replace(/[^a-z0-9-]/g,''))}/"> # ${esc(t)}</button>`).join('')}
|
|
</div>
|
|
</div>`;
|
|
searchPop.dataset.open = 'true';
|
|
return;
|
|
}
|
|
const hits = INDEX(q) || [];
|
|
const terms = q.toLowerCase().split(/\s+/).filter(t => t.length > 1);
|
|
if (!hits.length) {
|
|
searchPop.innerHTML = `<div class="sp-section"><p style="color:var(--ink-soft);font-size:13px;font-style:italic">No plates match — try <em>gothic</em>, <em>warrior</em>, or <em>neon</em>.</p></div>`;
|
|
searchPop.dataset.open = 'true';
|
|
return;
|
|
}
|
|
const shown = hits.slice(0, 6);
|
|
searchPop.innerHTML = `
|
|
<div class="sp-section">
|
|
<div class="sp-label">Plates <small>${hits.length}</small></div>
|
|
<div class="sp-hits">
|
|
${shown.map(p => `
|
|
<button class="sp-hit" data-jump="${esc(p.url)}">
|
|
${p.thumb ? `<img src="${esc(p.thumb)}" alt="" loading="lazy" />` : '<div style="width:44px;height:66px;background:var(--paper-2)"></div>'}
|
|
<div>
|
|
<div class="t">${highlight(p.title, terms)}</div>
|
|
<div class="s">${highlight((p.categories||[]).join(', '), terms)}</div>
|
|
</div>
|
|
<div class="n">${esc(p.id)}</div>
|
|
</button>`).join('')}
|
|
</div>
|
|
</div>`;
|
|
searchPop.dataset.open = 'true';
|
|
}
|
|
|
|
if (searchInput) {
|
|
searchInput.addEventListener('input', () => renderSearchPop(searchInput.value.trim()));
|
|
searchInput.addEventListener('focus', () => renderSearchPop(searchInput.value.trim()));
|
|
document.addEventListener('keydown', e => {
|
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); searchInput.focus(); renderSearchPop(''); }
|
|
if (e.key === 'Escape') closeSearch();
|
|
});
|
|
}
|
|
|
|
if (searchPop) {
|
|
searchPop.addEventListener('click', e => {
|
|
const btn = e.target.closest('[data-jump]');
|
|
if (btn) { closeSearch(); navigate(btn.dataset.jump); }
|
|
});
|
|
}
|
|
|
|
document.addEventListener('click', e => {
|
|
if (searchPop && searchPop.dataset.open === 'true') {
|
|
const inSearch = e.target.closest('label[for="searchInput"]')
|
|
|| e.target.id === 'searchInput'
|
|
|| e.target.closest('#searchpop');
|
|
if (!inSearch) closeSearch();
|
|
}
|
|
});
|
|
|
|
function closeSearch() {
|
|
if (searchPop) { searchPop.dataset.open = 'false'; searchPop.innerHTML = ''; }
|
|
if (searchInput) searchInput.blur();
|
|
}
|
|
|
|
// ── Lightbox
|
|
const lb = document.getElementById('lb');
|
|
const lbTrack = document.getElementById('lbTrack');
|
|
const lbMeta = document.getElementById('lbMeta');
|
|
const lbThumbs = document.getElementById('lbThumbs');
|
|
const lbIndex = document.getElementById('lbIndex');
|
|
|
|
let lbList = []; // current scoped post list
|
|
let lbIdx = -1; // index into lbList
|
|
let lbBuilt = false;
|
|
let lbReferrer = null; // URL to return to on close
|
|
let imgZoomOpen = false;
|
|
let imgZoomIdx = -1;
|
|
|
|
function lbOpen(slug, scopedList) {
|
|
lbList = scopedList || POSTS;
|
|
const idx = lbList.findIndex(p => p.slug === slug);
|
|
if (idx === -1) return;
|
|
lbIdx = idx;
|
|
if (!lbBuilt) buildLbSlides();
|
|
lb.dataset.open = 'true';
|
|
document.body.style.overflow = 'hidden';
|
|
lbBuildMeta(lbList[lbIdx]);
|
|
lbBuildThumbs();
|
|
goToSlide(lbIdx, false);
|
|
}
|
|
|
|
function lbClose() {
|
|
if (!lb) return;
|
|
lb.dataset.open = 'false';
|
|
document.body.style.overflow = '';
|
|
if (window.__ROUX_OPEN_SLUG) {
|
|
// Direct load of a single-post URL — go back to the issue page
|
|
const cur = lbList[lbIdx];
|
|
const issueId = (cur && cur.issue) || '01';
|
|
navigate(`/issues/${issueId}/`);
|
|
} else if (lbReferrer) {
|
|
// Opened from a grid page — return to it
|
|
const ref = lbReferrer;
|
|
lbReferrer = null;
|
|
navigate(ref);
|
|
}
|
|
}
|
|
|
|
function buildLbSlides() {
|
|
if (!lbTrack) return;
|
|
lbTrack.innerHTML = lbList.map((p, i) => {
|
|
const imgSrc = p.card || p.thumb || '';
|
|
return `<div class="lb-slide" data-i="${i}">
|
|
<div class="lb-frame">
|
|
<img class="lb-img" src="${esc(imgSrc)}" alt="${esc(p.title)}" loading="lazy" />
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
lbBuilt = true;
|
|
preloadNeighbors(lbIdx);
|
|
}
|
|
|
|
function goToSlide(idx, smooth = true) {
|
|
if (!lbTrack) return;
|
|
lbIdx = Math.max(0, Math.min(idx, lbList.length - 1));
|
|
lbTrack.style.transition = smooth ? '' : 'none';
|
|
lbTrack.style.transform = `translateX(-${lbIdx * 100}%)`;
|
|
if (!smooth) lbTrack.getBoundingClientRect(); // force reflow
|
|
if (lbIndex) {
|
|
lbIndex.innerHTML = `<b>${String(lbIdx + 1).padStart(3,'0')}</b> / ${lbList.length}`;
|
|
}
|
|
lbBuildMeta(lbList[lbIdx]);
|
|
syncThumbs();
|
|
preloadNeighbors(lbIdx);
|
|
}
|
|
|
|
function preloadNeighbors(idx) {
|
|
if (!lbTrack) return;
|
|
[-1, 0, 1, 2].forEach(d => {
|
|
const ni = idx + d;
|
|
if (ni < 0 || ni >= lbList.length) return;
|
|
const slide = lbTrack.querySelector(`.lb-slide[data-i="${ni}"]`);
|
|
if (!slide) return;
|
|
const img = slide.querySelector('img');
|
|
if (img && !img.src) img.src = lbList[ni].card || lbList[ni].thumb || '';
|
|
});
|
|
}
|
|
|
|
function lbBuildMeta(p) {
|
|
if (!lbMeta || !p) return;
|
|
const cats = (p.categories || []).join(', ');
|
|
const tags = (p.tags || []).slice(0, 8);
|
|
lbMeta.innerHTML = `
|
|
<div class="lb-cat">${esc(cats)}</div>
|
|
<h2 class="lb-title">${esc(p.title)}</h2>
|
|
<p class="lb-desc">${esc(p.description)}</p>
|
|
<dl class="lb-facts">
|
|
<div class="lb-fact"><dt>Plate</dt><dd>№ ${esc(p.id)}</dd></div>
|
|
<div class="lb-fact"><dt>Issue</dt><dd><a href="/issues/${esc(p.issue || '01')}/" style="color:inherit;border-bottom:1px solid currentColor">№ ${esc(p.issue || '01')}</a></dd></div>
|
|
<div class="lb-fact"><dt>Category</dt><dd>${esc((p.categories||['—'])[0])}</dd></div>
|
|
</dl>
|
|
<div class="lb-tags">${tags.map(t => {
|
|
const slug = t.toLowerCase().replace(/[\s,]+/g, '-').replace(/[^a-z0-9-]/g, '');
|
|
return `<a class="lb-tag" href="/tags/${slug}/"># ${esc(t)}</a>`;
|
|
}).join('')}</div>
|
|
<div class="lb-share">
|
|
<button class="lb-sh lb-sh-primary" id="lbCopy" data-url="${esc(p.url)}">
|
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.07 0l3-3a5 5 0 1 0-7.07-7.07l-1 1"/><path d="M14 11a5 5 0 0 0-7.07 0l-3 3a5 5 0 1 0 7.07 7.07l1-1"/></svg>
|
|
<span class="lb-sh-l">Copy link</span>
|
|
</button>
|
|
</div>`;
|
|
|
|
document.getElementById('lbCopy')?.addEventListener('click', function () {
|
|
const url = new URL(this.dataset.url, location.origin).href;
|
|
navigator.clipboard.writeText(url).then(() => {
|
|
this.classList.add('is-ok');
|
|
this.querySelector('.lb-sh-l').textContent = 'Copied!';
|
|
setTimeout(() => { this.classList.remove('is-ok'); this.querySelector('.lb-sh-l').textContent = 'Copy link'; }, 2000);
|
|
});
|
|
});
|
|
}
|
|
|
|
function lbBuildThumbs() {
|
|
if (!lbThumbs) return;
|
|
lbThumbs.innerHTML = lbList.map((p, i) =>
|
|
`<button class="lb-thumb" data-i="${i}" aria-current="${i === lbIdx}" aria-label="${esc(p.title)}">
|
|
<img src="${esc(p.thumb || p.card || '')}" alt="" loading="lazy" />
|
|
</button>`
|
|
).join('');
|
|
lbThumbs.addEventListener('click', e => {
|
|
const btn = e.target.closest('.lb-thumb');
|
|
if (btn) goToSlide(parseInt(btn.dataset.i));
|
|
});
|
|
}
|
|
|
|
function syncThumbs() {
|
|
if (!lbThumbs) return;
|
|
lbThumbs.querySelectorAll('.lb-thumb').forEach((b, i) => {
|
|
b.setAttribute('aria-current', i === lbIdx ? 'true' : 'false');
|
|
});
|
|
const active = lbThumbs.querySelector('.lb-thumb[aria-current="true"]');
|
|
if (active) active.scrollIntoView({ inline: 'center', behavior: 'smooth' });
|
|
}
|
|
|
|
// Lightbox controls
|
|
if (lb) {
|
|
lb.addEventListener('click', e => {
|
|
const act = e.target.closest('[data-act]')?.dataset.act;
|
|
if (act === 'close') lbClose();
|
|
else if (act === 'prev') goToSlide(lbIdx - 1);
|
|
else if (act === 'next') goToSlide(lbIdx + 1);
|
|
});
|
|
document.addEventListener('keydown', e => {
|
|
if (lb.dataset.open !== 'true') return;
|
|
if (e.key === 'Escape') { if (imgZoomOpen) closeImgZoom(); else lbClose(); }
|
|
if (e.key === 'ArrowLeft') { e.preventDefault(); imgZoomOpen ? goToZoomSlide(imgZoomIdx - 1) : goToSlide(lbIdx - 1); }
|
|
if (e.key === 'ArrowRight') { e.preventDefault(); imgZoomOpen ? goToZoomSlide(imgZoomIdx + 1) : goToSlide(lbIdx + 1); }
|
|
});
|
|
|
|
// Touch swipe
|
|
let touchX = null;
|
|
lb.addEventListener('touchstart', e => { touchX = e.touches[0].clientX; }, { passive: true });
|
|
lb.addEventListener('touchend', e => {
|
|
if (touchX === null) return;
|
|
const dx = e.changedTouches[0].clientX - touchX;
|
|
if (Math.abs(dx) > 50) { dx < 0 ? goToSlide(lbIdx + 1) : goToSlide(lbIdx - 1); }
|
|
touchX = null;
|
|
});
|
|
}
|
|
|
|
// ── Card clicks → open lightbox (no full page navigation for same-origin)
|
|
document.addEventListener('click', e => {
|
|
const card = e.target.closest('.card[data-slug]');
|
|
if (!card) return;
|
|
e.preventDefault();
|
|
const slug = card.dataset.slug;
|
|
const clicked = POSTS.find(p => p.slug === slug);
|
|
const issueId = clicked ? clicked.issue : null;
|
|
const scopedList = issueId ? POSTS.filter(p => p.issue === issueId) : POSTS;
|
|
lbReferrer = location.href;
|
|
history.pushState({ slug }, '', card.href);
|
|
lbOpen(slug, scopedList.length ? scopedList : POSTS);
|
|
});
|
|
|
|
// Handle browser back/forward
|
|
window.addEventListener('popstate', e => {
|
|
if (lb && lb.dataset.open === 'true') {
|
|
if (!e.state?.slug) { lb.dataset.open = 'false'; document.body.style.overflow = ''; }
|
|
}
|
|
});
|
|
|
|
// ── Ribbon
|
|
const ribbon = document.getElementById('ribbon');
|
|
const ribbonClose = document.getElementById('ribbonClose');
|
|
if (ribbonClose && ribbon) {
|
|
if (sessionStorage.getItem('ribbon-closed')) ribbon.classList.add('hidden');
|
|
ribbonClose.addEventListener('click', () => {
|
|
ribbon.classList.add('hidden');
|
|
sessionStorage.setItem('ribbon-closed', '1');
|
|
});
|
|
}
|
|
|
|
// ── Async page transitions (View Transitions API)
|
|
function isSameOrigin(url) {
|
|
try { return new URL(url).origin === location.origin; } catch { return false; }
|
|
}
|
|
|
|
async function navigate(url) {
|
|
if (!isSameOrigin(url)) { location.href = url; return; }
|
|
|
|
if (!document.startViewTransition) {
|
|
location.href = url;
|
|
return;
|
|
}
|
|
|
|
document.startViewTransition(async () => {
|
|
const res = await fetch(url, { headers: { 'X-Requested-With': 'fetch' } });
|
|
const text = await res.text();
|
|
const doc = new DOMParser().parseFromString(text, 'text/html');
|
|
|
|
const newContent = doc.getElementById('content');
|
|
const newData = doc.getElementById('roux-data');
|
|
const oldContent = document.getElementById('content');
|
|
|
|
if (newContent && oldContent) {
|
|
oldContent.replaceWith(newContent);
|
|
}
|
|
if (newData) {
|
|
try {
|
|
POSTS = JSON.parse(newData.textContent) || [];
|
|
lbBuilt = false; // force rebuild on next open
|
|
} catch {}
|
|
}
|
|
|
|
document.title = doc.title;
|
|
history.pushState({}, '', url);
|
|
|
|
// Re-init page state
|
|
setCount(POSTS.length);
|
|
syncTabs();
|
|
|
|
// If new page has a slug to open
|
|
const openSlug = window.__ROUX_OPEN_SLUG = doc.querySelector('[data-open-slug]')?.dataset.openSlug || null;
|
|
if (openSlug) {
|
|
const opened = POSTS.find(p => p.slug === openSlug);
|
|
const issueId = opened ? opened.issue : null;
|
|
const scoped = issueId ? POSTS.filter(p => p.issue === issueId) : POSTS;
|
|
lbOpen(openSlug, scoped.length ? scoped : POSTS);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Intercept link clicks for async transitions
|
|
document.addEventListener('click', e => {
|
|
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
const a = e.target.closest('a[href]');
|
|
if (!a || a.target || a.hasAttribute('download')) return;
|
|
if (!isSameOrigin(a.href)) return;
|
|
// Skip lightbox card clicks (handled above)
|
|
if (a.closest('.card[data-slug]')) return;
|
|
// Skip anchor links
|
|
if (a.href.includes('#')) return;
|
|
e.preventDefault();
|
|
if (lb && lb.dataset.open === 'true') lbClose();
|
|
navigate(a.href);
|
|
});
|
|
|
|
// ── On single-post pages: auto-open lightbox
|
|
if (window.__ROUX_OPEN_SLUG && POSTS.length) {
|
|
const opened = POSTS.find(p => p.slug === window.__ROUX_OPEN_SLUG);
|
|
const issueId = opened ? opened.issue : null;
|
|
const scoped = issueId ? POSTS.filter(p => p.issue === issueId) : POSTS;
|
|
lbOpen(window.__ROUX_OPEN_SLUG, scoped.length ? scoped : POSTS);
|
|
}
|
|
|
|
// ── Full-image zoom overlay
|
|
const imgZoom = document.getElementById('imgZoom');
|
|
const imgZoomImg = document.getElementById('imgZoomImg');
|
|
|
|
function openImgZoom(src) {
|
|
if (!imgZoom || !imgZoomImg || !src) return;
|
|
imgZoomImg.src = src;
|
|
imgZoomIdx = lbIdx;
|
|
imgZoom.dataset.open = 'true';
|
|
imgZoomOpen = true;
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
function closeImgZoom() {
|
|
if (!imgZoom) return;
|
|
imgZoom.dataset.open = 'false';
|
|
imgZoomOpen = false;
|
|
if (!lb || lb.dataset.open !== 'true') document.body.style.overflow = '';
|
|
}
|
|
|
|
function goToZoomSlide(idx) {
|
|
if (!lbList.length || !imgZoomImg) return;
|
|
imgZoomIdx = Math.max(0, Math.min(idx, lbList.length - 1));
|
|
const p = lbList[imgZoomIdx];
|
|
if (p) imgZoomImg.src = p.card || p.thumb || '';
|
|
goToSlide(imgZoomIdx);
|
|
}
|
|
|
|
// Hero "View full image" button
|
|
const viewFullBtn = document.getElementById('viewFull');
|
|
if (viewFullBtn) {
|
|
viewFullBtn.addEventListener('click', () => openImgZoom(viewFullBtn.dataset.src));
|
|
}
|
|
|
|
// Click on the lightbox main image → zoom
|
|
document.addEventListener('click', e => {
|
|
const img = e.target.closest('.lb-img');
|
|
if (img) openImgZoom(img.src);
|
|
});
|
|
|
|
// Close on backdrop click or close button
|
|
if (imgZoom) {
|
|
imgZoom.addEventListener('click', e => { if (e.target === imgZoom) closeImgZoom(); });
|
|
document.getElementById('imgZoomClose')?.addEventListener('click', closeImgZoom);
|
|
|
|
// Swipe to navigate
|
|
let zoomTouchX = null;
|
|
imgZoom.addEventListener('touchstart', e => { zoomTouchX = e.touches[0].clientX; }, { passive: true });
|
|
imgZoom.addEventListener('touchend', e => {
|
|
if (zoomTouchX === null) return;
|
|
const dx = e.changedTouches[0].clientX - zoomTouchX;
|
|
if (Math.abs(dx) > 50) dx < 0 ? goToZoomSlide(imgZoomIdx + 1) : goToZoomSlide(imgZoomIdx - 1);
|
|
zoomTouchX = null;
|
|
});
|
|
}
|
|
|
|
// Escape when lightbox is NOT open (standalone zoom)
|
|
document.addEventListener('keydown', e => {
|
|
if (e.key === 'Escape' && imgZoomOpen && (!lb || lb.dataset.open !== 'true')) closeImgZoom();
|
|
});
|
|
|
|
})();
|