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.