/* 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:
- Main image display (.images with multiple
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
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 */ // Constants const BREAKPOINTS = { MOBILE: 500, TABLET: 1024 }; const GALLERY_STATES = { DIALOG: 'dialog', THUMBNAILS: 'thumbnails', OPTIONS: 'options' }; // Utility functions const getImageSize = (width, dataset) => { if (width > BREAKPOINTS.TABLET) { return (dataset.full) ? dataset.full : dataset.large; } if (width > BREAKPOINTS.MOBILE) return dataset.medium; return dataset.small; }; class MediaManager extends window.UIHandler { constructor() { super(); this.previousWidth = window.innerWidth; // Bind methods that exist this.handleResize = this.handleResize.bind(this); this.debouncedResize = this.debounce(this.handleResize, 150); // Initialize this.bindElements(); this.initializeHandlers(this.defineHandlers()); this.bindComponentEvents(); this.bindEvents(); this.initialize(); // // Handle URL hash if it exists // if (window.location.hash) { // // Wait for DOM to be fully loaded // if (document.readyState === 'loading') { // document.addEventListener('DOMContentLoaded', () => this.handleUrlHash()); // } else { // // DOM is already ready // this.handleUrlHash(); // } // } } handleUrlHash() { const target = document.querySelector(window.location.hash); if (target?.tagName === 'LI') { const galleryTrigger = target.querySelector('.open-gallery'); if (galleryTrigger) { this.openDialog(galleryTrigger); } } } handleResize() { const currentWidth = window.innerWidth; if (Math.abs(currentWidth - this.previousWidth) > 100) { this.previousWidth = currentWidth; this.handleImageResize(); } } async handleImageResize() { const currentWidth = window.innerWidth; const loadPromises = []; // Handle all visible images for (const img of this.elements.images) { if (!img.closest('dialog')) { const size = getImageSize(currentWidth, img.dataset); if (size && img.src !== size) { loadPromises.push( new Promise((resolve) => { const tempImg = new Image(); tempImg.onload = () => { img.src = size; resolve(); }; tempImg.onerror = () => { console.error(`Failed to load image: ${size}`); resolve(); }; tempImg.src = size; }) ); } } } // Handle current gallery image if gallery is open const activeDialog = document.querySelector('dialog[open]'); if (activeDialog) { const activeFigure = activeDialog.querySelector('.focused'); if (activeFigure) { const img = activeFigure.querySelector('img'); if (img) { const size = getImageSize(currentWidth, activeFigure.dataset); if (size && img.src !== size) { loadPromises.push( new Promise((resolve) => { const tempImg = new Image(); tempImg.onload = () => { img.src = size; resolve(); }; tempImg.onerror = () => { console.error(`Failed to load image: ${size}`); resolve(); }; tempImg.src = size; }) ); } } } } // Wait for all images to load await Promise.all(loadPromises); } // Initialization Methods async initialize() { try { // Initialize image handling if (this.elements.images.length > 0) { this.initializeImageObserver(); window.addEventListener('resize', this.debouncedResize); } // Initialize gallery this.initializeGalleryFromData(); } catch (error) { console.error('Gallery initialization failed:', error); this.handleInitializationError(); } } async initializeGalleryFromData() { const galleryData = window.gallery?.images; if (galleryData && galleryData !== 'false') { try { const parsedGallery = JSON.parse(galleryData); if(parsedGallery && parsedGallery !== 'false'){ this.elements.mainContainer?.insertAdjacentHTML('beforeend', parsedGallery); // Wait for DOM update await new Promise(resolve => setTimeout(resolve, 0)); this.initializeGallery(); } } catch (e) { console.error('Failed to parse gallery data:', e); throw e; // Propagate error to main initialize catch block } } } initializeGallery() { // Bind gallery-specific elements and handlers after gallery is added to DOM this.bindGalleryElements(); this.initializeHandlers(this.defineGalleryHandlers()); this.bindComponentEvents(); // Bind keyboard navigation to each dialog this.elements.dialogs?.forEach(dialog => { dialog.addEventListener('keydown', (e) => { if (e.key === 'ArrowLeft') this.navigateImages(e, 'previous'); if (e.key === 'ArrowRight') this.navigateImages(e, 'next'); if (e.key === 'Escape') this.closeDialog(dialog); }); }); } bindElements() { this.elements = { images: document.querySelectorAll('img:not(.avatar)'), mainContainer: document.querySelector('.wp-site-blocks'), galleryTriggers: document.querySelectorAll('.open-gallery') }; } bindGalleryElements() { // Clean up any existing handlers first this.elements.dialogs?.forEach(dialog => { dialog._removeKeydownListener?.(); dialog._removeTouchListeners?.(); }); // Update elements Object.assign(this.elements, { dialogs: document.querySelectorAll('dialog'), galleryControls: document.querySelectorAll('dialog, .open-gallery, button, .thumbnails a') }); } // Event Handlers defineHandlers() { return { galleryTriggers: { click: (e) => { e.preventDefault(); e.stopPropagation(); const link = e.target.closest('.open-gallery'); if (link) { this.openDialog(link); } } } }; } defineGalleryHandlers() { return { galleryControls: { click: (e) => { const termLink = e.target.closest('.term-list a'); if (termLink) { // Allow term links to work normally return true; } e.preventDefault(); e.stopPropagation(); const target = e.target.closest('button, a'); if (!target) return; if (target.classList.contains('open-gallery')) { this.openDialog(target); } else if (target.classList.contains('close')) { this.closeDialog(target.closest('dialog')); } else if (target.classList.contains('visible')) { this.handleVisibilityToggle(e); } else if (target.classList.contains('open-thumbnails')) { this.handleThumbnailControl(e, 'open'); } else if (target.classList.contains('expand')) { this.handleThumbnailControl(e, 'expand'); } else if (target.classList.contains('previous')) { this.navigateImages(e, 'previous'); } else if (target.classList.contains('next')) { this.navigateImages(e, 'next'); } else if (target.closest('.thumbnails a')) { this.handleThumbnailSelection(e); } } } }; } handleVisibilityToggle(event) { const button = event.target.closest('button'); const dialog = button.closest('dialog'); if (!dialog) return; dialog.classList.toggle('hide-info'); const isVisible = !dialog.classList.contains('hide-info'); button.setAttribute('aria-expanded', isVisible.toString()); button.setAttribute('title', isVisible ? 'Hide Options' : 'Show Options'); button.setAttribute('aria-label', isVisible ? 'Hide Options' : 'Show Options'); } // Image Handling initializeImageObserver() { const options = { root: null, rootMargin: '50px', threshold: 0.1 }; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; if (!img.closest('dialog')) { this.loadAppropriateImage(img); observer.unobserve(img); } } }); }, options); this.elements.images.forEach(img => { if (!img.closest('dialog')) { observer.observe(img); } }); return observer; } loadAppropriateImage(img) { const size = getImageSize(window.innerWidth, img.dataset); if (!size) return; img.classList.add('loading'); const tempImg = new Image(); tempImg.onload = () => { img.src = size; img.classList.remove('loading'); img.classList.add('loaded'); }; tempImg.onerror = () => { console.error(`Failed to load image: ${size}`); img.classList.remove('loading'); img.classList.add('error'); if (img.dataset.small && size !== img.dataset.small) { this.loadAppropriateImage(img.dataset.small); } }; tempImg.src = size; } // Dialog Management openDialog(trigger) { const dialogId = trigger.dataset.opens; const dialog = document.getElementById(dialogId); if (!dialog) return; // Save last focused element and show modal this.lastFocusedElement = document.activeElement; dialog.showModal(); // Set up focus trap and accessibility this.setupFocusTrap(dialog); dialog.setAttribute('role', 'dialog'); dialog.setAttribute('aria-modal', 'true'); dialog.setAttribute('aria-labelledby', 'gallery-title'); // Setup touch handling this.setupTouchHandling(dialog); // Update component state this.setComponentState(GALLERY_STATES.DIALOG, true, { element: dialog, toggle: trigger, focusElement: dialog.querySelector('button.close'), ariaHidden: false, cleanup: () => this.closeDialog(dialog) }); // Focus initial image if specified const focusId = trigger.dataset.focus || dialog.querySelector('.images figure')?.id; if (focusId) { this.updateImage(dialog.querySelector(`#${focusId}`)); } } setupFocusTrap(dialog) { const focusableElements = dialog.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); const firstFocusable = focusableElements[0]; const lastFocusable = focusableElements[focusableElements.length - 1]; const handleKeydown = (e) => { if (e.key === 'Tab') { if (e.shiftKey && document.activeElement === firstFocusable) { lastFocusable.focus(); e.preventDefault(); } else if (!e.shiftKey && document.activeElement === lastFocusable) { firstFocusable.focus(); e.preventDefault(); } } }; dialog.addEventListener('keydown', handleKeydown); dialog._removeKeydownListener = () => { dialog.removeEventListener('keydown', handleKeydown); }; } setupTouchHandling(dialog) { let touchStartX = 0; let touchStartY = 0; const handleTouchStart = (e) => { touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; }; const handleTouchEnd = (e) => { const diffX = e.changedTouches[0].clientX - touchStartX; const diffY = e.changedTouches[0].clientY - touchStartY; if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) { this.navigateImages({target: dialog}, diffX < 0 ? 'next' : 'previous'); } }; dialog.addEventListener('touchstart', handleTouchStart); dialog.addEventListener('touchend', handleTouchEnd); // Store cleanup function dialog._removeTouchListeners = () => { dialog.removeEventListener('touchstart', handleTouchStart); dialog.removeEventListener('touchend', handleTouchEnd); }; } closeDialog(dialog) { if (!dialog) return; // Clean up event listeners dialog._removeKeydownListener?.(); dialog._removeTouchListeners?.(); dialog.close(); // Restore focus if (this.lastFocusedElement) { this.lastFocusedElement.focus(); } // Reset gallery states const thumbnailsNav = dialog.querySelector('nav.thumbnails'); if (thumbnailsNav) { thumbnailsNav.classList.remove('open', 'expanded'); this.setComponentState(GALLERY_STATES.THUMBNAILS, false, { element: thumbnailsNav, activeClass: 'open' }); this.setComponentState(GALLERY_STATES.THUMBNAILS + '-expand', false, { element: thumbnailsNav, activeClass: 'expanded' }); } } // Focus Management trapFocus(dialog) { const focusableElements = dialog.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); this.dialogFocusableElements = { first: focusableElements[0], last: focusableElements[focusableElements.length - 1] }; dialog.addEventListener('keydown', this.handleDialogKeydown); } handleDialogKeydown(e) { if (e.key === 'Tab') { const { first, last } = this.dialogFocusableElements; if (e.shiftKey && document.activeElement === first) { last.focus(); e.preventDefault(); } else if (!e.shiftKey && document.activeElement === last) { first.focus(); e.preventDefault(); } } } // Touch Handlers handleTouchStart(e) { this.touchStartX = e.touches[0].clientX; this.touchStartY = e.touches[0].clientY; } handleTouchEnd(e) { const diffX = e.changedTouches[0].clientX - this.touchStartX; const diffY = e.changedTouches[0].clientY - this.touchStartY; if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) { this.navigateImages(e, diffX < 0 ? 'next' : 'previous'); } } // Image Navigation and Updates updateImage(figure) { if (!figure) return; const dialog = figure.closest('dialog'); dialog.querySelector('.focused')?.classList.remove('focused'); figure.classList.add('focused'); const img = figure.querySelector('img'); if (img) { const size = getImageSize(window.innerWidth, img.dataset); if (size) { this.loadAppropriateImage(img); } } this.updateThumbnails(figure); this.preloadAdjacentImages(figure); } preloadAdjacentImages(figure) { const dialog = figure.closest('dialog'); if (!dialog) return; const next = figure.nextElementSibling || dialog.querySelector('.images figure:first-child'); const prev = figure.previousElementSibling || dialog.querySelector('.images figure:last-child'); [next, prev].forEach(adjacentFigure => { if (adjacentFigure) { const img = adjacentFigure.querySelector('img'); if (img && !img.src) { this.loadAppropriateImage(img); } } }); } navigateImages(event, direction) { const dialog = event.target.closest('dialog'); if (!dialog) return; const current = dialog.querySelector('.images .focused'); if (!current) return; const next = direction === 'next' ? (current.nextElementSibling || dialog.querySelector('.images figure:first-child')) : (current.previousElementSibling || dialog.querySelector('.images figure:last-child')); if (next) { this.updateImage(next); } } // Thumbnail Management updateThumbnails(figure) { const thumbnailsNav = figure.closest('dialog')?.querySelector('nav.thumbnails .thumbnails'); if (!thumbnailsNav) return; thumbnailsNav.querySelector('.focused')?.classList.remove('focused'); const thumbnail = thumbnailsNav.querySelector(`#for-${figure.id}`); if (thumbnail) { thumbnail.classList.add('focused'); thumbnail.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' }); } } handleThumbnailSelection(event) { event.preventDefault(); const link = event.target.closest('a'); if (!link) return; const figure = link.querySelector('figure'); if (!figure) return; const targetId = figure.id.replace('for-', ''); const dialog = link.closest('dialog'); const targetFigure = dialog.querySelector(`#${targetId}`); if (targetFigure) { this.updateImage(targetFigure); } } handleThumbnailControl(event, type) { const button = event.target.closest('button'); const nav = button.closest('nav.thumbnails'); if (!nav) return; if (type === 'open') { nav.classList.toggle('open'); if (!nav.classList.contains('open')) { nav.classList.remove('expanded'); const expandButton = nav.querySelector('button.expand'); if (expandButton) { expandButton.setAttribute('aria-expanded', 'false'); expandButton.setAttribute('title', 'Expand Thumbnails'); expandButton.setAttribute('aria-label', 'Expand Thumbnails'); } } button.setAttribute('aria-expanded', nav.classList.contains('open')); button.setAttribute('title', nav.classList.contains('open') ? 'Hide Thumbnails' : 'Show Thumbnails'); button.setAttribute('aria-label', nav.classList.contains('open') ? 'Hide Thumbnails' : 'Show Thumbnails'); } else if (type === 'expand') { nav.classList.toggle('expanded'); button.setAttribute('aria-expanded', nav.classList.contains('expanded')); button.setAttribute('title', nav.classList.contains('expanded') ? 'Condense Thumbnails' : 'Expand Thumbnails'); button.setAttribute('aria-label', nav.classList.contains('expanded') ? 'Condense Thumbnails' : 'Expand Thumbnails'); } } // Utility Methods debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } handleInitializationError() { const galleries = document.querySelectorAll('.open-gallery'); galleries.forEach(gallery => { const img = gallery.querySelector('img'); if (img && img.dataset.full) { gallery.href = img.dataset.full; } }); } handleOutsideClick(event) { if (this.isComponentActive(GALLERY_STATES.DIALOG)) { const dialog = document.querySelector('dialog[open]'); if (dialog && !dialog.contains(event.target)) { this.closeDialog(dialog); } } } handleEscapeKey(event) { if (event.key === 'Escape' && this.isComponentActive(GALLERY_STATES.DIALOG)) { const dialog = document.querySelector('dialog[open]'); if (dialog) { this.closeDialog(dialog); } } } // Cleanup cleanup() { super.cleanup(); window.removeEventListener('resize', this.debouncedResize); this.cleanupAllObservers(); // Dialog cleanup handled by individual dialog close this.elements.dialogs?.forEach(dialog => { dialog._removeKeydownListener?.(); dialog._removeTouchListeners?.(); }); } } // Initialize on DOM load document.addEventListener('DOMContentLoaded', () => { window.media = new MediaManager(); });