Jake Vanderwerf
2025-11-10 e9967fa22781d922ba4eb8fb44fe72d200ac4b14
assets/js/concise/Media.js
@@ -1,207 +1,98 @@
/*
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;
   }
});