1/* 2 * viewport-fit.js â track the actually-visible viewport on iOS Safari. 3 * 4 * iOS Safari's bottom URL bar (iOS 15+ Tab Bar) is rendered as an overlay 5 * on top of page content in some states. CSS units like 100svh and 6 * env(safe-area-inset-bottom) do not reliably subtract its height, so a 7 * naively-sized layout has its bottom clipped by the URL bar. 8 * 9 * The visualViewport API reports the actually-visible region in real time 10 * as the chrome animates. This script copies visualViewport.height into a 11 * CSS custom property (--visible-vh-px) so CSS can use it. 12 * 13 * Falls back gracefully: if visualViewport is unavailable, CSS uses its 14 * native fallback value (100svh / 100vh). 15 */ 16(function () { 17 if (!window.visualViewport) { 18 return; 19 } 20 21 const root = document.documentElement; 22 const wantsDebug = (function () { 23 if (typeof URLSearchParams === "undefined") return false; 24 return new URLSearchParams(window.location.search).get("debugvp") === "1"; 25 })(); 26 let overlay = null; 27 28 function ensureOverlay () { 29 if (!wantsDebug || overlay || !document.body) return; 30 overlay = document.createElement("div"); 31 overlay.style.cssText = [ 32 "position: fixed", 33 "top: 0", 34 "left: 0", 35 "right: 0", 36 "background: rgba(255,0,0,0.9)", 37 "color: white", 38 "font: 11px monospace", 39 "padding: 6px", 40 "padding-top: calc(env(safe-area-inset-top, 0px) + 6px)", 41 "z-index: 99999", 42 "pointer-events: none", 43 "white-space: pre" 44 ].join(";"); 45 document.body.appendChild(overlay); 46 } 47 48 function update () { 49 const h = window.visualViewport.height; 50 const heightPx = h + "px"; 51 root.style.setProperty("--visible-vh-px", heightPx); 52 // Inline style is the most reliable: bypass any CSS cascade issues. 53 if (document.body) { 54 document.body.style.setProperty("min-height", heightPx, "important"); 55 document.body.style.setProperty("max-height", heightPx, "important"); 56 } 57 58 // When an input is focused, iOS pans the page to "reveal" it â but the 59 // body is already sized to the visible area, so the pan just shoves the 60 // layout upward and leaves the input floating above the keyboard. 61 // Undo the pan (skip when pinch-zoomed: offsets are then user intent). 62 if (window.visualViewport.scale === 1 && 63 (window.scrollY !== 0 || window.visualViewport.offsetTop !== 0)) { 64 window.scrollTo(0, 0); 65 } 66 67 if (!wantsDebug) return; 68 ensureOverlay(); 69 if (!overlay || !document.body) return; 70 const styles = getComputedStyle(document.body); 71 const rect = document.body.getBoundingClientRect(); 72 overlay.textContent = 73 "innerH: " + window.innerHeight + "\n" + 74 "vvH: " + h.toFixed(1) + " (offsetTop:" + window.visualViewport.offsetTop.toFixed(1) + ")\n" + 75 "bodyH: " + rect.height.toFixed(1) + "\n" + 76 "minH: " + styles.minHeight + "\n" + 77 "maxH: " + styles.maxHeight + "\n" + 78 "pT/pB: " + styles.paddingTop + " / " + styles.paddingBottom + "\n" + 79 "--vh: " + getComputedStyle(root).getPropertyValue("--visible-vh-px"); 80 } 81 82 function init () { 83 ensureOverlay(); 84 update(); 85 } 86 87 if (document.readyState === "loading") { 88 document.addEventListener("DOMContentLoaded", init); 89 } else { 90 init(); 91 } 92 window.visualViewport.addEventListener("resize", update); 93 window.visualViewport.addEventListener("scroll", update); 94 window.addEventListener("orientationchange", update); 95 window.addEventListener("resize", update); 96})();
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.