175 lines
5.2 KiB
TypeScript
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;
|