| | |
| | | *********************************************************************/ |
| | | initElements() { |
| | | this.elements = { |
| | | imageSelector: 'a.open-gallery', |
| | | imageSelector: 'img[data-gallery]', |
| | | gallery: { |
| | | modal: 'dialog.gallery', |
| | | wrap: '.wrap', |
| | |
| | | closeMessage: 'Closed Gallery', |
| | | } |
| | | ); |
| | | this.modal.subscribe((event, data) => { |
| | | this.modal.subscribe((event) => { |
| | | if (event === 'modal-close') { |
| | | this.toggleGallery(false); |
| | | } |
| | | }); |
| | | } |
| | | buildGalleryItems(filtered = null) { |
| | | let selector = filtered ? `[data-opens="${filtered}"]` : this.elements.imageSelector; |
| | | let selector = filtered ? `[data-gallery="${filtered}"]` : this.elements.imageSelector; |
| | | this.items = Array.from(document.querySelectorAll(selector)) |
| | | .map((img, index) => { |
| | | let image = img.querySelector('img'); |
| | | |
| | | return { |
| | | id: img.dataset.id||index, |
| | | small: image.dataset.small || img.src, |
| | | medium: image.dataset.medium || img.src, |
| | | full: image.dataset.full || img.src, |
| | | alt: image.alt || '', |
| | | element: image |
| | | srcset: img.srcset || img.src, // Clone the srcset from page |
| | | sizes: img.sizes || '100vw', |
| | | src: img.currentSrc || img.src, // Fallback |
| | | full: img.dataset.full || img.src, |
| | | alt: img.alt || '', |
| | | element: img |
| | | }; |
| | | }); |
| | | } |
| | |
| | | let target = window.targetCheck(e, this.elements.imageSelector); |
| | | if (target && !this.modal.isOpen) { |
| | | e.preventDefault(); |
| | | this.buildGalleryItems((Object.hasOwn(target.dataset, 'opens')) ? target.dataset.opens : null); |
| | | this.buildGalleryItems(target.dataset.gallery || null); |
| | | |
| | | this.index = this.items.findIndex(item => item.element === target.querySelector('img')); |
| | | // Target is now the img element itself |
| | | this.index = this.items.findIndex(item => item.element === target); |
| | | this.toggleGallery(true); |
| | | } else if (this.modal.isOpen) { |
| | | if (window.targetCheck(e, this.elements.gallery.nextButton)) { |
| | |
| | | } |
| | | |
| | | onPointerDown(e) { |
| | | // Always prevent default to stop browser's native image drag |
| | | e.preventDefault(); |
| | | |
| | | this.swipe.startX = e.clientX; |
| | | this.swipe.startY = e.clientY; |
| | | this.ui.gallery.image.setPointerCapture(e.pointerId); |
| | |
| | | this.zoom.panning = true; |
| | | this.zoom.startX = e.clientX - this.zoom.x; |
| | | this.zoom.startY = e.clientY - this.zoom.y; |
| | | // Change cursor to grabbing |
| | | this.ui.gallery.image.style.cursor = 'grabbing'; |
| | | } |
| | | } |
| | | |
| | |
| | | this.pinchStartDist = 0; |
| | | } |
| | | |
| | | // Only check for swipe if we weren't panning and no more active pointers |
| | | if (!this.zoom.panning && this.activePointers.size === 0) { |
| | | // End of tap or swipe - detect swipe |
| | | this.swipe.endX = e.clientX; |
| | |
| | | this.nextElement(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Reset panning state when all pointers are released |
| | | if (this.activePointers.size === 0) { |
| | | this.zoom.panning = false; |
| | | // Reset cursor based on zoom state |
| | | this.ui.gallery.image.style.cursor = this.zoom.scale > 1 ? 'grab' : 'default'; |
| | | } |
| | | } |
| | | |
| | |
| | | this.handleZoom(increment, e.clientX, e.clientY); |
| | | } |
| | | |
| | | clampPan() { |
| | | const BORDER = 32; // 2rem |
| | | const wrap = this.ui.gallery.wrap; |
| | | if (!wrap) return; |
| | | |
| | | const wrapRect = wrap.getBoundingClientRect(); |
| | | |
| | | // MUST use natural dimensions |
| | | const naturalW = 1920; |
| | | const naturalH = 1920; |
| | | |
| | | // But they must be constrained by how the CSS fits them (max-width: 90vw, max-height: 85vh) |
| | | // So compute the base display size: |
| | | const displayRatio = Math.min( |
| | | wrapRect.width / naturalW, |
| | | wrapRect.height / naturalH |
| | | ); |
| | | |
| | | const baseWidth = naturalW * displayRatio; |
| | | const baseHeight = naturalH * displayRatio; |
| | | |
| | | const scaledWidth = baseWidth * this.zoom.scale; |
| | | const scaledHeight = baseHeight * this.zoom.scale; |
| | | |
| | | // Allowed pan range |
| | | const minX = wrapRect.width - scaledWidth - BORDER; |
| | | const maxX = BORDER; |
| | | |
| | | const minY = wrapRect.height - scaledHeight - BORDER; |
| | | const maxY = BORDER; |
| | | |
| | | // clamp |
| | | this.zoom.x = Math.min(maxX, Math.max(minX, this.zoom.x)); |
| | | this.zoom.y = Math.min(maxY, Math.max(minY, this.zoom.y)); |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | | handleZoom(increment, clientX=null, clientY=null) { |
| | | const oldScale = this.zoom.scale; |
| | | let newScale = oldScale + increment; |
| | |
| | | // this.clampPan(); |
| | | const img = this.ui.gallery.image; |
| | | img.style.transform = `translate(${this.zoom.x}px, ${this.zoom.y}px) scale(${this.zoom.scale})`; |
| | | // Update cursor based on zoom level |
| | | img.style.cursor = this.zoom.scale > 1 ? 'grab' : 'default'; |
| | | } |
| | | resetZoom() { |
| | | this.zoom.scale = 1; |
| | |
| | | /** |
| | | * |
| | | * @param {boolean} open |
| | | * @param {null|number} index |
| | | */ |
| | | toggleGallery(open, index= null) { |
| | | toggleGallery(open) { |
| | | if (open) { |
| | | // Disable native image dragging |
| | | this.ui.gallery.image.draggable = false; |
| | | this.ui.gallery.image.style.userSelect = 'none'; |
| | | |
| | | this.ui.gallery.image.addEventListener("pointerdown", this.pointerDownHandler); |
| | | this.ui.gallery.image.addEventListener("pointermove", this.pointerMoveHandler); |
| | | this.ui.gallery.image.addEventListener("pointerup", this.pointerUpHandler); |
| | |
| | | updateDisplay() { |
| | | const item = this.items[this.index]; |
| | | if (!item) return; |
| | | this.ui.gallery.image.src = item.full; |
| | | this.ui.gallery.image.alt = item.alt; |
| | | |
| | | const galleryImg = this.ui.gallery.image; |
| | | |
| | | // Set srcset first - browser uses cached version instantly (no wait) |
| | | if (item.srcset) { |
| | | galleryImg.srcset = item.srcset; |
| | | galleryImg.sizes = item.sizes; |
| | | } |
| | | galleryImg.src = item.src; // Fallback |
| | | galleryImg.alt = item.alt; |
| | | |
| | | // ALWAYS load full resolution for zoom quality |
| | | if (item.full && item.full !== item.src) { |
| | | const fullImg = new Image(); |
| | | fullImg.onload = () => { |
| | | if (this.items[this.index] === item) { |
| | | galleryImg.src = item.full; |
| | | galleryImg.removeAttribute('srcset'); // Switch to full res directly |
| | | galleryImg.removeAttribute('sizes'); |
| | | } |
| | | }; |
| | | fullImg.src = item.full; |
| | | } |
| | | |
| | | this.ui.gallery.counter.textContent = `${this.index + 1} / ${this.items.length}`; |
| | | |
| | | this.ui.gallery.prevButton.disabled = this.items.length <= 1; |