1/* cms-analytics-sections.js 2 Track "section views" (per-page sections) using IntersectionObserver. 3 Expects config either in window.cmsAnalytics OR in <body data-*>. 4 5 Required: 6 - pageId (number > 0) 7 - baseUrl (string) OR app_site_start global (string) OR "/" 8 9 Optional: 10 - device (0..3) 11 - source (0..4) 12 - endpoint (string) default: "ajaxrequest/cms-track-sections" 13 14 Section selection: 15 - elements matching ".section[id]" 16 Section key: 17 - uses element.id normalized to [a-z0-9_-], max 64 chars 18 19 Behavior: 20 - counts a section once per page load 21 - considers "viewed" when 50% visible 22 - batches events and sends via sendBeacon/fetch 23*/ 24 25(function () { 26 'use strict'; 27 28 // ----------------------------- 29 // Config (window.cmsAnalytics OR body dataset) 30 // ----------------------------- 31 var cfg = (window.cmsAnalytics && typeof window.cmsAnalytics === 'object') ? window.cmsAnalytics : null; 32 33 var ds = (document.body && document.body.dataset) ? document.body.dataset : {}; 34 function toInt(v, fallback) { 35 var n = parseInt(v, 10); 36 return Number.isFinite(n) ? n : fallback; 37 } 38 39 var PAGE_ID = toInt(cfg?.pageId ?? ds.pageId ?? window.CMS_PAGE_ID, 0); 40 if (!PAGE_ID) return; // Nothing to do 41 42 var DEVICE_ID = toInt(cfg?.device ?? ds.deviceId ?? window.CMS_DEVICE_ID, 0); 43 var SOURCE_ID = toInt(cfg?.source ?? ds.sourceId ?? window.CMS_SOURCE_ID, 0); 44 45 // Whitelists (keeps DB clean) 46 if (![0, 1, 2, 3].includes(DEVICE_ID)) DEVICE_ID = 0; 47 if (![0, 1, 2, 3, 4].includes(SOURCE_ID)) SOURCE_ID = 0; 48 49 var baseUrl = 50 (cfg?.baseUrl ?? ds.baseUrl ?? window.app_site_start ?? '').toString(); 51 52 // Normalize baseUrl to end with "/" 53 baseUrl = baseUrl ? baseUrl.replace(/\/?$/, '/') : '/'; 54 55 var endpoint = (cfg?.endpoint ?? ds.endpoint ?? 'ajaxrequest/cms-track-sections').toString(); 56 // If someone passes "/ajaxrequest/..." keep it; if they pass "ajaxrequest/..." keep it. 57 // We join safely: 58 function joinUrl(base, path) { 59 if (!path) return base; 60 // absolute URL 61 if (/^https?:\/\//i.test(path)) return path; 62 // absolute path 63 if (path[0] === '/') return base.replace(/\/+$/, '') + path; 64 return base + path; 65 } 66 67 var TRACK_URL = joinUrl(baseUrl, endpoint); 68 69 // ----------------------------- 70 // Section selection + key normalization 71 // ----------------------------- 72 function normalizeKey(id) { 73 if (!id) return null; 74 var key = String(id).toLowerCase().replace(/[^a-z0-9\-_]/g, ''); 75 if (!key.length) return null; 76 return key.substring(0, 64); 77 } 78 79 var sections = document.querySelectorAll('.section[data-codice-sezione]'); 80 if (!sections || !sections.length) return; 81 82 // ----------------------------- 83 // Queue + sending 84 // ----------------------------- 85 var seen = new Set(); 86 var queue = []; 87 var flushTimer = null; 88 89 function enqueue(sectionKey) { 90 if (!sectionKey || seen.has(sectionKey)) return; 91 seen.add(sectionKey); 92 queue.push(sectionKey); 93 if (!flushTimer) flushTimer = setTimeout(flush, 800); 94 } 95 96 function flush() { 97 flushTimer = null; 98 if (!queue.length) return; 99 100 var payload = { 101 page_id: PAGE_ID, 102 device: DEVICE_ID, 103 source: SOURCE_ID, 104 sections: queue.splice(0, queue.length) 105 }; 106 107 var ok = false; 108 109 // Prefer sendBeacon for reliability during navigation/unload 110 try { 111 if (navigator.sendBeacon) { 112 ok = navigator.sendBeacon( 113 TRACK_URL, 114 new Blob([JSON.stringify(payload)], { type: 'application/json' }) 115 ); 116 } 117 } catch (e) { 118 ok = false; 119 } 120 121 // Fallback fetch 122 if (!ok && window.fetch) { 123 fetch(TRACK_URL, { 124 method: 'POST', 125 headers: { 'Content-Type': 'application/json' }, 126 body: JSON.stringify(payload), 127 keepalive: true, 128 credentials: 'same-origin' 129 }).catch(function () { /* silent */ }); 130 return; 131 } 132 133 // Final fallback (very old browsers): image beacon (no JSON) 134 // We skip it here to keep server simple; enable only if needed. 135 } 136 137 // Flush on pagehide (better than beforeunload) 138 window.addEventListener('pagehide', flush); 139 140 // ----------------------------- 141 // IntersectionObserver (50% threshold) 142 // ----------------------------- 143 var hasIO = typeof IntersectionObserver !== 'undefined'; 144 145 // Pre-enqueue section that's already "center-visible" at load (helps in some edge cases) 146 (function preEnqueueVisible() { 147 try { 148 var vh = window.innerHeight || document.documentElement.clientHeight || 0; 149 if (!vh) return; 150 151 sections.forEach(function (el) { 152 var r = el.getBoundingClientRect(); 153 // center line of viewport at 50% 154 var center = vh * 0.5; 155 if (r.top < center && r.bottom > center) { 156 var key = el.dataset.codiceSezione;
157 if (key) enqueue(key); 158 } 159 }); 160 } catch (e) { 161 // ignore 162 } 163 })(); 164 165 if (!hasIO) { 166 // No IntersectionObserver: degrade gracefully â count only the first visible section 167 // (Better than nothing, avoids heavy scroll listeners) 168 return; 169 } 170 171 var observer = new IntersectionObserver(function (entries) { 172 entries.forEach(function (entry) { 173 if (!entry.isIntersecting) return; 174 if (entry.intersectionRatio < 0.5) return; 175 176 var key = entry.target.dataset.codiceSezione; 177 if (key) enqueue(key); 178 179 observer.unobserve(entry.target); // only once per section 180 }); 181 }, { threshold: [0.5] }); 182 183 sections.forEach(function (el) { 184 observer.observe(el); 185 }); 186})();
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.