1 2(function () { 3 var prefersReducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 4 5 function parseNumber(value) { 6 var clean = String(value || '0').replace(/,/g, '').match(/-?\d+(\.\d+)?/); 7 return clean ? parseFloat(clean[0]) : 0; 8 } 9 function decimals(value) { 10 var match = String(value || '').match(/\.(\d+)/); 11 return match ? match[1].length : 0; 12 } 13 function formatNumber(value, places) { 14 return value.toLocaleString(undefined, { minimumFractionDigits: places, maximumFractionDigits: places }); 15 } 16 function animateCounter(card) { 17 if (card.dataset.counted === 'true') return; 18 var valueEl = card.querySelector('.ncwc-stat-value'); 19 if (!valueEl) return; 20 var rawTarget = card.getAttribute('data-target') || valueEl.textContent || '0'; 21 var target = parseNumber(rawTarget); 22 var places = decimals(rawTarget); 23 var duration = Math.max(parseInt(card.getAttribute('data-duration') || '1200', 10), 1); 24 var startTime = null; 25 card.dataset.counted = 'true'; 26 27 if (prefersReducedMotion) { 28 valueEl.textContent = formatNumber(target, places); 29 return; 30 } 31 32 function tick(timestamp) { 33 if (!startTime) startTime = timestamp; 34 var progress = Math.min((timestamp - startTime) / duration, 1); 35 var eased = 1 - Math.pow(1 - progress, 3); 36 valueEl.textContent = formatNumber(target * eased, places); 37 if (progress < 1) requestAnimationFrame(tick); 38 else valueEl.textContent = formatNumber(target, places); 39 } 40 requestAnimationFrame(tick); 41 } 42 function initCounters() { 43 var counters = document.querySelectorAll('.ncwc-stat-card.has-counter[data-counter="true"]'); 44 if (!counters.length) return; 45 if (!('IntersectionObserver' in window)) { 46 counters.forEach(animateCounter); 47 return; 48 } 49 var observer = new IntersectionObserver(function (entries) { 50 entries.forEach(function (entry) { 51 if (entry.isIntersecting) { 52 animateCounter(entry.target); 53 observer.unobserve(entry.target); 54 } 55 }); 56 }, { threshold: 0.35 }); 57 counters.forEach(function (counter) { observer.observe(counter); }); 58 } 59 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initCounters); 60 else initCounters(); 61})(); 62 63(function () { 64 var faqSections = document.querySelectorAll('.ncwc-faq-section[data-accordion="true"]'); 65 66 if (!faqSections.length) return; 67 68 faqSections.forEach(function (section) { 69 section.addEventListener('toggle', function (event) { 70 var current = event.target; 71 72 if (!current || current.tagName !== 'DETAILS' || !current.open) return; 73 if (!current.classList.contains('ncwc-faq-item')) return; 74 75 Array.prototype.forEach.call(section.querySelectorAll('.ncwc-faq-item[open]'), function (item) { 76 if (item !== current) item.open = false; 77 }); 78 }, true); 79 }); 80})(); 81 82(function () { 83 var compareBlocks = document.querySelectorAll('.ncwc-before-after-block'); 84 85 if (!compareBlocks.length) return; 86 87 compareBlocks.forEach(function (block) { 88 var range = block.querySelector('.ncwc-ba-range'); 89 90 if (!range) return; 91 92 function clamp(value) { 93 var number = parseFloat(value); 94 95 if (Number.isNaN(number)) number = 50; 96 97 return Math.max(0, Math.min(100, number)); 98 } 99 100 function update(value) { 101 var position = clamp(value); 102 103 block.style.setProperty('--ncwc-ba-position', position + '%'); 104 range.value = position; 105 } 106 107 range.addEventListener('input', function () { 108 update(range.value); 109 }); 110 111 range.addEventListener('pointerdown', function () { 112 block.classList.add('is-dragging'); 113 }); 114 115 range.addEventListener('pointerup', function () { 116 block.classList.remove('is-dragging'); 117 }); 118 119 range.addEventListener('pointercancel', function () { 120 block.classList.remove('is-dragging'); 121 }); 122 123 range.addEventListener('keydown', function () { 124 window.requestAnimationFrame(function () { update(range.value); }); 125 }); 126 127 update(range.value || 50); 128 }); 129})(); 130 131(function () { 132 var reviewBlocks = document.querySelectorAll('.ncwc-google-reviews-block'); 133 134 if (!reviewBlocks.length) return; 135 136 var prefersReducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 137 138 function initReviewBlock(block) { 139 var scroller = block.querySelector('.ncwc-google-reviews-track'); 140 var prevButton = block.querySelector('.ncwc-google-reviews-control--prev'); 141 var nextButton = block.querySelector('.ncwc-google-reviews-control--next'); 142 var dragEnabled = block.classList.contains('has-drag'); 143 var equalHeight = block.classList.contains('has-equal-height'); 144 var autoScroll = block.getAttribute('data-auto-scroll') === 'true'; 145 var autoSpeed = Math.max(parseInt(block.getAttribute('data-auto-scroll-speed') || '6', 10), 2) * 1000; 146 var autoTimer = null; 147 148 if (!scroller) return; 149 150 function getCards() { 151 return Array.prototype.slice.call(scroller.querySelectorAll('.ncwc-google-review-card')); 152 } 153 154 function equalizeCardHeights() { 155 var cards = getCards(); 156 157 scroller.style.removeProperty('--ncwc-google-review-card-height'); 158 159 if (!equalHeight || !cards.length) return; 160 161 var tallestCard = cards.reduce(function (maxHeight, card) { 162 return Math.max(maxHeight, card.offsetHeight); 163 }, 0); 164 165 if (tallestCard) { 166 scroller.style.setProperty('--ncwc-google-review-card-height', tallestCard + 'px'); 167 } 168 } 169 170 function getPositions() { 171 var cards = getCards(); 172 var firstOffset = cards[0] ? cards[0].offsetLeft : 0; 173 174 return cards.map(function (card) { 175 return card.offsetLeft - firstOffset; 176 }); 177 } 178 179 function getCurrentIndex() { 180 var positions = getPositions(); 181 182 if (!positions.length) return 0; 183 184 return positions.reduce(function (closestIndex, position, index) { 185 var closestDistance = Math.abs(positions[closestIndex] - scroller.scrollLeft); 186 var currentDistance = Math.abs(position - scroller.scrollLeft); 187 188 return currentDistance < closestDistance ? index : closestIndex; 189 }, 0); 190 } 191 192 function scrollToIndex(index) { 193 var positions = getPositions(); 194 195 if (!positions.length) return; 196 197 var targetIndex = Math.max(0, Math.min(positions.length - 1, index)); 198 199 scroller.scrollTo({ 200 left: positions[targetIndex], 201 behavior: prefersReducedMotion ? 'auto' : 'smooth' 202 }); 203 } 204 205 function updateControls() { 206 var maxScroll = Math.max(0, scroller.scrollWidth - scroller.clientWidth - 2); 207 var hasOverflow = maxScroll > 2; 208 209 if (!prevButton || !nextButton) return; 210 211 prevButton.hidden = !hasOverflow; 212 nextButton.hidden = !hasOverflow; 213 prevButton.disabled = !hasOverflow || scroller.scrollLeft <= 2; 214 nextButton.disabled = !hasOverflow || scroller.scrollLeft >= maxScroll; 215 } 216 217 function startAutoScroll() { 218 if (!autoScroll || prefersReducedMotion || autoTimer) return; 219 220 autoTimer = window.setInterval(function () { 221 var currentIndex = getCurrentIndex(); 222 var nextIndex = currentIndex + 1; 223 224 if (nextIndex >= getPositions().length) nextIndex = 0; 225 226 scrollToIndex(nextIndex); 227 }, autoSpeed); 228 } 229 230 function stopAutoScroll() { 231 if (!autoTimer) return; 232 233 window.clearInterval(autoTimer); 234 autoTimer = null; 235 } 236 237 if (prevButton) prevButton.addEventListener('click', function () { scrollToIndex(getCurrentIndex() - 1); }); 238 if (nextButton) nextButton.addEventListener('click', function () { scrollToIndex(getCurrentIndex() + 1); }); 239 240 Array.prototype.slice.call(block.querySelectorAll('[data-google-review-toggle]')).forEac
240h(function (button) { 241 button.addEventListener('click', function () { 242 var card = button.closest('.ncwc-google-review-card'); 243 var isExpanded; 244 245 if (!card) return; 246 247 isExpanded = card.classList.toggle('is-review-expanded'); 248 button.setAttribute('aria-expanded', isExpanded ? 'true' : 'false'); 249 button.textContent = isExpanded ? 'Show less' : 'Read full review'; 250 251 equalizeCardHeights(); 252 updateControls(); 253 }); 254 }); 255 256 if (dragEnabled) { 257 var isPointerDown = false; 258 var hasDragged = false; 259 var pointerStartX = 0; 260 var scrollStartX = 0; 261 262 scroller.addEventListener('pointerdown', function (event) { 263 if (event.button !== 0 || event.target.closest('button, a, input, textarea, select, label')) return; 264 265 isPointerDown = true; 266 hasDragged = false; 267 pointerStartX = event.clientX; 268 scrollStartX = scroller.scrollLeft; 269 scroller.classList.add('is-dragging'); 270 scroller.setPointerCapture(event.pointerId); 271 stopAutoScroll(); 272 }); 273 274 scroller.addEventListener('pointermove', function (event) { 275 var deltaX; 276 277 if (!isPointerDown) return; 278 279 deltaX = event.clientX - pointerStartX; 280 281 if (Math.abs(deltaX) > 4) hasDragged = true; 282 283 scroller.scrollLeft = scrollStartX - deltaX; 284 }); 285 286 function finishDrag(event) { 287 if (!isPointerDown) return; 288 289 isPointerDown = false; 290 scroller.classList.remove('is-dragging'); 291 292 if (scroller.hasPointerCapture(event.pointerId)) { 293 scroller.releasePointerCapture(event.pointerId); 294 } 295 296 if (hasDragged) scrollToIndex(getCurrentIndex()); 297 298 startAutoScroll(); 299 } 300 301 scroller.addEventListener('pointerup', finishDrag); 302 scroller.addEventListener('pointercancel', finishDrag); 303 } 304 305 scroller.addEventListener('scroll', function () { window.requestAnimationFrame(updateControls); }, { passive: true }); 306 block.addEventListener('mouseenter', stopAutoScroll); 307 block.addEventListener('mouseleave', startAutoScroll); 308 block.addEventListener('focusin', stopAutoScroll); 309 block.addEventListener('focusout', startAutoScroll); 310 window.addEventListener('resize', function () { 311 equalizeCardHeights(); 312 updateControls(); 313 }); 314 315 window.requestAnimationFrame(function () { 316 equalizeCardHeights(); 317 updateControls(); 318 startAutoScroll(); 319 }); 320 } 321 322 if (!('IntersectionObserver' in window)) { 323 reviewBlocks.forEach(initReviewBlock); 324 return; 325 } 326 327 var reviewObserver = new IntersectionObserver(function (entries) { 328 entries.forEach(function (entry) { 329 if (!entry.isIntersecting) return; 330 331 reviewObserver.unobserve(entry.target); 332 initReviewBlock(entry.target); 333 }); 334 }, { rootMargin: '800px 0px' }); 335 336 reviewBlocks.forEach(function (block) { 337 reviewObserver.observe(block); 338 }); 339})();
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.