1/** 2 * Device Detection Utility 3 * 4 * Reliable JS-only detection for iPad (especially iPadOS Safari masquerading as Mac), 5 * mobile phones, tablets, and desktop devices. 6 * 7 * Features: 8 * - Detects device type: 'mobile' | 'tablet' | 'desktop' 9 * - Sets HTML class immediately: device-mobile / device-tablet / device-desktop 10 * - Exposes utilities: isIPad(), getDeviceType() 11 * - Sets cookies for PHP server-side detection 12 * - Provides debug output: window.__DEVICE_DEBUG__ 13 * 14 * This script runs immediately (synchronously) to set classes before animations initialize. 15 */ 16(function() { 17 'use strict'; 18 19 // Only run in browser environment 20 if (typeof navigator === 'undefined' || typeof document === 'undefined') { 21 return; 22 } 23 24 const ua = navigator.userAgent || ''; 25 const maxTouchPoints = navigator.maxTouchPoints || 0; 26 27 // Capability detection (pointer and hover media queries) 28 // Only use as supportive signal, not primary detection 29 let coarsePointer = false; 30 let noHover = false; 31 try { 32 coarsePointer = window.matchMedia && window.matchMedia('(pointer: coarse)').matches; 33 noHover = window.matchMedia && window.matchMedia('(hover: none)').matches; 34 } catch (e) { 35 // Media queries not supported, ignore 36 } 37 38 /** 39 * Detect if device is an iPad 40 * @returns {boolean} True if device is detected as iPad 41 */ 42 function isIPad() { 43 // Classic iPad detection (older iOS) 44 const classicIPad = /iPad/.test(ua); 45 46 // iPadOS masquerading as Mac detection 47 // iPadOS 13+ reports as Macintosh but has touch points 48 // Must check maxTouchPoints > 1 to avoid false positives on real Macs 49 const iPadOS = /Macintosh/.test(ua) && maxTouchPoints > 1; 50 51 return classicIPad || iPadOS; 52 } 53 54 /** 55 * Detect if device is a phone (mobile) 56 * @returns {boolean} True if device is detected as mobile phone 57 */ 58 function isMobile() { 59 // iPhone detection 60 const isIPhone = /iPhone|iPod/.test(ua); 61 62 // Android phone detection (Android AND Mobile in UA) 63 const isAndroidPhone = /Android/.test(ua) && /Mobile/.test(ua); 64 65 // Other mobile devices 66 const isOtherMobile = /webOS|BlackBerry|IEMobile|Opera Mini/i.test(ua); 67 68 return isIPhone || isAndroidPhone || isOtherMobile; 69 } 70 71 /** 72 * Detect if device is a tablet 73 * @returns {boolean} True if device is detected as tablet 74 */ 75 function isTablet() { 76 // iPad detection 77 if (isIPad()) { 78 return true; 79 } 80 81 // Android tablet detection (Android but NOT Mobile) 82 const isAndroidTablet = /Android/.test(ua) && !/Mobile/.test(ua); 83 84 // Capability fallback: only use if NOT a Mac (to avoid false positives) 85 // This catches edge cases but must not turn real Macs into tablets 86 const isMac = /Macintosh/.test(ua) && maxTouchPoints <= 1; 87 const capabilityFallback = !isMac && coarsePointer && noHover; 88 89 return isAndroidTablet || capabilityFallback; 90 } 91 92 /** 93 * Get device type 94 * @returns {'mobile' | 'tablet' | 'desktop'} Device type 95 */ 96 function getDeviceType() { 97 let device; 98 99 // Normal detection logic 100 if (isMobile()) { 101 device = 'mobile'; 102 } else if (isTablet()) { 103 device = 'tablet'; 104 } else { 105 device = 'desktop'; 106 } 107 108 // Height-based fallback: treat desktop with small screen height as tablet 109 // This handles small laptops, compact desktop windows, and edge cases 110 // where desktop UX should behave like tablet UX 111 if (device === 'desktop' && window.innerHeight <= 800) { 112 device = 'tablet'; 113 } 114 115 return device; 116 } 117 118 /** 119 * Apply device class to HTML element 120 * This runs immediately to ensure classes are set before animations initialize 121 */ 122 function applyDeviceClass() { 123 const deviceType = getDeviceType(); 124 const htmlElement = document.documentElement; 125 126 // Remove all device classes first 127 htmlElement.classList.remove('device-mobile', 'device-tablet', 'device-desktop'); 128 129 // Add the appropriate class 130 htmlElement.classList.add('device-' + deviceType); 131 132 // Also set data attribute for easy access 133 htmlElement.setAttribute('data-device', deviceType); 134 } 135 136 /** 137 * Set or remove cookies for PHP server-side detection 138 */ 139 function setDeviceCookies() { 140 const deviceType = getDeviceType(); 141 const isTabletDevice = deviceType === 'tablet'; 142 const cookiePath = '/'; 143 const maxAge = 2592000; // 30 days in seconds 144 145 // Add Secure flag if site is served over HTTPS 146 const isSecure = typeof window !== 'undefined' && 147 window.location && 148 window.location.protocol === 'https:'; 149 const secureFlag = isSecure ? '; Secure' : ''; 150 151 // Set device type cookie 152 const deviceTypeCookie = 'app_device_type=' + deviceType + 153 '; path=' + cookiePath + 154 '; max-age=' + maxAge + 155 '; SameSite=Lax' + secureFlag; 156 document.cookie = deviceTypeCookie; 157
158 // Set/remove tablet cookie for backward compatibility 159 const tabletCookieName = 'app_device_tablet'; 160 if (isTabletDevice) { 161 const tabletCookie = tabletCookieName + '=1; path=' + cookiePath + 162 '; max-age=' + maxAge + 163 '; SameSite=Lax' + secureFlag; 164 document.cookie = tabletCookie; 165 } else { 166 // Remove cookie by setting max-age=0 167 const removeCookie = tabletCookieName + '=; path=' + cookiePath + 168 '; max-age=0; SameSite=Lax' + secureFlag; 169 document.cookie = removeCookie; 170 } 171 } 172 173 /** 174 * Expose debug information 175 */ 176 function exposeDebugInfo() { 177 window.__DEVICE_DEBUG__ = { 178 ua: ua, 179 maxTouchPoints: maxTouchPoints, 180 coarse: coarsePointer, 181 noHover: noHover, 182 screenHeight: window.innerHeight, 183 isIPad: isIPad(), 184 isMobile: isMobile(), 185 isTablet: isTablet(), 186 device: getDeviceType() 187 }; 188 } 189 190 /** 191 * Expose utilities globally 192 */ 193 function exposeUtilities() { 194 // Expose isIPad utility 195 window.isIPad = isIPad; 196 197 // Expose getDeviceType utility 198 window.getDeviceType = getDeviceType; 199 } 200 201 // Run detection immediately (synchronously) 202 // This ensures classes are set before any animations or other scripts run 203 applyDeviceClass(); 204 setDeviceCookies(); 205 exposeDebugInfo(); 206 exposeUtilities(); 207 208 // Re-apply on orientation change (for mobile devices rotating) 209 window.addEventListener('orientationchange', function() { 210 // Small delay to allow orientation to settle 211 setTimeout(function() { 212 applyDeviceClass(); 213 setDeviceCookies(); 214 exposeDebugInfo(); 215 }, 100); 216 }); 217 218 // Re-apply on resize (for window resizing, but don't change device type on mobile rotation) 219 let resizeTimer; 220 window.addEventListener('resize', function() { 221 clearTimeout(resizeTimer); 222 resizeTimer = setTimeout(function() { 223 // Re-evaluate device type (height-based fallback may change device type) 224 // For mobile devices, device type should not change on rotation 225 const currentDevice = getDeviceType(); 226 const htmlElement = document.documentElement; 227 const currentClass = htmlElement.classList.contains('device-mobile') ? 'mobile' : 228 htmlElement.classList.contains('device-tablet') ? 'tablet' : 'desktop'; 229 230 // If device type changed (e.g., desktop -> tablet due to height change), re-apply 231 if (currentDevice !== currentClass) { 232 applyDeviceClass(); 233 setDeviceCookies(); 234 exposeDebugInfo(); 235 } 236 }, 250); 237 }); 238 239})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.