// PlazaIT Product Lightbox (vanilla, no CDN) — v4 (swipe-down to close) (function(){ const state = { items: [], open: false, idx: 0, scale: 1, minScale: 1, maxScale: 4, tx: 0, ty: 0, isDragging: false, dragStartX: 0, dragStartY: 0, baseTx: 0, baseTy: 0, touchStartX: 0, touchStartY: 0, lastTap: 0, swipeMode: 'none', // 'none' | 'h' | 'v' totalDx: 0, totalDy: 0 }; let root, imgEl, counterEl, btnPrev, btnNext, btnClose, canvas; const ORIGIN = (location.origin || (location.protocol + '//' + location.host)); function absUrl(u){ if (!u) return ''; try { if (/^https?:\/\//i.test(u)) return u; return new URL(u, ORIGIN).href; } catch(_) { return u; } } function fileName(u){ try { return new URL(u, ORIGIN).pathname.split('/').pop().toLowerCase(); } catch(_){ return String(u||'').split('/').pop().toLowerCase(); } } function buildItems(){ const gd = (window.galleryData || []).filter(Boolean); if (gd.length){ state.items = gd.map(g => ({ src: absUrl(g.url), alt: g.alt || '', slug: g.slug || '', w: g.w || 1200, h: g.h || 1200 })).filter(it => !!it.src); return; } const thumbs = document.querySelectorAll('.pd-thumb-btn'); state.items = Array.from(thumbs).map(btn => { const raw = btn.getAttribute('data-full') || ''; return { src: absUrl(raw), alt: btn.getAttribute('data-alt') || '', slug: btn.getAttribute('data-slug') || '', w: 1200, h: 1200 }; }).filter(it => !!it.src); } function ensureUI(){ if (root) return; root = document.createElement('div'); root.className = 'pd-lb'; root.id = 'pd-lightbox'; root.setAttribute('role','dialog'); root.setAttribute('aria-modal','true'); root.setAttribute('aria-hidden','true'); root.setAttribute('tabindex','-1'); // Canvas terlebih dulu (z-index di CSS membuat tombol di atas) root.innerHTML = `
1 / 1
`; document.body.appendChild(root); imgEl = root.querySelector('#pd-lb-img'); counterEl = root.querySelector('#pd-lb-counter'); btnPrev = root.querySelector('.pd-lb-prev'); btnNext = root.querySelector('.pd-lb-next'); btnClose = root.querySelector('.pd-lb-close'); canvas = root.querySelector('.pd-lb-canvas'); // Button clicks btnPrev.addEventListener('click', (e)=>{ e.stopPropagation(); prev(); }); btnNext.addEventListener('click', (e)=>{ e.stopPropagation(); next(); }); btnClose.addEventListener('click', (e)=>{ e.stopPropagation(); close(); }); // Klik backdrop untuk tutup root.addEventListener('click', (e)=>{ if (e.target === root) close(); }); // Keyboard global document.addEventListener('keydown', (e)=>{ if (!state.open) return; if (e.key === 'Escape') { e.preventDefault(); close(); } if (e.key === 'ArrowRight') { e.preventDefault(); next(); } if (e.key === 'ArrowLeft') { e.preventDefault(); prev(); } }); // Wheel zoom (desktop) root.addEventListener('wheel', (e)=>{ if (!state.open) return; e.preventDefault(); const delta = -Math.sign(e.deltaY) * 0.2; zoomAt(delta, e.clientX, e.clientY); }, { passive: false }); // Drag pan (mouse) canvas.addEventListener('mousedown', (e)=>{ if (state.scale <= 1) return; state.isDragging = true; state.dragStartX = e.clientX; state.dragStartY = e.clientY; state.baseTx = state.tx; state.baseTy = state.ty; e.preventDefault(); }); window.addEventListener('mousemove', (e)=>{ if (!state.isDragging) return; const dx = e.clientX - state.dragStartX; const dy = e.clientY - state.dragStartY; setTranslate(state.baseTx + dx, state.baseTy + dy); }); window.addEventListener('mouseup', ()=>{ state.isDragging = false; }); // Touch: swipe navigate (H), swipe-down close (V), double-tap zoom, drag pan canvas.addEventListener('touchstart', (e)=>{ if (!state.open) return; if (e.touches.length === 1) { const t = e.touches[0]; state.touchStartX = t.clientX; state.touchStartY = t.clientY; state.totalDx = 0; state.totalDy = 0; state.swipeMode = 'none'; state.isDragging = state.scale > 1; state.dragStartX = t.clientX; state.dragStartY = t.clientY; state.baseTx = state.tx; state.baseTy = state.ty; // Double-tap toggle zoom const now = Date.now(); if (now - state.lastTap < 300) { if (state.scale > 1) resetTransform(); else zoomAt(+1, t.clientX, t.clientY, 2); state.lastTap = 0; e.preventDefault(); return; } state.lastTap = now; } }, { passive: true }); canvas.addEventListener('touchmove', (e)=>{ if (!state.open) return; // Drag pan saat zoom if (state.isDragging && e.touches.length === 1 && state.scale > 1) { e.preventDefault(); const t = e.touches[0]; const dx = t.clientX - state.dragStartX; const dy = t.clientY - state.dragStartY; setTranslate(state.baseTx + dx, state.baseTy + dy); return; } // Gesture saat scale <= 1 if (state.scale <= 1 && e.touches.length === 1) { const t = e.touches[0]; const dx = t.clientX - state.touchStartX; const dy = t.clientY - state.touchStartY; state.totalDx = dx; state.totalDy = dy; // Tentukan mode swipe di awal gerakan if (state.swipeMode === 'none') { const absX = Math.abs(dx), absY = Math.abs(dy); if (absY > 12 && absY > absX * 1.3) { state.swipeMode = 'v'; // swipe vertical (untuk close) } else if (absX > 12 && absX > absY * 1.3) { state.swipeMode = 'h'; // swipe horizontal (ganti slide) } } if (state.swipeMode === 'v') { // Ikuti jari, kurangi opacity untuk feedback e.preventDefault(); setRootDrag(dy); } else if (state.swipeMode === 'h') { // Biarkan sampai touchend untuk menentukan next/prev (tidak perlu preventDefault di sini) } } }, { passive: false }); canvas.addEventListener('touchend', (e)=>{ if (!state.open) return; if (state.scale <= 1) { const dx = state.totalDx; const dy = state.totalDy; if (state.swipeMode === 'v') { const TH = 80; // threshold jarak piksel untuk menutup if (Math.abs(dy) > TH) { // Animasi keluar arah swipe animateClose(dy); } else { // Snap back resetRootDrag(true); } } else if (state.swipeMode === 'h') { if (Math.abs(dx) > 50) { dx < 0 ? next() : prev(); } } } state.isDragging = false; state.swipeMode = 'none'; state.totalDx = 0; state.totalDy = 0; }); } function setImage(idx){ if (!state.items.length) return; state.idx = (idx + state.items.length) % state.items.length; const it = state.items[state.idx]; imgEl.src = it.src; imgEl.alt = it.alt || ''; counterEl.textContent = (state.idx + 1) + ' / ' + state.items.length; resetTransform(); preloadNeighbors(); } function resetTransform(){ state.scale = 1; state.tx = 0; state.ty = 0; applyTransform(); } function setTranslate(tx, ty){ state.tx = tx; state.ty = ty; applyTransform(); } function applyTransform(){ imgEl.style.transform = `translate3d(${state.tx}px, ${state.ty}px, 0) scale(${state.scale})`; } function zoomAt(delta, clientX, clientY, snapTo){ const old = state.scale; let next = (typeof snapTo === 'number') ? snapTo : (old + delta); next = Math.max(state.minScale, Math.min(state.maxScale, next)); if (next !== old) { const rect = imgEl.getBoundingClientRect(); const cx = clientX - rect.left - rect.width/2; const cy = clientY - rect.top - rect.height/2; state.tx -= cx * (next/old - 1); state.ty -= cy * (next/old - 1); state.scale = next; applyTransform(); } } function preloadNeighbors(){ if (state.items.length < 2) return; const nextIdx = (state.idx + 1) % state.items.length; const prevIdx = (state.idx - 1 + state.items.length) % state.items.length; [nextIdx, prevIdx].forEach(i=>{ const img = new Image(); img.src = state.items[i].src; }); } // ----- Swipe-down visual helpers ----- function setRootDrag(dy){ const damped = dy * 0.6; // redam gerakan agar lembut const abs = Math.abs(dy); const fade = Math.max(0, Math.min(1, 1 - abs / 300)); // transparansi sesuai jarak root.style.transform = `translate3d(0, ${damped}px, 0)`; root.style.opacity = String(0.95 * fade + 0.05); // jangan jadi 0 total sebelum close } function resetRootDrag(withAnim){ if (withAnim) { root.style.transition = 'transform .18s ease, opacity .18s ease'; root.style.transform = 'translate3d(0,0,0)'; root.style.opacity = '1'; setTimeout(()=>{ root.style.transition = ''; }, 190); } else { root.style.transition = ''; root.style.transform = ''; root.style.opacity = ''; } } function animateClose(dy){ root.style.transition = 'transform .18s ease, opacity .18s ease'; const off = (dy > 0 ? window.innerHeight : -window.innerHeight); root.style.transform = `translate3d(0, ${off}px, 0)`; root.style.opacity = '0'; setTimeout(()=>{ root.style.transition = ''; close(); }, 180); } function setSiblingsInert(makeInert){ // Inert semua saudara root (elemen lain di body) agar fokus tidak lari const nodes = Array.from(document.body.children).filter(el => el !== root); nodes.forEach(el=>{ if (makeInert) { if (!el.hasAttribute('inert')) el.setAttribute('inert',''); } else { if (el.hasAttribute('inert')) el.removeAttribute('inert'); } }); } function open(idx){ if (!state.items.length) buildItems(); if (!state.items.length) return; ensureUI(); // Reset efek drag visual resetRootDrag(false); document.body.style.overflow = 'hidden'; root.classList.add('show'); root.removeAttribute('aria-hidden'); // hindari aria-hidden saat fokus child setSiblingsInert(true); state.open = true; setImage(typeof idx === 'number' ? idx : 0); // Fokuskan dialog agar keyboard pasti tertangkap try { root.focus(); } catch(_){} } function close(){ state.open = false; root.classList.remove('show'); root.setAttribute('aria-hidden','true'); setSiblingsInert(false); document.body.style.overflow = ''; // Bersihkan transform/opacity resetRootDrag(false); } function next(){ setImage(state.idx + 1); } function prev(){ setImage(state.idx - 1); } function findIndexByUrl(url){ const target = fileName(absUrl(url)); const idx = state.items.findIndex(it => fileName(it.src) === target); return idx >= 0 ? idx : 0; } function initPdLightbox(){ buildItems(); ensureUI(); // Klik gambar utama → buka LB di index yang sama const mainImg = document.getElementById('pd-main-img'); if (mainImg) { mainImg.style.cursor = 'zoom-in'; mainImg.addEventListener('click', ()=>{ open(findIndexByUrl(mainImg.src)); }); } // Klik gambar slider mobile → buka LB di gambar yang diketuk const slider = document.getElementById('pd-mobile-slider'); if (slider) { slider.addEventListener('click', (e)=>{ const im = e.target && e.target.tagName === 'IMG' ? e.target : e.target.closest('img'); if (!im) return; open(findIndexByUrl(im.src)); }); } // (Opsional) double-click pada thumbnail untuk buka langsung const thumbs = document.querySelectorAll('.pd-thumb-btn'); thumbs.forEach((btn)=>{ let last = 0; btn.addEventListener('click', ()=>{ const now = Date.now(); if (now - last < 300) { const src = btn.getAttribute('data-full') || ''; open(findIndexByUrl(absUrl(src))); last = 0; } else { last = now; } }); }); // Ekspos API window.PDLightbox = { open, close, next, prev, findIndexByUrl }; } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initPdLightbox); } else { initPdLightbox(); } })();