PageSourceSearch

https://www.tracpoint.com/pages.js

js tracpoint.com collected 2026-09-28 09:44:01 UTC 10,071 bytes, 270 lines download raw bytes

1/* TracPoint sub-pages — nav scroll, mobile drawer, scroll reveals, blog filter */
2(function () {
3  "use strict";
4
5  var reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
6
7  /* ---------- Count-up helper ---------- */
8  function countUp(el) {
9    if (el.dataset.done) return;
10    el.dataset.done = "1";
11    var target = parseFloat(el.dataset.count);
12    var dur = parseInt(el.dataset.duration || "1500", 10);
13    if (reduceMotion) { el.textContent = target.toLocaleString(); return; }
14    var start = performance.now();
15    function frame(now) {
16      var p = Math.min((now - start) / dur, 1);
17      var eased = 1 - Math.pow(1 - p, 3); // easeOutCubic
18      el.textContent = Math.round(target * eased).toLocaleString();
19      if (p < 1) requestAnimationFrame(frame);
20      else el.textContent = target.toLocaleString();
21    }
22    requestAnimationFrame(frame);
23  }
24
25  /* Chromium can leave background-clip:text spans (.grad-text) painted with the
26     fallback font if the webfont finishes loading after first paint. Nudging
27     a reflow once fonts are actually ready forces a repaint in the right font. */
28  if (document.fonts && document.fonts.ready) {
29    document.fonts.ready.then(function () {
30      document.querySelectorAll(".grad-text").forEach(function (el) {
31        el.style.display = "inline-block";
32        void el.offsetHeight;
33        el.style.display = "";
34      });
35    });
36  }
37
38  var nav = document.getElementById("nav");
39  function onScroll() {
40    if (!nav) return;
41    if (window.scrollY > 24) nav.classList.add("scrolled");
42    else nav.classList.remove("scrolled");
43  }
44  window.addEventListener("scroll", onScroll, { passive: true });
45  onScroll();
46
47  var footYear = document.getElementById("footYear");
48  if (footYear) footYear.textContent = new Date().getFullYear();
49
50  var hamburger = document.getElementById("hamburger");
51  if (hamburger) {
52    hamburger.addEventListener("click", function () {
53      var open = nav.classList.toggle("open");
54      hamburger.setAttribute("aria-expanded", open ? "true" : "false");
55    });
56    document.querySelectorAll(".drawer a").forEach(function (a) {
57      a.addEventListener("click", function () {
58        nav.classList.remove("open");
59        hamburger.setAttribute("aria-expanded", "false");
60      });
61    });
62  }
63
64  // scroll reveals
65  var io = new IntersectionObserver(function (entries) {
66    entries.forEach(function (e) {
67      if (!e.isIntersecting) return;
68      e.target.classList.add("visible");
69      e.target.querySelectorAll("[data-count]").forEach(countUp);
70      if (e.target.hasAttribute("data-count")) countUp(e.target);
71      io.unobserve(e.target);
72    });
73  }, { threshold: 0.14, rootMargin: "0px 0px -6% 0px" });
74  document.querySelectorAll(".reveal, [data-stagger]").forEach(function (el) {
75    if (el.hasAttribute("data-stagger")) {
76      Array.prototype.forEach.call(el.children, function (c, i) { c.style.transitionDelay = (i * 70) + "ms"; });
77    }
78    io.observe(el);
79  });
80  document.querySelectorAll("[data-count]").forEach(function (el) { io.observe(el); });
81
82  // blog category filter
83  var chips = document.querySelectorAll(".cat-chip");
84  if (chips.length) {
85    chips.forEach(function (chip) {
86      chip.addEventListener("click", function () {
87        chips.forEach(function (c) { c.classList.toggle("active", c === chip); });
88        var cat = chip.dataset.cat;
89        document.querySelectorAll("[data-post-cat]").forEach(function (card) {
90          var show = cat === "all" || card.dataset.postCat === cat;
91          card.style.display = show ? "" : "none";
92        });
93      });
94    });
95  }
96
97  /* ---------- Testimonials carousel ---------- */
98  document.querySelectorAll("[data-carousel]").forEach(function (root) {
99    var track = root.querySelector("[data-track]");
100    var cards = Array.prototype.slice.call(root.querySelectorAll(".tcard"));
101    var prevBtn = root.querySelector("[data-prev]");
102    var nextBtn = root.querySelector("[data-next]");
103    var dotsWrap = root.querySelector("[data-dots]");
104    if (!track || !cards.length) return;
105
106    var count = cards.length;
107    var index = 0;
108    var dots = [];
109
110    function step() {
111      return cards.length > 1 ? (cards[1].offsetLeft - cards[0].offsetLeft) : cards[0].offsetWidth;
112    }
113    function perView() {
114      return Math.max(1, Math.round(track.clientWidth / step()));
115    }
116    function maxIndex() {
117      return Math.max(0, count - perView());
118    }
119    function buildDots() {
120      if (!dotsWrap) return;
121      dotsWrap.innerHTML = "";
122      dots = [];
123      var pv = perView();
124      var pages = Math.max(1, Math.ceil(count / pv));
125      for (var i = 0; i < pages; i++) {
126        var dot = document.createElement("button");
127        dot.type = "button";
128        dot.className = "tcarousel-dot";
129        dot.setAttribute("aria-label", "Go to testimonials page " + (i + 1));
130        (function (pageIndex) {
131          dot.addEventListener("click", function () { goTo(pageIndex * pv); resetAuto(); });
132        })(i);
133        dotsWrap.appendChild(dot);
134        dots.push(dot);
135      }
136    }
137    function updateDots() {
138      if (!dots.length) return;
139      var active = Math.min(dots.length - 1, Math.round(index / perView()));
140      dots.forEach(function (d, i) { d.classList.toggle("active", i === active); });
141    }
142    function goTo(i) {
143      index = Math.max(0, Math.min(i, maxIndex()));
144      track.scrollTo({ left: cards[index].offsetLeft, behavior: "smooth" });
145      updateDots();
146    }
147    if (prevBtn) prevBtn.addEventListener("click", function () { goTo(index - perView()); resetAuto(); });
148    if (nextBtn) nextBtn.addEventListener("click", function () { goTo(index + perView()); resetAuto(); });
149
150    var scrollTimer;
151    track.addEventListener("scroll", function () {
152      clearTimeout(scrollTimer);
153      scrollTimer = setTimeout(function () {
154        index = Math.round(track.scrollLeft / step());
155        updateDots();
156      }, 120);
157    }, { passive: true });
158
159    var lastPerView = perView();
160    window.addEventListener("resize", function () {
161      var pv = perView();
162      if (pv !== lastPerView) { lastPerView = pv; buildDots(); }
163      track.scrollTo({ left: cards[index] ? cards[index].offsetLeft : 0, behavior: "auto" });
164      updateDots();
165    });
166
167    /* Mouse drag-to-scroll (touch keeps native momentum scrolling) */
168    var isDown = false, startX = 0, startScroll = 0, dragged = false;
169    track.addEventListener("pointerdown", function (e) {
170      if (e.pointerType !== "mouse") return;
171      isDown = true; dragged = false;
172      startX = e.clientX; startScroll = track.scrollLeft;
173      track.classList.add("dragging");
174      stopAuto();
175    });
176    window.addEventListener("pointermove", function (e) {
177      if (!isDown) return;
178      var dx = e.clientX - startX;
179      if (Math.abs(dx) > 3) dragged = true;
180      track.scrollLeft = startScroll - dx;
181    });
182    function endDrag() {
183      if (!isDown) return;
184      isDown = false;
185      track.classList.remove("dragging");
186      goTo(Math.round(track.scrollLeft / step()));
187      resetAuto();
188    }
189    window.addEventListener("pointerup", endDrag);
190    track.addEventListener("click", function (e) {
191      if (dragged) { e.preventDefault(); e.stopPropagation(); }
192    }, true);
193
194    var autoTimer;
195    function startAuto() {
196      if (reduceMotion || count <= perView()) return;
197      stopAuto();
198      autoTimer = setInterval(function () {
199        goTo(index + perView() > maxIndex() ? 0 : index + perView());
200      }, 6000);
201    }
202    function stopAuto() { clearInterval(autoTimer); }
203    function resetAuto() { startAuto(); }
204
205    root.addEventListener("mouseenter", stopAuto);
206    root.addEventListener("mouseleave", startAuto);
207    root.addEventListener("focusin", stopAuto);
208    root.addEventListener("focusout", startAuto);
209
210    buildDots();
211    updateDots();
212    startAuto();
213  });
214
215  /* ---------- Contact form ---------- */
216  var contactForm = document.getElementById("contactForm");
217  if (contactForm) {
218    var cfSubmit = document.getElementById("cfSubmit");
219    var cfStatus = document.getElementById("cfStatus");
220    var emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
221
222    contactForm.addEventListener("submit", function (e) {
223      e.preventDefault();
224      var name = document.getElementById("cfName").value.trim();
225      var email = document.getElementById("cfEmail").value.trim();
226      var message = document.getElementById("cfMessage").value.trim();
227
228      if (!name || !message || !emailRe.test(email)) {
229        cfStatus.textContent = "Please fill in your name, a valid email, and a message.";
230        cfStatus.className = "cf-status err";
231        return;
232      }
233
234      var payload = {
235        name: name,
236        email: email,
237        company: document.getElementById("cfCompany").value.trim(),
238        phone: document.getElementById("cfPhone").value.trim(),
239        message: message
240      };
241
242      cfSubmit.disabled = true;
243      cfStatus.textContent = "Sending…";
244      cfStatus.className = "cf-status";
245
246      fetch("https://app.tracpoint.com/api/contact", {
247        method: "POST",
248        headers: { "Content-Type": "application/json" },
249        body: JSON.stringify(payload)
250      }).then(function (res) {
251        return res.text().then(function (text) {
252          var json = null;
253          try { json = JSON.parse(text); } catch (_e) { /* non-JSON response */ }
254          if (!res.ok) throw new Error((json && json.error) || ("Request failed (" + res.status + ")"));
255          return json;
256        });
257      }).then(function (json) {
258        cfStatus.textContent = (json && json.message) || "Thanks — we'll be in touch shortly.";
259        cfStatus.className = "cf-status ok";
260        contactForm.reset();
261      }).catch(function (err) {
262        console.error("Contact form send failed:", err);
263        cfStatus.textContent = "Couldn't send automatically — email [email protected] and we'll follow up.";
264        cfStatus.className = "cf-status err";
265      }).finally(function () {
266        cfSubmit.disabled = false;
267      });
268    });
269  }
270})();

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.