1/** 2 * Campus IL â analytics event wiring. 3 * 4 * Wires all WP-only events in the GTM-MPGVQPXZ taxonomy via delegated 5 * listeners. Touches the DOM only by reading attributes â no markup changes 6 * required. 7 * 8 * All event names match the GTM container exactly. All handlers no-op 9 * gracefully when window.campusAnalytics is missing. 10 */ 11(function () { 12 'use strict'; 13 14 function track(eventName, params) { 15 var w = window; 16 if (!w.campusAnalytics || typeof w.campusAnalytics.track !== 'function') return false; 17 return w.campusAnalytics.track(eventName, params || {}); 18 } 19 20 // Buffered track for callbacks that may run before analytics.js loads 21 // (e.g. IntersectionObserver firing on DOMContentLoaded race). 22 function trackWhenReady(eventName, params, attempt) { 23 if (track(eventName, params)) return true; 24 attempt = attempt || 0; 25 if (attempt >= 50) return false; // ~5s max 26 setTimeout(function () { trackWhenReady(eventName, params, attempt + 1); }, 100); 27 return false; 28 } 29 30 function ready(fn) { 31 if (document.readyState === 'loading') { 32 document.addEventListener('DOMContentLoaded', fn); 33 } else { 34 fn(); 35 } 36 } 37 38 function host(url) { 39 try { return new URL(url, location.href).hostname; } catch (e) { return ''; } 40 } 41 42 function nearest(el, selector) { 43 while (el && el !== document) { 44 if (el.matches && el.matches(selector)) return el; 45 el = el.parentNode; 46 } 47 return null; 48 } 49 50 function trim100(s) { return (s || '').toString().trim().slice(0, 100); } 51 52 // ------------------------------------------------------------------------- 53 // 1. Search form submits â search_submit_{header|mobile|homepage|catalog} 54 // ------------------------------------------------------------------------- 55 // The homepage hero's field carries .cil-search â on the form, or on the bar around the legacy 56 // one â and nothing else on the site does. #homepageSearch stays beside it as the old wrapper. 57 ready(function () { 58 document.querySelectorAll('form').forEach(function (form) { 59 var id = form.id || ''; 60 var event = null; 61 var onHomepage = nearest(form, '.cil-search') || nearest(form, '#homepageSearch'); 62 if (id === 'headerSearchForm') event = 'search_submit_header'; 63 else if (id === 'mobileSearchForm') event = 'search_submit_mobile'; 64 else if (onHomepage) event = 'search_submit_homepage'; 65 else if (id === 'newCatalogSearchForm') event = 'search_submit_catalog'; 66 if (!event) return; 67 form.addEventListener('submit', function () { 68 var input = form.querySelector('input[type="text"], input[name="srch"], input[name="search_not_catalog"]'); 69 var term = input ? (input.value || '').trim() : ''; 70 if (term) track(event, { search_term: term }); 71 }); 72 }); 73 }); 74 75 // ------------------------------------------------------------------------- 76 // 2. Autocomplete result click â search_autocomplete_select 77 // ------------------------------------------------------------------------- 78 // Reads the term from the input that owns the active autocomplete widget 79 // (the closest text input to the suggestion list, regardless of which form 80 // it lives in). 81 document.addEventListener('click', function (e) { 82 var link = nearest(e.target, '.autocompleteResult a, .ui-autocomplete .ui-menu-item a, .autocomplete-suggestion a'); 83 if (!link) return; 84 var widget = link.closest('.ui-autocomplete, .autocompleteResult, .autocomplete-suggestion'); 85 var owner = widget && widget.previousElementSibling; 86 var input = 87 (owner && owner.matches && owner.matches('input') ? owner : null) || 88 (widget && widget.parentElement && widget.parentElement.querySelector('input[type="text"]')) || 89 document.activeElement && document.activeElement.matches && document.activeElement.matches('input[type="text"]') ? document.activeElement : null; 90 var term = input ? (input.value || '').trim() : ''; 91 track('search_autocomplete_select', { 92 search_term: term, 93 suggestion: trim100(link.textContent), 94 }); 95 }, true); 96 97 // ------------------------------------------------------------------------- 98 // 3. Catalog filter / sort / pagination 99 // ------------------------------------------------------------------------- 100 // Map matches the abbreviated input names emitted by inc_catalog_search_and_filter.php 101 // (subj[], clang[], ccert[], drtn[], crdt[], cpacing[], ctype[]). 102 var FILTER_TAX_TO_EVENT = { 103 'subj': 'catalog_filter_change_subject', 104 'clang': 'catalog_filter_change_language', 105 'ccert': 'catalog_filter_change_certificate', 106 'drtn': 'catalog_filter_change_duration', 107 'crdt': 'catalog_filter_change_credit', 108 'cpacing': 'catalog_filter_change_pacing', 109 'ctype': 'catalog_filter_change_type', 110 }; 111 var SORT_VALUE_TO_EVENT = { 112 'rel': 'catalog_sort_change_relevance', 113 'new': 'catalog_sort_change_newest', 114 'old': 'catalog_sort_change_oldest', 115 'az': 'catalog_sort_change_az', 116 'za': 'catalog_sort_change_za', 117 }; 118 119 document.addEventListener('change', function (e) { 120 var el = e.target; 121 if (!el.name) return; 122 123 var rawName = el.name.replace(/\[\]$/, ''); 124 if (FILTER_TAX_TO_EVENT[rawName]) { 125 track(FILTER_TAX_TO_EVENT[rawName], { 126 filter_value: trim100(el.value), 127 }); 128 return; 129 } 130 131 if (el.name === 'sort' || el.id === 'newCatalogSortSelect') { 132 var ev = SORT_VALUE_TO_EVENT[el.value]; 133 if (ev) track(ev, {}); 134 } 135 }, true); 136 137 // Pagination clicks. 138 document.addEventListener('click', function (e) { 139 var pg = nearest(e.target, '.paging[data-page]'); 140 if (!pg) return; 141 var n = parseInt(pg.getAttribute('data-page'), 10);
142 if (!isNaN(n)) track('catalog_pagination', { page_number: n }); 143 }, true); 144 145 // ------------------------------------------------------------------------- 146 // 4. Sharing â share_copy_link, share_facebook, share_linkedin 147 // ------------------------------------------------------------------------- 148 document.addEventListener('click', function (e) { 149 var copy = nearest(e.target, '#shareLink, #shareLink1, [data-share="copy"]'); 150 if (copy) { track('share_copy_link', {}); return; } 151 152 var fb = nearest(e.target, 'a[href*="facebook.com/sharer"]'); 153 if (fb) { track('share_facebook', {}); return; } 154 155 var li = nearest(e.target, 'a[href*="linkedin.com/sharing"], a[href*="linkedin.com/shareArticle"]'); 156 if (li) { track('share_linkedin', {}); return; } 157 }, true); 158 159 // ------------------------------------------------------------------------- 160 // 5. Course CTA â click_continue_to_course 161 // ------------------------------------------------------------------------- 162 // Three CTA classes: 163 // .linkToEdx â enrolled course button (header) 164 // .register_api_architect â unenrolled course enroll 165 // .register_api_architect_on_program â program-level enroll button 166 // .register_api_architect_on_program_single_course â course-card inside program 167 document.addEventListener('click', function (e) { 168 var cta = nearest(e.target, 169 '.linkToEdx, .register_api_architect, .register_api_architect_on_program, .register_api_architect_on_program_single_course' 170 ); 171 if (!cta) return; 172 var id = cta.getAttribute('data-id') || ''; 173 var ctxItemId = readDataLayerCtx('item_id') || ''; 174 var ctaLocation; 175 if (cta.classList.contains('linkToEdx')) ctaLocation = 'item_header'; 176 else if (cta.classList.contains('register_api_architect_on_program')) ctaLocation = 'program_enroll'; 177 else if (cta.classList.contains('register_api_architect_on_program_single_course')) ctaLocation = 'item_card'; 178 else ctaLocation = 'item_card'; 179 track('click_continue_to_course', { 180 item_id: (id || ctxItemId || '').toString(), 181 cta_location: ctaLocation, 182 }); 183 }, true); 184 185 function readDataLayerCtx(key) { 186 try { 187 var dl = window.dataLayer || []; 188 for (var i = dl.length - 1; i >= 0; i--) { 189 var row = dl[i]; 190 if (row && row.event === 'page_context' && key in row) return row[key]; 191 } 192 } catch (e) {} 193 return undefined; 194 } 195 196 // ------------------------------------------------------------------------- 197 // 6. Contact page â phone, email, social, form submit/failure 198 // ------------------------------------------------------------------------- 199 document.addEventListener('click', function (e) { 200 var link = nearest(e.target, 'a[href^="tel:"], a[href^="mailto:"]'); 201 if (link) { 202 if (link.href.indexOf('tel:') === 0) track('contact_phone_click', {}); 203 if (link.href.indexOf('mailto:') === 0) track('contact_email_click', {}); 204 return; 205 } 206 var contactSocial = nearest(e.target, '.template-page-contact a, [data-contact-social] a, .contact-page a, .the-link.campus-seo-strip-click'); 207 if (contactSocial) { 208 var href = contactSocial.getAttribute('href') || ''; 209 var platform = detectPlatform(href); 210 if (platform) track('contact_social_click_' + platform, {}); 211 return; 212 } 213 }, true); 214 215 function detectPlatform(href) { 216 if (/facebook\.com/i.test(href)) return 'facebook'; 217 if (/linkedin\.com/i.test(href)) return 'linkedin'; 218 if (/(t\.me|telegram\.me)/i.test(href)) return 'telegram'; 219 if (/(twitter\.com|x\.com)/i.test(href)) return 'twitter'; 220 if (/instagram\.com/i.test(href)) return 'instagram'; 221 if (/(youtube\.com|youtu\.be)/i.test(href)) return 'youtube'; 222 return null; 223 } 224 225 // Contact form success/failure are wired directly inside the form's 226 // jQuery validate config (invalidHandler + submitHandler) in 227 // template-page-contact.php â this avoids the dual-listener duplicate-fire. 228 229 // ------------------------------------------------------------------------- 230 // 7. Footer menu + footer socials 231 // ------------------------------------------------------------------------- 232 document.addEventListener('click', function (e) { 233 var footer = nearest(e.target, 'footer, .footer, .siteFooter'); 234 if (!footer) return; 235 var a = nearest(e.target, 'a'); 236 if (!a) return; 237 var platform = detectPlatform(a.getAttribute('href') || ''); 238 if (platform) { 239 track('footer_social_click_' + platform, {}); 240 } else { 241 track('footer_menu_click', { 242 link_text: trim100(a.textContent), 243 link_url: a.getAttribute('href') || '', 244 }); 245 } 246 }, true); 247 248 // ------------------------------------------------------------------------- 249 // 8. Mobile menu open / close 250 // ------------------------------------------------------------------------- 251 // Direct click listeners only â the previous MutationObserver fallback 252 // caused double-fires. 253 // The drawer's own controls: the burger opens it, and everything that closes it carries 254 // data-cil-drawer-close â the scrim as well as the cross. 255 ready(function () { 256 var btn = document.getElementById('cilMobileMenuToggle'); 257 if (btn) btn.addEventListener('click', function () { track('mobile_menu_open', {}); }); 258 document.querySelectorAll('[data-cil-drawer-close]').forEach(function (close) { 259 close.addEventListener('click', function () { track('mobile_menu_close', {}); }); 260 }); 261 }); 262 263 // ------------------------------------------------------------------------- 264 // 9. Language switch â language_change_to_{he|en|ar|ru} 265 // ------------------------------------------------------------------------- 266 // WPML renders the Hebrew variant with href="/" (no /he/ segment) and the 267 // language code only on a child span[lang]. Read all known sources. 268 document.addEventListener('click', function (e) { 269 var item = nearest(e.target, '#languagesMenu a, #cilMobileLanguageSheet a, .language-switch a, [data-lang]'); 270 if (!item) return; 271 var inner = item.querySelector('[lang]'); 272 // WPML's Hebrew (default) anchor has no data-lang/hreflang/inner-lang and href "/" 273 // â read the parent <li class="wpml-ls-item-{he|en|ar|ru}"> as a fallback. 274 var li = item.closest ? item.closest('li[class*="wpml-ls-item-"]') : null; 275 var liLang = li ? (((li.className || '').match(/wpml-ls-item-(he|en|ar|ru)\b/) || [])[1] || '') : ''; 276 var lang = 277 (item.getAttribute('data-lang') || '').toLowerCase() || 278 (item.getAttribute('hreflang') || '').toLowerCase() || 279 (inner ? (inner.getAttribute('lang') || '').toLowerCase() : '') || 280 (item.getAttribute('lang') || '').toLowerCase() || 281 ((item.getAttribute('href') || '').match(/\/(he|en|ar|ru)(?:\/|$)/i) || [])[1] || 282 liLang || 283 ''; 284 lang = lang.toLowerCase(); 285 if (/^(he|en|ar|ru)$/.test(lang)) { 286 track('language_change_to_' + lang, {}); 287 } 288 }, true); 289 290 // ------------------------------------------------------------------------- 291 // 10. Events archive â event_card_click_{upcoming|past} 292 // ------------------------------------------------------------------------- 293 document.addEventListener('click', function (e) { 294 var card = nearest(e.target, '.events2Item, .event-card'); 295 if (!card) return; 296 var id = ((card.id || '').match(/(\d+)/) || [])[1] || ''; 297 var status = /Upcoing|Upcoming/i.test(card.id) ? 'upcoming' : (/Past/i.test(card.id) ? 'past' : null); 298 if (!status || !id) return; 299 track('event_card_click_' + status, { event_id: id }); 300 }, true); 301 302 // ------------------------------------------------------------------------- 303 // 11. Course / item cards â select_item 304 // ------------------------------------------------------------------------- 305 // Catalog cards use [data-event-course-name] without data-id; fall back to
306 // extracting the course/program post ID from anchor URL or query string. 307 document.addEventListener('click', function (e) { 308 var card = nearest(e.target, '.courseCard, .courseCardNew, .courseInfoBlock, [data-event-course-name]'); 309 if (!card) return; 310 // Walk up to a wrapper with data-id (catalog uses `[data-event-course-name]` 311 // on the inner anchor without data-id, but `.courseCardNew` carries it). 312 var idHolder = card.hasAttribute('data-id') ? card : (card.closest ? card.closest('[data-id]') : null); 313 var id = (idHolder ? idHolder.getAttribute('data-id') : '') 314 || (card.id ? ((card.id.match(/(\d+)$/) || [])[1] || '') : '') 315 || readPostIdFromAnchor(e.target) 316 || ''; 317 if (!id) return; 318 track('select_item', { item_id: id.toString() }); 319 }, true); 320 321 function readPostIdFromAnchor(target) { 322 var a = nearest(target, 'a[href]'); 323 if (!a) return ''; 324 var href = a.getAttribute('href') || ''; 325 var m = href.match(/\/courses?\/(\d+)|\/programs?\/(\d+)/i); 326 if (m) return m[1] || m[2] || ''; 327 var pid = href.match(/[?&]pid=(\d+)/i); 328 return pid ? pid[1] : ''; 329 } 330 331 // ------------------------------------------------------------------------- 332 // 12. Subject / lecturer / institution cards 333 // ------------------------------------------------------------------------- 334 document.addEventListener('click', function (e) { 335 var subj = nearest(e.target, 'a.mainSubject, a.subSubject, a[href*="/course_subjects/"]'); 336 if (subj) { 337 track('select_subject', { subject_name: trim100(subj.textContent) }); 338 return; 339 } 340 var lec = nearest(e.target, '.lecturerCard a, a[href*="/lecturers/"], a[href*="/lecturer/"]'); 341 if (lec) { 342 var lecCard = nearest(lec, '.lecturerCard'); 343 var lecName = lecCard ? trim100((lecCard.querySelector('h3, .text h3, .name') || {}).textContent) : trim100(lec.textContent); 344 track('select_lecturer', { lecturer_name: lecName }); 345 return; 346 } 347 var inst = nearest(e.target, '.institutionCard a, a[href*="/institution/"]'); 348 if (inst) { 349 var instCard = nearest(inst, '.institutionCard'); 350 var instName = instCard ? trim100((instCard.querySelector('h3, .text h3, .name') || {}).textContent) : trim100(inst.textContent); 351 track('select_institution', { institution_name: instName }); 352 return; 353 } 354 }, true); 355 356 // ------------------------------------------------------------------------- 357 // 13. Outbound clicks â outbound_click 358 // ------------------------------------------------------------------------- 359 // Fires when a user clicks a link to a different host. Internal subdomains 360 // of campus.gov.il are treated as internal. 361 document.addEventListener('click', function (e) { 362 var a = nearest(e.target, 'a[href]'); 363 if (!a) return; 364 var href = a.getAttribute('href') || ''; 365 if (!/^https?:\/\//i.test(href)) return;
366 var h = host(href); 367 if (!h) return; 368 var current = location.hostname; 369 // Treat any *.campus.gov.il host as internal. 370 if (h === current || /(^|\.)campus\.gov\.il$/i.test(h)) return; 371 // Suppress when a more specific handler already claims the click 372 // (share/social platforms, footer menu, contact socials, share dialogs). 373 if (detectPlatform(href)) return; 374 if (/facebook\.com\/sharer|linkedin\.com\/(sharing|shareArticle)/i.test(href)) return; 375 if (nearest(a, 'footer, .footer, .siteFooter, .template-page-contact, .contact-page, [data-contact-social]')) return; 376 track('outbound_click', { 377 link_url: href, 378 link_domain: h, 379 }); 380 }, true); 381 382 // ------------------------------------------------------------------------- 383 // 14. List impressions â view_item_list_* 384 // ------------------------------------------------------------------------- 385 // IntersectionObserver fires once per matching container per page load. 386 ready(function () { 387 if (typeof IntersectionObserver !== 'function') return; 388 389 var lists = [ 390 { selector: '#catalogSearchResults, #catalogSearchResults2', event: function () { 391 return /[?&]srch=/.test(location.search) ? 'view_item_list_search_results' : 'view_item_list_catalog'; 392 }, cardSelector: '.courseCard, .courseCardNew, .courseInfoBlock, [data-event-course-name]' }, 393 { selector: '.stripeCoursesCarusel, #myCourses', event: 'view_item_list_home_strip', cardSelector: '.courseCard, .courseCardNew' }, 394 { selector: '.featuringSubject', event: 'view_item_list_featured', cardSelector: '.courseCard, .courseCardNew, a', extraParams: function (container) { 395 var h = container.querySelector('h3, h2, .text h3'); 396 return h ? { list_id: trim100(h.textContent) } : {}; 397 } }, 398 // view_item_list_related is server-pushed from single-program.php 399 // (IO race made client-side observation unreliable). 400 ]; 401 402 function tryFire(container, cfg) { 403 if (container.__campusListTracked) return; 404 var count = container.querySelectorAll(cfg.cardSelector).length; 405 if (count === 0) return; 406 var rect = container.getBoundingClientRect(); 407 if (rect.bottom < 0 || rect.top > (window.innerHeight || 0)) return; 408 var name = typeof cfg.event === 'function' ? cfg.event() : cfg.event; 409 var params = { item_count: count }; 410 if (typeof cfg.extraParams === 'function') { 411 var extra = cfg.extraParams(container) || {}; 412 Object.keys(extra).forEach(function (k) { params[k] = extra[k]; }); 413 } 414 trackWhenReady(name, params); 415 container.__campusListTracked = true; 416 if (container.__campusListIO) container.__campusListIO.disconnect(); 417 if (container.__campusListMO) container.__campusListMO.disconnect(); 418 } 419 420 function attachObservers() { 421 lists.forEach(function (cfg) { 422 document.querySelectorAll(cfg.selector).forEach(function (container) { 423 if (container.__campusListTracked) return; 424 tryFire(container, cfg); 425 if (container.__campusListTracked) return; 426 if (!container.__campusListIO) { 427 container.__campusListIO = new IntersectionObserver(function (entries) { 428 entries.forEach(function (entry) { 429 if (entry.isIntersecting) tryFire(container, cfg); 430 }); 431 }, { threshold: 0.1 }); 432 container.__campusListIO.observe(container); 433 } 434 if (!container.__campusListMO && typeof MutationObserver === 'function') { 435 container.__campusListMO = new MutationObserver(function () { tryFire(container, cfg); }); 436 container.__campusListMO.observe(container, { childList: true, subtree: true }); 437 } 438 }); 439 }); 440 } 441 442 // Initial pass + retries to catch AJAX-loaded containers (catalog results 443 // arrive after DOMContentLoaded). 444 attachObservers(); 445 [500, 1500, 3500].forEach(function (delay) { 446 setTimeout(attachObservers, delay); 447 }); 448 449 // Watch for late DOM additions of list containers. 450 if (typeof MutationObserver === 'function') { 451 new MutationObserver(attachObservers).observe(document.body, { childList: true, subtree: true }); 452 } 453 }); 454 455 // ------------------------------------------------------------------------- 456 // Cross-page attribution: municipality â course/program. 457 // On a municipality page, capture clicks on tagged cards and stash a 458 // short-lived referrer record. On the next course/program page, replay the 459 // referrer onto view_item and click_continue_to_course. 460 // ------------------------------------------------------------------------- 461 var ATTRIB_KEY = 'campus_attrib_referrer'; 462 var ATTRIB_TTL_MS = 30 * 60 * 1000; // 30 min 463 464 function readAttrib() { 465 try { 466 var raw = window.sessionStorage.getItem(ATTRIB_KEY); 467 if (!raw) return null; 468 var parsed = JSON.parse(raw); 469 if (!parsed || !parsed.ts || (Date.now() - parsed.ts) > ATTRIB_TTL_MS) { 470 window.sessionStorage.removeItem(ATTRIB_KEY); 471 return null; 472 } 473 return parsed; 474 } catch (e) { return null; } 475 } 476 477 function writeAttrib(rec) { 478 try { 479 window.sessionStorage.setItem(ATTRIB_KEY, JSON.stringify(rec)); 480 } catch (e) { /* sessionStorage disabled â silent */ } 481 } 482
483 function attribParams() { 484 var r = readAttrib(); 485 if (!r) return null; 486 var out = {}; 487 if (r.list_id) out.referrer_list_id = r.list_id; 488 if (r.municipality_id) out.referrer_municipality_id = r.municipality_id; 489 if (r.municipality_name) out.referrer_municipality_name = r.municipality_name; 490 return out; 491 } 492 493 ready(function () { 494 // Capture on municipality pages. 495 document.addEventListener('click', function (ev) { 496 var card = nearest(ev.target, '[data-campus-attrib-card]'); 497 if (!card) return; 498 var carousel = nearest(card, '[data-campus-attrib-list-id]'); 499 if (!carousel) return; 500 var rec = { 501 ts: Date.now(), 502 list_id: carousel.getAttribute('data-campus-attrib-list-id') || '', 503 municipality_id: carousel.getAttribute('data-campus-attrib-municipality-id') || '', 504 municipality_name: carousel.getAttribute('data-campus-attrib-municipality-name') || '' 505 }; 506 writeAttrib(rec); 507 track('select_item', { 508 item_id: card.getAttribute('data-campus-item-id') || '', 509 item_name: card.getAttribute('data-campus-item-name') || '', 510 item_type: card.getAttribute('data-campus-item-type') || '', 511 list_id: rec.list_id, 512 municipality_id: rec.municipality_id, 513 municipality_name: rec.municipality_name, 514 position: parseInt(card.getAttribute('data-campus-position') || '0', 10) || undefined 515 }); 516 }, true); 517 518 // Replay on the next page: stamp view_item (if server pushed one) and 519 // click_continue_to_course with referrer_* params. We re-push view_item 520 // with attribution as an additive event (lightweight; same item_id) only 521 // when an attribution exists. To avoid double-counting we send a dedicated 522 // param-only enrichment by re-using view_item with the same item_id â 523 // safe because GA4 dedupes by event+timestamp+params and analysts can 524 // filter on referrer_municipality_id IS NOT NULL. 525 var attrib = attribParams(); 526 if (!attrib) return; 527 528 var dl = window.dataLayer || []; 529 var lastViewItem = null; 530 for (var i = dl.length - 1; i >= 0; i--) { 531 if (dl[i] && dl[i].event === 'view_item') { lastViewItem = dl[i]; break; } 532 } 533 if (lastViewItem && (lastViewItem.item_type === 'course' || lastViewItem.item_type === 'program')) { 534 track('view_item', Object.assign({ 535 item_type: lastViewItem.item_type, 536 item_id: lastViewItem.item_id, 537 item_name: lastViewItem.item_name 538 }, attrib)); 539 } 540 541 // Wrap click_continue_to_course pushes elsewhere by intercepting CTA 542 // clicks here as a safety net. Existing click_continue_to_course wiring 543 // pushes its own event; we additionally listen for the same selectors and 544 // push an attribution-stamped variant. 545 document.addEventListener('click', function (ev) { 546 var cta = nearest(ev.target, '.continue-to-course, [data-campus-cta="continue_to_course"], a.courseActionBtnMain'); 547 if (!cta) return; 548 if (!lastViewItem || lastViewItem.item_type !== 'course') return; 549 track('click_continue_to_course', Object.assign({ 550 item_id: lastViewItem.item_id, 551 cta_location: cta.getAttribute('data-campus-cta-location') || 'course_page' 552 }, attrib)); 553 }, true); 554 }); 555 556 // ------------------------------------------------------------------------- 557 // Municipality grid catalog (.courseCardNew[data-id]) â covers muni pages 558 // whose courses come from the AJAX grid rather than the curated carousel. 559 // Fires view_item_list_municipality once the grid populates and stamps 560 // attribution + select_item on card click. 561 // ------------------------------------------------------------------------- 562 function muniCtx() { 563 var dl = window.dataLayer || []; 564 var ctx = dl.find(function (e) { return e && e.event === 'page_context' && e.page_type === 'municipality'; }); 565 if (!ctx) return null; 566 var slug = ''; 567 try { 568 var path = location.pathname.replace(/\/$/, '').split('/'); 569 slug = decodeURIComponent(path[path.length - 1] || ''); 570 } catch (e) { slug = ''; } 571 return { 572 municipality_id: ctx.municipality_id || ctx.item_id || '', 573 municipality_name: ctx.municipality_name || ctx.item_name || '', 574 list_id: 'municipality_' + (slug || ctx.item_id || ''), 575 list_name: ctx.municipality_name || ctx.item_name || '' 576 }; 577 } 578 579 ready(function () { 580 var ctx = muniCtx(); 581 if (!ctx) return; 582 583 // One-time impression once grid cards are present. 584 var impressionSent = false;
585 function maybeFireImpression() { 586 if (impressionSent) return; 587 var cards = document.querySelectorAll('.courseCardNew[data-id]'); 588 if (!cards.length) return; 589 impressionSent = true; 590 track('view_item_list_municipality', { 591 list_id: ctx.list_id, 592 list_name: ctx.list_name, 593 municipality_id: ctx.municipality_id, 594 municipality_name: ctx.municipality_name, 595 item_count: cards.length 596 }); 597 } 598 maybeFireImpression(); 599 [500, 1500, 3500].forEach(function (d) { setTimeout(maybeFireImpression, d); }); 600 if (typeof MutationObserver === 'function') { 601 new MutationObserver(maybeFireImpression).observe(document.body, { childList: true, subtree: true }); 602 } 603 604 // Click capture: stamp attribution and fire select_item. 605 document.addEventListener('click', function (ev) { 606 var card = nearest(ev.target, '.courseCardNew[data-id]'); 607 if (!card) return; 608 var itemId = card.getAttribute('data-id') || ''; 609 var nameEl = card.querySelector('[data-event-course-name]'); 610 var itemName = (nameEl && nameEl.getAttribute('data-event-course-name')) || trim100(card.querySelector('h3')?.textContent || ''); 611 // Position: index among siblings. 612 var position = 0; 613 var siblings = document.querySelectorAll('.courseCardNew[data-id]'); 614 for (var i = 0; i < siblings.length; i++) { if (siblings[i] === card) { position = i + 1; break; } } 615 writeAttrib({ 616 ts: Date.now(), 617 list_id: ctx.list_id, 618 municipality_id: ctx.municipality_id, 619 municipality_name: ctx.municipality_name 620 }); 621 track('select_item', { 622 item_id: itemId, 623 item_name: itemName, 624 item_type: 'course', 625 list_id: ctx.list_id, 626 municipality_id: ctx.municipality_id, 627 municipality_name: ctx.municipality_name, 628 position: position || undefined 629 }); 630 }, true); 631 }); 632 633}());
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.