1/** 2 * Fiscomania â nav.js 3 * Ticker, search overlay (con live search), dropdown a11y, sticky offset. 4 */ 5 6(function () { 7 'use strict'; 8 9 /* ââ TICKER ââ */ 10 const Ticker = { 11 items: [], 12 idx: 0, 13 interval: null, 14 15 init(posts) { 16 this.items = posts; 17 if (!this.items.length) return; 18 const slot = document.getElementById('ticker-slot'); 19 if (!slot) return; 20 const el = slot.querySelector('#ticker-current'); 21 if (!el) return; 22 el.innerHTML = `<a href="${this.items[0].url}">${this.items[0].title}</a>`; 23 el.className = 'ticker-item in'; 24 this.interval = setInterval(() => this.next(), 4500); 25 }, 26 27 next() { 28 this.idx = (this.idx + 1) % this.items.length; 29 const el = document.getElementById('ticker-current'); 30 if (!el) return; 31 el.classList.replace('in', 'out'); 32 setTimeout(() => { 33 el.innerHTML = `<a href="${this.items[this.idx].url}">${this.items[this.idx].title}</a>`; 34 el.classList.replace('out', 'in'); 35 }, 420); 36 } 37 }; 38 39 /* ââ LIVE SEARCH (REST: /wp-json/wp/v2/posts) ââ */ 40 const LiveSearch = { 41 input: null, 42 results: null, 43 controller: null, 44 debounceTimer: 0, 45 minChars: 2, 46 debounceMs: 220, 47 48 init() { 49 this.input = document.getElementById('search-input'); 50 this.results = document.getElementById('search-results'); 51 if (!this.input || !this.results) return; 52 53 this.input.addEventListener('input', () => this.onInput()); 54 this.input.addEventListener('keydown', (e) => this.onKeydown(e)); 55 }, 56 57 onInput() { 58 clearTimeout(this.debounceTimer); 59 const q = this.input.value.trim(); 60 if (q.length < this.minChars) { 61 this.clear(); 62 return; 63 } 64 this.results.setAttribute('aria-busy', 'true'); 65 this.debounceTimer = setTimeout(() => this.search(q), this.debounceMs); 66 }, 67 68 search(q) { 69 if (this.controller) this.controller.abort(); 70 this.controller = new AbortController(); 71 72 const url = '/wp-json/wp/v2/posts?per_page=10&_fields=id,link,title,date,excerpt&search=' 73 + encodeURIComponent(q); 74 75 fetch(url, { signal: this.controller.signal }) 76 .then((r) => r.ok ? r.json() : Promise.reject(r)) 77 .then((data) => this.render(data || [])) 78 .catch((err) => { 79 if (err && err.name === 'AbortError') return; 80 this.showError(); 81 }) 82 .finally(() => { 83 this.results.removeAttribute('aria-busy'); 84 }); 85 }, 86 87 render(items) { 88 if (!items.length) { 89 this.results.innerHTML = '<div class="search-results-empty">Nessun risultato.</div>'; 90 return; 91 } 92 const fmt = new Intl.DateTimeFormat('it-IT', { day: 'numeric', month: 'short', year: 'numeric' }); 93 this.results.innerHTML = items.map((p, i) => { 94 const title = (p.title && p.title.rendered) || ''; 95 const date = p.date ? fmt.format(new Date(p.date)) : ''; 96 return '<a class="search-results-item" href="' + p.link + '" role="option" data-idx="' + i + '">' 97 + '<span class="search-results-title">' + title + '</span>' 98 + '<span class="search-results-meta">' + date + '</span>' 99 + '</a>'; 100 }).join(''); 101 }, 102 103 clear() { 104 if (this.controller) this.controller.abort(); 105 if (this.results) this.results.innerHTML = ''; 106 }, 107 108 showError() { 109 if (this.results) { 110 this.results.innerHTML = '<div class="search-results-error">Errore nella ricerca.</div>'; 111 } 112 }, 113 114 onKeydown(e) { 115 const items = this.results.querySelectorAll('.search-results-item'); 116 if (!items.length) return; 117 const cur = this.results.querySelector('.search-results-item.is-highlighted'); 118 let idx = cur ? parseInt(cur.dataset.idx, 10) : -1; 119 120 if (e.key === 'ArrowDown') { 121 e.preventDefault(); 122 idx = (idx + 1) % items.length; 123 } else if (e.key === 'ArrowUp') { 124 e.preventDefault(); 125 idx = (idx - 1 + items.length) % items.length; 126 } else if (e.key === 'Enter' && cur) { 127 e.preventDefault(); 128 window.location = cur.href; 129 return; 130 } else { 131 return; 132 } 133
134 items.forEach((el) => el.classList.remove('is-highlighted')); 135 items[idx].classList.add('is-highlighted'); 136 items[idx].scrollIntoView({ block: 'nearest' }); 137 } 138 }; 139 140 /* ââ SEARCH OVERLAY (open/close + keyboard) ââ */ 141 const fiscoSearch = { 142 overlay: null, 143 input: null, 144 145 init() { 146 this.overlay = document.getElementById('search-overlay'); 147 this.input = document.getElementById('search-input'); 148 if (!this.overlay) return; 149 150 this.overlay.addEventListener('click', (e) => { 151 if (e.target === this.overlay) this.close(); 152 }); 153 154 document.addEventListener('keydown', (e) => { 155 if (e.key === 'Escape') this.close(); 156 if ((e.metaKey || e.ctrlKey) && e.key === 'k') { 157 e.preventDefault(); 158 this.toggle(); 159 } 160 }); 161 }, 162 163 toggle() { 164 if (!this.overlay) return; 165 this.overlay.classList.toggle('open'); 166 if (this.overlay.classList.contains('open')) { 167 if (this.input) this.input.focus(); 168 } else { 169 LiveSearch.clear(); 170 } 171 }, 172 173 close() { 174 if (!this.overlay) return; 175 this.overlay.classList.remove('open'); 176 LiveSearch.clear(); 177 } 178 }; 179 180 /* ââ DROPDOWN A11Y ââ */ 181 function initDropdowns() { 182 document.querySelectorAll('.topbar-drop').forEach((drop) => { 183 const btn = drop.querySelector('.topbar-drop-btn'); 184 const panel = drop.querySelector('.topbar-drop-panel'); 185 if (!btn || !panel) return; 186 187 btn.addEventListener('click', () => { 188 const expanded = btn.getAttribute('aria-expanded') === 'true'; 189 btn.setAttribute('aria-expanded', String(!expanded)); 190 panel.style.display = expanded ? '' : 'grid'; 191 }); 192 193 document.addEventListener('click', (e) => { 194 if (!drop.contains(e.target)) { 195 btn.setAttribute('aria-expanded', 'false'); 196 panel.style.display = ''; 197 } 198 }); 199 200 panel.addEventListener('keydown', (e) => { 201 if (e.key === 'Escape') { 202 btn.setAttribute('aria-expanded', 'false'); 203 panel.style.display = ''; 204 btn.focus(); 205 } 206 }); 207 }); 208 } 209 210 /* Sticky offset gestito via CSS var --sticky-top (default 30px). Niente JS dinamico. */ 211 212 /* ââ ARTICLE READING PROGRESS + TOC SCROLL-SPY ââ */ 213 const ArticleProgress = { 214 bar: null, 215 track: null, 216 body: null, 217 headings: [], 218 items: [], 219 activeIdx: -1, 220 rafPending: false, 221 222 init() { 223 this.bar = document.getElementById('reading-progress-bar'); 224 this.track = this.bar ? this.bar.parentElement : null; 225 this.body = document.querySelector('.art-main') || document.querySelector('.prose'); 226 227 // Headings dell'articolo che corrispondono alle voci TOC 228 const tocItems = document.querySelectorAll('.toc-aside-item'); 229 this.items = Array.from(tocItems); 230 this.headings = this.items 231 .map((li) => { 232 const id = li.getAttribute('data-target'); 233 return id ? document.getElementById(id) : null; 234 }) 235 .filter(Boolean); 236 237 // Niente da fare se non c'è né progress né TOC 238 if (!this.bar && !this.headings.length) return; 239 240 this.update = this.update.bind(this); 241 window.addEventListener('scroll', () => this.requestUpdate(), { passive: true }); 242 window.addEventListener('resize', () => this.requestUpdate(), { passive: true }); 243 this.update(); 244 }, 245 246 reinitTOC() { 247 const tocItems = document.querySelectorAll('.toc-aside-item'); 248 this.items = Array.from(tocItems); 249 this.headings = this.items 250 .map((li) => { 251 const id = li.getAttribute('data-target'); 252 return id ? document.getElementById(id) : null; 253 }) 254 .filter(Boolean); 255 this.activeIdx = -1; 256 if (this.bar || this.headings.length) this.requestUpdate(); 257 }, 258 259 requestUpdate() { 260 if (this.rafPending) return; 261 this.rafPending = true; 262 requestAnimationFrame(this.update); 263 }, 264 265 update() { 266 this.rafPending = false; 267 268 // Progress bar 269 if (this.bar && this.body) { 270 const rect = this.body.getBoundingClientRect(); 271 const total = rect.height + window.innerHeight; 272 const scrolled = -rect.top + window.innerHeight; 273 const pct = Math.max(0, Math.min(1, scrolled / total)); 274 this.bar.style.width = (pct * 100).toFixed(1) + '%'; 275 if (this.track) { 276 this.track.setAttribute('aria-valuenow', String(Math.round(pct * 100))); 277 } 278 } 279 280 // Scroll-spy: trova l'ultimo heading sopra la trigger zone 281 if (this.headings.length && this.items.length) { 282 const trigger = window.innerHeight * 0.25; 283 let active = -1; 284 for (let i = 0; i < this.headings.length; i++) { 285 const top = this.headings[i].getBoundingClientRect().top; 286 if (top <= trigger) active = i; 287 else break; 288 } 289 if (active !== this.activeIdx) { 290 this.activeIdx = active;
291 this.items.forEach((li, i) => { 292 li.classList.toggle('is-active', i === active); 293 li.classList.toggle('is-passed', i < active); 294 }); 295 } 296 } 297 } 298 }; 299 300 /* ââ FOOTER DETAILS: forza open su desktop (fix Chrome 120+ <details> behavior) ââ */ 301 function fmSyncFooterDetails() { 302 const isDesktop = window.matchMedia('(min-width:769px)').matches; 303 document.querySelectorAll('.f-col-wrap').forEach(function (d) { d.open = isDesktop; }); 304 } 305 306 /* ââ SMART STICKY HEADER ââ 307 Sticky di base; sopra 300px nasconde su scroll-down, mostra su scroll-up. 308 rAF-throttled, passive listeners. Aggiorna --sticky-top dinamicamente 309 così le sidebar sticky scendono/salgono in sync col header. */ 310 const StickyHeader = { 311 el: null, 312 lastY: 0, 313 threshold: 300, 314 delta: 6, // ignora micro-jitter 315 raf: false, 316 headerH: 0, 317 baseStickyTop: 30, 318 isHidden: false, 319 init() { 320 this.el = document.querySelector('.site-header'); 321 if (!this.el) return; 322 this.lastY = window.scrollY; 323 this.headerH = this.el.offsetHeight || 60; 324 // Default: header visibile â sticky-top più basso (sotto header) 325 this.applyStickyOffset(false); 326 window.addEventListener('scroll', () => this.request(), { passive: true }); 327 window.addEventListener('resize', () => { 328 this.headerH = this.el.offsetHeight || 60; 329 this.applyStickyOffset(this.isHidden); 330 }, { passive: true }); 331 }, 332 request() { 333 if (this.raf) return; 334 this.raf = true; 335 requestAnimationFrame(() => { this.raf = false; this.update(); }); 336 }, 337 update() { 338 const y = window.scrollY; 339 const dy = y - this.lastY; 340 if (Math.abs(dy) < this.delta) return; 341 342 this.el.classList.toggle('is-stuck', y > 4); 343 344 let nextHidden = this.isHidden; 345 if (y < this.threshold) { 346 nextHidden = false; 347 } else if (dy > 0) { 348 nextHidden = true; 349 } else if (dy < 0) { 350 nextHidden = false; 351 } 352 353 if (nextHidden !== this.isHidden) { 354 this.isHidden = nextHidden; 355 this.el.classList.toggle('is-hidden', nextHidden); 356 this.applyStickyOffset(nextHidden); 357 } 358 this.lastY = y; 359 }, 360 /** Quando header visibile: --sticky-top = headerH + 16px buffer. 361 * Quando hidden: torna al base 30px. Le sidebar transitano via CSS `transition: top .28s`. */ 362 applyStickyOffset(headerHidden) { 363 const value = headerHidden ? this.baseStickyTop : (this.headerH + 16); 364 document.documentElement.style.setProperty('--sticky-top', value + 'px'); 365 } 366 }; 367 368 /* ââ MOBILE CTA FIXED (scroll > 300px) ââ */ 369 const MobileCTA = { 370 el: null, 371 init() { 372 this.el = document.getElementById('fm-mob-cta'); 373 if (!this.el) return; 374 // Sposta a radice di <body>: evita che transform su antenati rompa position:fixed 375 document.body.appendChild(this.el); 376 window.addEventListener('scroll', () => this.tick(), { passive: true }); 377 this.tick(); 378 document.querySelectorAll('.fm-cta-modal-trigger').forEach(function (btn) { 379 btn.addEventListener('click', function () { FmCTAModal.open(); }); 380 }); 381 }, 382 tick() { 383 if (!this.el) return; 384 const show = window.scrollY > 300; 385 this.el.classList.toggle('is-visible', show); 386 this.el.setAttribute('aria-hidden', String(!show)); 387 } 388 }; 389 390 /* ââ FORM MODAL (Richiedi Consulenza) ââ */ 391 const FmCTAModal = { 392 dialog: null, 393 init() { 394 this.dialog = document.getElementById('fm-cta-dialog'); 395 if (!this.dialog) return; 396 const closeBtn = this.dialog.querySelector('.fm-cta-dialog-close'); 397 if (closeBtn) closeBtn.addEventListener('click', () => this.close()); 398 this.dialog.addEventListener('click', (e) => { if (e.target === this.dialog) this.close(); }); 399 document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.dialog.open) this.close(); }); 400 }, 401 open() { 402 if (!this.dialog) return; 403 this.dialog.showModal(); 404 document.body.classList.add('fm-no-scroll'); 405 }, 406 close() { 407 if (!this.dialog) return; 408 this.dialog.close(); 409 document.body.classList.remove('fm-no-scroll'); 410 } 411 }; 412 413 /* ââ INIT ââ */ 414 document.addEventListener('DOMContentLoaded', () => { 415 fiscoSearch.init(); 416 LiveSearch.init(); 417 initDropdowns(); 418 ArticleProgress.init(); 419 fmSyncFooterDetails(); 420 window.matchMedia('(min-width:769px)').addEventListener('change', fmSyncFooterDetails); 421 MobileCTA.init(); 422 FmCTAModal.init(); 423 StickyHeader.init(); 424
425 // Expose globally for header.php onclick 426 window.fiscoSearch = fiscoSearch; 427 window.fiscoArticleProgress = ArticleProgress; 428 429 if (typeof fiscoTickerData !== 'undefined') { 430 Ticker.init(fiscoTickerData.posts || []); 431 } 432 }); 433 434})();
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.