1/** 2 * Live blog: while the event is live, poll the theme REST route every 45 s; when the update count grows, 3 * hold the new HTML behind a "Show N new updates" button (so the reader's scroll position is never hijacked), 4 * then swap it in with a highlight. Stops polling when the newsroom marks the coverage as ended. 5 */ 6import { t } from './data.js'; 7 8const INTERVAL = 45000; 9 10export default function init() { 11 const root = document.querySelector('[data-liveblog]'); 12 if (!root || root.dataset.liveblogStatus !== 'live') return; 13 const feed = root.querySelector('[data-liveblog-feed]'); 14 const bar = root.querySelector('[data-liveblog-new]'); 15 const countEl = root.querySelector('[data-liveblog-new-count]'); 16 const updatedEl = root.querySelector('[data-liveblog-updated]'); 17 const key = document.querySelector('[data-liveblog-key] ol'); 18 let known = Number(root.dataset.liveblogCount || 0); 19 let pending = null; let timer = 0; 20 21 const apply = () => { 22 if (!pending) return; 23 const before = new Set([...feed.querySelectorAll('[data-lb-update]')].map((n) => n.dataset.lbUpdate)); 24 feed.innerHTML = pending.html; 25 feed.querySelectorAll('[data-lb-update]').forEach((n) => { if (!before.has(n.dataset.lbUpdate)) n.classList.add('is-new'); }); 26 if (key) key.innerHTML = [...feed.querySelectorAll('[data-lb-update]')].slice(0, 8).map((n) => `<li><a href="#${n.id}"><time>${n.querySelector('time')?.textContent || ''}</time><span>${n.querySelector('.lb__t')?.textContent || ''}</span></a></li>`).join(''); 27 if (updatedEl) updatedEl.textContent = t('updatedAt', 'updated %s').replace('%s', pending.updated); 28 known = pending.count; pending = null; bar.hidden = true; 29 document.title = document.title.replace(/^\(\d+\) /, ''); 30 }; 31 const poll = async () => { 32 try { 33 const res = await fetch(root.dataset.liveblogRest, { headers: { Accept: 'application/json' } }); 34 if (!res.ok) return; 35 const d = await res.json(); 36 if (d.status !== 'live') { root.dataset.liveblogStatus = 'ended'; root.classList.replace('lb--live', 'lb--ended'); clearInterval(timer); } 37 if (d.count > known) { 38 pending = d; 39 // reader is at the top of the feed â swap immediately, otherwise offer a button 40 if (feed.getBoundingClientRect().top > -40) apply(); 41 else { countEl.textContent = `(${d.count - known})`; bar.hidden = false; document.title = `(${d.count - known}) ${document.title.replace(/^\(\d+\) /, '')}`; } 42 } 43 } catch { /* offline â try again next tick */ } 44 }; 45 root.querySelector('[data-liveblog-show]')?.addEventListener('click', () => { apply(); feed.scrollIntoView({ behavior: 'smooth', block: 'start' }); }); 46 timer = setInterval(() => { if (!document.hidden) poll(); }, INTERVAL); 47 document.addEventListener('visibilitychange', () => { if (!document.hidden) poll(); }); 48}
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.