1/** 2 * Device Detection Utility 3 * 4 * A modern device detection utility that follows the recommended approach of using 5 * User-Agent Client Hints as primary detection method. Only loads ua-parser-js from CDN 6 * when UA-CH is not available, making it efficient and future-proof. 7 * Handles iOS 17+ iPads that identify as desktop Safari. 8 * 9 * Detection Priority: 10 * 1. User-Agent Client Hints (UA-CH) - Modern browser API 11 * 2. ua-parser-js library (dynamically loaded only when UA-CH unavailable) 12 * 3. Feature detection (additional checks for special cases) 13 * 4. Basic UA string patterns (final fallback) 14 * 15 * Loading Strategy: 16 * - If UA-CH available: No external dependencies, immediate initialization 17 * - If UA-CH unavailable: Dynamically loads ua-parser-js from CDN as fallback 18 * 19 */ 20 21(function(window) { 22 'use strict'; 23 24 /** 25 * Device Detection Class 26 */ 27 function DeviceDetector() { 28 this.parser = null; 29 this.cache = {}; 30 this.initialized = false; 31 32 // Initialize UAParser as fallback option 33 this._initializeParser(); 34 } 35 36 /** 37 * Initialize the UA parser (only if UA-CH is not available) 38 * @private 39 */ 40 DeviceDetector.prototype._initializeParser = function() { 41 // Check if User-Agent Client Hints are available 42 if (this._isUACHAvailable()) { 43 // UA-CH is available, no need for ua-parser-js 44 this.parser = null; 45 this.initialized = true; 46 } else { 47 // UA-CH not available, try to load ua-parser-js as fallback 48 if (typeof UAParser !== 'undefined') { 49 // Already loaded 50 this.parser = new UAParser(); 51 this.initialized = true; 52 } else { 53 // Dynamically load ua-parser-js 54 this._loadUAParser(); 55 } 56 } 57 }; 58 59 /** 60 * Dynamically load UAParser from CDN when UA-CH is not available 61 * @private 62 */ 63 DeviceDetector.prototype._loadUAParser = function() { 64 var self = this; 65 var script = document.createElement('script'); 66 script.src = 'https://cdn.smartsites.parentsquare.com/lib/uaparser/ua-parser.min.js'; 67 script.crossOrigin = 'anonymous'; 68 script.integrity = 'sha384-X/QJP4vk5ve2IvDaZK2ri7hmLKvVucgazjmRVmGCBiN+Y/cOI786DPPDe8ljOZw6'; 69 70 script.onload = function() { 71 self.parser = new UAParser(); 72 self.initialized = true; 73 self._clearCache(); 74 // Dispatch ready event (with iOS Safari compatibility) 75 self._dispatchReadyEvent(); 76 }; 77 78 script.onerror = function() { 79 console.warn('DeviceDetector: Failed to load ua-parser-js, using basic detection only'); 80 self.parser = null; 81 self.initialized = true; 82 self._dispatchReadyEvent(); 83 }; 84 85 document.head.appendChild(script); 86 }; 87 88 89 /** 90 * Check if User-Agent Client Hints are available 91 * @returns {boolean} 92 * @private 93 */ 94 DeviceDetector.prototype._isUACHAvailable = function() { 95 return !!(navigator.userAgentData && navigator.userAgentData.getHighEntropyValues); 96 }; 97 98 /** 99 * Get device information using UA-CH 100 * @returns {Promise<Object>|null} 101 * @private 102 */ 103 DeviceDetector.prototype._getUACHInfo = function() { 104 if (!this._isUACHAvailable()) { 105 return null; 106 } 107 108 return navigator.userAgentData.getHighEntropyValues([ 109 'architecture', 110 'model', 111 'platform', 112 'platformVersion', 113 'fullVersionList' 114 ]); 115 }; 116 117 /** 118 * Get device information object 119 * @returns {Object} Device information 120 */ 121 DeviceDetector.prototype.getDeviceInfo = function() { 122 if (this.cache.deviceInfo) { 123 return this.cache.deviceInfo; 124 } 125 126 var deviceInfo = { 127 isMobile: this.isMobile(), 128 isTablet: this.isTablet(), 129 isDesktop: this.isDesktop(), 130 isIPad: this.isIPad(), 131 isTouchDevice: this.isTouchDevice(), 132 browser: this.getBrowserName(), 133 os: this.getOSName(), 134 userAgent: navigator.userAgent, 135 platform: navigator.platform 136 }; 137 138 this.cache.deviceInfo = deviceInfo; 139 return deviceInfo; 140 }; 141 142 /** 143 * Check if device is mobile (phone) 144 * @returns {boolean} 145 */ 146 DeviceDetector.prototype.isMobile = function() { 147 if (this.cache.isMobile !== undefined) { 148 return this.cache.isMobile; 149 } 150 151 var result = false;
152 153 // Primary: User-Agent Client Hints (if available) 154 if (this._isUACHAvailable()) { 155 result = this._detectMobileFromUACH(); 156 } 157 // Fallback: ua-parser-js (only if UA-CH not available) 158 else if (this.parser) { 159 var deviceType = this.parser.getDevice().type; 160 result = deviceType === 'mobile'; 161 } 162 163 // Feature detection as additional check 164 if (!result) { 165 result = this._featureBasedMobileDetection(); 166 } 167 168 // Final fallback: Basic UA string detection 169 if (!result) { 170 result = this._fallbackMobileDetection(); 171 } 172 173 this.cache.isMobile = result; 174 return result; 175 }; 176 177 /** 178 * Check if device is tablet 179 * @returns {boolean} 180 */ 181 DeviceDetector.prototype.isTablet = function() { 182 if (this.cache.isTablet !== undefined) { 183 return this.cache.isTablet; 184 } 185 186 var result = false; 187 188 // Primary: User-Agent Client Hints (if available) 189 if (this._isUACHAvailable()) { 190 result = this._detectTabletFromUACH(); 191 } 192 // Fallback: ua-parser-js (only if UA-CH not available) 193 else if (this.parser) { 194 var deviceType = this.parser.getDevice().type; 195 result = deviceType === 'tablet'; 196 } 197 198 // Feature detection for special cases (iOS 17+ iPad issue) 199 if (!result && this.isIPad()) { 200 result = true; 201 } 202 203 // Additional feature-based detection 204 if (!result) { 205 result = this._featureBasedTabletDetection(); 206 } 207 208 // Final fallback: Basic detection 209 if (!result) { 210 result = this._fallbackTabletDetection(); 211 } 212 213 this.cache.isTablet = result; 214 return result; 215 }; 216 217 /** 218 * Check if device is desktop 219 * @returns {boolean} 220 */ 221 DeviceDetector.prototype.isDesktop = function() { 222 return !this.isMobile() && !this.isTablet(); 223 }; 224 225 /** 226 * Check if device is iPad (handles iOS 17+ desktop user agent) 227 * @returns {boolean} 228 */ 229 DeviceDetector.prototype.isIPad = function() { 230 if (this.cache.isIPad !== undefined) { 231 return this.cache.isIPad; 232 } 233 234 var result = false;
235 236 // Check explicit iPad mentions 237 if (/iPad/.test(navigator.userAgent)) { 238 result = true; 239 } 240 // Check for iOS 17+ iPad (reports as Mac with touch) 241 else if (navigator.platform === 'MacIntel' && this.isTouchDevice()) { 242 result = this._isIPadByScreenSize(); 243 } 244 245 this.cache.isIPad = result; 246 return result; 247 }; 248 249 /** 250 * Check if device supports touch 251 * @returns {boolean} 252 */ 253 DeviceDetector.prototype.isTouchDevice = function() { 254 if (this.cache.isTouchDevice !== undefined) { 255 return this.cache.isTouchDevice; 256 } 257 258 var result = ( 259 'ontouchstart' in window || 260 navigator.maxTouchPoints > 0 || 261 navigator.msMaxTouchPoints > 0 262 ); 263 264 this.cache.isTouchDevice = result; 265 return result; 266 }; 267 268 /** 269 * Get browser name 270 * @returns {string} 271 */ 272 DeviceDetector.prototype.getBrowserName = function() { 273 if (this.parser) { 274 return this.parser.getBrowser().name || 'Unknown'; 275 } 276 return this._fallbackBrowserDetection(); 277 }; 278 279 /** 280 * Get OS name 281 * @returns {string} 282 */ 283 DeviceDetector.prototype.getOSName = function() { 284 if (this.parser) { 285 return this.parser.getOS().name || 'Unknown'; 286 } 287 return this._fallbackOSDetection(); 288 }; 289 290 /** 291 * Determine if should show PDF link (mobile devices get links) 292 * @returns {boolean} 293 */ 294 DeviceDetector.prototype.shouldShowPDFLink = function() { 295 return this.isMobile() || this.isTablet(); 296 }; 297 298 /** 299 * Check if iPad by screen dimensions 300 * @returns {boolean} 301 * @private 302 */ 303 DeviceDetector.prototype._isIPadByScreenSize = function() { 304 var screenWidth = window.screen.width; 305 var screenHeight = window.screen.height; 306 307 // Common iPad screen dimensions 308 var iPadSizes = [ 309 [768, 1024], // iPad Mini, Air, 9.7" Pro 310 [834, 1112], // 10.5" Pro 311 [1024, 1366], // 12.9" Pro 312 [820, 1180], // 10.9" Air (4th gen) 313 [1668, 2388], // 11" Pro (high DPI) 314 [2048, 2732] // 12.9" Pro (high DPI) 315 ]; 316 317 var currentSize = [ 318 Math.min(screenWidth, screenHeight), 319 Math.max(screenWidth, screenHeight) 320 ]; 321 322 for (var i = 0; i < iPadSizes.length; i++) { 323 var size = iPadSizes[i]; 324 if (Math.abs(currentSize[0] - size[0]) <= 10 && 325 Math.abs(currentSize[1] - size[1]) <= 10) { 326 return true; 327 } 328 } 329 330 return false; 331 }; 332 333 /** 334 * Detect mobile using User-Agent Client Hints 335 * @returns {boolean} 336 * @private 337 */ 338 DeviceDetector.prototype._detectMobileFromUACH = function() { 339 if (!navigator.userAgentData) { 340 return false; 341 } 342 343 // Check if it's mobile from basic UA-CH data 344 return navigator.userAgentData.mobile === true; 345 }; 346 347 /** 348 * Detect tablet using User-Agent Client Hints 349 * @returns {boolean} 350 * @private 351 */ 352 DeviceDetector.prototype._detectTabletFromUACH = function() { 353 if (!navigator.userAgentData) { 354 return false; 355 } 356 357 // UA-CH doesn't explicitly indicate tablets, so we need to combine with feature detection 358 // If it's not mobile according to UA-CH but has touch, it might be a tablet 359 var isMobileByUACH = navigator.userAgentData.mobile === true; 360 var hasTouch = this.isTouchDevice(); 361 362 // Exclude desktop operating systems - touchscreen PCs are not tablets 363 var platform = navigator.userAgentData.platform || ''; 364 if (this._isDesktopPlatform(platform)) { 365 return false; 366 } 367 368 // Tablet: not mobile by UA-CH but has touch capabilities 369 return !isMobileByUACH && hasTouch && this._featureBasedTabletDetection(); 370 }; 371 372 /** 373 * Feature-based mobile detection 374 * @returns {boolean} 375 * @private 376 */ 377 DeviceDetector.prototype._featureBasedMobileDetection = function() { 378 // Check for explicit mobile indicators
379 if (/iPhone|iPod|Android.*Mobile|webOS|BlackBerry|Windows Phone/i.test(navigator.userAgent)) { 380 return true; 381 } 382 383 // Feature-based detection: small touch screen 384 if (this.isTouchDevice()) { 385 var screenWidth = window.screen.width; 386 var screenHeight = window.screen.height; 387 var minDimension = Math.min(screenWidth, screenHeight); 388 389 // Typical phone dimensions (up to ~6.5" phones) 390 return minDimension <= 480; 391 } 392 393 return false; 394 }; 395 396 /** 397 * Feature-based tablet detection (primary method) 398 * @returns {boolean} 399 * @private 400 */ 401 DeviceDetector.prototype._featureBasedTabletDetection = function() { 402 if (!this.isTouchDevice()) { 403 return false; 404 } 405 406 // Exclude desktop operating systems - touchscreen PCs are not tablets 407 if (this._isDesktopOS()) { 408 return false; 409 } 410 411 var screenWidth = window.screen.width; 412 var screenHeight = window.screen.height; 413 var minDimension = Math.min(screenWidth, screenHeight); 414 var maxDimension = Math.max(screenWidth, screenHeight); 415 416 // Exclude phones first 417 if (minDimension <= 480) { 418 return false; 419 } 420 421 // Typical tablet dimensions (7" to 13" tablets) 422 return minDimension >= 600 && maxDimension >= 800; 423 }; 424 425 /** 426 * Fallback mobile detection 427 * @returns {boolean} 428 * @private 429 */ 430 DeviceDetector.prototype._fallbackMobileDetection = function() { 431 var ua = navigator.userAgent; 432 return /iPhone|iPod|Android.*Mobile|webOS|BlackBerry|Windows Phone/i.test(ua); 433 }; 434 435 /** 436 * Fallback tablet detection 437 * @returns {boolean} 438 * @private 439 */ 440 DeviceDetector.prototype._fallbackTabletDetection = function() { 441 if (!this.isTouchDevice()) { 442 return false; 443 } 444 445 // Exclude desktop operating systems - touchscreen PCs are not tablets 446 if (this._isDesktopOS()) { 447 return false; 448 } 449 450 var screenWidth = window.screen.width; 451 var screenHeight = window.screen.height; 452 var minDimension = Math.min(screenWidth, screenHeight); 453 var maxDimension = Math.max(screenWidth, screenHeight); 454 455 // Typical tablet dimensions (7" to 13" tablets) 456 return minDimension >= 600 && maxDimension >= 800; 457 }; 458 459 /** 460 * Fallback browser detection 461 * @returns {string} 462 * @private 463 */ 464 DeviceDetector.prototype._fallbackBrowserDetection = function() { 465 var ua = navigator.userAgent; 466 if (/Safari/.test(ua) && !/Chrome/.test(ua)) return 'Safari'; 467 if (/Chrome/.test(ua)) return 'Chrome'; 468 if (/Firefox/.test(ua)) return 'Firefox'; 469 if (/Edge/.test(ua)) return 'Edge'; 470 return 'Unknown'; 471 }; 472 473 /** 474 * Fallback OS detection 475 * @returns {string} 476 * @private 477 */ 478 DeviceDetector.prototype._fallbackOSDetection = function() { 479 var ua = navigator.userAgent; 480 if (/Windows/.test(ua)) return 'Windows'; 481 if (/Mac OS X|macOS/.test(ua)) return 'Mac OS'; 482 if (/Android/.test(ua)) return 'Android'; 483 if (/iPhone|iPad|iPod/.test(ua)) return 'iOS'; 484 if (/Linux/.test(ua)) return 'Linux'; 485 return 'Unknown'; 486 }; 487 488 /** 489 * Check if platform string indicates a desktop OS (for UA-CH) 490 * @param {string} platform - The platform string from UA-CH 491 * @returns {boolean} 492 * @private 493 */ 494 DeviceDetector.prototype._isDesktopPlatform = function(platform) { 495 var desktopPlatforms = ['Windows', 'Linux', 'Chrome OS']; 496 return desktopPlatforms.indexOf(platform) !== -1; 497 }; 498 499 /** 500 * Check if the current OS is a desktop OS (using UA string fallback) 501 * Excludes macOS because iPads report as Mac, handled separately by isIPad() 502 * @returns {boolean} 503 * @private 504 */ 505 DeviceDetector.prototype._isDesktopOS = function() { 506 var ua = navigator.userAgent; 507 508 // Windows or Linux are definitely desktop OSes 509 if (/Windows|Linux/.test(ua) && !/Android/.test(ua)) { 510 return true; 511 } 512 513 // Chrome OS is a desktop OS 514 if (/CrOS/.test(ua)) { 515 return true; 516 } 517 518 return false;
519 }; 520 521 /** 522 * Clear cache 523 */ 524 DeviceDetector.prototype.clearCache = function() { 525 this.cache = {}; 526 }; 527 528 /** 529 * Clear cache 530 * @private 531 */ 532 DeviceDetector.prototype._clearCache = function() { 533 this.clearCache(); 534 }; 535 536 /** 537 * Dispatch ready event with iOS Safari compatibility 538 * @private 539 */ 540 DeviceDetector.prototype._dispatchReadyEvent = function() { 541 var event; 542 try { 543 // Modern browsers 544 event = new CustomEvent('deviceDetectorReady'); 545 } catch (e) { 546 // iOS Safari and older browsers fallback 547 event = document.createEvent('Event'); 548 event.initEvent('deviceDetectorReady', true, true); 549 } 550 window.dispatchEvent(event); 551 }; 552 553 /** 554 * Get debug information 555 * @returns {Object} 556 */ 557 DeviceDetector.prototype.getDebugInfo = function() { 558 return { 559 initialized: this.initialized, 560 parserAvailable: !!this.parser, 561 userAgent: navigator.userAgent, 562 platform: navigator.platform, 563 maxTouchPoints: navigator.maxTouchPoints, 564 screenSize: { 565 width: window.screen.width, 566 height: window.screen.height 567 }, 568 deviceInfo: this.getDeviceInfo(), 569 timestamp: new Date().toISOString() 570 }; 571 }; 572 573 // Create and export singleton instance 574 var deviceDetectorInstance = new DeviceDetector(); 575 576 // Export factory function that follows the example pattern 577 window.getDeviceInfo = function() { 578 return deviceDetectorInstance.getDeviceInfo(); 579 }; 580 581 // Export the instance for backward compatibility 582 window.DeviceDetector = deviceDetectorInstance; 583 584 // Notify when ready 585 // If UA-CH is available or UAParser was already loaded, dispatch immediately 586 if (deviceDetectorInstance.initialized) { 587 setTimeout(function() { 588 deviceDetectorInstance._dispatchReadyEvent(); 589 }, 0); 590 } 591 // Otherwise, the ready event will be dispatched after ua-parser-js loads 592 593})(window);
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.