PageSourceSearch

https://getflookup.com/global-scripts.js

js getflookup.com collected 2026-10-02 00:16:29 UTC 19,449 bytes, 479 lines download raw bytes

1(function initGA() {
2  const GA_ID = "G-VEJD43P16C";
3  const BLOCKED = ["BI", "KE", "RW", "SS", "TZ", "UG"];
4  const FLAG = "ga-disable-" + GA_ID;
5  const GEO_KEY = "flookup_geo_country";
6  const GEO_TTL = 24 * 60 * 60 * 1000;
7  function startGA() {
8    try {
9      window.dataLayer = window.dataLayer || [];
10      window.gtag = function () {
11        window.dataLayer.push(arguments);
12      };
13      window.gtag("js", new Date());
14      window.gtag("config", GA_ID);
15      const s = document.createElement("script");
16      s.async = true;
17      s.src = "https://www.googletagmanager.com/gtag/js?id=" + GA_ID;
18      document.head.appendChild(s);
19    } catch (e) {
20      let detail = "unknown error";
21      if (e) {
22        if (e.message) {
23          detail = e.message;
24        }
25      }
26      if (window.console) {
27        window.console.warn("Analytics init failed: " + detail);
28      }
29    }
30  }
31  function isBlocked(country) {
32    try {
33      return BLOCKED.indexOf(String(country || "").toUpperCase()) !== -1;
34    } catch (err) {
35      if (window.console) {
36        window.console.warn("Geo check failed.", err);
37      }
38      return false;
39    }
40  }
41  function readGeo() {
42    try {
43      const raw = localStorage.getItem(GEO_KEY);
44      if (!raw) {
45        return null;
46      }
47      const parsed = JSON.parse(raw);
48      if (!parsed.ts || Date.now() - parsed.ts > GEO_TTL) {
49        localStorage.removeItem(GEO_KEY);
50        return null;
51      }
52      return parsed.country || null;
53    } catch (err) {
54      if (window.console) {
55        window.console.warn("Geo cache read failed.", err);
56      }
57      return null;
58    }
59  }
60  function writeGeo(country) {
61    try {
62      localStorage.setItem(GEO_KEY, JSON.stringify({ ts: Date.now(), country: country }));
63    } catch (err) {
64      if (window.console) {
65        window.console.warn("Geo cache write failed.", err);
66      }
67    }
68  }
69  try {
70    let cached = readGeo();
71    if (cached) {
72      if (isBlocked(cached)) {
73        window[FLAG] = true;
74        return;
75      }
76      startGA();
77      return;
78    }
79    let timedOut = false;
80    const timer = setTimeout(function () {
81      timedOut = true;
82      startGA();
83    }, 1500);
84    fetch("/api/geo-country", { cache: "no-store" })
85      .then(function (r) {
86        if (!r.ok) {
87          throw new Error("geo " + r.status);
88        }
89        return r.json();
90      })
91      .then(function (data) {
92        if (timedOut) {
93          return;
94        }
95        clearTimeout(timer);
96        const country = String((data && data.country) || "").toUpperCase();
97        if (country) {
98          writeGeo(country);
99        }
100        if (isBlocked(country)) {
101          window[FLAG] = true;
102          return;
103        }
104        startGA();
105      })
106      .catch(function (err) {
107        if (timedOut) {
108          return;
109        }
110        clearTimeout(timer);
111        if (window.console) {
112          window.console.warn("Geo lookup failed, starting analytics.", err);
113        }
114        startGA();
115      });
116  } catch (e) {
117    let detail = "unknown error";
118    if (e) {
119      if (e.message) {
120        detail = e.message;
121      }
122    }
123    if (window.console) {
124      window.console.warn("Analytics init failed: " + detail);
125    }
126    try {
127      startGA();
128    } catch (err) {
129      if (window.console) {
130        window.console.warn("Analytics retry failed.", err);
131      }
132    }
133  }
134})();
135const loc = window.location;
136const pathname = loc.pathname;
137const search = loc.search;
138const TRACKING_PARAMS = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content", "aff", "affiliate", "ref", "source", "src", "campaign", "track", "gclid", "fbclid"];
139const hasTrackingParam = TRACKING_PARAMS.some((p) => search.includes(p + "="));
140if (pathname.includes(".html") || hasTrackingParam) {
141  const origin = loc.protocol + "//" + loc.host;
142  let newPath = pathname;
143  let needsPathUpdate = false;
144  if (pathname.endsWith(".html")) {
145    newPath = pathname.replace(".html", "/");
146    needsPathUpdate = true;
147  }
148  let finalSearchString = search;
149  let needsSearchUpdate = false;
150  if (hasTrackingParam) {
151    const params = new URLSearchParams(search);
152    TRACKING_PARAMS.forEach((p) => params.delete(p));
153    finalSearchString = params.toString() ? "?" + params.toString() : "";
154    needsSearchUpdate = true;
155  }
156  if (needsPathUpdate || needsSearchUpdate) {
157    const canonicalURL = origin + newPath + finalSearchString + loc.hash;
158    try {
159      window.history.replaceState(null, "", canonicalURL);
160      window.dispatchEvent(
161        new CustomEvent("canonicalUrlChanged", {
162          detail: { newPath, newSearch: finalSearchString },
163        }),
164      );
165    } catch (e) {
166      console.error("Error updating URL:", e);
167    }
168  }
169}
170window.showToast = function (msg) {
171  const toast = document.getElementById("toast");
172  if (!toast) return;
173  if (window._toastTimer) clearTimeout(window._toastTimer);
174  toast.classList.remove("show");
175  void toast.offsetWidth;
176  toast.textContent = msg;
177  toast.classList.add("show");
178  window._toastTimer = setTimeout(function () {
179    toast.classList.remove("show");
180    window._toastTimer = null;
181  }, 3000);
182};
183window.smoothScrollTo = function (target) {
184  const header = document.querySelector("header");
185  const headerHeight = header ? header.offsetHeight : 0;
186  const offset = target === 0 ? 0 : headerHeight + 24;
187  const start = window.pageYOffset || document.documentElement.scrollTop;
188  const end = typeof target === "number" ? target : target.getBoundingClientRect().top + start - offset;
189  const distance = end - start;
190  const duration = 600;
191  let startTime = null;
192  function step(timestamp) {
193    if (!startTime) startTime = timestamp;
194    const elapsed = timestamp - startTime;
195    const progress = Math.min(elapsed / duration, 1);
196    const eased = 1 - Math.pow(1 - progress, 3);
197    window.scrollTo(0, start + distance * eased);
198    if (elapsed < duration) {
199      requestAnimationFrame(step);
200    }
201  }
202  if (Math.abs(distance) < 2) {
203    window.scrollTo(0, end);
204  } else {
205    requestAnimationFrame(step);
206  }
207};
208function getIsoWeek(date) {
209  const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
210  const dayNum = (d.getUTCDay() + 6) % 7;
211  d.setUTCDate(d.getUTCDate() - dayNum + 3);
212  const firstThursday = new Date(Date.UTC(d.getUTCFullYear(), 0, 4));
213  const firstDayNum = (firstThursday.getUTCDay() + 6) % 7;
214  firstThursday.setUTCDate(firstThursday.getUTCDate() - firstDayNum + 3);
215  return 1 + Math.round((d - firstThursday) / (7 * 24 * 3600 * 1000));
216}
217document.addEventListener("DOMContentLoaded", function () {
218  function setActiveMenuItem() {
219    const currentPath = window.location.pathname;
220    const navLinks = document.querySelectorAll(".nav-links a");
221    navLinks.forEach((link) => {
222      const href = link.getAttribute("href");
223      if (!href && !link.classList.contains("dropbttn")) {
224        return;
225      }
226      link.classList.remove("active");
227      const normalizedPath = currentPath.endsWith("/") ? currentPath : currentPath + "/";
228      if (href) {
229        const normalizedHref = href.endsWith("/") ? href : href + "/";
230        if (href === "/" || href === "/index.html") {
231          const homePaths = ["/", "/index.html", "/index/", "/index"];
232          if (homePaths.includes(currentPath)) {
233            link.classList.add("active");
234          }
235        } else if (normalizedPath.startsWith(normalizedHref)) {
236          link.classList.add("active");
237        }
238      }
239    });
240  }
241  setActiveMenuItem();
242  window.addEventListener("canonicalUrlChanged", setActiveMenuItem);
243  const featuredBlogCardsContainer = document.querySelector(".featured-blog-cards");
244  if (featuredBlogCardsContainer) {
245    async function getAllBlogArticles() {
246      try {
247        const response = await fetch("/blog/");
248        if (!response.ok) {
249          throw new Error("Failed to fetch blog index");
250        }
251        const html = await response.text();
252        const parser = new DOMParser();
253        const doc = parser.parseFromString(html, "text/html");
254        const tiles = Array.from(doc.querySelectorAll(".blog-grid .blog-tile, .blog-tile"));
255        return tiles
256          .map((tile) => {
257            let href = tile.getAttribute("href") || tile.querySelector("a")?.getAttribute("href");
258            if (!href) {
259              return null;
260            }
261            const title = tile.querySelector("h3")?.textContent?.trim() || "";
262            const description = tile.querySelector("p")?.textContent?.trim() || "";
263            const published = parsePublished(tile.querySelector(".tile-meta")?.textContent || "");
264            if (!title || !description) {
265              return null;
266            }
267            return { href, title, description, published };
268          })
269          .filter(Boolean);
270      } catch {
271        return [];
272      }
273    }
274    function parsePublished(text) {
275      const match = (text || "").match(/(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{4})/i);
276      if (!match) {
277        return 0;
278      }
279      const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "
279November", "December"];
280      const month = months.findIndex((m) => m.toLowerCase() === match[1].toLowerCase());
281      return new Date(Number(match[2]), month, 1).getTime();
282    }
283    function renderFeaturedCards(pool) {
284      const slotCount = Math.min(featuredBlogCardsContainer.children.length || 3, pool.length);
285      const today = new Date();
286      const seed = today.getFullYear() * 100 + getIsoWeek(today);
287      function seededShuffle(array, seed) {
288        let arr = array.slice();
289        let m = arr.length,
290          t,
291          i;
292        let s = seed;
293        while (m) {
294          i = Math.floor(Math.abs(Math.sin(s++) * 10000) % m--);
295          t = arr[m];
296          arr[m] = arr[i];
297          arr[i] = t;
298        }
299        return arr;
300      }
301      const pinnedGuide = pool.filter((item) => item.href === "/blog/what-is-data-cleaning/");
302      const compareHrefs = ["/blog/power-query-vs-flookup-data-wrangler/", "/blog/ablebits-power-tools-vs-flookup-data-wrangler/", "/blog/alteryx-vs-flookup-data-wrangler/"];
303      const newestCompare = compareHrefs.map((href) => pool.find((item) => item.href === href)).find(Boolean);
304      const pinned = newestCompare ? pinnedGuide.concat([newestCompare]) : pinnedGuide;
305      const rest = pool.filter((item) => item.href !== "/blog/what-is-data-cleaning/" && (!newestCompare || item.href !== newestCompare.href));
306      const fresh = rest
307        .slice()
308        .sort((a, b) => (b.published || 0) - (a.published || 0))
309        .slice(0, 6);
310      const shuffled = seededShuffle(fresh, seed);
311      const selected = pinned.concat(shuffled.slice(0, Math.max(0, slotCount - pinned.length)));
312      const section = featuredBlogCardsContainer.closest("section");
313      const countMeta = section && section.querySelector('meta[itemprop="numberOfItems"]');
314      if (countMeta) {
315        countMeta.setAttribute("content", String(selected.length));
316      }
317      function escapeHtmlAttr(s) {
318        return String(s).replace(/[&<>"']/g, function (ch) {
319          return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[ch];
320        });
321      }
322      function isSafeHref(href) {
323        return typeof href === "string" && href.startsWith("/") && !href.startsWith("//") && !href.includes('"') && !href.includes("'") && href.length < 300;
324      }
325      featuredBlogCardsContainer.innerHTML = selected
326        .map(function (item, idx) {
327          const safeHref = isSafeHref(item.href) ? escapeHtmlAttr(item.href) : "/";
328          const safeTitle = escapeHtmlAttr(item.title);
329          const safeDesc = escapeHtmlAttr(item.description);
330          return '<a href="' + safeHref + '" class="blog-card" itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem" title="' + safeTitle + '"><meta itemprop="position" content="' + (idx + 1) + '" /><div itemprop="item" itemscope itemtype="http://schema.org/Thing"><h3 itemprop="name">' + safeTitle + '</h3><p itemprop="description">' + safeDesc + '</p><span class="blog-card-meta">Read guide</span></div></a>';
331        })
332        .join("");
333    }
334    getAllBlogArticles().then(renderFeaturedCards);
335  }
336  const operationsGrid = document.getElementById("operations-grid");
337  if (operationsGrid) {
338    const operationPool = [
339      { href: "/documentation/menu-functions/#fuzzy-match", title: "Fuzzy Matching", description: "Reconcile records across datasets despite typos, abbreviations and inconsistent formatting." },
340      { href: "/documentation/menu-functions/#deduplicate", title: "Deduplication", description: "Remove near-duplicate rows that exact matching misses, including phonetic and typo variants." },
341      { href: "/documentation/templates/", title: "Workflow Templates", description: "One-click AP reconciliation, order-to-shipping and user list consolidation jobs." },
342      { href: "/documentation/learn-from-examples/", title: "Learn from Examples", description: "Show dirty-clean example pairs; the system learns the transformation with no formulas." },
343      { href: "/documentation/schedule-functions/", title: "Data Cleaning Automation", description: "Schedule deduplication and standardisation from every 15 minutes to daily." },
344      { href: "/documentation/sheets-ai-assistant/", title: "Sheets AI Assistant", description: "Describe your data problem in plain language; get formulas or execution." },
345      { href: "/documentation/custom-functions/", title: "Data Cleaning Spreadsheet Formulas", description: "Type FLOOKUP, FUZZYSIM, DEDUPE and ULIST directly into cells." },
346      { href: "/documentation/menu-functions/#profile-column", title: "Column Profiling", description: "Scan a column for row counts, nulls, duplicates and format inconsistencies." },
347      { href: "/documentation/menu-functions/#enrich-data", title: "Enrich Data", description: "Add ISO c
347ountries, US states, company suffixes and TLDs next to your input." },
348      { href: "/documentation/menu-functions/#standardize", title: "Text Standardisation", description: "Strip punctuation, diacritics and stop words into one consistent format." },
349      { href: "/documentation/menu-functions/#compare", title: "Compare Similarity", description: "Score how alike two values are with a percentage similarity readout." },
350      { href: "/documentation/menu-functions/#merge-row", title: "Fuzzy Merge Rows", description: "Merge near-duplicate rows into one, keeping the best value from each." },
351      { href: "/documentation/menu-functions/#extract-unique", title: "Extract Unique Rows", description: "Pull unique rows from messy data into an auditable working copy." },
352      { href: "/documentation/menu-functions/#fuzzy-conditional-formatting", title: "Fuzzy Conditional Formatting", description: "Highlight near-duplicate groups with fuzzy matching before you deduplicate." },
353    ];
354    function renderOperations(pool) {
355      const slotCount = Math.min(9, pool.length);
356      const today = new Date();
357      const seed = today.getFullYear() * 100 + (today.getMonth() + 1);
358      function seededShuffle(array, seed) {
359        let arr = array.slice();
360        let m = arr.length,
361          t,
362          i;
363        let s = seed;
364        while (m) {
365          i = Math.floor(Math.abs(Math.sin(s++) * 10000) % m--);
366          t = arr[m];
367          arr[m] = arr[i];
368          arr[i] = t;
369        }
370        return arr;
371      }
372      const pinned = pool.filter((item) => item.href === "/documentation/templates/");
373      const rest = pool.filter((item) => item.href !== "/documentation/templates/");
374      const selected = pinned.concat(seededShuffle(rest, seed).slice(0, Math.max(0, slotCount - pinned.length)));
375      const section = operationsGrid.closest("section");
376      const countMeta = section && section.querySelector('meta[itemprop="numberOfItems"]');
377      if (countMeta) {
378        countMeta.setAttribute("content", String(selected.length));
379      }
380      operationsGrid.innerHTML = selected
381        .map(
382          (item, idx) => `
383            <a href="${item.href}" class="op-item" itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem" title="${item.title}">
384              <meta itemprop="position" content="${idx + 1}" />
385              <span class="op-num" aria-hidden="true">${idx + 1}</span>
386              <div class="op-body" itemprop="item" itemscope itemtype="http://schema.org/Thing">
387                <h3 itemprop="name">${item.title}</h3>
388                <p itemprop="description">${item.description}</p>
389              </div>
390            </a>`,
391        )
392        .join("");
393    }
394    renderOperations(operationPool);
395  }
396  const toggleButtons = document.querySelectorAll(".mobile-menu-toggle");
397  const overlay = document.querySelector(".mobile-overlay");
398  const navLinks = document.querySelector(".nav-links");
399  function toggleMobileMenu(open) {
400    if (!navLinks) {
401      return;
402    }
403    const isActive = open ?? !navLinks.classList.contains("active");
404    navLinks.classList.toggle("active", isActive);
405    toggleButtons.forEach((bttn) => {
406      bttn.classList.toggle("active", isActive);
407      bttn.setAttribute("aria-expanded", isActive);
408    });
409    if (overlay) {
410      overlay.classList.toggle("active", isActive);
411    }
412  }
413  toggleButtons.forEach((toggle) => {
414    toggle.addEventListener("click", () => toggleMobileMenu());
415  });
416  if (overlay) {
417    overlay.addEventListener("click", () => toggleMobileMenu(false));
418  }
419  const currentYearEl = document.getElementById("currentYear");
420  if (currentYearEl) {
421    const year = new Date().getFullYear();
422    currentYearEl.textContent = "2018-" + year;
423  }
424  document.querySelectorAll('.toc a[href^="#"]').forEach(function (link) {
425    link.addEventListener("click", function (e) {
426      const id = link.getAttribute("href").slice(1);
427      const target = document.getElementById(id);
428      if (target) {
429        e.preventDefault();
430        window.smoothScrollTo(target);
431      }
432    });
433  });
434});
435(function ipCacheHelper() {
436  const KEY = "flookup_ip_cache";
437  const TTL = 24 * 60 * 60 * 1000;
438  window.readIpCache = function () {
439    try {
440      const raw = localStorage.getItem(KEY);
441      if (!raw) return null;
442      const parsed = JSON.parse(raw);
443      if (!parsed.ts || Date.now() - parsed.ts > TTL) {
444        localStorage.removeItem(KEY);
445        return null;
446      }
447      return parsed.data;
448    } catch (e) {
449      return null;
450    }
451  };
452  window.writeIpCache = function (data) {
453    try {
454      const payload = JSON.stringify({ ts: Date.now(), data: data });
455      localStorage.setItem(KEY, payload);
456      sessionStorage.setItem(KEY, payload);
457    } catch (e) {}
458  };
459  window.fetchIpCached = function (url) {
460    let cached = window.readIpCache();
461    if (cached) return Promise.resolve(cached);
462    try {
463      const sess = sessionStorage.getItem(KEY);
464      if (sess) {
465        const sp = JSON.parse(sess);
466        if (sp.ts && Date.now() - sp.ts < TTL) return Promise.resolve(sp.data);
467      }
468    } catch (e) {}
469    return fetch(url, { cache: "force-cache" })
470      .then(function (r) {
471        if (!r.ok) throw new Error("ip fetch " + r.status);
472        return r.json();
473      })
474      .then(function (data) {
475        window.writeIpCache(data);
476        return data;
477      });
478  };
479})();

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.