feat(galeria): masonry em JS com lightbox, em vez do slider
- «Conhece as nossas instalações» (e a galeria do CrossFit) passa de slider Splide a masonry: layout próprio em JS (sem biblioteca), fotos com a proporção original, coluna mais baixa primeiro e algumas a ocupar duas colunas (só quando não deixam buraco). Reorganiza ao mudar de largura (ResizeObserver + resize) e as fotos entram em cascata ao aparecer. Sem JS fica uma grelha CSS. - Clicar abre a foto num <dialog>: anterior/seguinte, setas, Esc, deslizar. - O Splide deixa de ser carregado (CSS e JS). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cb0e4540f2
commit
d70f13aea0
5 files changed
+379
-36
No files matched your search
@@ -695,3 +695,144 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
btn.addEventListener('click', function () { select.value = btn.getAttribute('data-membership'); });
|
||||
});
|
||||
});
|
||||
|
||||
/* ── Galeria: lightbox ───────────────────────────────────────────────────────
|
||||
Tiles are plain links to the full image (works without JS). With JS a click
|
||||
opens the <dialog> instead: prev / next buttons, ← → keys, Esc, swipe on
|
||||
touch, and a click on the dark backdrop closes it. */
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.querySelectorAll('.gal-masonry').forEach(function (grid) {
|
||||
var box = grid.parentElement.nextElementSibling;
|
||||
if (!box || !box.classList.contains('gal-lightbox') || typeof box.showModal !== 'function') return;
|
||||
var links = Array.prototype.slice.call(grid.querySelectorAll('.gal-link'));
|
||||
var img = box.querySelector('.gal-lb-figure img');
|
||||
var count = box.querySelector('.gal-lb-count');
|
||||
var current = 0;
|
||||
|
||||
function show(i) {
|
||||
current = (i + links.length) % links.length;
|
||||
var src = links[current].querySelector('img');
|
||||
img.src = links[current].getAttribute('href');
|
||||
img.alt = src ? src.alt : '';
|
||||
// Restart the fade-in for each photo.
|
||||
img.style.animation = 'none'; void img.offsetWidth; img.style.animation = '';
|
||||
count.textContent = (current + 1) + ' / ' + links.length;
|
||||
}
|
||||
|
||||
links.forEach(function (a, i) {
|
||||
a.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
show(i);
|
||||
box.showModal();
|
||||
});
|
||||
});
|
||||
box.querySelector('.gal-lb-prev').addEventListener('click', function () { show(current - 1); });
|
||||
box.querySelector('.gal-lb-next').addEventListener('click', function () { show(current + 1); });
|
||||
box.querySelector('.gal-lb-close').addEventListener('click', function () { box.close(); });
|
||||
// Clicks that land on the dialog itself (not the photo or a button) are the backdrop.
|
||||
box.addEventListener('click', function (e) { if (e.target === box) box.close(); });
|
||||
box.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'ArrowLeft') show(current - 1);
|
||||
else if (e.key === 'ArrowRight') show(current + 1);
|
||||
});
|
||||
box.addEventListener('close', function () { links[current].focus(); });
|
||||
|
||||
var x0 = null;
|
||||
box.addEventListener('touchstart', function (e) { x0 = e.touches[0].clientX; }, { passive: true });
|
||||
box.addEventListener('touchend', function (e) {
|
||||
if (x0 === null) return;
|
||||
var dx = e.changedTouches[0].clientX - x0;
|
||||
if (Math.abs(dx) > 40) show(current + (dx < 0 ? 1 : -1));
|
||||
x0 = null;
|
||||
}, { passive: true });
|
||||
});
|
||||
});
|
||||
|
||||
/* ── Galeria: masonry layout ─────────────────────────────────────────────────
|
||||
A small hand-rolled masonry (no library): the grid is split into columns
|
||||
(4 / 3 / 2 by width), and each tile — at its photo's true aspect ratio, from
|
||||
the width/height attributes the PHP writes — is dropped into the shortest
|
||||
column; tiles marked .gal-w take two adjacent columns (the pair whose taller
|
||||
one is lowest), which is what staggers the rows. Tiles are absolutely
|
||||
positioned with transforms, so a relayout on resize glides into place.
|
||||
The first time the gallery scrolls into view the tiles rise in one by one. */
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.querySelectorAll('.gal-masonry').forEach(function (grid) {
|
||||
var tiles = Array.prototype.slice.call(grid.querySelectorAll('.gal-tile'));
|
||||
if (!tiles.length) return;
|
||||
grid.classList.add('is-masonry');
|
||||
|
||||
function ratio(tile) {
|
||||
var img = tile.querySelector('img');
|
||||
var w = +img.getAttribute('width') || img.naturalWidth || 4;
|
||||
var h = +img.getAttribute('height') || img.naturalHeight || 3;
|
||||
return h / w;
|
||||
}
|
||||
function layout() {
|
||||
var width = grid.clientWidth;
|
||||
var cols = width >= 900 ? 4 : width >= 520 ? 3 : 2;
|
||||
var gap = width >= 520 ? 12 : 8;
|
||||
var colW = (width - gap * (cols - 1)) / cols;
|
||||
var heights = [];
|
||||
for (var c = 0; c < cols; c++) heights.push(0);
|
||||
|
||||
tiles.forEach(function (tile) {
|
||||
var span = (tile.classList.contains('gal-w') && cols > 1) ? 2 : 1;
|
||||
// Best column (or pair of columns) = lowest top edge.
|
||||
var pick = function (span) {
|
||||
var best = 0, bestTop = Infinity, waste = 0;
|
||||
for (var i = 0; i <= cols - span; i++) {
|
||||
var cells = heights.slice(i, i + span);
|
||||
var top = Math.max.apply(null, cells);
|
||||
if (top < bestTop - 0.5) { bestTop = top; best = i; waste = top - Math.min.apply(null, cells); }
|
||||
}
|
||||
return [best, bestTop, waste];
|
||||
};
|
||||
var p = pick(span);
|
||||
// A wide tile whose two columns end at different heights would
|
||||
// leave a hole under the shorter one — use a single column then.
|
||||
if (span === 2 && p[2] > 60) { span = 1; p = pick(1); }
|
||||
var best = p[0], bestTop = p[1];
|
||||
var w = colW * span + gap * (span - 1);
|
||||
var h = Math.round(w * ratio(tile));
|
||||
tile.style.width = w + 'px';
|
||||
tile.style.height = h + 'px';
|
||||
tile.style.transform = 'translate(' + (best * (colW + gap)) + 'px,' + bestTop + 'px)';
|
||||
for (var j = best; j < best + span; j++) heights[j] = bestTop + h + gap;
|
||||
});
|
||||
grid.style.height = Math.max(0, Math.max.apply(null, heights) - gap) + 'px';
|
||||
}
|
||||
|
||||
layout();
|
||||
// Re-lay out whenever the grid's own width changes (window resize, but
|
||||
// also a gallery that was laid out while hidden / zero-width).
|
||||
var t, lastW = grid.clientWidth;
|
||||
var relayout = function () {
|
||||
if (grid.clientWidth === lastW) return;
|
||||
lastW = grid.clientWidth;
|
||||
clearTimeout(t); t = setTimeout(layout, 80);
|
||||
};
|
||||
if ('ResizeObserver' in window) new ResizeObserver(relayout).observe(grid);
|
||||
window.addEventListener('resize', relayout, { passive: true }); // belt and braces; relayout() is idempotent
|
||||
// Re-run once images report their size, in case an attribute was missing.
|
||||
tiles.forEach(function (tile) {
|
||||
var img = tile.querySelector('img');
|
||||
if (!img.getAttribute('width')) img.addEventListener('load', layout, { once: true });
|
||||
});
|
||||
|
||||
// Staggered entrance.
|
||||
var enter = function () {
|
||||
tiles.forEach(function (tile, i) {
|
||||
setTimeout(function () { tile.classList.add('is-in'); }, Math.min(i, 12) * 70);
|
||||
});
|
||||
};
|
||||
if ('IntersectionObserver' in window) {
|
||||
var io = new IntersectionObserver(function (entries) {
|
||||
if (entries.some(function (e) { return e.isIntersecting; })) { enter(); io.disconnect(); }
|
||||
}, { rootMargin: '0px 0px -80px 0px' });
|
||||
io.observe(grid);
|
||||
} else {
|
||||
enter();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user