PageSourceSearch

https://www.legab.it/typo3temp/assets/compressed/merged-6b16b37685…6bd37498141254d09f57c02f365da.js?1784280633

js legab.it collected 2026-09-24 21:29:43 UTC 51,103 bytes, 1,515 lines download raw bytes

1
2/**
3 * Legab cookie consent: catalogo JSON, categorie GDPR, script/iframe bloccati fino al consenso.
4 */
5(function () {
6  'use strict';
7
8  var STORAGE_KEY = 'legab_cookie_consent';
9  var STORAGE_SCHEMA = 1;
10  var EVENT_NAME = 'legab-consent-changed';
11  var HOST_ID = 'legab-cookie-mount';
12  var observerStarted = false;
13  var observerQueued = false;
14  var autoBlockInProgress = false;
15  var activeCatalog = null;
16  var blockedNodeSeq = 0;
17  // Captured at load: later consent activation must re-apply this nonce (CSP blocks bare inline scripts).
18  var pageCspNonce =
19    (document.currentScript && document.currentScript.nonce) || '';
20  var EMBED_PROVIDERS = [
21    { serviceId: 'youtube-embed', categoryId: 'marketing', hosts: ['youtube.com', 'youtu.be', 'youtube-nocookie.com', 'googlevideo.com'] },
22    { serviceId: 'facebook-embed', categoryId: 'marketing', hosts: ['facebook.com', 'facebook.net', 'fbcdn.net'] },
23    { serviceId: 'instagram-embed', categoryId: 'marketing', hosts: ['instagram.com', 'cdninstagram.com'] },
24    { serviceId: 'x-embed', categoryId: 'marketing', hosts: ['x.com', 'twitter.com', 'twimg.com'] },
25    { serviceId: 'tiktok-embed', categoryId: 'marketing', hosts: ['tiktok.com', 'tiktokcdn.com'] },
26    { serviceId: 'spotify-embed', categoryId: 'marketing', hosts: ['spotify.com', 'scdn.co'] },
27  ];
28
29  function getMount() {
30    return document.getElementById(HOST_ID);
31  }
32
33  function readPageConfig() {
34    var el = getMount();
35    if (!el) return { catalogUrl: '', privacyUrl: '' };
36    // privacyUrl arriva da TYPO3: constants(privacyPageUid) -> setup typolink -> data-privacy-url.
37    var privacyUrl = (el.getAttribute('data-privacy-url') || '').trim();
38    return {
39      catalogUrl: el.getAttribute('data-catalog-url') || '',
40      privacyUrl: privacyUrl,
41    };
42  }
43
44  function loadStored() {
45    try {
46      var raw = localStorage.getItem(STORAGE_KEY);
47      if (!raw) return null;
48      return JSON.parse(raw);
49    } catch (e) {
50      return null;
51    }
52  }
53
54  function saveStored(payload) {
55    localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
56  }
57
58  function cloneConsentState(state) {
59    return {
60      categories: Object.assign({}, state.categories || {}),
61      services: Object.assign({}, state.services || {}),
62    };
63  }
64
65  function defaultConsentFromCatalog(catalog) {
66    var out = { categories: {}, services: {} };
67    if (!catalog || !catalog.categories) return out;
68    catalog.categories.forEach(function (c) {
69      out.categories[c.id] = !!c.required;
70    });
71    (catalog.services || []).forEach(function (s) {
72      out.services[s.id] = !!out.categories[s.categoryId];
73    });
74    return out;
75  }
76
77  function normalizeConsent(catalog, stored) {
78    var normalized = defaultConsentFromCatalog(catalog);
79    if (!stored || !stored.categories) {
80      return normalized;
81    }
82
83    Object.keys(normalized.categories).forEach(function (catId) {
84      if (Object.prototype.hasOwnProperty.call(stored.categories, catId)) {
85        normalized.categories[catId] = !!stored.categories[catId];
86      }
87    });
88
89    if (stored.services) {
90      Object.keys(normalized.services).forEach(function (serviceId) {
91        if (Object.prototype.hasOwnProperty.call(stored.services, serviceId)) {
92          normalized.services[serviceId] = !!stored.services[serviceId];
93        }
94      });
95    } else {
96      (catalog.services || []).forEach(function (svc) {
97        normalized.services[svc.id] = !!normalized.categories[svc.categoryId];
98      });
99    }
100    return normalized;
101  }
102
103  function isAllowed(categoryId, serviceId, consentState) {
104    if (serviceId) {
105      return !!(consentState.services && consentState.services[serviceId]);
106    }
107    if (!categoryId) return true;
108    return !!(consentState.categories && consentState.categories[categoryId]);
109  }
110
111  function getCspNonce() {
112    if (pageCspNonce) return pageCspNonce;
113    var mount = getMount();
114    if (mount) {
115      var fromMount = (mount.getAttribute('data-csp-nonce') || '').trim();
116      if (fromMount) {
117        pageCspNonce = fromMount;
118        return pageCspNonce;
119      }
120    }
121    var scripts = document.querySelectorAll('script[nonce]');
122    for (var i = 0; i < scripts.length; i++) {
123      if (scripts[i].nonce) {
124        pageCspNonce = scripts[i].nonce;
125        return pageCspNonce;
126      }
127    }
128    return '';
129  }
130
131  function activateScripts(consentState) {
132    var nodes = document.querySelectorAll('script[type="text/plain"][data-cookie-category]');
133    var nonce = getCspNonce();
134    nodes.forEach(function (oldScript) {
135      var cat = oldScript.getAttribute('data-cookie-category');
136      var serviceId = oldScript.getAttribute('data-cookie-service');
137      if (!isAllowed(cat, serviceId, consentState)) return;
138
139      var nu = document.createElement('script');
140      for (var i = 0; i < oldScript.attributes.length; i++) {
141        var a = oldScript.attributes[i];
142        // Skip type (kept as text/plain while blocked), category marker, and nonce
143        // (getAttribute('nonce') is empty in browsers — would wipe a valid nonce).
144        if (a.name === 'type' || a.name === 'data-cookie-category' || a.name === 'nonce') continue;
145        nu.setAttribute(a.name, a.value);
146      }
147      nu.setAttribute('data-cookie-unblocked', '1');
148      var inline = oldScript.textContent;
149      if (inline && inline.trim()) {
150        nu.textContent = inline;
151      }
152      // CSP script-src-elem requires nonce/hash for dynamically inserted inline scripts.
153      if (nonce) {
154        nu.setAttribute('nonce', nonce);
155        nu.nonce = nonce;
156      }
157      oldScript.parentNode.replaceChild(nu, oldScript);
158    });
159  }
160
161  function activateIframes(consentState) {
162    document.querySelectorAll('iframe[data-cookie-src][data-cookie-category]').forEach(function (iframe) {
163      var cat = iframe.getAttribute('data-cookie-category');
164      var serviceId = iframe.getAttribute('data-cookie-service');
165      if (!isAllowed(cat, serviceId, consentState)) return;
166      var src = iframe.getAttribute('data-cookie-src');
167      if (src) {
168        iframe.setAttribute('src', src);
169        iframe.setAttribute('data-cookie-unblocked', '1');
170      }
171    });
172  }
173
174  function applyTechnical(consentState) {
175    activateScripts(consentState);
176    activateIframes(consentState);
177    syncBlockedEmbedClasses(consentState);
178  }
179
180  function persistAndNotify(catalogVersion, consentState) {
181    saveStored({
182      schema: STORAGE_SCHEMA,
183      catalogVersion: catalogVersion,
184      categories: consentState.categories,
185      services: consentState.services,
186      updatedAt: Date.now(),
187    });
188    window.dispatchEvent(new CustomEvent(EVENT_NAME, { detail: cloneConsentState(consentState) }));
189  }
190
191  function applyConsent(catalogVersion, consentState) {
192    applyTechnical(consentState);
193    persistAndNotify(catalogVersion, consentState);
194  }
195
196  function legalBasisLabel(ui, key) {
197    if (key === 'consent') return ui.legalBasisLabel + ': Consenso';
198    if (key === 'legitimate_interest') return ui.legalBasisLabel + ': Interesse legittimo';
199    return ui.legalBasisLabel + ': ' + (key || '—');
200  }
201
202  function cookieTypeLabel(t) {
203    if (t === 'first_party') return 'First party';
204    if (t === 'third_party') return 'Terza parte';
205    return t || '—';
206  }
207
208  function cookieClassLabel(v) {
209    if (v === 'technical') return 'Tecnico';
210    if (v === 'analytics') return 'Analytics';
211    if (v === 'marketing') return 'Marketing';
212    return v || '—';
213  }
214
215  function sourceLabel(ui, source) {
216    if (source === 'observed') {
217      return ui.sourceObservedLabel || 'Rilevato nei nostri test';
218    }
219    if (source === 'provider_declared') {
220      return ui.sourceProviderDeclaredLabel || 'Possibile secondo il fornitore';
221    }
222    return source || '—';
223  }
224
225  function mandatoryLabel(v) {
226    if (v === 'required') return 'Richiesto';
227    if (v === 'optional') return 'Opzionale';
228    return v || '—';
229  }
230
231  function buildConsentToggleButton(baseClass, label, enabled, disabled, attrs) {
232    var checked = enabled ? 'true' : 'false';
233    var disabledAttr = disabled ? ' disabled' : '';
234    return (
235      '<button type="button" class="' +
236      baseClass +
237      ' legab-cookie-toggle-btn" aria-pressed="' +
238      checked +
239      '" data-checked="' +
240      checked +
241      '"' +
242      (attrs || '') +
243      disabledAttr +
244      '>' +
245      '<span class="legab-cookie-toggle-btn__label">' +
246      esc(label) +
247      '</span>' +
248      '<span class="legab-cookie-toggle-btn__switch" aria-hidden="true"><span class="legab-cookie-toggle-btn__knob"></span></span>' +
249      '</button>'
250    );
251  }
252
253  function transferOutsideEuLabel(ui, transferObj) {
254    if (!transferObj) return '—';
255    var base = transferObj.possible ? 'Si' : 'No';
256    if (transferObj.notes) {
257      return ui.transferOutsideEuLabel + ': ' + base + ' (' + transferObj.notes + ')';
258    }
259    return ui.transferOutsideEuLabel + ': ' + base;
260  }
261
262  function esc(s) {
263    if (s == null) return '';
264    var d = document.createElement('div');
265    d.textContent = s;
266    return d.innerHTML;
267  }
268
269  function detectProviderByHost(hostname) {
270    if (!hostname) return null;
271    var host = String(hostname).toLowerCase();
272    for (var i = 0; i < EMBED_PROVIDERS.length; i++) {
273      var provider = EMBED_PROVIDERS[i];
274      for (var j = 0; j < provider.hosts.length; j++) {
275        var providerHost = provider.hosts[j];
276        if (host === providerHost || host.slice(-(providerHost.length + 1)) === '.' + providerHost) {
277          return provider;
278        }
279      }
280    }
281    return null;
282  }
283
284  function detectProviderByUrl(url) {
285    if (!url) return null;
286    try {
287      var parsed = new URL(url, window.location.origin);
288      return detectProviderByHost(parsed.hostname);
289    } catch (e) {
290      return null;
291    }
292  }
293
294  function nextBlockedNodeId() {
295    blockedNodeSeq += 1;
296    return 'legab-cookie-blocked-' + String(blockedNodeSeq);
297  }
298
299  function ensureBlockedNodeId(node) {
300    var id = node.getAttribute('data-cookie-blocked-id');
301    if (!id) {
302      id = nextBlockedNodeId();
303      node.setAttribute('data-cookie-blocked-id', id);
304    }
305    return id;
306  }
307
308  function getServiceLabel(serviceId) {
309    if (!activeCatalog || !activeCatalog.services) return '';
310    for (var i = 0; i < activeCatalog.services.length; i++) {
311      if (activeCatalog.services[i].id === serviceId) {
312        return activeCatalog.services[i].name || '';
313      }
314    }
315    return '';
316  }
317
318  function buildEmbedPlaceholder(node) {
319    var serviceId = node.getAttribute('data-cookie-service') || '';
320    var serviceLabel = getServiceLabel(serviceId) || 'contenuto esterno';
321    var acceptServiceBtn = '';
322    if (serviceId) {
323      acceptServiceBtn =
324        '<button type="button" class="btn btn-sm btn-success legab-cookie-accept-service" data-cookie-accept-service="' +
325        esc(serviceId) +
326        '">Accetta questo servizio</button>';
327    }
328    return (
329      '<div class="legab-cookie-placeholder__title">Contenuto bloccato per preferenze cookie</div>' +
330      '<div class="legab-cookie-placeholder__text">Per visualizzare questo ' +
331      esc(serviceLabel) +
332      ', abilita la categoria e il consenso specifico servizio.</div>' +
333      '<div class="legab-cookie-placeholder__actions">' +
334      acceptServiceBtn +
335      '<button type="button" class="btn btn-sm btn-primary legab-cookie-open-settings">Apri impostazioni cookie</button>' +
336      '</div>'
337    );
338  }
339
340  function ensureEmbedPlaceholder(node) {
341    if (!node || !node.parentNode) return;
342    if (node.tagName === 'SCRIPT') return;
343    var wrapper = node.parentNode;
344    if (!wrapper.classList || !wrapper.classList.contains('legab-cookie-embed-wrapper')) {
345      wrapper = document.createElement('div');
346      wrapper.className = 'legab-cookie-embed-wrapper';
347      node.parentNode.insertBefore(wrapper, node);
348      wrapper.appendChild(node);
349    }
350    var blockedId = ensureBlockedNodeId(node);
351    var existing = wrapper.querySelector('[data-cookie-placeholder-for="' + blockedId + '"]');
352    if (existing) return;
353    var placeholder = document.createElement('div');
354    placeholder.className = 'legab-cookie-placeholder';
355    placeholder.setAttribute('data-cookie-placeholder-for', blockedId);
356    placeholder.innerHTML = buildEmbedPlaceholder(node);
357    wrapper.appendChild(placeholder);
358  }
359
360  function removeEmbedPlaceholder(node) {
361    if (!node || !node.parentNode) return;
362    var blockedId = node.getAttribute('data-cookie-blocked-id');
363    if (!blockedId) return;
364    var wrapper = node.parentNode;
365    var existing = wrapper.querySelector('[data-cookie-placeholder-for="' + blockedId + '"]');
366    if (existing) existing.remove();
367  }
368
369  function markBlocked(node, provider) {
370    if (!node || !provider) return;
371    if (!node.getAttribute('data-cookie-category')) {
372      node.setAttribute('data-cookie-category', provider.categoryId);
373    }
374    if (!node.getAttribute('data-cookie-service')) {
375      node.setAttribute('data-cookie-service', provider.serviceId);
376    }
377    node.setAttribute('data-cookie-auto-blocked', '1');
378  }
379
380  function shouldAutoBlockNode(node) {
381    if (!node || !node.closest) return false;
382    if (node.closest('header, footer, .footer-legal, .footer-social, nav')) {
383      return false;
384    }
385    return true;
386  }
387
388  function detectProviderByBlockquote(blockquote) {
389    var cls = (blockquote.className || '').toLowerCase();
390    if (cls.indexOf('twitter-tweet') !== -1) {
391      return detectProviderByHost('twitter.com');
392    }
393    if (cls.indexOf('instagram-media') !== -1 || blockquote.getAttribute('data-instgrm-permalink')) {
394      return detectProviderByHost('instagram.com');
395    }
396    if (cls.indexOf('fb-post') !== -1 || cls.indexOf('fb-video') !== -1 || cls.indexOf('fb-xfbml-parse-ignore') !== -1) {
397      return detectProviderByHost('facebook.com');
398    }
399    if (cls.indexOf('tiktok-embed') !== -1) {
400      return detectProviderByHost('tiktok.com');
401    }
402    var citeProvider = detectProviderByUrl(blockquote.getAttribute('cite') || '');
403    return citeProvider;
404  }
405
406  function getAutoBlockRoots() {
407    var explicitRoots = document.querySelectorAll('[data-cookie-autoblock-root]');
408    if (explicitRoots.length) return explicitRoots;
409    var mainRoots = document.querySelectorAll('main');
410    if (mainRoots.length) return mainRoots;
411    return [];
412  }
413
414  function forEachInAutoBlockRoots(selector, callback) {
415    var roots = getAutoBlockRoots();
416    if (!roots.length) return;
417    roots.forEach(function (root) {
418      root.querySelectorAll(selector).forEach(callback);
419    });
420  }
421
422  function autoBlockEmbeds() {
423    forEachInAutoBlockRoots('iframe[src]:not([data-cookie-src]):not([data-cookie-unblocked="1"])', function (iframe) {
424      if (!shouldAutoBlockNode(iframe)) return;
425      var src = iframe.getAttribute('src');
426      var provider = detectProviderByUrl(src);
427      if (!provider) return;
428      iframe.setAttribute('data-cookie-src', src);
429      iframe.removeAttribute('src');
430      markBlocked(iframe, provider);
431      iframe.classList.add('legab-cookie-embed-blocked');
432    });
433
434    forEachInAutoBlockRoots('script[src]:not([data-cookie-category]):not([data-cookie-unblocked="1"])', function (script) {
435      if (!shouldAutoBlockNode(script)) return;
436      if (script.type === 'text/plain') return;
437      var src = script.getAttribute('src');
438      var provider = detectProviderByUrl(src);
439      if (!provider) return;
440      script.setAttribute('type', 'text/plain');
441      markBlocked(script, provider);
442    });
443
444    forEachInAutoBlockRoots('blockquote:not([data-cookie-category])', function (blockquote) {
445      if (!shouldAutoBlockNode(blockquote)) return;
446      var provider = detectProviderByBlockquote(blockquote);
447      if (!provider) return;
448      markBlocked(blockquote, provider);
449      blockquote.classList.add('legab-cookie-embed-blocked');
450    });
451  }
452
453  function startEmbedAutoBlockObserver() {
454    if (observerStarted) return;
455    observerStarted = true;
456    var target = document.documentElement;
457    if (!target) return;
458    var observer = new MutationObserver(function () {
459      if (observerQueued || autoBlockInProgress) return;
460      observerQueued = true;
461      window.requestAnimationFrame(function () {
462        observerQueued = false;
463        if (autoBlockInProgress) return;
464        autoBlockInProgress = true;
465        try {
466          autoBlockEmbeds();
467          var stored = loadStored();
468          if (activeCatalog && stored && stored.categories) {
469            applyTechnical(normalizeConsent(activeCatalog, stored));
470          }
471        } finally {
472          autoBlockInProgress = false;
473        }
474      });
475    });
476    observer.observe(target, { childList: true, subtree: true });
477  }
478
479  function syncBlockedEmbedClasses(consentState) {
480    document.querySelectorAll('[data-cookie-auto-blocked="1"]').forEach(function (node) {
481      var cat = node.getAttribute('data-cookie-category');
482      var serviceId = node.getAttribute('data-cookie-service');
483      var allowed = isAllowed(cat, serviceId, consentState);
484      if (node.tagName === 'IFRAME') {
485        var blockedSrc = node.getAttribute('data-cookie-src');
486        if (allowed && blockedSrc && !node.getAttribute('src')) {
487          node.setAttribute('src', blockedSrc);
488        }
489        if (!allowed && blockedSrc && node.getAttribute('src')) {
490          node.removeAttribute('src');
491        }
492      }
493      node.classList.toggle('legab-cookie-embed-blocked', !allowed);
494      if (allowed) {
495        node.removeAttribute('aria-hidden');
496        removeEmbedPlaceholder(node);
497      } else {
498        node.setAttribute('aria-hidden', 'true');
499        ensureEmbedPlaceholder(node);
500      }
501    });
502  }
503
504  function buildServiceBlock(ui, svc, consentState, categoryRequired) {
505    var rows = '';
506    (svc.cookies || []).forEach(function (ck) {
507      rows +=
508        '<tr><td>' +
509        esc(ck.name) +
510        '</td><td>' +
511        esc(ck.duration) +
512        '</td><td>' +
513        esc(cookieTypeLabel(ck.type)) +
514        '</td><td>' +
515        esc(ck.domain || '—') +
516        '</td><td>' +
517        esc(ck.purpose || '—') +
518        '</td><td>' +
519        esc(cookieClassLabel(ck.cookieClass)) +
520        '</td><td>' +
521        esc(mandatoryLabel(ck.mandatory)) +
522        '</td></tr>';
523    });
524    var privacy =
525      svc.providerPrivacyUrl && svc.providerPrivacyUrl.length
526        ? '<p class="legab-cookie-service__row"><a href="' +
527          esc(svc.providerPrivacyUrl) +
528          '" target="_blank" rel="noopener noreferrer">' +
529          esc(ui.privacyLinkLabel) +
530          '</a></p>'
531        : '';
532    var notes =
533      svc.notes && svc.notes.length
534        ? '<p class="legab-cookie-service__notes"><strong>' +
535          esc(ui.notesLabel) +
536          '</strong> ' +
537          esc(svc.notes) +
538          '</p>'
539        : '';
540    var recipients =
541      svc.dataRecipients && svc.dataRecipients.length
542        ? '<p class="legab-cookie-service__row"><strong>' +
543          esc(ui.dataRecipientsLabel || 'Destinatari') +
544          '</strong> ' +
545          esc(svc.dataRecipients) +
546          '</p>'
547        : '';
548    var retention =
549      svc.retentionPolicy && svc.retentionPolicy.length
550        ? '<p class="legab-cookie-service__row"><strong>' +
551          esc(ui.retentionPolicyLabel || 'Conservazione') +
552          '</strong> ' +
553          esc(svc.retentionPolicy) +
554          '</p>'
555        : '';
556    var transferOutsideEu = '';
557    if (svc.transferOutsideEU) {
558      transferOutsideEu =
559        '<p class="legab-cookie-service__row">' +
560        esc(transferOutsideEuLabel(ui, svc.transferOutsideEU)) +
561        '</p>';
562    }
563    var serviceEnabled = !!(consentState.services && consentState.services[svc.id]);
564    var serviceConsentControl = '';
565    if (!categoryRequired) {
566      serviceConsentControl =
567        '<div class="legab-cookie-service-consent">' +
568        buildConsentToggleButton(
569          'legab-cookie-service-switch',
570          'Consenso specifico servizio',
571          serviceEnabled,
572          false,
573          ' data-service="' + esc(svc.id) + '" data-category="' + esc(svc.categoryId) + '"'
574        ) +
575        '</div>';
576    }
577    return (
578      '<article class="legab-cookie-service" data-service-id="' +
579      esc(svc.id) +
580      '">' +
581      '<button type="button" class="legab-cookie-service__toggle" aria-expanded="false">' +
582      '<span class="legab-cookie-service__name">' +
583      esc(svc.name) +
584      '</span>' +
585      '<span class="legab-cookie-service__chev" aria-hidden="true"></span>' +
586      '</button>' +
587      serviceConsentControl +
588      '<div class="legab-cookie-service__panel" hidden>' +
589      '<p class="legab-cookie-service__row"><strong>' +
590      esc(ui.providerLabel) +
591      '</strong> ' +
592      esc(svc.provider) +
593      '</p>' +
594      '<p class="legab-cookie-service__row"><strong>' +
595      esc(ui.purposeLabel) +
596      '</strong> ' +
597      esc(svc.purpose) +
598      '</p>' +
599      '<p class="legab-cookie-service__row">' +
600      esc(legalBasisLabel(ui, svc.legalBasis)) +
601      '</p>' +
602      recipients +
603      transferOutsideEu +
604      retention +
605      privacy +
606      notes +
607      '<table class="legab-cookie-table"><caption class="visually-hidden">' +
608      esc(svc.name) +
609      '</caption><thead><tr><th>' +
610      esc(ui.cookieTableName) +
611      '</th><th>' +
612      esc(ui.cookieTableDuration) +
613      '</th><th>' +
614      esc(ui.cookieTableType) +
615      '</th><th>' +
616      esc(ui.cookieTableDomain) +
617      '</th><th>' +
618      esc(ui.cookieTablePurpose || 'Finalita') +
619      '</th><th>' +
620      esc(ui.cookieTableClass || 'Classe cookie') +
621      '</th><th>' +
622      esc(ui.cookieTableMandatory || 'Obbligatorieta') +
623      '</th></tr></thead><tbody>' +
624      rows +
625      '</tbody></table>' +
626      '</div></article>'
627    );
628  }
629
630  function buildSettingsPanel(catalog, ui, consentDraft, privacyUrl) {
631    var catBlocks = '';
632    catalog.categories.forEach(function (cat) {
633      var services = catalog.services.filter(function (s) {
634        return s.categoryId === cat.id;
635      });
636      var svcHtml = services.map(function (s) {
637        return buildServiceBlock(ui, s, consentDraft, !!cat.required);
638      }).join('');
639
640      var toggle = '';
641      if (!cat.required) {
642        toggle =
643          '<div class="legab-cookie-cat-toggle">' +
644          buildConsentToggleButton(
645            'legab-cookie-category-switch',
646            ui.categoryOptionalHint,
647            !!consentDraft.categories[cat.id],
648            false,
649            ' data-category="' + esc(cat.id) + '"'
650          ) +
651          '</div>';
652      } else {
653        toggle = '<p class="legab-cookie-cat-required">' + esc(ui.categoryRequiredHint) + '</p>';
654      }
655
656      catBlocks +=
657        '<section class="legab-cookie-category" data-category-section="' +
658        esc(cat.id) +
659        '">' +
660        '<h3 class="legab-cookie-category__title">' +
661        esc(cat.label) +
662        '</h3>' +
663        '<p class="legab-cookie-category__desc">' +
664        esc(cat.description) +
665        '</p>' +
666        toggle +
667        '<div class="legab-cookie-category__services">' +
668        svcHtml +
669        '</div></section>';
670    });
671
672    var normalizedPrivacyUrl = (privacyUrl || '').trim();
673    var privacy =
674      normalizedPrivacyUrl && normalizedPrivacyUrl.length
675        ? '<p class="legab-cookie-privacy">Scopri di più sui cookie e su come li usiamo nella nostra <a href="' +
676          esc(normalizedPrivacyUrl) +
677          '">Cookie Policy</a>.</p>'
678        : '<p class="legab-cookie-privacy">Scopri di più sui cookie e su come li usiamo nella nostra Cookie Policy.</p>';
679    var sourceIntro =
680      ui.sourceIntro && ui.sourceIntro.length
681        ? '<p class="legab-cookie-source-intro">' + esc(ui.sourceIntro) + '</p>'
682        : '';
683
684    return (
685      '<div class="legab-cookie-settings" role="dialog" aria-modal="true" aria-labelledby="legab-cookie-settings-title">' +
686      '<div class="legab-cookie-settings__inner">' +
687      '<header class="legab-cookie-settings__head">' +
688      '<h2 id="legab-cookie-settings-title">' +
689      esc(ui.settingsTitle) +
690      '</h2>' +
691      '<button type="button" class="legab-cookie-settings__close" aria-label="' +
692      esc(ui.close) +
693      '">&times;</button>' +
694      '</header>' +
695      '<p class="legab-cookie-settings__intro">' +
696      esc(ui.settingsIntro) +
697      '</p>' +
698      sourceIntro +
699      privacy +
700      '<div class="legab-cookie-settings__body">' +
701      catBlocks +
702      '</div>' +
703      '<footer class="legab-cookie-settings__foot">' +
704      '<button type="button" class="btn btn-outline-secondary legab-cookie-btn-reject">' +
705      esc(ui.rejectOptional) +
706      '</button>' +
707      '<button type="button" class="btn btn-primary legab-cookie-btn-save">' +
708      esc(ui.save) +
709      '</button>' +
710      '</footer></div></div>'
711    );
712  }
713
714  function buildBanner(ui, privacyUrl) {
715    var normalizedPrivacyUrl = (privacyUrl || '').trim();
716    var policyTail =
717      normalizedPrivacyUrl && normalizedPrivacyUrl.length
718        ? ' Scopri di più sui cookie e su come li usiamo nella nostra <a class="legab-cookie-bar__policy-link" href="' +
719          esc(normalizedPrivacyUrl) +
720          '">' +
721          'Cookie Policy</a>.'
722        : ' Scopri di più sui cookie e su come li usiamo nella nostra Cookie Policy.';
723    return (
724      '<div class="legab-cookie-bar" role="region" aria-label="' +
725      esc(ui.bannerTitle) +
726      '">' +
727      '<div class="legab-cookie-bar__inner">' +
728      '<p class="legab-cookie-bar__text"><strong>' +
729      esc(ui.bannerTitle) +
730      '</strong> — ' +
731      esc(ui.bannerText) +
732      policyTail +
733      '</p>' +
734      '<div class="legab-cookie-bar__actions">' +
735      '<button type="button" class="btn btn-outline-light legab-cookie-btn-customize">' +
736      esc(ui.customize) +
737      '</button>' +
738      '<button type="button" class="btn btn-outline-light legab-cookie-btn-reject">' +
739      esc(ui.rejectOptional) +
740      '</button>' +
741      '<button type="button" class="btn btn-light legab-cookie-btn-accept">' +
742      esc(ui.acceptAll) +
743      '</button>' +
744      '</div></div></div>'
745    );
746  }
747
748  function wireServiceToggles(root) {
749    root.querySelectorAll('.legab-cookie-service__toggle').forEach(function (btn) {
750      btn.addEventListener('click', function () {
751        var art = btn.closest('.legab-cookie-service');
752        var panel = art && art.querySelector('.legab-cookie-service__panel');
753        if (!panel) return;
754        var open = panel.hasAttribute('hidden');
755        panel.toggleAttribute('hidden', !open);
756        btn.setAttribute('aria-expanded', open ? 'true' : 'false');
757      });
758    });
759  }
760
761  function refreshServiceInputs(root, consentDraft) {
762    root.querySelectorAll('.legab-cookie-service-switch').forEach(function (btn) {
763      var serviceId = btn.getAttribute('data-service');
764      var enabled = !!consentDraft.services[serviceId];
765      btn.disabled = false;
766      btn.setAttribute('aria-pressed', enabled ? 'true' : 'false');
767      btn.setAttribute('data-checked', enabled ? 'true' : 'false');
768    });
769
770    root.querySelectorAll('.legab-cookie-category-switch').forEach(function (btn) {
771      var categoryId = btn.getAttribute('data-category');
772      var enabled = !!consentDraft.categories[categoryId];
773      btn.setAttribute('aria-pressed', enabled ? 'true' : 'false');
774      btn.setAttribute('data-checked', enabled ? 'true' : 'false');
775    });
776  }
777
778  function openSettings(host, catalog, ui, consentDraft, privacyUrl, onSaveAll) {
779    var existing = host.querySelector('.legab-cookie-settings');
780    if (existing) existing.remove();
781
782    var wrap = document.createElement('div');
783    wrap.className = 'legab-cookie-settings-wrap';
784    wrap.innerHTML = buildSettingsPanel(catalog, ui, consentDraft, privacyUrl);
785    host.appendChild(wrap);
786
787    var panel = wrap.querySelector('.legab-cookie-settings');
788    var closeBtn = wrap.querySelector('.legab-cookie-settings__close');
789
790    function close() {
791      wrap.remove();
792      document.body.classList.remove('legab-cookie-no-scroll');
793    }
794
795    closeBtn.addEventListener('click', close);
796    panel.addEventListener('click', function (e) {
797      if (e.target === panel) close();
798    });
799
800    wrap.querySelectorAll('.legab-cookie-category-switch').forEach(function (btn) {
801      btn.addEventListener('click', function () {
802        var id = btn.getAttribute('data-category');
803        if (id) {
804          var nextValue = !(consentDraft.categories[id] === true);
805          consentDraft.categories[id] = nextValue;
806          // Categoria come azione bulk: allinea i servizi della categoria
807          // mantenendo comunque la possibilità di override singolo subito dopo.
808          (catalog.services || []).forEach(function (service) {
809            if (service.categoryId !== id) return;
810            consentDraft.services[service.id] = nextValue;
811          });
812          refreshServiceInputs(wrap, consentDraft);
813        }
814      });
815    });
816
817    wrap.querySelectorAll('.legab-cookie-service-switch').forEach(function (btn) {
818      btn.addEventListener('click', function () {
819        var serviceId = btn.getAttribute('data-service');
820        if (!serviceId) return;
821        consentDraft.services[serviceId] = !(consentDraft.services[serviceId] === true);
822        refreshServiceInputs(wrap, consentDraft);
823      });
824    });
825
826    wrap.querySelector('.legab-cookie-btn-save').addEventListener('click', function () {
827      onSaveAll(consentDraft);
828      close();
829    });
830    wrap.querySelector('.legab-cookie-btn-reject').addEventListener('click', function () {
831      var minimal = defaultConsentFromCatalog(catalog);
832      onSaveAll(minimal);
833      close();
834    });
835
836    wireServiceToggles(wrap);
837    refreshServiceInputs(wrap, consentDraft);
838    document.body.classList.add('legab-cookie-no-scroll');
839  }
840
841  function showBanner(host, catalog, ui, privacyUrl, callbacks) {
842    var barWrap = document.createElement('div');
843    barWrap.className = 'legab-cookie-bar-wrap';
844    barWrap.innerHTML = buildBanner(ui, privacyUrl);
845    host.appendChild(barWrap);
846
847    var consentDraft = defaultConsentFromCatalog(catalog);
848    catalog.categories.forEach(function (c) {
849      if (!c.required) consentDraft.categories[c.id] = false;
850    });
851
852    function acceptAll() {
853      catalog.categories.forEach(function (c) {
854        consentDraft.categories[c.id] = true;
855      });
856      (catalog.services || []).forEach(function (s) {
857        consentDraft.services[s.id] = true;
858      });
859      callbacks.onApply(catalog.catalogVersion, consentDraft);
860      barWrap.remove();
861    }
862
863    function rejectMinimal() {
864      consentDraft = defaultConsentFromCatalog(catalog);
865      callbacks.onApply(catalog.catalogVersion, consentDraft);
866      barWrap.remove();
867    }
868
869    barWrap.querySelector('.legab-cookie-btn-accept').addEventListener('click', acceptAll);
870    barWrap.querySelector('.legab-cookie-btn-reject').addEventListener('click', rejectMinimal);
871    barWrap.querySelector('.legab-cookie-btn-customize').addEventListener('click', function () {
872      var draft = cloneConsentState(consentDraft);
873      catalog.categories.forEach(function (c) {
874        if (!c.required && draft.categories[c.id] === undefined) draft.categories[c.id] = false;
875      });
876      openSettings(host, catalog, ui, draft, privacyUrl, function (map) {
877        callbacks.onApply(catalog.catalogVersion, map);
878        barWrap.remove();
879      });
880    });
881  }
882
883  function exposeApi(catalog, getConsentState) {
884    window.LegabCookieConsent = {
885      getState: function () {
886        return cloneConsentState(getConsentState());
887      },
888      setServiceConsent: function (serviceId, enabled) {
889        if (!serviceId || !catalog || !catalog.services) return false;
890        var exists = false;
891        catalog.services.forEach(function (service) {
892          if (service.id === serviceId) {
893            exists = true;
894          }
895        });
896        if (!exists) return false;
897        var stored = loadStored();
898        var nextState = normalizeConsent(catalog, stored);
899        nextState.services[serviceId] = !!enabled;
900        applyConsent(catalog.catalogVersion, nextState);
901        return true;
902      },
903      openSettings: function () {
904        var cfg = readPageConfig();
905        if (!catalog) return;
906        var stored = loadStored();
907        var draft = normalizeConsent(catalog, stored);
908        var host = getMount() || document.body;
909        openSettings(host, catalog, catalog.ui, draft, cfg.privacyUrl, function (map) {
910          applyConsent(catalog.catalogVersion, map);
911        });
912      },
913      getCatalogVersion: function () {
914        return catalog ? catalog.catalogVersion : null;
915      },
916    };
917  }
918
919  function init() {
920    startEmbedAutoBlockObserver();
921    var cfg = readPageConfig();
922    if (!cfg.catalogUrl) {
923      return;
924    }
925
926    fetch(cfg.catalogUrl, { credentials: 'same-origin' })
927      .then(function (r) {
928        if (!r.ok) throw new Error('catalog');
929        return r.json();
930      })
931      .then(function (catalog) {
932        activeCatalog = catalog;
933        var ui = catalog.ui || {};
934        var stored = loadStored();
935        var needBanner = true;
936        var consentState = defaultConsentFromCatalog(catalog);
937
938        autoBlockEmbeds();
939
940        if (
941          stored &&
942          stored.schema === STORAGE_SCHEMA &&
943          stored.catalogVersion === catalog.catalogVersion &&
944          stored.categories
945        ) {
946          needBanner = false;
947          consentState = normalizeConsent(catalog, stored);
948        } else {
949          consentState = defaultConsentFromCatalog(catalog);
950        }
951        applyTechnical(consentState);
952
953        var host = getMount();
954        if (!host) return;
955
956        exposeApi(catalog, function () {
957          var s = loadStored();
958          return s && s.categories ? normalizeConsent(catalog, s) : consentState;
959        });
960
961        document.addEventListener('click', function (e) {
962          var t = e.target;
963          if (t && t.closest && t.closest('.legab-cookie-open-settings')) {
964            e.preventDefault();
965            window.LegabCookieConsent.openSettings();
966            return;
967          }
968          if (t && t.closest && t.closest('.legab-cookie-accept-service')) {
969            e.preventDefault();
970            var btn = t.closest('.legab-cookie-accept-service');
971            var serviceId = btn.getAttribute('data-cookie-accept-service');
972            if (serviceId && window.LegabCookieConsent && window.LegabCookieConsent.setServiceConsent) {
973              window.LegabCookieConsent.setServiceConsent(serviceId, true);
974            }
975          }
976        });
977
978        if (needBanner) {
979          showBanner(host, catalog, ui, cfg.privacyUrl, {
980            onApply: function (version, map) {
981              applyConsent(version, map);
982            },
983          });
984        }
985      })
986      .catch(function () {
987        /* catalog fetch failed: block optional scripts remains */
988      });
989  }
990
991  if (document.readyState === 'loading') {
992    document.addEventListener('DOMContentLoaded', init);
993  } else {
994    init();
995  }
996})();
997
998document.addEventListener('DOMContentLoaded', function() {
999  const navBtn = document.getElementById('responsiveNav');
1000  // Selezioniamo anche il div .top-fade
1001  const topFade = document.querySelector('.top-fade');
1002  
1003  if (!navBtn) return;
1004
1005  let ticking = false;
1006
1007  function updateButton() {
1008    const scrollStep = window.scrollY;
1009    const width = window.innerWidth;
1010    
1011    const themeDiv = document.getElementById('bannerSkin');
1012    const hasSkin = themeDiv && themeDiv.classList.contains('show-skin');
1013    
1014    let threshold;
1015
1016    if (hasSkin) {
1017      threshold = 16; 
1018    } else {
1019      if (width < 768) {
1020        threshold = 16; 
1021      } else {
1022        threshold = 150; 
1023      }
1024    }
1025
1026    // Applichiamo la logica a entrambi gli elementi
1027    if (scrollStep >= threshold) {
1028      navBtn.classList.add('issticky');
1029      if (topFade) topFade.classList.add('issticky');
1030    } else {
1031      navBtn.classList.remove('issticky');
1032      if (topFade) topFade.classList.remove('issticky');
1033    }
1034  
1035    ticking = false;
1036  }
1037
1038  updateButton();
1039
1040  window.addEventListener('scroll', function() {
1041    if (!ticking) {
1042      window.requestAnimationFrame(updateButton);
1043      ticking = true;
1044    }
1045  });
1046
1047  window.addEventListener('resize', function() {
1048    updateButton();
1049  });
1050});
1051
1052// luminanza background card
1053
1054document.querySelectorAll('.card').forEach(card => {
1055    const bgColor = window.getComputedStyle(card).backgroundColor;
1056
1057    if (bgColor) {
1058        const rgb = bgColor.match(/\d+/g).map(Number);
1059        
1060        // Conversione dei valori RGB in sRGB lineare
1061        const rescaled = rgb.map(v => {
1062            v /= 255;
1063            return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
1064        });
1065
1066        // Calcolo della luminanza (Standard WCAG)
1067        const luminance = 0.2126 * rescaled[0] + 0.7152 * rescaled[1] + 0.0722 * rescaled[2];
1068
1069        // Gestione delle classi in base alla soglia
1070        if (luminance < 0.179) {
1071            card.classList.add('is-dark');
1072            card.classList.remove('is-light');
1073        } else {
1074            card.classList.add('is-light');
1075            card.classList.remove('is-dark');
1076        }
1077    }
1078});
1079
1080
1081
1082
1083// luminanza background page club e player
1084
1085document.querySelectorAll('.luminance').forEach(card => {
1086    const bgColor = window.getComputedStyle(card).backgroundColor;
1087
1088    if (bgColor) {
1089
1090        const rgb = bgColor.match(/\d+/g).map(Number);
1091        
1092        const rescaled = rgb.map(v => {
1093            v /= 255;
1094            return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
1095        });
1096
1097        const luminance = 0.2126 * rescaled[0] + 0.7152 * rescaled[1] + 0.0722 * rescaled[2];
1098
1099        if (luminance < 0.179) {
1100            card.classList.add('is-dark');
1101        } else {
1102            card.classList.remove('is-dark');
1103        }
1104    }
1105});
1106
1107document.addEventListener('DOMContentLoaded', () => {
1108    const tabButtons = document.querySelectorAll('button[data-bs-toggle="pill"]');
1109
1110    tabButtons.forEach(button => {
1111        button.addEventListener('shown.bs.tab', (event) => {
1112            const targetId = event.target.getAttribute('data-bs-target');
1113            const targetPane = document.querySelector(targetId);
1114
1115            if (targetPane) {
1116                // Troviamo il genitore specifico "classifiche-container"
1117                const container = targetPane.closest('.classifiche-container');
1118                
1119                // Se lo trova, scrolliamo verso il contenitore, altrimenti verso il pane
1120                const elementToScroll = container || targetPane;
1121
1122                const elementPosition = elementToScroll.getBoundingClientRect().top + window.scrollY;
1123                const offsetPosition = elementPosition - 140;
1124
1125                window.scrollTo({
1126                    top: offsetPosition,
1127                    behavior: 'smooth'
1128                });
1129            }
1130        });
1131    });
1132});
1133
1134
1135document.addEventListener('DOMContentLoaded', function() {
1136    const dropdownItems = document.querySelectorAll('.dropdown-item');
1137    const label = document.getElementById('active-tab-label');
1138
1139    dropdownItems.forEach(item => {
1140        item.addEventListener('click', function(e) {
1141            e.preventDefault();
1142            
1143            // 1. Recupera l'ID della tab reale da attivare
1144            const targetId = this.getAttribute('data-target-tab');
1145            const targetTab = document.getElementById(targetId);
1146
1147            if (targetTab) {
1148                // 2. Attiva la tab tramite l'istanza Bootstrap
1149                const tabInstance = new bootstrap.Tab(targetTab);
1150                tabInstance.show();
1151
1152                // 3. Aggiorna l'interfaccia del dropdown
1153                label.innerText = this.innerText;
1154                
1155                // Rimuove active da altri item e aggiunge a questo
1156                dropdownItems.forEach(el => el.classList.remove('active'));
1157                this.classList.add('active');
1158            }
1159        });
1160    });
1161
1162    // Sincronizzazione inversa: se clicco su desktop, aggiorno il testo mobile
1163    const allTabs = document.querySelectorAll('[data-bs-toggle="pill"]');
1164    allTabs.forEach(tab => {
1165        tab.addEventListener('shown.bs.tab', function (event) {
1166            label.innerText = event.target.innerText;
1167        });
1168    });
1169});
1170
1171document.addEventListener( 'DOMContentLoaded', function () {
1172  // Seleziona tutti gli elementi con la classe .splide
1173  var elms = document.querySelectorAll( '.splide' );
1174
1175  // Cicla su ogni elemento e inizializzalo
1176  elms.forEach( function( splideElement ) {
1177    try {
1178      splideElement.splide = new Splide( splideElement, 
1179        {
1180          "type":"slide",
1181          "perPage":1,
1182          "gap":"1rem",
1183          "arrows":true,
1184          "pagination":true,
1185          "autoplay":false,
1186          "i18n": {
1187            "prev": "Slide precedente",
1188            "next": "Slide successiva",
1189            "first": "Vai alla prima slide",
1190            "last": "Vai all'ultima slide",
1191            "slideX": "Vai alla slide %s",
1192            "pageX": "Vai alla pagina %s",
1193            "play": "Avvia riproduzione automatica",
1194            "pause": "Sospendi riproduzione automatica",
1195            "carousel": "Galleria immagini",
1196            "select": "Seleziona una slide da visualizzare"
1197          }
1198        }
1199      ).mount();
1200    } catch (error) {
1201      console.error('Errore durante l\'inizializzazione di Splide:', error);
1202    }
1203  } );
1204} );
1205
1206
1207// Scroll verso l'alto al drag calendario primavera 2
1208
1209window.addEventListener('load', function() {
1210  var el = document.querySelector('#p2-slider');
1211  if (!el) return;
1212
1213el.querySelectorAll('.splide__arrow').forEach(function(btn) {
1214  btn.addEventListener('click', function() {
1215    var top = el.getBoundingClientRect().top + window.scrollY - 120;
1216    window.scrollTo({ top: top, behavior: 'smooth' });
1217  });
1218});
1219
1220  var startX = 0;
1221  var startY = 0;
1222
1223  el.querySelector('.splide__track').addEventListener('touchstart', function(e) {
1224    startX = e.touches[0].clientX;
1225    startY = e.touches[0].clientY;
1226  });
1227
1228  el.querySelector('.splide__track').addEventListener('touchend', function(e) {
1229    var diffX = Math.abs(e.changedTouches[0].clientX - startX);
1230    var diffY = Math.abs(e.changedTouches[0].clientY - startY);
1231    if (diffX > diffY && diffX > 60) {
1232setTimeout(function() {
1233  var top = el.getBoundingClientRect().top + window.scrollY - 120;
1234  window.scrollTo({ top: top, behavior: 'smooth' });
1235}, 400);
1236    }
1237  });
1238});
1239
1240
1241// home calendar
1242
1243document.addEventListener('DOMContentLoaded', function() {
1244    const slider = document.querySelector('.calendar-carousel');
1245    const nextBtn = document.getElementById('nextBtn');
1246    const prevBtn = document.getElementById('prevBtn');
1247
1248    if (slider) {
1249        let isDown = false;
1250        let startX;
1251        let scrollLeft;
1252
1253        function getScrollAmount() {
1254            const card = document.querySelector('.match-card');
1255            const gap = 15;
1256            return card ? card.offsetWidth + gap : 300;
1257        }
1258
1259        if (nextBtn) {
1260            nextBtn.addEventListener('click', () => {
1261                slider.scrollBy({ left: getScrollAmount(), behavior: 'smooth' });
1262            });
1263        }
1264
1265        if (prevBtn) {
1266            prevBtn.addEventListener('click', () => {
1267                slider.scrollBy({ left: -getScrollAmount(), behavior: 'smooth' });
1268            });
1269        }
1270
1271        slider.addEventListener('mousedown', (e) => {
1272            isDown = true;
1273            slider.classList.add('active');
1274            startX = e.pageX - slider.offsetLeft;
1275            scrollLeft = slider.scrollLeft;
1276        });
1277
1278        window.addEventListener('mouseup', () => {
1279            isDown = false;
1280            slider.classList.remove('active');
1281        });
1282
1283        slider.addEventListener('mouseleave', () => {
1284            isDown = false;
1285        });
1286
1287        slider.addEventListener('mousemove', (e) => {
1288            if (!isDown) return;
1289            e.preventDefault();
1290            const x = e.pageX - slider.offsetLeft;
1291            const walk = (x - startX) * 1.2;
1292            slider.scrollLeft = scrollLeft - walk;
1293        });
1294
1295        function updateArrowState() {
1296            const isAtStart = slider.scrollLeft <= 10;
1297            const isAtEnd = slider.scrollLeft + slider.offsetWidth >= slider.scrollWidth - 10;
1298
1299            if (prevBtn) {
1300                prevBtn.disabled = isAtStart;
1301                prevBtn.setAttribute('aria-disabled', isAtStart);
1302            }
1303
1304            if (nextBtn) {
1305                nextBtn.disabled = isAtEnd;
1306                nextBtn.setAttribute('aria-disabled', isAtEnd);
1307            }
1308        }
1309
1310        slider.addEventListener('scroll', updateArrowState);
1311
1312        updateArrowState();
1313    }
1314
1315});
1316
1317// news share
1318
1319document.addEventListener('DOMContentLoaded', function() {
1320  const shareBtn = document.getElementById('shareBtn');
1321  if (shareBtn) {
1322    shareBtn.addEventListener('click', async () => {
1323
1324      const shareData = {
1325        title: shareBtn.getAttribute('data-title'),
1326        text: shareBtn.getAttribute('data-text'),
1327        url: window.location.href
1328      };
1329
1330      if (navigator.share) {
1331        try {
1332          await navigator.share(shareData);
1333        } catch (err) {
1334          console.log('Errore:', err);
1335        }
1336      } else {
1337        alert("Copia l'URL: " + shareData.url);
1338      }
1339    });
1340  }
1341
1342});
1343
1344
1345// tooltip
1346
1347const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
1348const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
1349
1350
1351
1352// navscroll
1353document.addEventListener('DOMContentLoaded', function () {
1354    const targetDesktop = '#navbar-partita';
1355    const targetMobile = '#navbar-partita-mobile';
1356    const EXTRA_OFFSET = 30; 
1357    
1358    const allLinks = document.querySelectorAll(`${targetDesktop} .nav-link, ${targetMobile} .nav-link`);
1359    
1360
1361    const sections = [];
1362    allLinks.forEach(link => {
1363        const id = link.getAttribute('href');
1364        if (id && id.startsWith('#')) {
1365            const section = document.querySelector(id);
1366            if (section) sections.push(section);
1367        }
1368    });
1369
1370
1371    const observerOptions = {
1372        root: null,
1373
1374        rootMargin: '-100px 0px -70% 0px', 
1375        threshold: 0 
1376    };
1377
1378    const observer = new IntersectionObserver((entries) => {
1379        entries.forEach(entry => {
1380            if (entry.isIntersecting) {
1381                const id = entry.target.getAttribute('id');
1382                allLinks.forEach(link => {
1383                    link.classList.toggle('active', link.getAttribute('href') === `#${id}`);
1384                });
1385            }
1386        });
1387    }, observerOptions);
1388
1389    sections.forEach(section => observer.observe(section));
1390
1391
1392    allLinks.forEach(anchor => {
1393        anchor.addEventListener('click', function (e) {
1394            const targetId = this.getAttribute('href');
1395            if (targetId && targetId.startsWith('#')) {
1396                e.preventDefault();
1397                const targetElement = document.querySelector(targetId);
1398
1399                if (targetElement) {
1400
1401                    const activeNav = document.querySelector('.onepage-navbar.sticky-top');
1402                    const navHeight = activeNav ? activeNav.offsetHeight : 0;
1403                    const totalOffset = navHeight + EXTRA_OFFSET;
1404                    
1405                    const elementPosition = targetElement.getBoundingClientRect().top;
1406                    const offsetPosition = elementPosition + window.pageYOffset - totalOffset;
1407
1408                    window.scrollTo({
1409                        top: offsetPosition,
1410                        behavior: 'smooth'
1411                    });
1412                }
1413            }
1414        });
1415    });
1416});
1417
1418// photoswipe add caption to image
1419document.addEventListener("DOMContentLoaded", function() {
1420  if (window.imageLightbox) {
1421      window.imageLightbox.on('uiRegister', function() { 
1422          window.imageLightbox.pswp.ui.registerElement({
1423              name: 'caption-content',
1424              order: 9,
1425              isButton: false,
1426              appendTo: 'root',
1427              html: 'Caption text',
1428              onInit: (el, pswp) => {
1429                window.imageLightbox.pswp.on('change', () => {
1430                  const currSlideElement = window.imageLightbox.pswp.currSlide.data.element;
1431                  let captionHTML = '';
1432                  if (currSlideElement) {
1433                    const hiddenCaption = currSlideElement.querySelector('.hidden-caption-content');
1434                    console.log(currSlideElement);
1435                    if (hiddenCaption) {
1436                      // get caption from element with class hidden-caption-content
1437                      captionHTML = hiddenCaption.innerHTML;
1438                    } else {
1439                      // get caption from alt attribute
1440                      captionHTML = currSlideElement.querySelector('img').getAttribute('alt');
1441                    }
1442                  }
1443                  el.innerHTML = captionHTML || '';
1444                });
1445              }
1446            });
1447      });
1448  }
1449});
1450
1451
1452document.addEventListener('DOMContentLoaded', function() {
1453  sal({
1454    threshold: 0.1, 
1455    once: true  
1456  });
1457});
1458
1459
1460
1461
1462function calcolaMasonry() {
1463    const container = document.getElementById('masonryFooter');
1464    if (!container) return;
1465
1466    const items = container.querySelectorAll('.masonry-item');
1467    const containerWidth = container.offsetWidth;
1468    
1469
1470    let columnsCount = 4; 
1471    const gap = 24;      
1472
1473    if (containerWidth < 576) {
1474        columnsCount = 2; 
1475    } else if (containerWidth < 992) {
1476        columnsCount = 2; 
1477    } else if (containerWidth < 1200) {
1478        columnsCount = 3; 
1479    }
1480
1481
1482    const totalGapWidth = gap * (columnsCount - 1);
1483    const columnWidth = (containerWidth - totalGapWidth) / columnsCount;
1484
1485
1486    const columnHeights = Array(columnsCount).fill(0);
1487
1488 
1489    items.forEach(item => {
1490
1491        item.style.width = `${columnWidth}px`;
1492
1493
1494        const minHeightIndex = columnHeights.indexOf(Math.min(...columnHeights));
1495
1496 
1497        const posX = minHeightIndex * (columnWidth + gap);
1498        const posY = columnHeights[minHeightIndex];
1499
1500
1501        item.style.left = `${posX}px`;
1502        item.style.top = `${posY}px`;
1503
1504
1505        columnHeights[minHeightIndex] += item.offsetHeight + gap;
1506    });
1507
1508
1509    container.style.height = `${Math.max(...columnHeights)}px`;
1510}
1511
1512
1513window.addEventListener('load', calcolaMasonry);
1514
1515window.addEventListener('resize', calcolaMasonry);

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.