1(() => { 2 'use strict'; 3 4 // ===== Waste counter with period selector (One Last Look section) ===== 5 var wasteEl = document.getElementById('wastedTodayCounter'); 6 var wasteLabelEl = document.getElementById('wasteCounterLabel'); 7 var wasteTickEl = document.getElementById('wasteCounterTick'); 8 var wastePills = document.querySelectorAll('.waste-period-pill'); 9 var DAILY_WASTE = 0.8; 10 var liveInterval = null; 11 var animFrame = null; 12 13 function formatGBP(n, isLive) { 14 if (isLive || n < 100) { 15 return '£' + n.toFixed(2); 16 } 17 return '£' + Math.round(n).toLocaleString('en-GB'); 18 } 19 20 function updateLiveTicker() { 21 if (!wasteEl) return; 22 var now = new Date(); 23 var midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate()); 24 var secondsSinceMidnight = (now.getTime() - midnight.getTime()) / 1000; 25 var wasted = (DAILY_WASTE / 86400) * secondsSinceMidnight; 26 wasteEl.textContent = formatGBP(wasted, true); 27 } 28 29 function animateToValue(targetValue, isLive) { 30 if (!wasteEl) return; 31 if (animFrame) cancelAnimationFrame(animFrame); 32 var startText = wasteEl.textContent.replace(/[£,\s]/g, ''); 33 var startValue = parseFloat(startText) || 0; 34 var duration = 800; 35 var startTime = performance.now(); 36 37 function tick(now) { 38 var elapsed = now - startTime; 39 var progress = Math.min(elapsed / duration, 1); 40 var eased = 1 - Math.pow(1 - progress, 3); 41 var current = startValue + (targetValue - startValue) * eased; 42 wasteEl.textContent = formatGBP(current, isLive); 43 if (progress < 1) { 44 animFrame = requestAnimationFrame(tick); 45 } else { 46 animFrame = null; 47 } 48 } 49 50 animFrame = requestAnimationFrame(tick); 51 } 52 53 function setWastePeriod(periodKey, days) { 54 if (!wasteEl || !wasteLabelEl) return; 55 56 if (liveInterval) { 57 clearInterval(liveInterval); 58 liveInterval = null; 59 } 60 61 wastePills.forEach(function (p) { 62 var isActive = p.dataset.period === periodKey; 63 p.classList.toggle('is-active', isActive); 64 p.setAttribute('aria-selected', isActive ? 'true' : 'false'); 65 }); 66 67 wasteEl.classList.remove('is-flashing'); 68 void wasteEl.offsetWidth; 69 wasteEl.classList.add('is-flashing'); 70 71 var labels = { 72 today: 'Wasted today, on average, by an out-of-contract household', 73 month: 'Wasted over 1 month out of contract, on average', 74 '3months': 'Wasted over 3 months out of contract, on average', 75 '6months': 'Wasted over 6 months out of contract, on average', 76 year: 'Wasted over 12 months out of contract, on average', 77 '2years': 'Wasted over 2 years out of contract, on average', 78 }; 79 wasteLabelEl.textContent = labels[periodKey] || labels.year; 80 81 if (periodKey === 'today') { 82 if (wasteTickEl) wasteTickEl.hidden = false; 83 updateLiveTicker(); 84 liveInterval = setInterval(updateLiveTicker, 1000); 85 return; 86 } 87 88 if (wasteTickEl) wasteTickEl.hidden = true; 89 animateToValue(days * DAILY_WASTE, false); 90 } 91 92 wastePills.forEach(function (p) { 93 p.addEventListener('click', function () { 94 var periodKey = p.dataset.period; 95 var days = parseInt(p.dataset.days, 10); 96 setWastePeriod(periodKey, days); 97 }); 98 }); 99 100 if (wasteEl) { 101 document.addEventListener('visibilitychange', function () { 102 var todayActive = document.querySelector('.waste-period-pill[data-period="today"].is-active'); 103 if (document.hidden && liveInterval) { 104 clearInterval(liveInterval); 105 liveInterval = null; 106 } else if (!document.hidden && todayActive && !liveInterval) { 107 updateLiveTicker(); 108 liveInterval = setInterval(updateLiveTicker, 1000); 109 } 110 }); 111 } 112 113 // v3: Postcode pulse manager 114 (function postcodePulse() { 115 var forms = document.querySelectorAll('.postcode-form-v5'); 116 forms.forEach(function (form) { 117 var input = form.querySelector('.postcode-input'); 118 if (!input) return; 119 function check() { 120 if (input.value && input.value.length > 0) { 121 form.classList.add('has-input'); 122 } else { 123 form.classList.remove('has-input'); 124 } 125 } 126 input.addEventListener('input', check); 127 input.addEventListener('change', check); 128 check(); 129 }); 130 })(); 131 132 // YouTube video lazy embed 133 var videoBtn = document.getElementById('videoPlayBtn'); 134 var videoFrame = document.getElementById('videoFrame'); 135 var videoThumb = document.getElementById('videoThumb'); 136 var VIDEO_ID = 'Toe-fMy0YCI'; 137 138 if (videoThumb && VIDEO_ID) { 139 videoThumb.src = 'https://i.ytimg.com/vi/' + VIDEO_ID + '/maxresdefault.jpg'; 140 videoThumb.alt = 'BroadbandSwitch.uk: 90 second animated explainer thumbnail'; 141 videoThumb.addEventListener('load', function () {
142 if (videoThumb.naturalWidth <= 120) { 143 videoThumb.src = 'https://i.ytimg.com/vi/' + VIDEO_ID + '/hqdefault.jpg'; 144 } else { 145 videoThumb.classList.add('loaded'); 146 } 147 }); 148 videoThumb.addEventListener('error', function () { 149 videoThumb.src = 'https://i.ytimg.com/vi/' + VIDEO_ID + '/hqdefault.jpg'; 150 }); 151 } 152 153 if (videoBtn && videoFrame) { 154 videoBtn.addEventListener('click', function (e) { 155 e.preventDefault(); 156 if (videoFrame.classList.contains('is-playing')) return; 157 var iframe = document.createElement('iframe'); 158 iframe.src = 'https://www.youtube-nocookie.com/embed/' + VIDEO_ID + '?autoplay=1&rel=0&modestbranding=1&playsinline=1'; 159 iframe.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'); 160 iframe.setAttribute('allowfullscreen', ''); 161 iframe.setAttribute('frameborder', '0'); 162 iframe.setAttribute('title', 'BroadbandSwitch.uk: 90 second animated guide'); 163 iframe.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;border:0;z-index:5;'; 164 videoFrame.appendChild(iframe); 165 videoFrame.classList.add('is-playing'); 166 }); 167 } 168 169 // Editor bubble interactions 170 var bubble = document.getElementById('editorBubble'); 171 var bubbleToggle = document.getElementById('editorBubbleToggle'); 172 var bubbleClose = document.getElementById('editorBubbleClose'); 173 174 if (bubble && bubbleToggle) { 175 bubbleToggle.addEventListener('click', function () { 176 var isOpen = bubble.classList.toggle('is-open'); 177 bubbleToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); 178 }); 179 if (bubbleClose) { 180 bubbleClose.addEventListener('click', function (e) { 181 e.stopPropagation(); 182 bubble.classList.remove('is-open'); 183 bubbleToggle.setAttribute('aria-expanded', 'false'); 184 }); 185 } 186 document.addEventListener('click', function (e) { 187 if (!bubble.contains(e.target)) { 188 bubble.classList.remove('is-open'); 189 bubbleToggle.setAttribute('aria-expanded', 'false'); 190 } 191 }); 192 document.addEventListener('keydown', function (e) { 193 if (e.key === 'Escape') { 194 bubble.classList.remove('is-open'); 195 bubbleToggle.setAttribute('aria-expanded', 'false'); 196 } 197 }); 198 } 199 200 // Sticky mobile CTA + editor bubble visibility 201 var hero = document.getElementById('hero'); 202 var stickyCta = document.getElementById('stickyCta'); 203 204 // Show sticky postcode + editor bubble only after the hero has fully scrolled off-screen 205 // (any intersect with viewport = hide bar). Avoid rootMargin here: a bottom shrink like -100% 206 // collapses the intersection root and forces the bar visible on load. 207 if (hero && 'IntersectionObserver' in window) { 208 var observer = new IntersectionObserver( 209 function (entries) { 210 entries.forEach(function (entry) { 211 var showSticky = !entry.isIntersecting; 212 if (stickyCta) { 213 stickyCta.classList.toggle('is-visible', showSticky); 214 stickyCta.setAttribute('aria-hidden', showSticky ? 'false' : 'true'); 215 } 216 if (bubble) { 217 bubble.classList.toggle('is-visible', showSticky); 218 bubble.setAttribute('aria-hidden', showSticky ? 'false' : 'true'); 219 } 220 }); 221 }, 222 { threshold: 0 } 223 ); 224 observer.observe(hero); 225 } 226 227 // Lightweight in-view marker for compare section 228 var comparison = document.getElementById('comparison-results'); 229 if (comparison && 'IntersectionObserver' in window) { 230 var compObserver = new IntersectionObserver( 231 function (entries) { 232 entries.forEach(function (entry) { 233 if (entry.isIntersecting) { 234 comparison.setAttribute('data-in-view', 'true'); 235 compObserver.disconnect(); 236 } 237 }); 238 }, 239 { rootMargin: '300px' } 240 ); 241 compObserver.observe(comparison); 242 } 243})();
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.