monacousa-portal/utils/viewport-manager.ts

143 lines
4.7 KiB
TypeScript
Raw Normal View History

/**
* 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();
}
}