1/** 2 * LNC Editorial â front-end behaviors. 3 * - Mobile nav toggle 4 * - Dark mode toggle (persists to localStorage) 5 * - Share buttons 6 * - (Market ticker moved to assets/js/ticker.js 2026-09-06; conditionally 7 * enqueued by inc/enqueue.php when lnc_ticker_is_active() returns true. 8 * Nothing here depends on it.) 9 */ 10(function () { 11 'use strict'; 12 13 const $ = (s, r = document) => r.querySelector(s); 14 const $$ = (s, r = document) => Array.from(r.querySelectorAll(s)); 15 16 /* ---------- 1. Mobile nav -------------------------------------- */ 17 const navToggle = $('[data-nav-toggle]'); 18 const primaryNav = $('#primary-nav'); 19 if (navToggle && primaryNav) { 20 const closeNav = () => { 21 if (!primaryNav.classList.contains('is-open')) return; 22 primaryNav.classList.remove('is-open'); 23 navToggle.setAttribute('aria-expanded', 'false'); 24 }; 25 navToggle.addEventListener('click', () => { 26 const open = primaryNav.classList.toggle('is-open'); 27 navToggle.setAttribute('aria-expanded', open ? 'true' : 'false'); 28 }); 29 document.addEventListener('keydown', e => { 30 if (e.key === 'Escape') closeNav(); 31 }); 32 primaryNav.addEventListener('click', e => { 33 if (e.target.closest('a')) closeNav(); 34 }); 35 } 36 37 /* ---------- 1b. Dropdown parents on touch screens --------------- 38 Above 820px the More panel opens on :hover / :focus-within. On a touch screen 39 that wide (a tablet in landscape) one tap fires hover AND click together, so 40 the browser followed More's own link before the panel could be used: Sports, 41 Crypto, Elections and the rest were unreachable. On touch, the first tap now 42 opens the panel and a second tap follows the link. Mouse and keyboard are 43 unchanged. At 820px and below the sub-menu is always shown inline in the 44 hamburger menu, so this does nothing there. */ 45 const dropdownParents = $$('.primary-nav .menu-item-has-children'); 46 if (dropdownParents.length) { 47 const wideNav = window.matchMedia('(min-width: 821px)'); 48 const touchFirst = window.matchMedia('(hover: none), (pointer: coarse)'); 49 const closeDropdowns = keep => dropdownParents.forEach(li => { 50 if (li === keep || !li.classList.contains('is-open')) return; 51 li.classList.remove('is-open'); 52 const a = li.querySelector(':scope > a'); 53 if (a) a.removeAttribute('aria-expanded'); 54 }); 55 dropdownParents.forEach(li => { 56 const link = li.querySelector(':scope > a'); 57 if (!link) return; 58 link.addEventListener('click', e => { 59 if (!wideNav.matches || !touchFirst.matches) return; 60 if (li.classList.contains('is-open')) return; // second tap: follow the link 61 e.preventDefault(); 62 closeDropdowns(li); 63 li.classList.add('is-open'); 64 // Only set while tap-opened, so it never claims "collapsed" to a 65 // mouse or keyboard user who is looking at the hover/focus panel. 66 link.setAttribute('aria-expanded', 'true'); 67 }); 68 }); 69 document.addEventListener('click', e => { 70 if (!e.target.closest('.primary-nav .menu-item-has-children')) closeDropdowns(null); 71 }); 72 document.addEventListener('keydown', e => { 73 if (e.key === 'Escape') closeDropdowns(null); 74 }); 75 } 76 77 /* ---------- 2. Theme ---------------------------------------------- 78 DARK ONLY since 2026-09-12, fixed server-side as <html data-theme="dark"> 79 in header.php. This section used to read the saved light/dark choice from 80 localStorage and apply it - but main.js runs after first paint, so a phone 81 in light mode drew the page light and then snapped to dark. That was the 82 flash. The toggle is gone; clear the stored key so it cannot linger. */ 83 try { localStorage.removeItem('lnc-theme'); } catch {} 84 85 /* ---------- 3. Orphaned table-of-contents blocks --------------- 86 (Section 3 used to be the old template's `.share-btn` row. That row was removed 87 from single.php on 2026-09-07 - the real share bar comes from inc/share-bar.php - 88 so its listener went with it.) 89 90 16 posts and pages still carry the rendered HTML of a Spectra / UAGB 91 "Table of Contents" block. The plugin itself is GONE - only litespeed-cache and 92 lnc-video-ads are active - so the markup ships with no stylesheet and n
92o script: 93 94 - the chevron is an <svg viewBox="0 0 384 512"> with NO width or height, so with 95 no CSS to size it, it stretched to fill the column. Measured on 96 /jack-smith-testimony-trump-caused-jan-6th/: 924 x 1232 CSS pixels. 97 - the panel had no stylesheet, so `.uagb-toc__list-wrap` got no `display` rule of 98 its own and the whole list rendered open and unstyled. (An earlier note here 99 claimed the list carries `uagb-toc__list-hidden`. Measured 2026-09-09 across the 100 served HTML of all 16 rows: that class is on exactly one of them.) 101 102 Rather than strip the block out of 16 posts' content, the theme adopts it: main.css 103 sizes the icon and the panel, and this makes the toggle work. The markup is inert 104 HTML now, ours to style. If the blocks are ever removed from the content, all of 105 this becomes dead code and can go with them. */ 106 $$('.wp-block-uagb-table-of-contents').forEach(toc => { 107 const title = toc.querySelector('.uagb-toc__title'); 108 const wrap = toc.querySelector('.uagb-toc__list-wrap'); 109 if (!title || !wrap) return; 110 111 /* Start collapsed - on every page, unconditionally. 112 113 This line used to read `!toc.querySelector('.uagb-toc__list-hidden')`. Measured 114 2026-09-09 on the served HTML of all 16 published rows carrying this block: 115 `uagb-toc__list-hidden` appears on one row, and the plugin's real collapsed 116 marker `uagb-toc__collapse` appears on fifteen - on the ROOT element, not a 117 descendant, so querySelector() could never match it and classList.contains() 118 is what reads it. So the old probe matched on exactly one row - post 2201, 119 where the class really does sit on the descendant `.uagb-toc__list-wrap` - 120 and that page alone already loaded collapsed. On the other fifteen startOpen 121 was true and the TOC opened, pushing the ad pod and the player down the page 122 (CNN desktop measured 2026-09-09: TOC 901px tall, pod y=1852, player y=2145; 123 production's pod sits at y=795). 124 125 Honouring the markers rather than hard-coding false would still leave one page 126 open: post 1891, /10-best-ai-story-video-generator-tools-for-story-driven-content/, 127 carries neither marker. Spectra is uninstalled, so a marker is a fossil of an 128 editor nobody runs, not an editorial choice - and the requirement is that every 129 TOC starts closed. So the theme decides, and the answer is closed. main.css 130 already agrees: `.uagb-toc__list-wrap` is display:none until `.is-open` is added, 131 so the pre-JS paint is collapsed too and there is no open-then-collapse flash. 132 133 `wrap.classList.remove(...)` below still matters for the one row that carries 134 the class: without it, opening that page's TOC would re-expose a class the 135 plugin's stylesheet used to hide. */ 136 const startOpen = false; 137 wrap.classList.remove('uagb-toc__list-hidden'); 138 toc.classList.toggle('is-open', startOpen); 139 140 // It was a plain <div>. Make it a real control so keyboard and AT can reach it. 141 title.setAttribute('role', 'button'); 142 title.setAttribute('tabindex', '0'); 143 title.setAttribute('aria-expanded', String(startOpen)); 144 const panelId = wrap.id || ('lnc-toc-panel-' + Math.random().toString(36).slice(2, 8)); 145 wrap.id = panelId; 146 title.setAttribute('aria-controls', panelId); 147 148 const toggle = () => { 149 const open = !toc.classList.contains('is-open'); 150 toc.classList.toggle('is-open', open); 151 title.setAttribute('aria-expanded', String(open)); 152 }; 153 title.addEventListener('click', toggle); 154 title.addEventListener('keydown', e => { 155 if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } 156 }); 157 }); 158 159 /* ---------- 5. Decorate external article links ----------------- */ 160 // Anything in article content that leaves this origin gets target="_blank". 161 $$('.article__content a[href^="http"]').forEach(a => { 162 try { 163 const u = new URL(a.href); 164 if (u.origin !== location.origin) { 165 a.target = '_blank'; 166 a.rel = 'noopener noreferrer'; 167 } 168 } catch {} 169 }); 170 171 /* ---------- 6. Live-nav scroll arrows --------------------------- */ 172 const liveScroll = $('[data-live-scroll]'); 173 if (liveScroll) { 174 const arrows = $$('[data-scroll]'); 175 const step = 320; 176 const updateArrows = () => {
177 const maxScroll = liveScroll.scrollWidth - liveScroll.clientWidth; 178 arrows.forEach(a => { 179 const dir = parseInt(a.dataset.scroll, 10); 180 if (dir < 0) a.disabled = liveScroll.scrollLeft <= 2; 181 if (dir > 0) a.disabled = liveScroll.scrollLeft >= maxScroll - 2; 182 }); 183 }; 184 arrows.forEach(a => { 185 a.addEventListener('click', () => { 186 const dir = parseInt(a.dataset.scroll, 10); 187 liveScroll.scrollBy({ left: dir * step, behavior: 'smooth' }); 188 }); 189 }); 190 liveScroll.addEventListener('scroll', updateArrows, { passive: true }); 191 window.addEventListener('resize', updateArrows); 192 updateArrows(); 193 } 194 195 /* ---------- 7. Streams drawer (Watch Live) --------------------- */ 196 const drawer = $('#streams-drawer'); 197 if (drawer) { 198 const openers = $$('[data-drawer-open="streams"]'); 199 const closers = $$('[data-drawer-close]', drawer); 200 201 /* FINDING 38. The drawer declared aria-modal="true" but nothing actually 202 contained Tab, so a keyboard user tabbed straight out of an element the 203 markup told assistive tech was modal - and the opener never reported 204 aria-expanded, so there was no way to know the drawer had opened at all. 205 Escape, focus-on-open and focus-restore-on-close were already correct and 206 are left alone. */ 207 const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]),' + 208 'select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; 209 const focusables = () => 210 $$(FOCUSABLE, drawer).filter(el => el.offsetWidth || el.offsetHeight || el.getClientRects().length); 211 212 const setExpanded = state => openers.forEach(o => o.setAttribute('aria-expanded', state ? 'true' : 'false')); 213 214 let lastFocus = null; 215 const open = () => { 216 lastFocus = document.activeElement; 217 drawer.removeAttribute('hidden'); 218 document.body.style.overflow = 'hidden'; 219 setExpanded(true); 220 // Focus the close button for a11y 221 const close = drawer.querySelector('.streams-drawer__close'); 222 if (close) close.focus(); 223 }; 224 const close = () => { 225 drawer.setAttribute('hidden', ''); 226 document.body.style.overflow = ''; 227 setExpanded(false); 228 if (lastFocus && lastFocus.focus) lastFocus.focus(); 229 }; 230 231 openers.forEach(o => o.addEventListener('click', open)); 232 closers.forEach(c => c.addEventListener('click', close)); 233 document.addEventListener('keydown', e => { 234 if (drawer.hasAttribute('hidden')) return; 235 if (e.key === 'Escape') { close(); return; } 236 if (e.key !== 'Tab') return; 237 238 /* The trap. Wrap at both ends, and pull focus back in if it has somehow 239 escaped (e.g. the drawer was opened while focus sat outside it). */ 240 const items = focusables(); 241 if (!items.length) return; 242 const first = items[0]; 243 const last = items[items.length - 1]; 244 if (!drawer.contains(document.activeElement)) { 245 e.preventDefault(); first.focus(); return; 246 } 247 if (e.shiftKey && document.activeElement === first) { 248 e.preventDefault(); last.focus(); 249 } else if (!e.shiftKey && document.activeElement === last) { 250 e.preventDefault(); first.focus(); 251 } 252 }); 253 } 254})();
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.