| 1 | /** |
| 2 | * Detects the input type: 'pointer' (e.g., mouse, supports hover) or 'touch' (e.g., finger, no reliable hover). |
| 3 | * On hybrids, resolves based on first user interaction (mouse vs. touch). |
| 4 | * @returns {Promise<string>} Resolves to 'pointer' or 'touch'. |
| 5 | */ |
| 6 | // Module variable to store detected input type |
| 7 | let detectedInputType; |
| 8 | |
| 9 | // Detects and stores the input type: 'pointer' or 'touch'. On hybrids, resolves on first user interaction. |
| 10 | export function determineInputType() { |
| 11 | return new Promise((resolve) => { |
| 12 | let inputType = "pointer"; // Default (overridden below) |
| 13 | let resolved = false; |
| 14 | |
| 15 | // Helper to resolve and clean up listeners |
| 16 | const resolveType = (type) => { |
| 17 | if (resolved) return; |
| 18 | resolved = true; |
| 19 | inputType = type; |
| 20 | detectedInputType = type; // store in module variable |
| 21 | resolve(inputType); |
| 22 | // Remove listeners to avoid memory leaks |
| 23 | document.removeEventListener("touchstart", onTouch, { passive: true }); |
| 24 | document.removeEventListener("mousemove", onMouse, { passive: true }); |
| 25 | document.removeEventListener("mouseenter", onMouse, { passive: true }); |
| 26 | }; |
| 27 | |
| 28 | // Dynamic listeners for hybrids (detect first interaction) |
| 29 | const onTouch = () => resolveType("touch"); |
| 30 | const onMouse = () => resolveType("pointer"); |
| 31 | |
| 32 | // Static detection (inspired by detect-it: https://github.com/rafgraph/detect-it) |
| 33 | // Step 1: Check for touch capability (touchOnly or hybrid) |
| 34 | const hasTouch = () => { |
| 35 | if ("maxTouchPoints" in navigator) return navigator.maxTouchPoints > 0; |
| 36 | if (window.matchMedia) |
| 37 | return window.matchMedia("(any-pointer: coarse)").matches; |
| 38 | return "ontouchstart" in window || navigator.msMaxTouchPoints > 0; |
| 39 | }; |
| 40 | |
| 41 | // Step 2: Check for pointer/hover capability (pointerOnly or hybrid) |
| 42 | const hasPointer = () => { |
| 43 | if (window.matchMedia) { |
| 44 | const finePointer = window.matchMedia("(any-pointer: fine)").matches; |
| 45 | const hover = window.matchMedia("(any-hover: hover)").matches; |
| 46 | return finePointer || hover; |
| 47 | } |
| 48 | return false; // Fallback: Assume no pointer if media queries unavailable |
| 49 | }; |
| 50 | |
| 51 | const touchSupported = hasTouch(); |
| 52 | const pointerSupported = hasPointer(); |
| 53 | |
| 54 | if (touchSupported && !pointerSupported) { |
| 55 | // Touch-only (e.g., phones) |
| 56 | resolveType("touch"); |
| 57 | } else if (!touchSupported && pointerSupported) { |
| 58 | // Pointer-only (e.g., desktops) |
| 59 | resolveType("pointer"); |
| 60 | } else if (touchSupported && pointerSupported) { |
| 61 | // Hybrid: Wait for first interaction to determine usageDit is the user's active input |
| 62 | // Default to pointer, but add listeners if hybrid |
| 63 | inputType = "pointer"; // Default for hybrids until interaction |
| 64 | document.addEventListener("touchstart", onTouch, { passive: true }); |
| 65 | document.addEventListener("mousemove", onMouse, { passive: true }); |
| 66 | document.addEventListener("mouseenter", onMouse, { passive: true }); |
| 67 | // Optional: Timeout fallback (e.g., after 10s, assume pointer for hybrids) |
| 68 | setTimeout(() => resolveType("pointer"), 10000); |
| 69 | } else { |
| 70 | // Rare fallback: No touch or pointer detected (assume pointer) |
| 71 | resolveType("pointer"); |
| 72 | } |
| 73 | }); |
| 74 | } |
| 75 | |
| 76 | // Exported function to get the detected input type (defaults to 'pointer' if undetermined) |
| 77 | export function getInputType() { |
| 78 | return detectedInputType || "pointer"; |
| 79 | } |