143 lines
4.7 KiB
TypeScript
143 lines
4.7 KiB
TypeScript
/**
|
|
* 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();
|
|
}
|
|
}
|