Refactor mobile detection to use built-in Nuxt device module
Some checks failed
Build And Push Image / docker (push) Failing after 2m27s
Some checks failed
Build And Push Image / docker (push) Failing after 2m27s
Replace custom useMobileDetection composable with Nuxt's useDevice(), removing reactive mobile detection in favor of static detection to prevent reload loops and simplify viewport handling
This commit is contained in:
@@ -1,171 +0,0 @@
|
||||
/**
|
||||
* Mobile Safari Detection and Optimization Utilities
|
||||
* Handles Safari-specific issues and performance optimizations
|
||||
*/
|
||||
|
||||
export interface DeviceInfo {
|
||||
isMobile: boolean;
|
||||
isSafari: boolean;
|
||||
isMobileSafari: boolean;
|
||||
isIOS: boolean;
|
||||
safariVersion?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect device and browser information
|
||||
*/
|
||||
export function getDeviceInfo(): DeviceInfo {
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
isMobile: false,
|
||||
isSafari: false,
|
||||
isMobileSafari: false,
|
||||
isIOS: false
|
||||
};
|
||||
}
|
||||
|
||||
const userAgent = navigator.userAgent;
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
|
||||
const isIOS = /iPad|iPhone|iPod/.test(userAgent);
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent);
|
||||
const isMobileSafari = isIOS && isSafari;
|
||||
|
||||
// Extract Safari version if possible
|
||||
let safariVersion: number | undefined;
|
||||
if (isSafari) {
|
||||
const match = userAgent.match(/Version\/(\d+)/);
|
||||
if (match) {
|
||||
safariVersion = parseInt(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isMobile,
|
||||
isSafari,
|
||||
isMobileSafari,
|
||||
isIOS,
|
||||
safariVersion
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the device needs performance optimizations
|
||||
*/
|
||||
export function needsPerformanceOptimization(): boolean {
|
||||
const { isMobileSafari, isMobile } = getDeviceInfo();
|
||||
return isMobileSafari || isMobile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if backdrop-filter should be disabled
|
||||
*/
|
||||
export function shouldDisableBackdropFilter(): boolean {
|
||||
const { isMobileSafari, safariVersion } = getDeviceInfo();
|
||||
// Disable backdrop-filter on mobile Safari or older Safari versions
|
||||
return isMobileSafari || Boolean(safariVersion && safariVersion < 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get optimized CSS class names based on device
|
||||
*/
|
||||
export function getOptimizedClasses(): string[] {
|
||||
const classes: string[] = [];
|
||||
const { isMobile, isMobileSafari, isIOS } = getDeviceInfo();
|
||||
|
||||
if (isMobile) classes.push('is-mobile');
|
||||
if (isMobileSafari) classes.push('is-mobile-safari');
|
||||
if (isIOS) classes.push('is-ios');
|
||||
if (needsPerformanceOptimization()) classes.push('performance-mode');
|
||||
|
||||
return classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimized viewport height for mobile Safari
|
||||
*/
|
||||
export function getOptimizedViewportHeight(): string {
|
||||
const { isMobileSafari, safariVersion } = getDeviceInfo();
|
||||
|
||||
if (isMobileSafari) {
|
||||
// Use 100vh for older Safari, -webkit-fill-available for newer
|
||||
return safariVersion && safariVersion >= 15 ? '-webkit-fill-available' : '100vh';
|
||||
}
|
||||
|
||||
// Use dvh for modern browsers, vh as fallback
|
||||
return '100vh';
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply mobile Safari specific fixes
|
||||
*/
|
||||
export function applyMobileSafariFixes(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const { isMobileSafari } = getDeviceInfo();
|
||||
if (!isMobileSafari) return;
|
||||
|
||||
// Fix viewport height issues
|
||||
const setViewportHeight = () => {
|
||||
const vh = window.innerHeight * 0.01;
|
||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||
};
|
||||
|
||||
// Set initial value
|
||||
setViewportHeight();
|
||||
|
||||
// Update on resize (debounced)
|
||||
let resizeTimeout: NodeJS.Timeout;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(setViewportHeight, 100);
|
||||
});
|
||||
|
||||
// Add performance optimization classes
|
||||
document.documentElement.classList.add(...getOptimizedClasses());
|
||||
}
|
||||
|
||||
/**
|
||||
* Throttle function for performance
|
||||
*/
|
||||
export function throttle<T extends (...args: any[]) => void>(
|
||||
func: T,
|
||||
wait: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
let previous = 0;
|
||||
|
||||
return function(this: any, ...args: Parameters<T>) {
|
||||
const now = Date.now();
|
||||
const remaining = wait - (now - previous);
|
||||
|
||||
if (remaining <= 0 || remaining > wait) {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
previous = now;
|
||||
func.apply(this, args);
|
||||
} else if (!timeout) {
|
||||
timeout = setTimeout(() => {
|
||||
previous = Date.now();
|
||||
timeout = null;
|
||||
func.apply(this, args);
|
||||
}, remaining);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounce function for performance
|
||||
*/
|
||||
export function debounce<T extends (...args: any[]) => void>(
|
||||
func: T,
|
||||
wait: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
return function(this: any, ...args: Parameters<T>) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||
};
|
||||
}
|
||||
142
utils/viewport-manager.ts
Normal file
142
utils/viewport-manager.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user