monacousa-portal/composables/useMobileDetection.ts

139 lines
3.9 KiB
TypeScript

/**
* Unified Mobile Detection Composable
* Provides consistent mobile detection across the app
*/
export interface MobileDetectionState {
isMobile: boolean;
isSafari: boolean;
isMobileSafari: boolean;
isIOS: boolean;
isAndroid: boolean;
safariVersion?: number;
viewportHeight: number;
isInitialized: boolean;
}
// Global state to ensure single source of truth
const globalState = reactive<MobileDetectionState>({
isMobile: false,
isSafari: false,
isMobileSafari: false,
isIOS: false,
isAndroid: false,
safariVersion: undefined,
viewportHeight: 0,
isInitialized: false
});
// Track if listeners are already set up
let listenersSetup = false;
export const useMobileDetection = () => {
// Initialize detection on first use
if (!globalState.isInitialized && typeof window !== 'undefined') {
detectDevice();
globalState.isInitialized = true;
// Set up listeners only once globally
if (!listenersSetup) {
setupListeners();
listenersSetup = true;
}
}
// Return readonly state to prevent external modifications
return readonly(globalState);
};
function detectDevice() {
if (typeof window === 'undefined') return;
const userAgent = navigator.userAgent;
// Detect mobile
globalState.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
// Detect iOS
globalState.isIOS = /iPad|iPhone|iPod/.test(userAgent) && !(window as any).MSStream;
// Detect Android
globalState.isAndroid = /Android/i.test(userAgent);
// Detect Safari
globalState.isSafari = /^((?!chrome|android).)*safari/i.test(userAgent);
// Detect Mobile Safari specifically
globalState.isMobileSafari = globalState.isIOS && globalState.isSafari;
// Extract Safari version
if (globalState.isSafari) {
const match = userAgent.match(/Version\/(\d+)/);
if (match) {
globalState.safariVersion = parseInt(match[1]);
}
}
// Set initial viewport height
globalState.viewportHeight = window.innerHeight;
}
function setupListeners() {
if (typeof window === 'undefined') return;
let resizeTimeout: NodeJS.Timeout;
let lastHeight = window.innerHeight;
const handleResize = () => {
clearTimeout(resizeTimeout);
// Debounce and only update if height actually changed
resizeTimeout = setTimeout(() => {
const newHeight = window.innerHeight;
// Only update if there's a significant change (more than 20px)
// This prevents minor fluctuations from triggering updates
if (Math.abs(newHeight - lastHeight) > 20) {
lastHeight = newHeight;
globalState.viewportHeight = newHeight;
// Update CSS custom property for mobile Safari
if (globalState.isMobileSafari) {
const vh = newHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
}
}
}, 150); // Increased debounce time
};
// Add resize listener with passive option for better performance
window.addEventListener('resize', handleResize, { passive: true });
// Also listen for orientation changes on mobile
if (globalState.isMobile) {
window.addEventListener('orientationchange', () => {
// Wait for orientation change to complete
setTimeout(handleResize, 100);
});
}
// Clean up on app unmount (if needed)
if (typeof window !== 'undefined') {
const cleanup = () => {
window.removeEventListener('resize', handleResize);
if (globalState.isMobile) {
window.removeEventListener('orientationchange', handleResize);
}
};
// Store cleanup function for manual cleanup if needed
(window as any).__mobileDetectionCleanup = cleanup;
}
}
// Export helper functions
export const isMobileSafari = () => globalState.isMobileSafari;
export const isMobile = () => globalState.isMobile;
export const isIOS = () => globalState.isIOS;
export const getViewportHeight = () => globalState.viewportHeight;