1(function () { 2 const SCRIPT_LOAD_TS = Date.now(); 3 const endpoint = webUrl('/rest/web/search_tracking'); 4 const SS_KEY = 'pa_search:guids'; 5 const SS_RESULTS_KEY = 'pa_search:results_pushed'; 6 const SS_SEEN_KEY = 'pa_search:seen'; 7 const SS_LAST_CLICK_KEY = 'pa_search:last_click'; 8 // CW (CanvasWorld) mobile renders thumbs directly into .shutterstock_list 9 // (no #category_thumb_wrapper), so include it to keep impressions working there. 10 const searchResultWrapperSelector = '#category_thumb_wrapper, #filter_design_list, .shutterstock_list'; 11 // Covers all brand variants: 12 // - PA/STI headers: form.site-search[name="siteSearch"] 13 // - PA/STI search pages: form.site-search[name="searchPage"], form.search-page[name="searchPage"] 14 // - MicroSite (PCRUS etc.): form[name="menu_search"] (customer/shared/templates/components/common/search.php) 15 // - CW (CanvasWorld): form.search_form with input[name="search_keywords"] 16 // (customer/cw/templates/parts/search_bar.php) 17 const searchFormSelector = 'form.site-search, form.search-page, form[name="siteSearch"], form[name="searchPage"], form[name="menu_search"], form.search_form'; 18 const thumbSelector = '.category_thumb[data-design-id], .design-item-wrapper[data-id]'; 19 const CPC_SITE_IDS = [170, 171, 172, 173]; 20 21 const INIT_TS = Math.floor(Date.now() / 1000); 22 23 function nowTs() { 24 return Math.floor(Date.now() / 1000); 25 } 26 27 function uuid() { 28 if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { 29 return crypto.randomUUID(); 30 } 31 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 32 const r = (Math.random() * 16) | 0; 33 const v = c === 'x' ? r : (r & 0x3) | 0x8; 34 return v.toString(16); 35 }); 36 } 37 38 function getQueryParam(name) { 39 try { 40 return new URLSearchParams(location.search).get(name) || ''; 41 } catch (e) { 42 return ''; 43 } 44 } 45 46 function isCpcSite() { 47 if (typeof SITE_ID === 'undefined') { 48 return false; 49 } 50 if (typeof SITE_CAFUS !== 'undefined') { 51 return SITE_ID === SITE_CAFUS || SITE_ID === SITE_CAFCA || SITE_ID === SITE_CAFAU || SITE_ID === SITE_CAFUK; 52 } 53 return CPC_SITE_IDS.indexOf(SITE_ID) !== -1; 54 } 55 56 // PA/STI thumbs expose design id via data-design-id; CPB (B2BDesigns) uses data-id. 57 function getThumbDesignId(node) { 58 return node.getAttribute('data-design-id') || node.getAttribute('data-id') || null; 59 } 60 61 // Mirrors PHP ThirdPartyImageController::getSearchKeywordsFromUri: 62 // decode the path segment, replace '-' with spaces, collapse whitespace, lowercase. 63 function normalizeCwTerm(raw) { 64 let term = raw; 65 try { 66 term = decodeURIComponent(term); 67 } catch (e) { 68 } 69 term = term.replace(/-/g, ' '); 70 term = term.replace(/\s+/g, ' ').trim(); 71 return term.toLowerCase(); 72 } 73 74 function getCwSearchTermFromPath() { 75 let m; 76 try { 77 m = location.pathname.match(/^\/search\/([^/]+)\/?/); 78 } catch (e) { 79 return ''; 80 } 81 if (!m) { 82 return ''; 83 } 84 return normalizeCwTerm(m[1]); 85 } 86 87 // CPC search-result PLP: /+term-slug (mirrors CpRoutingService::isSearchUri term extraction). 88 function getCpSearchTermFromPath() { 89 let m; 90 try { 91 m = location.pathname.match(/^\/(\+[\+\-\w]{1,100})$/); 92 } catch (e) { 93 return ''; 94 } 95 if (!m) { 96 return ''; 97 } 98 let raw = m[1]; 99 try { 100 raw = decodeURIComponent(raw); 101 } catch (e) { 102 } 103 const parts = raw.replace(/^\+/, '').split('+'); 104 let term = parts[0] || ''; 105 if (term.endsWith(' gifts')) { 106 term = term.slice(0, -6); 107 } 108 term = term.replace(/-/g, ' ').replace(/\s+/g, ' ').trim(); 109 return term; 110 } 111 112 function getSearchTermFromMenuForm() { 113 const input = document.querySelector('form[name="menu_search"] input[name="q"]'); 114 if (input && input.value) { 115 return input.value.trim(); 116 } 117 return ''; 118 } 119 120 function getSearchTermFromUrl() { 121 const q = getQueryParam('q'); 122 if (q) { 123 return q; 124 } 125 const menuTerm = getSearchTermFromMenuForm(); 126 if (menuTerm) { 127 return menuTerm; 128 } 129 if (getQueryParam('from_search') === '1') { 130 const cpTerm = getCpSearchTermFromPath(); 131 if (cpTerm) { 132 return cpTerm; 133 } 134 } 135 return getCwSearchTermFromPath(); 136 } 137 138 function getSearchResultsPageNumber() { 139 const pageParam = getQueryParam('page'); 140 if (pageParam) {
141 const p = parseInt(pageParam, 10); 142 if (!isNaN(p) && p > 0) { 143 return p; 144 } 145 } 146 try { 147 if (window.QUERY_PARAMS && window.QUERY_PARAMS.page) { 148 const p = parseInt(window.QUERY_PARAMS.page, 10); 149 if (!isNaN(p) && p > 0) { 150 return p; 151 } 152 } 153 } catch (e) { 154 } 155 return 1; 156 } 157 158 function isSearchPage() { 159 if (getQueryParam('from_search') === '1' && getSearchTermFromUrl() !== '') { 160 return true; 161 } 162 // CW (CanvasWorld): /search/{keyword}/ (optional ?nr=t) 163 return getCwSearchTermFromPath() !== ''; 164 } 165 166 function readGuidMap() { 167 return readMap(SS_KEY); 168 } 169 170 function writeGuidMap(map) { 171 writeMap(SS_KEY, map); 172 } 173 174 function getGuidForTerm(term) { 175 const map = readGuidMap(); 176 return map[term] ? map[term].guid : null; 177 } 178 179 function setGuidForTerm(term, guid) { 180 const map = readGuidMap(); 181 map[term] = {guid: guid, ts: nowTs()}; 182 writeGuidMap(map); 183 } 184 185 function readMap(key) { 186 try { 187 const raw = sessionStorage.getItem(key); 188 return raw ? JSON.parse(raw) : {}; 189 } catch (e) { 190 return {}; 191 } 192 } 193 194 function writeMap(key, map) { 195 try { 196 sessionStorage.setItem(key, JSON.stringify(map)); 197 } catch (e) { 198 } 199 } 200 201 function isResultsPushed(guid) { 202 return Boolean(readMap(SS_RESULTS_KEY)[guid]); 203 } 204 205 function markResultsPushed(guid) { 206 const map = readMap(SS_RESULTS_KEY); 207 map[guid] = true; 208 writeMap(SS_RESULTS_KEY, map); 209 } 210 211 function isSeen(guid, designId) { 212 const map = readMap(SS_SEEN_KEY); 213 return Boolean(map[guid] && map[guid][designId]); 214 } 215 216 function markSeen(guid, designId) { 217 const map = readMap(SS_SEEN_KEY); 218 if (!map[guid]) { 219 map[guid] = {}; 220 } 221 map[guid][designId] = true; 222 writeMap(SS_SEEN_KEY, map); 223 } 224 225 function writeLastClick(guid, designId, searchTerm, clickGuid) { 226 try { 227 const payload = { 228 guid: guid, 229 design_id: String(designId), 230 search_term: searchTerm, 231 ts: nowTs() 232 }; 233 if (clickGuid) { 234 payload.click_guid = clickGuid; 235 } 236 sessionStorage.setItem(SS_LAST_CLICK_KEY, JSON.stringify(payload)); 237 } catch (e) { 238 } 239 } 240 241 function readLastClick() { 242 try { 243 const raw = sessionStorage.getItem(SS_LAST_CLICK_KEY); 244 return raw ? JSON.parse(raw) : null; 245 } catch (e) { 246 return null; 247 } 248 } 249 250 // Clear any per-design tracking recorded under a previous search interaction 251 // (impressions via SS_SEEN_KEY, results-set push via SS_RESULTS_KEY) so a new 252 // search guid starts from a clean slate. 253 function clearGuidTracking(guid) { 254 if (!guid) { 255 return; 256 } 257 const seen = readMap(SS_SEEN_KEY); 258 if (seen[guid]) { 259 delete seen[guid]; 260 writeMap(SS_SEEN_KEY, seen); 261 } 262 const results = readMap(SS_RESULTS_KEY); 263 if (results[guid]) { 264 delete results[guid]; 265 writeMap(SS_RESULTS_KEY, results); 266 } 267 } 268 269 // --- batched sendBeacon sender --- 270 const queue = []; 271 let flushScheduled = false; 272 273 function flush() { 274 if (!queue.length) { 275 return; 276 } 277 const payload = JSON.stringify(queue.splice(0, queue.length)); 278 try { 279 if (navigator.sendBeacon) { 280 navigator.sendBeacon(endpoint, payload); 281 return; 282 } 283 } catch (e) { 284 } 285 try { 286 fetch(endpoint, { 287 method: 'POST', 288 body: payload, 289 keepalive: true, 290 headers: {'Content-Type': 'application/json'} 291 }).catch(function () {}); 292 } catch (e) { 293 } 294 } 295 296 function scheduleFlush() { 297 if (flushScheduled) { 298 return; 299 } 300 flushScheduled = true; 301 Promise.resolve().then(function () { 302 flushScheduled = false;
303 flush(); 304 }); 305 } 306 307 window.addEventListener('pagehide', flush); 308 309 // --- the real _pa_search object --- 310 const api = { 311 push: function (event, data, timestamp) { 312 const ts = timestamp != null ? timestamp : nowTs(); 313 queue.push({event: event, timestamp: ts, data: data}); 314 scheduleFlush(); 315 } 316 }; 317 318 // --- replay pre-registered queue --- 319 const queued = window._pa_search; 320 window._pa_search = api; 321 if (Array.isArray(queued)) { 322 for (let i = 0; i < queued.length; i++) { 323 const item = queued[i]; 324 if (Array.isArray(item) && item.length >= 1) { 325 api.push(item[0], item[1], item[2] != null ? item[2] : INIT_TS); 326 } 327 } 328 } 329 330 // --- Feature 1: search event --- 331 // Dedup window: the click fallback and the submit handler both fire on a 332 // normal submit-button click (click precedes submit). Treat repeated calls 333 // for the same term within this window as one search so we don't push two 334 // `search` events with two different GUIDs for a single interaction. 335 const SEARCH_DEDUP_WINDOW_SEC = 2; 336 337 function recordSearch(term) { 338 const map = readGuidMap(); 339 const existing = map[term]; 340 const now = nowTs(); 341 if (existing && existing.guid && (now - existing.ts) <= SEARCH_DEDUP_WINDOW_SEC) { 342 // Reuse the GUID just created for this term; don't push again. 343 return existing.guid; 344 } 345 const previousGuid = existing ? existing.guid : null; 346 const guid = uuid(); 347 map[term] = {guid: guid, ts: now}; 348 writeGuidMap(map); 349 if (previousGuid && previousGuid !== guid) { 350 clearGuidTracking(previousGuid); 351 } 352 api.push('search', { 353 guid: guid, 354 search_term: term, 355 user_agent: navigator.userAgent 356 }); 357 return guid; 358 } 359 360 function getSearchTermFromForm(form) { 361 const q = form.querySelector('input[name="q"]'); 362 if (q && q.value) { 363 return q.value.trim(); 364 } 365 // CW uses input[name="search_keywords"]; normalize to the same form the 366 // results page will expose in the URL (see getSearchTermFromUrl), so the 367 // GUID recorded on submit matches the one looked up on the results page. 368 const cw = form.querySelector('input[name="search_keywords"]'); 369 if (cw && cw.value) { 370 return normalizeCwTerm(cw.value); 371 } 372 return ''; 373 } 374 375 document.addEventListener('submit', function (e) { 376 const form = e.target; 377 if (!form || !form.matches || !form.matches(searchFormSelector)) { 378 return; 379 } 380 const term = getSearchTermFromForm(form); 381 if (!term) { 382 return; 383 } 384 recordSearch(term); 385 }, true); 386 387 // Fallback: generate the GUID on submit-button click too, in case submit is prevented 388 // or the form navigates before the submit handler runs. 389 document.addEventListener('click', function (e) { 390 const form = e.target.closest && e.target.closest(searchFormSelector); 391 if (!form) { 392 return; 393 } 394 if (e.target.matches && (e.target.matches('input[type="submit"]') || e.target.matches('button[type="submit"]'))) { 395 const term = getSearchTermFromForm(form); 396 if (!term) { 397 return; 398 } 399 recordSearch(term); 400 } 401 }, true); 402 403 // Enter in the search input: MicroSite (PCRUS etc.) calls preventDefault() on 404 // Enter keydown and re-triggers submit via jQuery `.trigger('submit')`, which 405 // does not reach our native addEventListener submit handler. Catch Enter at 406 // capture phase (before that preventDefault) so the search event still fires. 407 // On brands where native submit also fires, the dedup window in recordSearch 408 // collapses the two calls into one event. 409 document.addEventListener('keydown', function (e) { 410 if ((e.key !== 'Enter' && e.keyCode !== 13) || !e.target) { 411 return; 412 } 413 const form = e.target.closest && e.target.closest(searchFormSelector); 414 if (!form) { 415 return; 416 } 417 const term = getSearchTermFromForm(form); 418 if (!term) { 419 return; 420 } 421 recordSearch(term); 422 }, true); 423
424 // --- Feature 2: search results (only on search page) --- 425 if (isSearchPage()) { 426 const term = getSearchTermFromUrl(); 427 let guid = getGuidForTerm(term); 428 if (!guid) { 429 // No matching GUID for this term (e.g. direct link to results, or 430 // term mismatch). Create a new one and push a 'search' event for it. 431 guid = uuid(); 432 setGuidForTerm(term, guid); 433 api.push('search', { 434 guid: guid, 435 search_term: term, 436 user_agent: navigator.userAgent 437 }); 438 } 439 440 // search_results = the searched result set, sourced from qFilterObj.items. 441 // Pushed at most once per guid per tab (deduped across back/forward via 442 // sessionStorage). qFilterObj is built on document ready, so this may be 443 // retried until items are available. 444 function pushSearchResultsOnce() { 445 if (isResultsPushed(guid)) { 446 return; 447 } 448 let designIds = null; 449 let pageSize = 0; 450 451 const qf = window.qFilterObj || window.qfDataObj; 452 if (qf && qf.items && qf.items.length) { 453 // PA/STI/MicroSite use window.qFilterObj; CW (CanvasWorld) uses 454 // window.qfDataObj. Both are QuickFilter instances with the same 455 // items / fullData.structure_indexes / itemsPerPage shape. 456 let idx = 0; 457 try { 458 idx = (qf.fullData && qf.fullData.structure_indexes && qf.fullData.structure_indexes.ID) || 0; 459 } catch (e) { 460 } 461 designIds = []; 462 for (let i = 0; i < qf.items.length; i++) { 463 const id = qf.items[i] ? qf.items[i][idx] : null; 464 if (id) { 465 designIds.push(id); 466 } 467 } 468 // page_size = configured items per page (default 36 / mobile 18 / `ipp` override), 469 // NOT pageCounting which is the count of items rendered on the current page. 470 pageSize = Number(qf.itemsPerPage) || 0; 471 } else if (window.SimpleFilterFullData && Array.isArray(window.SimpleFilterFullData.items) && window.SimpleFilterFullData.items.length) { 472 // CPB (cpbus) search page: designs come from SimpleFilterFullData (server returns the 473 // full set; B2BDesigns paginates client-side). page_size is the configured per-page 474 // size from the B2BDesigns instance (parseInt(ipp) || 24). 475 designIds = window.SimpleFilterFullData.items.map(function (v) { 476 return v && v.id; 477 }).filter(Boolean); 478 try { 479 pageSize = Number(window.b2bDesigns && window.b2bDesigns.pagination && window.b2bDesigns.pagination.page_size) || 0; 480 } catch (e) { 481 } 482 } else if (isCpcSite() && window.PRODUCT_ITEMS) { 483 // CPC (CAFUS/CAFCA/CAFAU/CAFUK): server-rendered first page only for search_results. 484 if (getSearchResultsPageNumber() > 1) { 485 return; 486 } 487 let items = window.PRODUCT_ITEMS; 488 if (!Array.isArray(items)) { 489 items = Object.values(items); 490 } 491 designIds = []; 492 for (let i = 0; i < items.length; i++) { 493 const item = items[i]; 494 const id = item && (item.design_id != null ? item.design_id : item.id); 495 if (id) { 496 designIds.push(id); 497 } 498 } 499 try { 500 pageSize = Number(window.QUERY_PARAMS && window.QUERY_PARAMS.n) || 60; 501 } catch (e) { 502 pageSize = 60; 503 } 504 } 505 506 if (!designIds || !designIds.length) { 507 return; 508 } 509 markResultsPushed(guid); 510 api.push('search_results', { 511 guid: guid, 512 search_term: term, 513 design_ids: designIds, 514 page_size: pageSize 515 }); 516 } 517 518 // impressions (design_view) â handled only by the IntersectionObserver. 519 // Deduped per (guid, design) via sessionStorage so back/forward does not 520 // re-count the same design under the same guid. All newly-seen designs in 521 // one observer tick are pushed together as a single event with an array.
522 // 523 // PA/STI QuickFilter stores each card's thumb block inside a 524 // <script type="text/html"> template that its own IO expands on demand. 525 // Until hydration the card only contains the description (~65px) instead 526 // of its final ~49vw+description height, so many short cards temporarily 527 // fit the first viewport and inflate design_view impressions. To prevent 528 // this, a .category_thumb is only observed once its .thumb_wrapper is in 529 // the live DOM. Other card types (e.g. CPB .design-item-wrapper) don't 530 // use this lazy pattern and are observed immediately. 531 function observeCardIfReady(card) { 532 if (!card || card._paSearchObserved) { 533 return false; 534 } 535 if (card.matches('.category_thumb') && !card.querySelector('.thumb_wrapper')) { 536 return false; 537 } 538 card._paSearchObserved = true; 539 io.observe(card); 540 return true; 541 } 542 543 const io = new IntersectionObserver(function (entries) { 544 const newIds = []; 545 entries.forEach(function (entry) { 546 if (!entry.isIntersecting) { 547 return; 548 } 549 const id = getThumbDesignId(entry.target); 550 if (id && !isSeen(guid, id)) { 551 markSeen(guid, id); 552 newIds.push(id); 553 } 554 }); 555 if (newIds.length) { 556 api.push('design_view', {guid: guid, search_term: term, design_ids: newIds}); 557 } 558 }, {threshold: 0.5}); 559 560 function observeThumbs(root) { 561 (root || document).querySelectorAll(thumbSelector).forEach(function (node) { 562 observeCardIfReady(node); 563 }); 564 } 565 566 function watchWrapper(w) { 567 observeThumbs(w); 568 pushSearchResultsOnce(); 569 570 const mo = new MutationObserver(function (mutations) { 571 let addedAny = false; 572 mutations.forEach(function (m) { 573 m.addedNodes.forEach(function (node) { 574 if (node.nodeType !== 1) { 575 return; 576 } 577 // Collect candidate cards from this addition: both 578 // new .category_thumb nodes and existing cards whose 579 // .thumb_wrapper was just injected by lazy hydration. 580 const candidates = []; 581 if (node.matches && node.matches(thumbSelector)) { 582 candidates.push(node); 583 } 584 if (node.matches && node.matches('.thumb_wrapper')) { 585 const card = node.closest(thumbSelector); 586 if (card) candidates.push(card); 587 } 588 if (node.querySelectorAll) { 589 node.querySelectorAll(thumbSelector).forEach(function (t) { 590 candidates.push(t); 591 }); 592 node.querySelectorAll('.thumb_wrapper').forEach(function (wrapper) { 593 const card = wrapper.closest(thumbSelector); 594 if (card) candidates.push(card); 595 }); 596 } 597 candidates.forEach(function (card) { 598 observeCardIfReady(card); 599 addedAny = true; 600 }); 601 }); 602 }); 603 if (addedAny) { 604 pushSearchResultsOnce(); 605 } 606 }); 607 mo.observe(w, {childList: true, subtree: true}); 608 } 609 610 const wrapper = document.querySelector(searchResultWrapperSelector); 611 if (wrapper) { 612 watchWrapper(wrapper); 613 } else { 614 // wrapper not present yet; watch the whole document until it appears 615 const bodyMo = new MutationObserver(function () { 616 const w = document.querySelector(searchResultWrapperSelector); 617 if (w) { 618 bodyMo.disconnect(); 619 watchWrapper(w); 620 } 621 }); 622 bodyMo.observe(document.documentElement, {childList: true, subtree: true}); 623 } 624 625 // On mobile, jQuery Mobile turns a touch into a `tap` event and some 626 // QuickFilter templates re-trigger a synthetic `click` from `tap` 627 // (e.g. `return $(this).trigger('click')`). Both the native click and 628 // that synthetic click reach this capture-phase listener and would 629 // otherwise emit two identical `design_click` beacons. Dedup by 630 // (guid, designId) within this window; the window is short enough that 631 // a deliberate second click on the same card still records. 632 const CLICK_DEDUP_WINDOW_SEC = 1; 633 const lastClickByKey = new Map(); 634 635 function shouldRecordClick(guid, designId) {
636 const key = guid + ':' + designId; 637 const now = nowTs(); 638 const last = lastClickByKey.get(key); 639 if (last != null && (now - last) <= CLICK_DEDUP_WINDOW_SEC) { 640 return false; 641 } 642 lastClickByKey.set(key, now); 643 return true; 644 } 645 646 document.addEventListener('click', function (e) { 647 const card = e.target.closest && e.target.closest(thumbSelector); 648 if (!card) { 649 return; 650 } 651 const id = getThumbDesignId(card); 652 if (id && shouldRecordClick(guid, id)) { 653 const clickGuid = uuid(); 654 writeLastClick(guid, id, term, clickGuid); 655 api.push('design_click', { 656 guid: guid, 657 search_term: term, 658 design_ids: [id], 659 click_guid: clickGuid 660 }); 661 } 662 }, true); 663 } 664 665 // --- Feature 3: PDP funnel events (gated by last design_click) --- 666 function getPdpDesignId() { 667 if (typeof window.pa_product_id !== 'undefined' && window.pa_product_id !== null && window.pa_product_id !== '') { 668 return String(window.pa_product_id); 669 } 670 try { 671 if (window.product_options && window.product_options.design_id) { 672 return String(window.product_options.design_id); 673 } 674 } catch (e) { 675 } 676 return null; 677 } 678 679 function isPdpPage() { 680 if (document.body && ( 681 document.body.classList.contains('details_page') || 682 document.body.classList.contains('b2c_detail_page') 683 )) { 684 return true; 685 } 686 try { 687 if (/\/design-\d+/.test(location.pathname) || /-p-\d+/.test(location.pathname)) { 688 return true; 689 } 690 } catch (e) { 691 } 692 return getPdpDesignId() != null; 693 } 694 695 function isDesignerPage() { 696 if (document.body && ( 697 document.body.classList.contains('designer_page') || 698 document.body.classList.contains('designer_preview_page') 699 )) { 700 return true; 701 } 702 try { 703 const path = location.pathname; 704 if (path.indexOf('/products/designer') !== -1) { 705 return true; 706 } 707 if (path.indexOf('/preview-tool/step-2') !== -1 || path.indexOf('/preview-tool/step-1') !== -1) { 708 return true; 709 } 710 } catch (e) { 711 } 712 return false; 713 } 714 715 function getDwellMs() { 716 return Date.now() - SCRIPT_LOAD_TS; 717 } 718 719 function buildPdpEventData(context, extra) { 720 const data = { 721 guid: context.guid, 722 search_term: context.search_term, 723 design_id: context.design_id 724 }; 725 if (context.click_guid) { 726 data.click_guid = context.click_guid; 727 } 728 if (extra) { 729 for (const key in extra) { 730 if (Object.prototype.hasOwnProperty.call(extra, key)) { 731 data[key] = extra[key]; 732 } 733 } 734 } 735 return data; 736 } 737 738 function initPdpTracking() { 739 const lastClick = readLastClick(); 740 if (!lastClick || !lastClick.design_id) { 741 return; 742 } 743 744 let pdpInitialized = false; 745 746 function tryInitPdp() { 747 const designId = getPdpDesignId(); 748 if (!designId) { 749 return false; 750 } 751 if (designId !== String(lastClick.design_id)) { 752 return true; 753 } 754 if (pdpInitialized) { 755 return true; 756 } 757 pdpInitialized = true; 758 759 const context = { 760 guid: lastClick.guid, 761 search_term: lastClick.search_term, 762 design_id: designId, 763 click_guid: lastClick.click_guid || null 764 }; 765 766 let exitPushed = false; 767 768 function pushExitPdp() { 769 if (exitPushed) { 770 return; 771 } 772 exitPushed = true; 773 api.push('exit_pdp', buildPdpEventData(context, {dwell: getDwellMs()})); 774 flush(); 775 } 776 777 function pushHidePdp() { 778 api.push('hide_pdp', buildPdpEventData(context, {dwell: getDwellMs()})); 779 flush(); 780 } 781 782 if (!isDesignerPage()) { 783 window.addEventListener('pagehide', pushExitPdp); 784 document.addEventListener('visibilitychange', function () { 785 if (document.visibilityState === 'hidden') { 786 pushHidePdp(); 787 } 788 }); 789 } 790 791 document.addEventListener('click', function (e) { 792 const btn = e.target.closest && e.target.closest('[data-role-search-tracking]'); 793 if (!btn || btn.disabled || btn.classList.contains('out-of-stock') 794 || btn.classList.contains('disabled') || btn.classList.contains('disable')) { 795 return; 796 } 797 const role = btn.getAttribute('data-role-search-tracking'); 798 if (role === 'click_personalize' || role === 'add_to_cart') { 799 api.push(role, buildPdpEventData(context)); 800 } 801 }, true); 802 803 return true; 804 } 805 806 if (tryInitPdp()) { 807 return; 808 } 809 810 let attempts = 0; 811 const maxAttempts = 50; 812 813 function pollPdp() { 814 attempts++; 815 if (tryInitPdp() || attempts >= maxAttempts) { 816 return; 817 } 818 setTimeout(pollPdp, 100); 819 } 820 821 if (document.readyState === 'loading') { 822 document.addEventListener('DOMContentLoaded', pollPdp); 823 } else { 824 pollPdp(); 825 } 826 } 827 828 if (isPdpPage()) { 829 initPdpTracking(); 830 } 831})();
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.