monacousa-portal/composables/useMobileDetection.ts

175 lines
5.2 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;
let initialHeight = window.innerHeight;
let keyboardOpen = false;
const handleResize = () => {
// Don't handle resize if we're in the middle of a transition
if (document.hidden) return;
clearTimeout(resizeTimeout);
// Debounce with longer delay for mobile
const debounceDelay = globalState.isMobile ? 300 : 150;
resizeTimeout = setTimeout(() => {
const newHeight = window.innerHeight;
const heightDiff = newHeight - lastHeight;
const absoluteDiff = Math.abs(heightDiff);
// Detect keyboard open/close on mobile
if (globalState.isMobile) {
// Keyboard likely opened (viewport got smaller)
if (heightDiff < -100 && newHeight < initialHeight * 0.75) {
keyboardOpen = true;
// Don't trigger viewport updates for keyboard changes
return;
}
// Keyboard likely closed (viewport got bigger)
if (heightDiff > 100 && keyboardOpen) {
keyboardOpen = false;
// Don't trigger viewport updates for keyboard changes
return;
}
}
// Only update for significant non-keyboard changes (more than 50px)
// or orientation changes (height differs significantly from initial)
const isOrientationChange = absoluteDiff > initialHeight * 0.3;
const isSignificantChange = absoluteDiff > 50;
if (isOrientationChange || (isSignificantChange && !keyboardOpen)) {
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`);
}
// Update initial height after orientation change
if (isOrientationChange) {
initialHeight = newHeight;
}
}
}, debounceDelay);
};
// 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', () => {
// Reset keyboard state on orientation change
keyboardOpen = false;
// Wait for orientation change to complete
setTimeout(handleResize, 200);
});
}
// 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;