1//====================================================================================== 2// T27 Homepage AB test - ASSIGN + IN-PAGE ANIMATE (front page only) 3//-------------------------------------------------------------------------------------- 4// - Loaded RENDER-BLOCKING at the very top of <head> (see header.php) so the hero can 5// be cloaked before first paint -> variant users never see the control content flash. 6// - Assigns EVERY front-page visitor 50/50 to T27-0 (control) or T27-1 (variant), using 7// the shared "inflow-marketingTestVariants" cookie + gtag user_property (90 days), 8// exactly like T21/T23/T24/T25/T26. 9// - CONTROL (group 0): homepage H1 (#main-headline) left completely untouched. 10// - VARIANT (group 1): H1 is rebuilt as two lines - 11// Line 1 (static): "Inventory software that" in the updated brand colour (#994800). 12// Line 2 (animated): cycles through 4 endings, sliding upward every 3s, then stops 13// on the 4th and stays there. 14// - Anti-flash: cloak (visibility:hidden, preserves layout box) via injected <style> the 15// instant this script runs in <head> -> before the body paints. On DOM-ready we build 16// the two-line markup and reveal. A FAILSAFE always reveals, so a missing element or a 17// build error can never leave the hero permanently hidden. 18// - Accessibility: the animated line is aria-hidden (it's decorative motion, and a live 19// region firing every 3s would spam screen reader users). A visually-hidden (sr-only) 20// node states the complete, final sentence once so screen reader / no-JS-motion users 21// get the same information sighted users end up seeing, without the animation noise. 22// prefers-reduced-motion: reduce skips the animation and shows the final line directly. 23// - Same-origin only (cookie domain derived from the live host), so this works unchanged 24// on inflowstaging.com and production inflowinventory.com. 25//====================================================================================== 26(function () { 27 // --- CONFIG --- 28 var VARIANT_1_PERCENT = 50; 29 var testName = "T27"; 30 31 var HEADLINE_SEL = "#main-headline"; 32 var CLOAK_CLASS = "t27-cloak"; // hides the H1 pre-swap; removed on reveal 33 var VARIANT_CLASS = "t27-v1"; // persistent hook for the variant layout/animation CSS 34 35 var LINE1_TEXT = "Inventory software that"; 36 var LINE2_PHRASES = [ 37 "shows you exactly what's in stock", 38 "tracks your sales across all channels", 39 "helps you reorder ahead of time", 40 "takes the hassle out of inventory management" 41 ]; 42 var HEADLINE_COLOR = "#994800"; 43 var SLIDE_INTERVAL_MS = 3000; 44 var SLIDE_TRANSITION_MS = 400; 45 46 // --- Front page only --- 47 function normalizePath(p) { 48 return (p || "").replace(/\/$/, "") || "/"; 49 } 50 if (normalizePath(window.location.pathname) !== "/") return; 51 52 // --- gtag safety shim --- 53 if (typeof window.gtag !== "function") { 54 window.dataLayer = window.dataLayer || []; 55 window.gtag = function () { window.dataLayer.push(arguments); }; 56 } 57 58 // --- Cookie helpers --- 59 function getCookie(cname) { 60 var name = cname + "="; 61 var decodedCookie = decodeURIComponent(document.cookie); 62 var ca = decodedCookie.split(";"); 63 for (var i = 0; i < ca.length; i++) { 64 var c = ca[i].trim(); 65 if (c.indexOf(name) === 0) return c.substring(name.length); 66 } 67 return null; 68 } 69 70 // Derive the registrable domain from the live host so the cookie is shared across 71 // subdomains and is correct on every environment (mirrors inflow-homepage-test-t25.js). 72 function getCookieDomain() { 73 var host = window.location.hostname; 74 var parts = host.split("."); 75 if (parts.length <= 2) return "." + host; // localhost / bare domain 76 return "." + parts.slice(-2).join("."); 77 } 78 79 function setNewCookie(cname, cvalue, exdays) { 80 var d = new Date(); 81 d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000); 82 var expires = "expires=" + d.toUTCString(); 83 document.cookie = cname + "=" + cvalue + ";" + expires + ";domain=" + getCookieDomain() + ";path=/"; 84 } 85 86 // --- AB test assignment (weighted) --- 87 function clampPercent(p) { 88 p = parseFloat(p); 89 if (isNaN(p)) return 50; 90 if (p < 0) return 0; 91 if (p > 100) return 100; 92 return p; 93 } 94 95 function safeParseMarketingTests(raw) { 96 if (!raw) return []; 97 try { 98 var parsed = JSON.parse(raw); 99 return Array.isArray(parsed) ? parsed : []; 100 } catch (e) { 101 return []; 102 } 103 } 104 105 function getOrAssignVariant(tName, variant1Percent) { 106 var marketingTests = safeParseMarketingTests(getCookie("inflow-marketingTestVariants")); 107 108 var existing = null; 109 for (var i = 0; i < marketingTests.length; i++) { 110 if (marketingTests[i].indexOf(tName + "-") === 0) { 111 existing = marketingTests[i]; 112 break; 113 } 114 } 115 116 var variant; 117 if (existing) { 118 variant = parseInt(existing.split("-")[1], 10); 119 variant = (variant === 1) ? 1 : 0; 120 } else { 121 var pct = clampPercent(variant1Percent); 122 variant = (Math.random() * 100 < pct) ? 1 : 0; 123 124 marketingTests.push(tName + "-" + variant); 125 setNewCookie("inflow-marketingTestVariants", JSON.stringify(marketingTests), 90); 126 } 127 128 // Report the placement for BOTH groups so analytics sees the full 50/50 split. 129 window.gtag("set", "user_properties", { ab_test: marketingTests }); 130 return variant; 131 } 132 133 // --- Cloak (runs pre-paint: we're render-blocking in <head>) ------------------------ 134 function injectToHead(node) { 135 (document.head || document.documentElement).appendChild(node); 136 } 137 138 function injectStyle(id, css) { 139 var style = document.createElement("style"); 140 style.id = id; 141 style.appendChild(document.createTextNode(css)); 142 injectToHead(style); 143 } 144 145 function cloak() { 146 var el = document.documentElement; 147 el.classList.add(CLOAK_CLASS); 148 el.classList.add(VARIANT_CLASS); 149 150 // Cloak: hide the H1 until rebuilt. visibility:hidden (not display:none) keeps the 151 // original layout box, so hiding reserves its space and the reveal doesn't shift. 152 injectStyle("t27-cloak-style", 153 "." + CLOAK_CLASS + " " + HEADLINE_SEL + "{visibility:hidden!important}"); 154 155 // Variant headline colour + slide-animation styling (persists after reveal). 156 injectStyle("t27-style",
157 "." + VARIANT_CLASS + " " + HEADLINE_SEL + 158 "{color:" + HEADLINE_COLOR + "}" + 159 "." + VARIANT_CLASS + " " + HEADLINE_SEL + " .t27-line2-viewport" + 160 "{display:block;overflow:hidden;position:relative}" + 161 "." + VARIANT_CLASS + " " + HEADLINE_SEL + " .t27-line2-text" + 162 "{display:block;transition:transform " + SLIDE_TRANSITION_MS + "ms ease,opacity " + SLIDE_TRANSITION_MS + "ms ease}" + 163 "." + VARIANT_CLASS + " " + HEADLINE_SEL + " .t27-line2-text.t27-slide-out" + 164 "{transform:translateY(-100%);opacity:0}" + 165 "." + VARIANT_CLASS + " " + HEADLINE_SEL + " .t27-line2-text.t27-slide-in-start" + 166 "{transform:translateY(100%);opacity:0;transition:none}" + 167 "." + VARIANT_CLASS + " " + HEADLINE_SEL + " .t27-sr-only" + 168 "{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;" + 169 "margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;" + 170 "white-space:nowrap!important;border:0!important}" + 171 "@media (prefers-reduced-motion:reduce){" + 172 "." + VARIANT_CLASS + " " + HEADLINE_SEL + " .t27-line2-text" + 173 "{transition:none!important}" + 174 "}"); 175 } 176 177 // --- Reveal (idempotent) ------------------------------------------------------------ 178 var revealed = false; 179 function reveal() { 180 if (revealed) return; 181 revealed = true; 182 document.documentElement.classList.remove(CLOAK_CLASS); 183 } 184 185 // --- Build + animate (variant only) -------------------------------------------------- 186 var built = false; 187 function build() { 188 if (built) return true; 189 190 var h1 = document.querySelector(HEADLINE_SEL); 191 if (!h1) return false; // H1 not fully parsed yet - try again next frame 192 193 h1.textContent = ""; 194 195 var line1 = document.createElement("span"); 196 line1.className = "t27-line1"; 197 line1.textContent = LINE1_TEXT; 198 199 var viewport = document.createElement("span"); 200 viewport.className = "t27-line2-viewport"; 201 viewport.setAttribute("aria-hidden", "true"); // decorative motion; SR text is below 202 203 var line2 = document.createElement("span"); 204 line2.className = "t27-line2-text"; 205 line2.textContent = LINE2_PHRASES[0]; 206 viewport.appendChild(line2); 207 208 // Screen reader / no-motion equivalent: state the complete, final sentence once 209 // rather than announcing every 3s slide. 210 var srText = document.createElement("span"); 211 srText.className = "t27-sr-only"; 212 srText.textContent = LINE1_TEXT + " " + LINE2_PHRASES[LINE2_PHRASES.length - 1]; 213 214 h1.appendChild(line1); 215 h1.appendChild(document.createElement("br")); 216 h1.appendChild(viewport); 217 h1.appendChild(srText); 218 219 var reduceMotion = window.matchMedia && 220 window.matchMedia("(prefers-reduced-motion: reduce)").matches; 221 222 if (!reduceMotion && LINE2_PHRASES.length > 1) { 223 var index = 0; 224 var raf = window.requestAnimationFrame || function (cb) { return setTimeout(cb, 16); }; 225 var timer = setInterval(function () { 226 index++; 227 if (index >= LINE2_PHRASES.length) { 228 clearInterval(timer); 229 return; // stop on the 4th (final) phrase, permanently 230 } 231 line2.classList.add("t27-slide-out"); 232 setTimeout(function () { 233 line2.textContent = LINE2_PHRASES[index]; 234 line2.classList.remove("t27-slide-out"); 235 line2.classList.add("t27-slide-in-start"); 236 // Force a reflow so the browser registers the "start" position before we 237 // transition to the resting position - otherwise the browser coalesces both 238 // class changes and the slide-in never animates. 239 void line2.offsetWidth; 240 raf(function () { 241 line2.classList.remove("t27-slide-in-start"); 242 }); 243 }, SLIDE_TRANSITION_MS); 244 }, SLIDE_INTERVAL_MS); 245 } else if (reduceMotion) { 246 // Reduced motion: skip straight to the final phrase, no animation. 247 line2.textContent = LINE2_PHRASES[LINE2_PHRASES.length - 1]; 248 } 249 250 built = true; 251 return true; 252 } 253 254 // --- Run ---------------------------------------------------------------------------- 255 var variant = getOrAssignVariant(testName, VARIANT_1_PERCENT); 256 257 // Control: leave the homepage completely untouched. 258 if (variant !== 1) return; 259 260 cloak(); 261 262 // Reveal the moment the H1 is rebuilt. No magic timeout: poll per frame while the 263 // document is still parsing, then give up once it's fully parsed (readyState !== 264 // "loading"). Giving up means the H1 markup is absent/renamed, so there's nothing to 265 // un-cloak and reveal() is harmless. A guaranteed DOMContentLoaded backstop covers the 266 // case where rAF is throttled (e.g. a background tab). 267 var raf = window.requestAnimationFrame || function (cb) { return setTimeout(cb, 16); }; 268 function tick() { 269 if (revealed) return; 270 if (build()) { 271 reveal(); 272 return; 273 } 274 if (document.readyState !== "loading") { 275 if (window.console && console.warn) { 276 console.warn("[T27] headline not built: " + HEADLINE_SEL + " missing"); 277 } 278 reveal(); 279 return; 280 } 281 raf(tick); 282 } 283 raf(tick); 284 document.addEventListener("DOMContentLoaded", function () { 285 build(); // build first... 286 reveal(); // ...so we never reveal an unbuilt hero 287 }); 288})();
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.