129 lines
3.6 KiB
TypeScript
129 lines
3.6 KiB
TypeScript
/**
|
|
* Static Device Detection Utility
|
|
* Provides non-reactive device detection for Safari iOS reload loop prevention
|
|
* Uses direct navigator.userAgent analysis without creating Vue reactive dependencies
|
|
*/
|
|
|
|
export interface DeviceInfo {
|
|
isMobile: boolean;
|
|
isIos: boolean;
|
|
isSafari: boolean;
|
|
isMobileSafari: boolean;
|
|
isAndroid: boolean;
|
|
userAgent: string;
|
|
}
|
|
|
|
let cachedDeviceInfo: DeviceInfo | null = null;
|
|
|
|
/**
|
|
* Get static device information without creating reactive dependencies
|
|
* Results are cached to prevent multiple userAgent parsing
|
|
*/
|
|
export function getStaticDeviceInfo(): DeviceInfo {
|
|
// Return cached result if available
|
|
if (cachedDeviceInfo) {
|
|
return cachedDeviceInfo;
|
|
}
|
|
|
|
// Only run on client-side
|
|
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
|
cachedDeviceInfo = {
|
|
isMobile: false,
|
|
isIos: false,
|
|
isSafari: false,
|
|
isMobileSafari: false,
|
|
isAndroid: false,
|
|
userAgent: ''
|
|
};
|
|
return cachedDeviceInfo;
|
|
}
|
|
|
|
const userAgent = navigator.userAgent;
|
|
|
|
// Device detection logic
|
|
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
|
|
const isIos = /iPad|iPhone|iPod/.test(userAgent) && !(window as any).MSStream;
|
|
const isAndroid = /Android/i.test(userAgent);
|
|
const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent);
|
|
const isMobileSafari = isIos && isSafari;
|
|
|
|
// Cache the result
|
|
cachedDeviceInfo = {
|
|
isMobile,
|
|
isIos,
|
|
isSafari,
|
|
isMobileSafari,
|
|
isAndroid,
|
|
userAgent
|
|
};
|
|
|
|
return cachedDeviceInfo;
|
|
}
|
|
|
|
/**
|
|
* Get CSS classes for device-specific styling
|
|
* Returns a space-separated string of CSS classes
|
|
*/
|
|
export function getDeviceCssClasses(baseClass: string = ''): string {
|
|
const device = getStaticDeviceInfo();
|
|
const classes = [baseClass].filter(Boolean);
|
|
|
|
if (device.isMobile) classes.push('is-mobile');
|
|
if (device.isIos) classes.push('is-ios');
|
|
if (device.isSafari) classes.push('is-safari');
|
|
if (device.isMobileSafari) classes.push('is-mobile-safari');
|
|
if (device.isAndroid) classes.push('is-android');
|
|
|
|
return classes.join(' ');
|
|
}
|
|
|
|
/**
|
|
* Check if current device is mobile Safari specifically
|
|
* This is the primary problematic browser for reload loops
|
|
*/
|
|
export function isMobileSafari(): boolean {
|
|
return getStaticDeviceInfo().isMobileSafari;
|
|
}
|
|
|
|
/**
|
|
* Apply mobile Safari specific optimizations to DOM element
|
|
* Should be called once per component to prevent reactive updates
|
|
*/
|
|
export function applyMobileSafariOptimizations(element?: HTMLElement): void {
|
|
if (!isMobileSafari()) return;
|
|
|
|
const targetElement = element || document.documentElement;
|
|
|
|
// Apply performance optimization classes
|
|
targetElement.classList.add('is-mobile-safari', 'performance-optimized');
|
|
|
|
// Set viewport height CSS variable for mobile Safari
|
|
const vh = window.innerHeight * 0.01;
|
|
targetElement.style.setProperty('--vh', `${vh}px`);
|
|
|
|
// Disable problematic CSS features for performance
|
|
targetElement.style.setProperty('--backdrop-filter', 'none');
|
|
targetElement.style.setProperty('--will-change', 'auto');
|
|
}
|
|
|
|
/**
|
|
* Get viewport meta content optimized for mobile Safari
|
|
*/
|
|
export function getMobileSafariViewportMeta(): string {
|
|
const device = getStaticDeviceInfo();
|
|
|
|
if (device.isMobileSafari) {
|
|
// Prevent zoom on input focus for iOS Safari
|
|
return 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
|
|
}
|
|
|
|
return 'width=device-width, initial-scale=1.0';
|
|
}
|
|
|
|
/**
|
|
* Clear cached device info (useful for testing)
|
|
*/
|
|
export function clearDeviceInfoCache(): void {
|
|
cachedDeviceInfo = null;
|
|
}
|