PageSourceSearch

https://www.inflowinventory.com/wp-content/themes/inflow2024-theme…inflow-homepage-test-t27.js?v=2026-08-31-01

js inflowinventory.com collected 2026-09-24 17:19:22 UTC 11,642 bytes, 288 lines download raw bytes

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.