| | |
| | | /* |
| | | SITE GALLERY MANAGER |
| | | Handles two main functions: |
| | | |
| | | 1. Responsive Images |
| | | - Automatically loads appropriate image sizes based on screen width |
| | | - Listens for resize events and updates images accordingly |
| | | - Uses data-small, data-medium, and data-full attributes on images |
| | | |
| | | 2. Gallery Management |
| | | - HTML Structure: |
| | | <a class="open-gallery" data-opens="gallery-id" data-focus="image-to-show"> |
| | | <figure><img...></figure> |
| | | </a> |
| | | |
| | | <dialog class="gallery" id="gallery-id"> |
| | | - Main image display (.images with multiple <figure>s) |
| | | - Navigation bar on bottom (nav.thumbnails) with: |
| | | - Previous/Next buttons |
| | | - Thumbnail list (toggles with .open class) |
| | | - Expand button (toggles .expanded class) |
| | | - Visibility toggle (toggles .hide-info class) |
| | | - Close button |
| | | </dialog> |
| | | |
| | | Key Features: |
| | | - Opens in modal dialog with keyboard navigation |
| | | - Manages thumbnail navigation state |
| | | - Handles responsive image loading |
| | | - Uses class-based state management (open, expanded, hide-info) |
| | | - All event handling through delegation |
| | | - Built on UIHandler base class for state management |
| | | |
| | | State Management: |
| | | - Dialog: open/closed |
| | | - Thumbnails: collapsed/open/expanded |
| | | - Info panel: visible/hidden |
| | | - Active image: tracked with .focused class |
| | | */ |
| | | class Media { |
| | | constructor() { |
| | | this.currentWidth = window.innerWidth; |
| | | this.initElements(); |
| | | if (this.images.length === 0) { |
| | | return; |
| | | } |
| | | this.store = new window.jvbStore({ |
| | | name: 'images', |
| | | TTL: 604800 |
| | | }); |
| | | this.touch = { |
| | | x: null, |
| | | y: null |
| | | } |
| | | this.a11y = window.jvbA11y; |
| | | this.debouncer = window.debouncer; |
| | | this.isTouching = false; |
| | | this.images = document.querySelectorAll('.wp-site-blocks img[data-small]'); |
| | | |
| | | if (this.images.length === 0) return; |
| | | |
| | | // Immediately load visible images |
| | | this.loadVisibleImages(); |
| | | |
| | | this.initListeners(); |
| | | document.addEventListener('beforeunload', this.cleanup); |
| | | } |
| | | |
| | | initElements(){ |
| | | this.images = document.querySelectorAll('.wp-site-blocks img'); |
| | | this.gallery = document.querySelector('dialog.gallery'); |
| | | this.modal = new window.jvbModal(this.gallery, { |
| | | openMessage: 'Opened Gallery', |
| | | closeMessage: 'Closed Gallery', |
| | | open: '.open-gallery', |
| | | close: '.close' |
| | | }); |
| | | loadVisibleImages() { |
| | | // Load first image immediately, plus any in viewport |
| | | this.images.forEach((img, index) => { |
| | | const rect = img.getBoundingClientRect(); |
| | | const isVisible = rect.top < window.innerHeight && rect.bottom > 0; |
| | | |
| | | this.modal.subscribe((event, data) => { |
| | | if (event === 'modal-open') { |
| | | this.openGallery(); |
| | | } else if (event === 'modal-close') { |
| | | this.closeGallery(); |
| | | // Always load first image, or if currently visible |
| | | if (index === 0 || isVisible) { |
| | | this.loadAppropriateImage(img); |
| | | img.dataset.loaded = 'true'; // Mark so we don't observe it |
| | | } |
| | | }); |
| | | } |
| | | |
| | | initListeners() { |
| | | this.resizeHandler = this.handleResize.bind(this); |
| | | this.clickHandler = this.handleClick.bind(this); |
| | | this.keysHandler = this.handleKeys.bind(this); |
| | | this.touchStartHandler = this.handleTouchStart.bind(this); |
| | | this.touchEndHandler = this.handleTouchEnd.bind(this); |
| | | |
| | | document.addEventListener('click', this.clickHandler); |
| | | window.addEventListener('resize', this.resizeHandler); |
| | | console.log('window hash: ',window.location.hash); |
| | | let target = document.querySelector(window.location.hash); |
| | | if (target && target.tagName === 'LI') { |
| | | let trigger = target.querySelector('.open-gallery'); |
| | | if (trigger) { |
| | | this.openGallery(trigger); |
| | | } |
| | | } |
| | | |
| | | this.observer = new IntersectionObserver((entries) =>{ |
| | | // Only observe images that weren't immediately loaded |
| | | this.observer = new IntersectionObserver((entries) => { |
| | | entries.forEach(entry => { |
| | | if (entry.isIntersecting) { |
| | | let img = entry.target; |
| | | if (!img.closest('dialog')) { |
| | | this.loadAppropriateImage(img); |
| | | this.observer.unobserve(img); |
| | | } |
| | | this.loadAppropriateImage(entry.target); |
| | | this.observer.unobserve(entry.target); |
| | | } |
| | | }) |
| | | }); |
| | | }, { |
| | | root: null, |
| | | rootMargin: '50px', |
| | | threshold: .1 |
| | | threshold: 0.1 |
| | | }); |
| | | |
| | | this.images.forEach(img => { |
| | | if (!img.closest('dialog')) { |
| | | if (!img.dataset.loaded) { |
| | | this.observer.observe(img); |
| | | } |
| | | }) |
| | | }); |
| | | } |
| | | |
| | | initTouchHandling() { |
| | | this.isTouching = true; |
| | | this.touch.x = 0; |
| | | this.touch.y = 0; |
| | | |
| | | this.gallery.addEventListener('touchstart', this.touchStartHandler); |
| | | this.gallery.addEventListener('touchend', this.touchEndHandler); |
| | | } |
| | | cancelTouchHandling() { |
| | | this.isTouching = false; |
| | | this.gallery.removeEventListener('touchstart', this.touchStartHandler); |
| | | this.gallery.removeEventListener('touchend', this.touchEndHandler); |
| | | } |
| | | |
| | | handleTouchStart(e) { |
| | | this.touch.x = e.touches[0].clientX; |
| | | this.touch.y = e.touches[0].clientY; |
| | | } |
| | | handleTouchEnd(e) { |
| | | const diffX = e.changedTouches[0].clientX - this.touch.x; |
| | | const diffY = e.changedTouches[0].clientY - this.touch.y; |
| | | |
| | | if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) { |
| | | this.navigateImages({target: this.gallery}, diffX < 0 ? 'next' : 'previous'); |
| | | } |
| | | } |
| | | handleResize(e) { |
| | | window.debouncer.schedule( |
| | | 'resize', |
| | | ()=> { |
| | | const currentWidth = window.innerWidth; |
| | | if (Math.abs(currentWidth - this.currentWidth) > 100) { |
| | | this.currentWidth = currentWidth; |
| | | this.handleImageResize(); |
| | | } |
| | | }, |
| | | 150 |
| | | ); |
| | | } |
| | | handleClick(e) { |
| | | |
| | | } |
| | | handleKeys(e) { |
| | | //Escape handled by Modal.js |
| | | if (e.key === 'Tab') { |
| | | if (e.shiftKey) { |
| | | |
| | | } else { |
| | | |
| | | handleResize() { |
| | | window.debouncer.schedule('image-resize', () => { |
| | | const newWidth = window.innerWidth; |
| | | if (Math.abs(newWidth - this.currentWidth) > 100) { |
| | | this.currentWidth = newWidth; |
| | | this.updateVisibleImages(); |
| | | } |
| | | }, 150); |
| | | } |
| | | |
| | | updateVisibleImages() { |
| | | this.images.forEach(img => { |
| | | const rect = img.getBoundingClientRect(); |
| | | if (rect.top < window.innerHeight && rect.bottom > 0) { |
| | | this.loadAppropriateImage(img, true); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | loadAppropriateImage(img, forceUpdate = false) { |
| | | const targetSize = this.getTargetSize(); |
| | | const newSrc = img.dataset[targetSize]; |
| | | |
| | | if (newSrc && (forceUpdate || newSrc !== img.currentSrc)) { |
| | | img.src = newSrc; |
| | | } |
| | | } |
| | | |
| | | openGallery(target = null) { |
| | | document.addEventListener('keydown', this.clickHandler); |
| | | const id = target.dataset.opens; |
| | | this.initTouchHandling(); |
| | | let focus; |
| | | if (!target) { |
| | | focus = this.images[0]; |
| | | } else { |
| | | focus = target.dataset.focus; |
| | | } |
| | | if (focus) { |
| | | this.updateImage(this.gallery.querySelector(`#${focus}`)); |
| | | } |
| | | } |
| | | |
| | | closeGallery() { |
| | | this.cancelTouchHandling(); |
| | | document.removeEventListener('keydown', this.keysHandler); |
| | | //Restore focus |
| | | if (this.lastFocusedElement) { |
| | | this.lastFocusedElement.focus(); |
| | | } |
| | | getTargetSize() { |
| | | if (this.currentWidth < 768) return 'small'; |
| | | if (this.currentWidth < 1200) return 'medium'; |
| | | return 'full'; |
| | | } |
| | | |
| | | cleanup() { |
| | | this.observer.disconnect(); |
| | | window.removeEventListener('resize', this.resizeHandler) |
| | | if (this.isTouching) { |
| | | this.cancelTouchHandling(); |
| | | document.removeEventListener('keydown', this.keysHandler); |
| | | } |
| | | document.removeEventListener('click', this.clickHandler); |
| | | this.observer?.disconnect(); |
| | | window.removeEventListener('resize', this.resizeHandler); |
| | | } |
| | | } |
| | | |
| | | window.isLoaded = false; |
| | | document.addEventListener('readystatechange', () => { |
| | | if (!window.isLoaded && document.querySelector('.wp-site-blocks img')) { |
| | | window.jvbMedia = new Media(); |
| | | window.isLoaded = true; |
| | | } |
| | | }); |