1/** 2 * The first block's "Programi i sotëm" (home/live.php): 3 * · the button unfolds the rest of today's schedule under the two columns (.stage.is-open â 4 * the panel is a 0frâ1fr grid row, the rows reveal with a stagger set here as --i, capped); 5 * the chevron turns, the label flips to "Mbyll", Esc closes, focus lands on the first row and 6 * comes back to the button. On phones the button is a different one that opens the bottom 7 * sheet (modules/sheet.js), so nothing here runs there. 8 * · the rows read the clock: a cached page may be an hour old, so which programme is on and 9 * which are over is decided here (is-now / is-past) and once a minute after that. Earlier rows 10 * hide behind "Shfaq ⦠programet e mëparshme" (.is-hide-past, the guide's own pattern). 11 * · ?epg=open opens the panel on load â a harmless way to look at the open state without a click. 12 */ 13import { toMin, nowMin, nowSec } from './data.js'; 14 15export default function heroBlock() { 16 const stage = document.querySelector('[data-stage]'); 17 if (!stage) return; 18 const btn = stage.querySelector('[data-stage-toggle]'); 19 const panel = stage.querySelector('[data-stage-panel]'); 20 const label = btn && btn.querySelector('[data-stage-label]'); 21 const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 22 const visible = (el) => getComputedStyle(el).display !== 'none'; 23 24 /* --- the rows, from the clock --- */ 25 const paintRows = () => { 26 const n = nowMin(); 27 document.querySelectorAll('[data-epg-row]').forEach((row) => { 28 if (!row.dataset.start) return; 29 const s = toMin(row.dataset.start); 30 let e = toMin(row.dataset.end); 31 if (e <= s) e = 24 * 60; 32 row.classList.toggle('is-past', e <= n); 33 row.classList.toggle('is-now', s <= n && n < e); 34 }); 35 document.querySelectorAll('[data-epg-list]').forEach((list) => { 36 const toggle = list.querySelector('[data-epg-past]'); 37 if (!toggle) return; 38 const count = list.querySelectorAll('.epgl__row.is-past').length; 39 toggle.hidden = !count; 40 toggle.textContent = list.classList.contains('is-hide-past') 41 ? (toggle.dataset.i18nShow || 'Show the %d earlier programmes').replace('%d', count) 42 : (toggle.dataset.i18nHide || 'Hide the earlier programmes'); 43 }); 44 }; 45 46 /* stagger indexes over the rows that will actually show (past ones may be hidden) */ 47 const stagger = () => { 48 if (!panel) return; 49 let i = 0; 50 panel.querySelectorAll('.epgl__row').forEach((row) => { 51 row.style.setProperty('--i', reduced ? 0 : Math.min(i, 12)); 52 if (visible(row)) i += 1; 53 }); 54 }; 55 56 document.addEventListener('click', (e) => { 57 const toggle = e.target.closest('[data-epg-past]'); 58 if (!toggle) return; 59 const list = toggle.closest('[data-epg-list]'); 60 if (!list) return; 61 list.classList.toggle('is-hide-past'); 62 paintRows(); 63 stagger(); 64 }); 65 66 /* --- open / close --- */ 67 const settle = reduced ? 0 : 460; // matches the panel's grid-row transition in live.css 68 let timer = 0; 69 const isOpen = () => stage.classList.contains('is-open'); 70 71 const open = (focus = true) => { 72 if (!btn || !panel || isOpen()) return; 73 clearTimeout(timer); 74 paintRows(); 75 stagger(); 76 panel.removeAttribute('inert'); 77 stage.classList.add('is-open'); 78 btn.setAttribute('aria-expanded', 'true'); 79 if (label) label.textContent = btn.dataset.i18nClose || label.textContent; 80 if (!focus) return; // opened on page load: no focus jump, no scroll 81 timer = setTimeout(() => { 82 const first = panel.querySelector('.epgl__row.is-now') || [...panel.querySelectorAll('.epgl__row')].find(visible); 83 if (first) first.focus({ preventScroll: true }); 84 if (first) first.scrollIntoView({ block: 'nearest', behavior: reduced ? 'auto' : 'smooth' }); 85 }, settle); 86 }; 87 const close = (refocus = true) => { 88 if (!btn || !panel || !isOpen()) return; 89 clearTimeout(timer); 90 stage.classList.remove('is-open'); 91 btn.setAttribute('aria-expanded', 'false'); 92 if (label) label.textContent = btn.dataset.i18nOpen || label.textContent; 93 timer = setTimeout(() => { if (!isOpen()) panel.setAttribute('inert', ''); }, settle); 94 if (refocus) btn.focus({ preventScroll: true }); 95 }; 96 97 if (btn && panel) { 98 panel.setAttribute('inert', ''); 99 btn.addEventListener('click', () => (isOpen() ? close() : open())); 100 document.addEventListener('keydown', (e) => { 101 if (e.key === 'Escape' && isOpen()) { e.preventDefault(); close(); } 102 }); 103 /* the phone layout swaps the panel for the sheet â an open panel left over from a resize 104 must not keep the toggle claiming it is expanded */ 105 window.matchMedia('(max-width: 767.98px)').addEventListener('change', (e) => { if (e.matches && !stage.hasAttribute('data-stage-open')) close(false); }); 106 /* the Live TV page ships the panel open (data-stage-open); ?epg=open does the same anywhere */ 107 if ((stage.hasAttribute('data-stage-open') || new URLSearchParams(window.location.search).get('epg') === 'open') && visible(btn)) { 108 stage.classList.remove('is-open'); // the server-rendered open state, so no-JS readers see the schedule 109 open(false); 110 } else { 111 stage.classList.remove('is-open'); 112 } 113 } 114 115 /* --- keep the rows current, on the minute --- */ 116 paintRows(); 117 const loop = () => setTimeout(() => { paintRows(); loop(); }, Math.max(2000, (60 - nowSec()) * 1000 + 250)); 118 loop(); 119 document.addEventListener('visibilitychange', () => { if (!document.hidden) paintRows(); }); 120}
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.