/** * CSS-Only Viewport Management System * Handles mobile Safari viewport height changes through CSS custom properties only, * without triggering any Vue component reactivity. */ class ViewportManager { private static instance: ViewportManager; private initialized = false; private resizeTimeout: NodeJS.Timeout | null = null; static getInstance(): ViewportManager { if (!ViewportManager.instance) { ViewportManager.instance = new ViewportManager(); } return ViewportManager.instance; } init() { if (this.initialized || typeof window === 'undefined') return; console.log('[ViewportManager] Initializing CSS-only viewport management'); // Static device detection (no reactive dependencies) const userAgent = navigator.userAgent; const isIOS = /iPad|iPhone|iPod/.test(userAgent) && !(window as any).MSStream; const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent); const isMobileSafari = isIOS && isSafari; // Only apply to mobile Safari where viewport issues occur if (!isMobileSafari) { console.log('[ViewportManager] Not mobile Safari, skipping viewport management'); return; } let lastHeight = window.innerHeight; let initialHeight = window.innerHeight; let keyboardOpen = false; const handleResize = () => { // Skip if document is hidden (tab not active) if (document.hidden) return; // Clear any existing timeout if (this.resizeTimeout) { clearTimeout(this.resizeTimeout); } // Debounce with longer delay for mobile Safari this.resizeTimeout = setTimeout(() => { const newHeight = window.innerHeight; const heightDiff = newHeight - lastHeight; const absoluteDiff = Math.abs(heightDiff); // Detect keyboard open/close patterns if (heightDiff < -100 && newHeight < initialHeight * 0.75) { keyboardOpen = true; console.log('[ViewportManager] Keyboard opened, skipping update'); return; } if (heightDiff > 100 && keyboardOpen) { keyboardOpen = false; console.log('[ViewportManager] Keyboard closed, skipping update'); return; } // Only update for significant non-keyboard changes const isOrientationChange = absoluteDiff > initialHeight * 0.3; const isSignificantChange = absoluteDiff > 50; if (isOrientationChange || (isSignificantChange && !keyboardOpen)) { lastHeight = newHeight; // Update CSS custom property only - no Vue reactivity const vh = newHeight * 0.01; document.documentElement.style.setProperty('--vh', `${vh}px`); console.log('[ViewportManager] Updated --vh to:', `${vh}px`); // Update initial height after orientation change if (isOrientationChange) { initialHeight = newHeight; console.log('[ViewportManager] Orientation change detected, updated initial height'); } } }, 300); // Longer debounce for mobile Safari }; // Set initial CSS custom property const initialVh = initialHeight * 0.01; document.documentElement.style.setProperty('--vh', `${initialVh}px`); console.log('[ViewportManager] Set initial --vh to:', `${initialVh}px`); // Add resize listener with passive option for better performance window.addEventListener('resize', handleResize, { passive: true }); // Also listen for orientation changes on mobile window.addEventListener('orientationchange', () => { keyboardOpen = false; // Reset keyboard state on orientation change console.log('[ViewportManager] Orientation change event, scheduling resize handler'); // Wait for orientation change to complete setTimeout(handleResize, 200); }); // Add visibility change listener to pause updates when tab is hidden document.addEventListener('visibilitychange', () => { if (document.hidden && this.resizeTimeout) { clearTimeout(this.resizeTimeout); this.resizeTimeout = null; } }); this.initialized = true; console.log('[ViewportManager] Initialization complete'); } cleanup() { if (this.resizeTimeout) { clearTimeout(this.resizeTimeout); this.resizeTimeout = null; } this.initialized = false; console.log('[ViewportManager] Cleanup complete'); } } // Export singleton instance export const viewportManager = ViewportManager.getInstance(); // Auto-initialize on client side if (typeof window !== 'undefined') { // Initialize after DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { viewportManager.init(); }); } else { // DOM is already ready viewportManager.init(); } }