1/* global pysOptions */ 2 3! function ($, options) { 4 if (options.debug) { 5 console.log('PYS:', options); 6 } 7 8 var uniqueId = {}; 9 10 // State tracking for AND trigger logic: { eventId: { triggered: {}, total: N, fired: false } } 11 var pysCustomEventsState = {}; 12 13 // State tracking for "once per page" fire frequency: { eventId: true } 14 var pysEventFiredOnce = {}; 15 if (options.triggerLogic) { 16 Object.entries(options.triggerLogic).forEach(function([eventId, logic]) { 17 if (logic === 'AND') { 18 var count = 0; 19 Object.values(options.triggerEventTypes).forEach(function(events) { 20 if (events.hasOwnProperty(eventId)) count++; 21 }); 22 pysCustomEventsState[eventId] = { triggered: {}, total: count, fired: false }; 23 } 24 }); 25 } 26 27 var firstVisit = false; 28 29 var isTrackEventForGA = []; 30 31 var isAdsLoad = false; 32 33 let gtm_variables = {}; 34 let gtm_datalayername = "dynamicVariable"; 35 // Resolved by cookieDomain() on first use, not here. 36 var probedCookieDomain = null; 37 38 /** 39 * Domain attribute for cookies we write. Probed once, on first use (not 40 * at load) so the probe's own write lands behind whatever consent gate 41 * the caller is behind. 42 * 43 * Not suppressed by options.cookie.disabled_all_cookie: deleting a 44 * cookie needs the same domain it was written on, so suppressing here 45 * would leave parent-scoped cookies in place while appearing removed. 46 * 47 * Memoised only after the probe runs, so a disabled_all_cookie or option 48 * arriving later over REST is still honored. 49 * 50 * An answer already reported to PHP in pys_cd is reused, so the probe 51 * runs once per visitor, not once per page load. 52 */ 53 function cookieDomain() { 54 if (probedCookieDomain !== null) { 55 return probedCookieDomain; 56 } 57 if (!options || !options.hasOwnProperty("track_cookie_for_subdomains") 58 || !options.track_cookie_for_subdomains) { 59 return ''; 60 } 61 probedCookieDomain = reportedCookieDomain(); 62 if (probedCookieDomain === null) { 63 probedCookieDomain = probeCookieDomain(); 64 } 65 return probedCookieDomain; 66 } 67 68 /** 69 * The domain an earlier page load verified and stored in pys_cd, or 70 * null when there is nothing usable and the probe has to run. 71 * 72 * The browser sending us pys_cd scoped to that domain is itself proof 73 * the scope is accepted, so no probe is needed. The value is checked 74 * against the current host on a label boundary; anything else (another 75 * site's value, a tampered one) is ignored and the probe decides. 76 * 77 * A value with the shape of a public suffix (co.uk, github.io) is 78 * test-written before it is trusted: the value is not the cookie's own 79 * scope, so a sibling subdomain can plant one, and reusing it would 80 * make every PYS cookie this visitor gets vanish. PHP validates the 81 * same value only for its own writes; this covers ours. The test costs 82 * one throwaway write, and only for values of that shape â see 83 * suspiciousCookieDomain(). 84 * 85 * 'host-only' is deliberately not reused: it may be a false negative 86 * from a full cookie jar, and reportCookieDomain() replaces it once a 87 * probe succeeds, so re-probing lets it self-heal. The other false 88 * negative â a domain narrower than the registrable one, from the same 89 * full jar â is accepted as is: it is reused and stays until pys_cd 90 * expires (365 days) or is cleared, because re-probing it would put the 91 * throwaway write back on every page load for every visitor. 92 */ 93 function reportedCookieDomain() { 94 var reported, host; 95 try { 96 reported = Cookies.get('pys_cd'); 97 host = String(location.hostname || '').toLowerCase(); 98 } catch (e) { 99 return null; 100 } 101 if (!reported || reported === 'host-only') return null; 102 reported = String(reported).toLowerCase(); 103 if (reported.indexOf('.') === -1 || !host) return null; 104 if (host !== reported && host.slice(-(reported.length + 1)) !== '.' + reported) { 105 return null; 106 } 107 if (suspiciousCookieDomain(reported) && !cookieDomainAccepted('.' + reported)) { 108 return null; 109 } 110 return '.' + reported; 111 } 112 113 /** 114 * Whether a reported domain has the shape of a public suffix and so 115 * needs a test write before it is reused: two labels that are both 116 * short (co.uk, com.au, net.br, ac.jp), or one of the multi-tenant 117 * hosts whose registrable part is a subdomain (github.io, vercel.app). 118 * A real two-label domain of that shape (hp.com) pays one throwaway 119 * write per page load â what every site paid before pys_cd was reused. 120 */ 121 function suspiciousCookieDomain(domain) { 122 var labels = String(domain).split('.'); 123 if (labels.length === 2 && labels[0].length <= 3 && labels[1].length <= 3) { 124 return true; 125 } 126 var known = ['github.io', 'vercel.app', 'pages.dev', 'wpcomstaging.com', 127 'appspot.com', 'netlify.app', 'herokuapp.com', 'myshopify.com']; 128 return known.indexOf(domain) !== -1; 129 } 130 131 /** 132 * Find the broadest domain this browser will actually accept a cookie 133 * on, walking from the broadest candidate inward. 134 * 135 * We used to rely on tldjs.getDomain(), but its bundled Public Suffix 136 * List predates ~2018 and returns the public suffix itself for newer 137 * domains (vercel.app, pages.dev, wpcomstaging.com), so cookies scoped 138 * there were silently dropped. Asking the browser instead keeps us in 139 * sync with its own current PSL. 140 * 141 * The probe cookie is written and removed synchronously, carries no 142 * data, and never outlives this call. If a consent tool blocks the 143 * write, cookieDomainAccepted() reports it rejected and we fall through 144 * to host-only â the correct outcome. 145 * 146 * Returns '' when nothing works (IP/single-label host, or all writes 147 * blocked); host-only is never rejected, so it's the safe floor. 148 */ 149 function probeCookieDomain() { 150 var host = ''; 151 try { host = String(location.hostname || ''); } catch (e) { return ''; } 152 153 // No Domain attribute is valid for an IP literal or a dotless host. 154 if (!host || host.indexOf('.') === -1) return ''; 155 if (host.indexOf(':') !== -1) return ''; // IPv6 literal 156 if (/^[\d.]+$/.test(host)) return ''; // IPv4 literal 157 158 var labels = host.split('.'); 159 // Broadest first: last two labels, then three, ... up to the full host. 160 for (var take = 2; take <= labels.length; take++) { 161 var candidate = '.' + labels.slice(labels.length - take).join('.'); 162 if (cookieDomainAccepted(candidate)) return candidate; 163 } 164 return ''; 165 } 166 167 /** 168 * Write a throwaway cookie at $candidate and report whether it stuck. 169 * A false negative is possible if the jar is at its per-domain limit; 170 * we then fall through to a narrower candidate and ultimately host-only. 171 * 172 * The name is fixed: consent scanners (Cookiebot and the like) record 173 * every write, not the jar afterwards, and a random name made each 174 * crawled page a new entry in the site's cookie declaration. The random 175 * part lives in the value instead, which still tells this write apart
176 * from a stale cookie left behind by an earlier, interrupted probe. 177 */ 178 function cookieDomainAccepted(candidate) { 179 var name = 'pys_dt'; 180 var value = String(Math.floor(Math.random() * 1e9)); 181 var accepted = false; 182 try { 183 document.cookie = name + '=' + value + '; path=/; domain=' + candidate + '; SameSite=Lax'; 184 accepted = ('; ' + document.cookie + ';').indexOf('; ' + name + '=' + value + ';') !== -1; 185 } catch (e) { 186 return false; 187 } finally { 188 // Remove either way; deleting an unset cookie is a no-op. 189 try { 190 document.cookie = name + '=; path=/; domain=' + candidate + '; max-age=0'; 191 } catch (e) {} 192 } 193 return accepted; 194 } 195 196 /** 197 * Hand PHP the cookie domain that probeCookieDomain() verified, since 198 * PHP has no Public Suffix List and guesses wrong on hosts like 199 * shop.example.co.uk or mysite.github.io â scoping its cookies to a 200 * public suffix the browser then drops. 201 * 202 * Value is the site's registrable domain (no PII, same for every 203 * visitor); PHP validates it against the request host before use (see 204 * pys_cookie_domain_resolve). 205 * 206 * Written from manageCookies() under the same consent gates as every 207 * other cookie, not unconditionally. 208 * 209 * No domain at all is an answer too â see reportHostOnlyCookieDomain(). 210 */ 211 function reportCookieDomain(resolved) { 212 var bare = resolved ? String(resolved).replace(/^\./, '') : ''; 213 if (!bare || bare.indexOf('.') === -1) { 214 reportHostOnlyCookieDomain(); 215 return; 216 } 217 try { 218 if (Cookies.get('pys_cd') === bare) return; // already reported 219 // A 'host-only' answer from an earlier load is a separate jar 220 // entry (no Domain attribute), and PHP reads whichever the 221 // browser sends first â so it is removed, not outranked. 222 Cookies.remove('pys_cd', { path: '/', domain: '' }); 223 Cookies.set('pys_cd', bare, { 224 expires: 365, 225 path: '/', 226 domain: resolved, 227 sameSite: 'Lax' 228 }); 229 } catch (e) {} 230 } 231 232 /** 233 * Tell PHP that host-only is the scope in use: the option asks for a 234 * Domain attribute, but probeCookieDomain() found none this browser 235 * accepts (a public-suffix host such as example.github.io, or every 236 * candidate write refused). Without this PHP cannot tell "not answered 237 * yet" from "never will", and withholds its own cookies for ever. 238 * 239 * Only with the option on: off, PHP writes host-only already and waits 240 * for nothing. Never overwrites an existing report â a domain accepted 241 * on an earlier load outranks a probe that failed on this one (a full 242 * cookie jar gives a false negative). 243 */ 244 function reportHostOnlyCookieDomain() { 245 if (!options || !options.track_cookie_for_subdomains) return; 246 try { 247 if (Cookies.get('pys_cd') !== undefined) return; // an answer exists 248 Cookies.set('pys_cd', 'host-only', { 249 expires: 365, 250 path: '/', 251 sameSite: 'Lax' 252 }); 253 } catch (e) {} 254 } 255 256 // Per-page referrer for Facebook CAPI events. Captured once per script 257 // load so SPA-style navigations within the same HTML document do not 258 // overwrite it. Internal/external is NOT filtered â Meta expects this 259 // field to describe the URL of the previous page for THIS event. 260 var pageReferrer = ''; 261 try { 262 pageReferrer = stripUrlQuery((document.referrer || '').toString()); 263 } catch (e) {} 264 265 // Session-entry referrer (fallback). Captured at session start the first 266 // time the visitor lands from an EXTERNAL site, mirrored to a session 267 // cookie so PHP can read it for orphan events (async/cron) that arrive 268 // without a per-page payload. Never the primary source.
269 var PYS_SESSION_ENTRY_REFERRER_KEY = 'pys_session_entry_referrer'; 270 function stripUrlQuery(url) { 271 if (!url) return ''; 272 var clean = String(url); 273 var q = clean.indexOf('?'); 274 if (q !== -1) clean = clean.substring(0, q); 275 var h = clean.indexOf('#'); 276 if (h !== -1) clean = clean.substring(0, h); 277 return clean; 278 } 279 var sessionEntryReferrer = ''; 280 function getOrInitSessionEntryReferrer(consentGranted) { 281 var value = ''; 282 try { 283 if (typeof window.sessionStorage !== 'undefined') { 284 value = window.sessionStorage.getItem(PYS_SESSION_ENTRY_REFERRER_KEY) || ''; 285 } 286 } catch (e) {} 287 288 if (!value) { 289 var cookieValue = Cookies.get(PYS_SESSION_ENTRY_REFERRER_KEY); 290 if (cookieValue && cookieValue !== 'undefined') { 291 value = cookieValue; 292 try { 293 if (typeof window.sessionStorage !== 'undefined') { 294 window.sessionStorage.setItem(PYS_SESSION_ENTRY_REFERRER_KEY, value); 295 } 296 } catch (e) {} 297 } 298 } 299 300 if (!value) { 301 var raw = ''; 302 try { 303 raw = stripUrlQuery((document.referrer || '').toString()); 304 } catch (e) {} 305 if (raw) { 306 var refHost = ''; 307 try { 308 refHost = new URL(raw).hostname; 309 } catch (e) {} 310 if (refHost && refHost !== window.location.hostname) { 311 value = raw; 312 try { 313 if (typeof window.sessionStorage !== 'undefined') { 314 window.sessionStorage.setItem(PYS_SESSION_ENTRY_REFERRER_KEY, value); 315 } 316 } catch (e) {} 317 } 318 } 319 } 320 321 // Mirror to a session cookie only once consent is given. The cookie 322 // is session-scoped (no expires) and dies with the browser, just 323 // like the sessionStorage entry. 324 if (value && consentGranted && options && options.cookie 325 && !options.cookie.disabled_all_cookie) { 326 var existingCookie = Cookies.get(PYS_SESSION_ENTRY_REFERRER_KEY); 327 if (!existingCookie || existingCookie === 'undefined') { 328 Cookies.set(PYS_SESSION_ENTRY_REFERRER_KEY, value, { path: '/', domain: cookieDomain() }); 329 } 330 } 331 332 sessionEntryReferrer = value; 333 return value; 334 } 335 336 // Populate sessionStorage / sessionEntryReferrer immediately. Cookie 337 // mirroring is deferred to the consent-gated path in Facebook.loadPixel(). 338 getOrInitSessionEntryReferrer(false); 339 340 // Per-page event referrer (session cookie). Snapshot of document.referrer 341 // for the CURRENT page, refreshed on every page load. Read server-side 342 // by saveFbTagsInOrder() to capture the buyer's checkout-page referrer 343 // for orphan CAPI events (APT, gateway webhooks, manual status changes). 344 // Internal URLs are intentionally NOT filtered â the goal is the page 345 // that led the buyer to checkout, which is usually internal. 346 // If document.referrer is empty (direct navigation), the previous value 347 // is preserved so a session that began with an external referrer keeps 348 // it across same-tab navigations. 349 var PYS_EVENT_REFERRER_KEY = 'pys_event_referrer'; 350 function writeEventReferrerCookie() { 351 if (!pageReferrer) { 352 return; 353 } 354 if (!options || !options.cookie || options.cookie.disabled_all_cookie) { 355 return; 356 } 357 try { 358 Cookies.set(PYS_EVENT_REFERRER_KEY, pageReferrer, { path: '/', domain: cookieDomain() }); 359 } catch (e) {} 360 } 361 362 /** 363 * Resolve parameter value based on mode (static or dynamic) 364 * 365 * @param {Object|string} param - Parameter object with { value, selector } or string value 366 * @returns {string|null} - Resolved value 367 */ 368 function resolveParamValue(param, key = '') { 369 370 // 1ï¸â£ null / undefined - skip this parameter 371 if (param === null || param === undefined) { 372 return null; 373 } 374 375 // 2ï¸â£ primitive 376 if (typeof param !== 'object') { 377 return param; 378 } 379 380 // 3ï¸â£ array 381 if (Array.isArray(param)) { 382 return param.map(item => resolveParamValue(item, key)); 383 } 384 385 // 4ï¸â£ NEW FORMAT OBJECT (config-object) 386 const isConfigObject = 387 ('value' in param || 'selector' in param || 'dynamic' in param || 'input_type' in param) && 388 Object.keys(param).every(k => 389 ['value','selector','dynamic','input_type','name'].includes(k) 390 ); 391 392 if (isConfigObject) { 393 394 // STATIC MODE 395 const isStatic = 396 !param.dynamic || 397 !param.selector || 398 param.selector.trim() === ''; 399 400 if (isStatic) { 401 let value = param.value ?? null; 402 403 if (param.input_type === "float" || param.input_type === "int") { 404 value = extractNumericValue( 405 value, 406 param.input_type === "int" 407 ); 408 } 409 410 return value; 411 } 412 413 // DYNAMIC MODE 414 try { 415 const el = document.querySelector(param.selector); 416 if (!el) return null; 417 418 let value = 419 el.value ?? 420 el.innerText ?? 421 el.textContent ?? 422 el.getAttribute("content") ?? 423 el.getAttribute("data-value") ?? 424 null; 425 426 if (param.input_type === "float" || param.input_type === "int") { 427 value = extractNumericValue( 428 value, 429 param.input_type === "int" 430 ); 431 } 432 433 return value; 434 435 } catch { 436 return null; 437 } 438 } 439 440 // 5ï¸â£ Plain object (recursive traversal) - filter out null/undefined values 441 return Object.fromEntries( 442 Object.entries(param) 443 .map(([k, v]) => [k, resolveParamValue(v, k)]) 444 .filter(([k, v]) => v !== null && v !== undefined) 445 ); 446 } 447 448 function extractNumericValue(value, isInt=false) { 449 if (value === null || value === undefined) { 450 return null; 451 } 452 if (isInt) { 453 return parseInt(value); 454 } 455 return parseFloat(value); 456 } 457 458 var loadTags = []; 459 460 var dummyPinterest = function () { 461 462 /** 463 * Public API 464 */ 465 return { 466 tag: function() { 467 return "pinterest"; 468 }, 469 isEnabled: function () {}, 470 471 disable: function () {}, 472 473 loadPixel: function () {}, 474 475 fireEvent: function (name, data) { 476 return false;
477 }, 478 479 onAdSenseEvent: function (event) {}, 480 481 onClickEvent: function (params) {}, 482 483 onWatchVideo: function (params) {}, 484 485 onCommentEvent: function (event) {}, 486 487 onFormEvent: function (params) {}, 488 489 onDownloadEvent: function (params) {}, 490 491 492 493 onWooAddToCartOnButtonEvent: function (product_id) {}, 494 495 onWooAddToCartOnSingleEvent: function (product_id, qty, product_type, is_external, $form) {}, 496 497 onWooRemoveFromCartEvent: function (cart_item_hash) {}, 498 499 onWooAffiliateEvent: function (product_id) {}, 500 501 onWooPayPalEvent: function (event) {}, 502 503 onEddAddToCartOnButtonEvent: function (download_id, price_index, qty) {}, 504 505 onEddRemoveFromCartEvent: function (item) {}, 506 507 onPageScroll: function (event) {}, 508 onTime: function (event) { 509 510 }, 511 512 } 513 514 }(); 515 516 var dummyBing = function () { 517 518 /** 519 * Public API 520 */ 521 return { 522 tag: function() { 523 return "bing"; 524 }, 525 isEnabled: function () {}, 526 527 disable: function () {}, 528 529 loadPixel: function () {}, 530 531 fireEvent: function (name, data) { 532 return false; 533 }, 534 535 onAdSenseEvent: function (event) {}, 536 537 onClickEvent: function (params) {}, 538 539 onWatchVideo: function (params) {}, 540 541 onCommentEvent: function (event) {}, 542 543 onFormEvent: function (params) {}, 544 545 onDownloadEvent: function (params) {}, 546 547 548 onWooAddToCartOnButtonEvent: function (product_id) {}, 549 550 onWooAddToCartOnSingleEvent: function (product_id, qty, product_type, is_external, $form) {}, 551 552 onWooRemoveFromCartEvent: function (cart_item_hash) {}, 553 554 onWooAffiliateEvent: function (product_id) {}, 555 556 onWooPayPalEvent: function (event) {}, 557 558 onEddAddToCartOnButtonEvent: function (download_id, price_index, qty) {}, 559 560 onEddRemoveFromCartEvent: function (item) {}, 561 onPageScroll: function (event) {}, 562 onTime: function (event) { 563 564 }, 565 } 566 567 }(); 568 569 var dummyReddit = function () { 570 /** 571 * Public API 572 */ 573 return { 574 tag: function () { 575 return "reddit"; 576 }, 577 isEnabled: function () { 578 }, 579 580 disable: function () { 581 }, 582 583 loadPixel: function () { 584 }, 585 586 fireEvent: function ( name, data ) { 587 return false; 588 }, 589 590 onAdSenseEvent: function ( event ) { 591 }, 592 593 onClickEvent: function ( params ) { 594 }, 595 596 onWatchVideo: function ( params ) { 597 }, 598 599 onCommentEvent: function ( event ) { 600 }, 601 602 onFormEvent: function ( params ) { 603 }, 604 605 onDownloadEvent: function ( params ) { 606 }, 607 608 onWooAddToCartOnButtonEvent: function ( product_id ) { 609 }, 610 611 onWooAddToCartOnSingleEvent: function ( product_id, qty, product_type, is_external, $form ) { 612 }, 613 614 onWooRemoveFromCartEvent: function ( cart_item_hash ) { 615 }, 616 617 onWooAffiliateEvent: function ( product_id ) { 618 }, 619 620 onWooPayPalEvent: function ( event ) { 621 }, 622 623 onEddAddToCartOnButtonEvent: function ( download_id, price_index, qty ) { 624 }, 625 626 onEddRemoveFromCartEvent: function ( item ) { 627 }, 628 629 onPageScroll: function ( event ) { 630 }, 631 632 onTime: function ( event ) { 633 }, 634 } 635 }(); 636 637 var Utils = function (options) { 638 639 640 var Pinterest = dummyPinterest; 641 642 var Bing = dummyBing; 643 644 var Reddit = dummyReddit; 645 646 var gtag_loaded = false; 647 648 var gtm_loaded = false; 649 650 // The GTM container's consent state is sent once per page load. Two code 651 // paths reach it (loadGTMScript() and the gtm_just_data_layer branch of 652 // GTM.loadPixel(), which can both run for the same page), and Consent Mode 653 // expects a single transition, not a stream of them. 654 var gtm_consent_state_pushed = false;
655 656 let isNewSession = checkSession(); 657 658 // In-flight pys_get_pbid request. Several code paths call manageCookies() 659 // before the first reply lands; one request is enough for all of them. 660 var pbidRequest = null; 661 // True once a pixel has waited for that reply (or given up on it), so 662 // the hold in holdForIdentity() happens at most once per page. 663 var pbidSettled = false; 664 var PBID_WAIT_MS = 1000; 665 666 let dataLayerName = 'dataLayerPYS'; 667 668 let GTMdataLayerName = 'dataLayer'; 669 670 671 function loadPixels() { 672 673 Utils.refreshAddonPixels(); 674 675 if (!options.gdpr.all_disabled_by_api) { 676 if (!options.gdpr.tiktok_disabled_by_api) { 677 TikTok.loadPixel(); 678 } 679 680 if (!options.gdpr.openai_disabled_by_api) { 681 OpenAI.loadPixel(); 682 } 683 684 if (!options.gdpr.facebook_disabled_by_api) { 685 Facebook.loadPixel(); 686 } 687 688 if (!options.gdpr.analytics_disabled_by_api) { 689 Analytics.loadPixel(); 690 } 691 692 if (!options.gdpr.google_ads_disabled_by_api) { 693 GAds.loadPixel(); 694 } 695 696 if (!options.gdpr.analytics_disabled_by_api) { 697 GTM.loadPixel(); 698 } 699 700 if (!options.gdpr.pinterest_disabled_by_api) { 701 Pinterest.loadPixel(); 702 } 703 704 if (!options.gdpr.bing_disabled_by_api) { 705 Bing.loadPixel(); 706 } 707 708 if (!options.gdpr.reddit_disabled_by_api) { 709 Reddit.loadPixel(); 710 } 711 712 } 713 if (options.gdpr.consent_magic_integration_enabled && typeof CS_Data !== "undefined") { 714 if (typeof CS_Data.cs_google_analytics_consent_mode !== "undefined" && CS_Data.cs_google_analytics_consent_mode == 1) { 715 Analytics.loadPixel(); 716 } 717 if (typeof CS_Data.cs_google_ads_consent_mode !== "undefined" && CS_Data.cs_google_ads_consent_mode == 1 ) { 718 GAds.loadPixel(); 719 } 720 } 721 } 722 723 /** 724 * WATCHVIDEO UTILS 725 */ 726 727 function isJSApiAttrEnabled(url) { 728 return url.indexOf('enablejsapi') > -1; 729 } 730 731 function isOriginAttrEnabled(url) { 732 return url.indexOf('origin') > -1; 733 } 734 735 // Returns key/value pairs of percentages: number of seconds to achieve 736 function getVideoCompletionMarks(duration) { 737 738 var marks = {}; 739 var points = [0, 10, 50, 90, 100]; 740 741 for (var i = 0; i < points.length; i++) { 742 743 var _point = points[i]; 744 var _mark = _point + '%'; 745 var _time = duration * _point / 100; 746 747 if (_point === 100) { 748 _time = _time - 1; 749 } 750 751 // 10% => 123 752 marks[_mark] = Math.floor(_time); 753 754 } 755 756 return marks; 757 758 } 759 760 // Determine if the element is a YouTube video or not 761 function tagIsYouTubeVideo(tag) { 762 var src = tag.src || ''; 763 return src.indexOf('youtube.com/embed/') > -1 || src.indexOf('youtube.com/v/') > -1; 764 } 765 766 function tagIsYouTubeAsyncVideo(tag) { 767 if(tag.src && tag.src.indexOf("data:image") !== -1) return false; // video is loaded 768 var keys = Object.keys(tag.dataset); 769 for(var i = 0;i<keys.length;i++) { 770 if(keys[i].toLowerCase().indexOf("src") > -1) { 771 var src = tag.dataset[keys[i]]; 772 return src.indexOf('youtube.com/embed/') > -1 || src.indexOf('youtube.com/v/') > -1; 773 } 774 } 775 return false; //not find src 776 } 777 778 // Turn embed objects into iframe objects and ensure they have the right parameters 779 function normalizeYouTubeIframe(tag) { 780 781 var loc = window.location; 782 var a = document.createElement('a'); 783 a.href = tag.src; 784 a.hostname = 'www.youtube.com'; 785 a.protocol = loc.protocol; 786 var tmpPathname = a.pathname.charAt(0) === '/' ? a.pathname : '/' + a.pathname; // IE10 shim 787 788 if (!isJSApiAttrEnabled(a.search)) { 789 a.search = (a.search.length > 0 ? a.search + '&' : '') + 'enablejsapi=1'; 790 } 791 792 // for security reasons, YouTube wants an origin parameter set that matches our hostname
793 if (!isOriginAttrEnabled(a.search) && loc.hostname.indexOf('localhost') === -1) { 794 795 var port = loc.port ? ':' + loc.port : ''; 796 var origin = loc.protocol + '%2F%2F' + loc.hostname + port; 797 798 a.search = a.search + '&origin=' + origin; 799 800 } 801 802 if (tag.type === 'application/x-shockwave-flash') { 803 804 var newIframe = document.createElement('iframe'); 805 newIframe.height = tag.height; 806 newIframe.width = tag.width; 807 tmpPathname = tmpPathname.replace('/v/', '/embed/'); 808 809 tag.parentNode.parentNode.replaceChild(newIframe, tag.parentNode); 810 811 tag = newIframe; 812 813 } 814 815 a.pathname = tmpPathname; 816 817 if (tag.src !== a.href + a.hash) { 818 tag.src = a.href + a.hash; 819 } 820 821 return tag; 822 823 } 824 825 // Add event handlers for events emitted by the YouTube API 826 function addYouTubeEvents(iframe) { 827 828 var player = YT.get(iframe.id); 829 if (!player) { 830 player = new YT.Player(iframe, {}); 831 } 832 833 if (typeof iframe.pauseFlag === 'undefined') { 834 835 iframe.pauseFlag = false; 836 player.addEventListener('onStateChange', function (evt) { 837 onYouTubePlayerStateChange(evt, iframe); 838 }); 839 840 player.addEventListener('onPlaybackRateChange', function(evt) { 841 var currRate = evt.data; 842 var prevRate = iframe._pysSpeedRate !== undefined ? iframe._pysSpeedRate : 1; 843 iframe._pysSpeedRate = currRate; 844 if(currRate == prevRate) return; 845 if (options.triggerEventTypes && options.triggerEventTypes.video_speed) { 846 var href = window.location.href; 847 var ytVideoId = ''; 848 try { ytVideoId = player.getVideoData().video_id || ''; } catch(e) {} 849 Object.keys(options.triggerEventTypes.video_speed).forEach(function(evtId) { 850 var evtConfig = options.triggerEventTypes.video_speed[evtId]; 851 var urls = Array.isArray(evtConfig) ? evtConfig : (evtConfig.urls || []); 852 var speedRate = Array.isArray(evtConfig) ? 'any' : (evtConfig.speed_rate || 'any'); 853 var evtTriggers = Array.isArray(evtConfig) ? [] : (evtConfig.triggers || []); 854 var pageOk = !urls || urls.length === 0 || urls.some(function(u) { return u === '*' || u === '' || href.indexOf(u) !== -1; }); 855 var rateOk = speedRate === 'any' ? currRate !== prevRate : (speedRate === 'increase' ? currRate > prevRate : currRate < prevRate); 856 var videoOk = evtTriggers.length === 0 || evtTriggers.some(function(t) { return t.type === 'youtube' && t.rule === ytVideoId; }); 857 if (pageOk && rateOk && videoOk) { 858 Utils.handleTriggerFired(evtId, 'video_speed'); 859 } 860 }); 861 } 862 if (options.dynamicEvents && options.dynamicEvents.automatic_event_video_speed) { 863 var ytSpeedUrl = iframe.src || ''; 864 var ytSpeedTitle = ''; 865 try { ytSpeedTitle = player.getVideoData().title || ''; } catch(e) {} 866 var params = { 867 video_url: ytSpeedUrl, 868 video_type: 'youtube', 869 video_title: ytSpeedTitle || 'Unknown Title', 870 playback_speed: currRate, 871 element: 'iframe' 872 }; 873 Object.keys(options.dynamicEvents.automatic_event_video_speed).forEach(function(slug) { 874 var pysEvent = Utils.clone(options.dynamicEvents.automatic_event_video_speed[slug]); 875 Utils.copyProperties(params, pysEvent.params); 876 var px = getPixelBySlag(slug); if (px) px.fireEvent(pysEvent.name, pysEvent); 877 }); 878 } 879 }); 880 881 } 882 883 } 884 885 function addDynYouTubeVideos(el) { 886 // We only bind to iFrames with a YouTube URL with the enablejsapi=1 and 887 // origin=<<hostname>> parameters 888 if (el.tagName === 'IFRAME' 889 && tagIsYouTubeVideo(el) 890 && isJSApiAttrEnabled(el.src) 891 && isOriginAttrEnabled(el.src)) 892 { 893 addYouTubeEvents(el); 894 } 895 } 896 897 // Event handler for events emitted from the YouTube API 898 function onYouTubePlayerStateChange(evt, iframe) { 899 900 var stateIndex = evt.data; 901 var player = evt.target; 902 var targetVideoUrl = player.getVideoUrl(); 903 var targetVideoId = targetVideoUrl.match(/[?&]v=([^&#]*)/)[1]; // Extract the ID 904 var playerState = player.getPlayerState(); 905 var marks = getVideoCompletionMarks(player.getDuration()); 906 907 iframe.playTracker = iframe.playTracker || {}; 908 909 if (playerState === YT.PlayerState.PLAYING && !iframe.timer) { 910 911 clearInterval(iframe.timer); 912 913 // check every second to see if we've hit any of our percentage viewed marks 914 iframe.timer = setInterval(function () { 915 checkYouTubeCompletion(player, marks, iframe.videoId); 916 }, 1000); 917 918 } else { 919 920 clearInterval(iframe.timer); 921 iframe.timer = false;
922 923 } 924 925 // playlist edge-case handler 926 if (stateIndex === YT.PlayerState.PLAYING) { 927 iframe.playTracker[targetVideoId] = true; 928 iframe.videoId = targetVideoId; 929 iframe.pauseFlag = false; 930 } 931 932 if (!iframe.playTracker[iframe.videoId]) { 933 return false; // this video hasn't started yet, so this is spam 934 } 935 936 if (stateIndex === YT.PlayerState.PAUSED) { 937 938 if (!iframe.pauseFlag) { 939 iframe.pauseFlag = true; 940 } else { 941 return false; // we don't want to fire consecutive pause events 942 } 943 944 } 945 946 } 947 948 // Trigger event if YouTube video mark was reached 949 function checkYouTubeCompletion(player, marks, videoId) { 950 951 var currentTime = player.getCurrentTime(); 952 953 player[videoId] = player[videoId] || {}; 954 955 for (var key in marks) { 956 957 if (marks[key] <= currentTime && !player[videoId][key]) { 958 player[videoId][key] = true; 959 960 var data = player.getVideoData(); 961 962 if (key === '0%') { 963 key = 'play'; 964 } 965 966 var params = { 967 video_type: 'youtube', 968 video_id: videoId, 969 video_title: data.title, 970 }; 971 972 let disable_watch_video = []; 973 974 //Custom 975 if ( options.triggerEventTypes.hasOwnProperty( "video_view" ) ) { 976 Object.entries( options.triggerEventTypes.video_view ).forEach( function ( [ trigger_id, triggers ] ) { 977 triggers.forEach( function ( trigger ) { 978 if ( trigger.type === 'youtube' && trigger.rule === videoId ) { 979 980 let pixels = Object.keys( options.triggerEvents[ trigger_id ] ); 981 for ( let i = 0; i < pixels.length; i++ ) { 982 let event = Utils.clone( options.triggerEvents[ trigger_id ][ pixels[ i ] ] ); 983 984 if ( trigger.disable_watch_video ) { 985 disable_watch_video.push( pixels[ i ] ); 986 } 987 988 if ( currentTime >= marks[ trigger.value ] ) { 989 if (pixels[i] !== 'reddit') { 990 event.params["progress"] = key; 991 Utils.copyProperties( params, event.params ); 992 } 993 994 if ( event.fired !== true ) { 995 if ( Utils.isEventInTimeWindow( event.name, event, 'dyn_' + pixels[ i ] + '_' + trigger_id ) ) { 996 event = Utils.getFormFilledData( event ); 997 getPixelBySlag( pixels[ i ] ).fireEvent( event.name, event ); 998 options.triggerEvents[ trigger_id ][ pixels[ i ] ].fired = true; 999 } 1000 } 1001 } 1002 } 1003 } 1004 } ) 1005 } ) 1006 } 1007 1008 // Auto 1009 if( options.automatic.enable_video 1010 && options.automatic.enable_youtube 1011 && options.dynamicEvents.hasOwnProperty("automatic_event_video") 1012 ) { 1013 var pixels = Object.keys(options.dynamicEvents.automatic_event_video); 1014 1015 for (var i = 0; i < pixels.length; i++) { 1016 if ( disable_watch_video.includes( pixels[ i ] ) ) { 1017 continue; 1018 } 1019 var event = Utils.clone(options.dynamicEvents.automatic_event_video[pixels[i]]); 1020 1021 if ( pixels[ i ] !== 'reddit' ) {
1022 event.params[ "progress" ] = key; 1023 Utils.copyProperties(params, event.params) 1024 } 1025 1026 if ( pixels[i] === 'tiktok' || pixels[i] === 'reddit' ) { 1027 var time_trigger = event.automatic_event_video_trigger; 1028 if ( currentTime >= marks[time_trigger] && event.fired !== true ) { 1029 getPixelBySlag(pixels[i]).onWatchVideo(event); 1030 1031 //tiktok and reddit watch video fire once 1032 options.dynamicEvents.automatic_event_video[pixels[i]].fired = true; 1033 } 1034 } else { 1035 Utils.copyProperties(Utils.getRequestParams(), event.params); 1036 getPixelBySlag(pixels[i]).onWatchVideo(event); 1037 } 1038 } 1039 } 1040 1041 if(key == "play") { 1042 1043 $.each(options.triggerEventTypes, function (triggerType, events) { 1044 $.each(events, function (eventId, triggers) { 1045 switch (triggerType) { 1046 case 'video_play': 1047 Utils.handleTriggerFired(eventId, triggerType); 1048 break; 1049 } 1050 1051 }); 1052 }); 1053 } 1054 1055 } 1056 1057 } 1058 1059 } 1060 1061 // Determine if the element is a Vimeo video or not 1062 function tagIsVimeoVideo(tag) { 1063 var src = tag.src || ''; 1064 return src.indexOf('player.vimeo.com/video/') > -1; 1065 } 1066 1067 function tagIsAsincVimeoVideo(tag) { 1068 if(tag.src) return false; // video is loaded 1069 var keys = Object.keys(tag.dataset); 1070 for(var i = 0;i<keys.length;i++) { 1071 if(keys[i].toLowerCase().indexOf("src") > -1) { 1072 var src = tag.dataset[keys[i]]; 1073 return src.indexOf('player.vimeo.com/video/') > -1; 1074 } 1075 } 1076 return false; //not find src 1077 } 1078 1079 function attachVimeoPlayerToTag(tag) { 1080 var player = new Vimeo.Player(tag); 1081 1082 player.getDuration().then(function (pl,seconds) { 1083 pl.pysMarks = getVideoCompletionMarks(seconds); 1084 }.bind(null,player)); 1085 1086 player.getVideoTitle().then(function (pl,title) { 1087 pl.pysVideoTitle = title; 1088 }.bind(null,player)); 1089 1090 player.getVideoId().then(function (pl,id) { 1091 pl.pysVideoId = id; 1092 }.bind(null,player)); 1093 1094 player.pysCompletedMarks = {}; 1095 1096 player.on('play', function () { 1097 1098 if (this.pysTimer) { 1099 return; 1100 } 1101 1102 clearInterval(this.pysTimer); 1103 1104 var player = this; 1105 1106 this.pysTimer = setInterval(function () { 1107 checkVimeoCompletion(player); 1108 }, 1000); 1109 1110 }); 1111 1112 player.on('pause', function () { 1113 clearInterval(this.pysTimer); 1114 this.pysTimer = false; 1115 }); 1116 1117 player.on('ended', function () { 1118 clearInterval(this.pysTimer); 1119 this.pysTimer = false; 1120 }); 1121 1122 player.on('playbackratechange', function(data) { 1123 var currRate = data.playbackRate; 1124 var prevRate = player._pysSpeedRate !== undefined ? player._pysSpeedRate : 1; 1125 player._pysSpeedRate = currRate; 1126 if(currRate == prevRate) return; 1127 if (options.triggerEventTypes && options.triggerEventTypes.video_speed) { 1128 var href = window.location.href; 1129 var vmVideoId = player.pysVideoId ? String(player.pysVideoId) : ''; 1130 Object.keys(options.triggerEventTypes.video_speed).forEac
1130h(function(evtId) { 1131 var evtConfig = options.triggerEventTypes.video_speed[evtId]; 1132 var urls = Array.isArray(evtConfig) ? evtConfig : (evtConfig.urls || []); 1133 var speedRate = Array.isArray(evtConfig) ? 'any' : (evtConfig.speed_rate || 'any'); 1134 var evtTriggers = Array.isArray(evtConfig) ? [] : (evtConfig.triggers || []); 1135 var pageOk = !urls || urls.length === 0 || urls.some(function(u) { return u === '*' || u === '' || href.indexOf(u) !== -1; }); 1136 var rateOk = speedRate === 'any' ? currRate !== prevRate : (speedRate === 'increase' ? currRate > prevRate : currRate < prevRate); 1137 var videoOk = evtTriggers.length === 0 || evtTriggers.some(function(t) { return t.type === 'vimeo' && t.rule == vmVideoId; }); 1138 if (pageOk && rateOk && videoOk) { 1139 Utils.handleTriggerFired(evtId, 'video_speed'); 1140 } 1141 }); 1142 } 1143 if (options.dynamicEvents && options.dynamicEvents.automatic_event_video_speed) { 1144 var vimeoEl = player.element || null; 1145 var params = { 1146 video_url: vimeoEl ? (vimeoEl.src || '') : '', 1147 video_type: 'vimeo', 1148 video_title: player.pysVideoTitle || 'Unknown Title', 1149 playback_speed: currRate, 1150 element: 'iframe' 1151 }; 1152 Object.keys(options.dynamicEvents.automatic_event_video_speed).forEach(function(slug) { 1153 var pysEvent = Utils.clone(options.dynamicEvents.automatic_event_video_speed[slug]); 1154 Utils.copyProperties(params, pysEvent.params); 1155 var px = getPixelBySlag(slug); if (px) px.fireEvent(pysEvent.name, pysEvent); 1156 }); 1157 } 1158 }); 1159 } 1160 1161 // Trigger event if Vimeo video mark was reached 1162 function checkVimeoCompletion(player) { 1163 1164 player.getCurrentTime().then(function (seconds) { 1165 1166 for (var key in player.pysMarks) { 1167 1168 if (player.pysMarks[key] <= seconds && !player.pysCompletedMarks[key]) { 1169 1170 player.pysCompletedMarks[key] = true; 1171 1172 if (key === '0%') { 1173 key = 'play'; 1174 } 1175 1176 var params = { 1177 video_type: 'vimeo', 1178 video_id: player.pysVideoId, 1179 video_title: player.pysVideoTitle, 1180 }; 1181 1182 let disable_watch_video = []; 1183 1184 //Custom 1185 if ( options.triggerEventTypes.hasOwnProperty( "video_view" ) ) { 1186 Object.entries( options.triggerEventTypes.video_view ).forEach( function ( [ trigger_id, triggers ] ) { 1187 triggers.forEach( function ( trigger ) { 1188 if ( trigger.type === 'vimeo' && trigger.rule == player.pysVideoId ) { 1189 let pixels = Object.keys( options.triggerEvents[ trigger_id ] ); 1190 for ( let i = 0; i < pixels.length; i++ ) { 1191 let event = Utils.clone( options.triggerEvents[ trigger_id ][ pixels[ i ] ] ); 1192 1193 if ( trigger.disable_watch_video ) { 1194 disable_watch_video.push( pixels[ i ] ); 1195 } 1196 1197 if ( seconds >= player.pysMarks[ trigger.value ] ) { 1198 if ( pixels[ i ] !== 'reddit' ) { 1199 event.params[ "progress" ] = key; 1200 Utils.copyProperties( params, event.params ); 1201 } 1202 1203 if ( event.fired !== true ) { 1204 if ( Utils.isEventInTimeWindow( event.name, event, 'dyn_' + pixels[ i ] + '_' + trigger_id ) ) { 1205 event = Utils.getFormFilledData( event ); 1206 getPixelBySlag( pixels[ i ] ).fireEvent( event.name, event ); 1207 options.triggerEvents[ trigger_id ][ pixels[ i ] ].fired = true; 1208 } 1209 } 1210 } 1211 } 1212 } 1213 } ) 1214 } ) 1215 } 1216 1217 // Auto 1218 if(options.automatic.enable_video 1219 && options.automatic.enable_vimeo 1220 && options.dynamicEvents.hasOwnProperty("automatic_event_video") 1221 ) { 1222 var pixels = Object.keys(options.dynamicEvents.automatic_event_video); 1223 1224 for (var i = 0; i < pixels.length; i++) { 1225 if ( disable_watch_video.includes( pixels[ i ] ) ) { 1226 continue; 1227 } 1228 1229 var event = Utils.clone(options.dynamicEvents.automatic_event_video[pixels[i]]); 1230 1231 if ( pixels[i] !== 'reddit' ) { 1232 event.params["progress"] = key; 1233 Utils.copyProperties(params, event.params); 1234 } 1235 1236 if ( pixels[i] === 'tiktok' || pixels[i] === 'reddit' ) { 1237 var time_trigger = event.automatic_event_video_trigger; 1238 if ( seconds >= player.pysMarks[time_trigger] && event.fired !== true ) { 1239 getPixelBySlag(pixels[i]).onWatchVideo(event); 1240 1241 //tiktok and reddit watch video fire once 1242 options.dynamicEvents.automatic_event_video[pixels[i]].fired = true; 1243 } 1244 } else { 1245 Utils.copyProperties(Utils.getRequestParam
1245s(), event.params); 1246 getPixelBySlag(pixels[i]).onWatchVideo(event); 1247 } 1248 } 1249 } 1250 1251 if(key == "play") { 1252 $.each(options.triggerEventTypes, function (triggerType, events) { 1253 $.each(events, function (eventId, triggers) { 1254 switch (triggerType) { 1255 case 'video_play': 1256 Utils.handleTriggerFired(eventId, triggerType); 1257 break; 1258 } 1259 1260 }); 1261 }); 1262 } 1263 } 1264 1265 } 1266 1267 }); 1268 1269 } 1270 1271 // ââ HTML5 <video> WatchVideo tracking âââââââââââââââââââââââââââââââââââ 1272 function initHtml5VideoTracking() { 1273 1274 if ( !options.automatic.enable_video 1275 || !options.dynamicEvents.hasOwnProperty('automatic_event_video') 1276 ) { 1277 return; 1278 } 1279 1280 // play event â fires once per element when playback starts 1281 document.addEventListener('play', function (e) { 1282 var el = e.target; 1283 if ( !el || el.tagName !== 'VIDEO' ) return; 1284 1285 el.pysCompletedMarks = el.pysCompletedMarks || {}; 1286 if ( el.pysCompletedMarks['play'] ) return; 1287 1288 var url = el.src || el.currentSrc || ''; 1289 el.pysCompletedMarks['play'] = true; 1290 1291 fireHtml5VideoMilestone( el, url, 'play' ); 1292 1293 }, true ); 1294 1295 // timeupdate â check percentage milestones 1296 document.addEventListener('timeupdate', function (e) { 1297 var el = e.target; 1298 if ( !el || el.tagName !== 'VIDEO' ) return; 1299 1300 var duration = el.duration; 1301 if ( !duration || isNaN(duration) || !isFinite(duration) ) return; 1302 1303 var currentTime = el.currentTime; 1304 var marks = getVideoCompletionMarks( duration ); 1305 var url = el.src || el.currentSrc || ''; 1306 1307 el.pysCompletedMarks = el.pysCompletedMarks || {}; 1308 1309 for ( var key in marks ) { 1310 if ( key === '0%' ) continue; // handled by 'play' listener 1311 1312 if ( marks[ key ] <= currentTime && !el.pysCompletedMarks[ key ] ) { 1313 el.pysCompletedMarks[ key ] = true; 1314 fireHtml5VideoMilestone( el, url, key ); 1315 } 1316 } 1317 1318 }, true ); 1319 } 1320 1321 function fireHtml5VideoMilestone( el, url, key ) { 1322 1323 var progressLabel = ( key === '0%' ) ? 'play' : key; 1324 1325 var params = { 1326 video_type: 'html5', 1327 video_id: el.id || url, 1328 video_title: Utils.getHtml5VideoTitle( url ), 1329 }; 1330 1331 var pixels = Object.keys( options.dynamicEvents.automatic_event_video ); 1332 1333 for ( var i = 0; i < pixels.length; i++ ) { 1334 var event = Utils.clone( options.dynamicEvents.automatic_event_video[ pixels[ i ] ] ); 1335 1336 if ( pixels[ i ] !== 'reddit' ) { 1337 event.params[ 'progress' ] = progressLabel; 1338 Utils.copyProperties( params, event.params ); 1339 } 1340 1341 if ( pixels[ i ] === 'tiktok' || pixels[ i ] === 'reddit' ) { 1342 var time_trigger = event.automatic_event_video_trigger; 1343 var marks = getVideoCompletionMarks( el.duration || 0 ); 1344 if ( el.currentTime >= marks[ time_trigger ] && event.fired !== true ) { 1345 getPixelBySlag( pixels[ i ] ).onWatchVideo( event ); 1346 options.dynamicEvents.automatic_event_video[ pixels[ i ] ].fired = true; 1347 } 1348 } else {
1349 Utils.copyProperties( Utils.getRequestParams(), event.params ); 1350 getPixelBySlag( pixels[ i ] ).onWatchVideo( event ); 1351 } 1352 } 1353 1354 // handle custom video_view triggers (mirrors YouTube/Vimeo pattern) 1355 if ( key === 'play' ) { 1356 $.each( options.triggerEventTypes, function ( triggerType, events ) { 1357 $.each( events, function ( eventId ) { 1358 if ( triggerType === 'video_play' ) { 1359 Utils.handleTriggerFired( eventId, triggerType ); 1360 } 1361 } ); 1362 } ); 1363 } 1364 } 1365 1366 /** 1367 * COOKIES UTILS 1368 */ 1369 1370 var utmTerms = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content' ,'utm_term']; 1371 var utmId = ['fbadid', 'gadid', 'padid', 'bingid']; 1372 var requestParams = []; 1373 1374 1375 function getDomain(url) { 1376 1377 url = url.replace(/(https?:\/\/)?(www.)?/i, ''); 1378 1379 if (url.indexOf('/') !== -1) { 1380 return url.split('/')[0]; 1381 } 1382 1383 return url; 1384 } 1385 1386 function checkSession() { 1387 1388 if( Cookies.get('pys_start_session') === undefined || 1389 Cookies.get('pys_session_limit') === undefined) { 1390 firstVisit = true; 1391 return true 1392 } 1393 return false 1394 1395 } 1396 1397 function getTrafficSource() { 1398 1399 try { 1400 1401 let referrer = stripUrlQuery(document.referrer.toString()), 1402 source; 1403 1404 let direct = referrer.length === 0; 1405 let internal = direct ? false : referrer.indexOf(options.siteUrl) === 0; 1406 let external = !direct && !internal; 1407 1408 if (external === false) { 1409 source = 'direct'; 1410 } else { 1411 source = referrer; 1412 } 1413 1414 if (source !== 'direct') { 1415 // leave only domain (Issue #70) 1416 return getDomain(source); 1417 } else { 1418 return source; 1419 } 1420 1421 } catch (e) { 1422 console.error(e); 1423 return 'direct'; 1424 } 1425 1426 } 1427 1428 /** 1429 * Return query variables object with where property name is query variable 1430 * and property value is query variable value. 1431 */ 1432 function getQueryVars() { 1433 1434 try { 1435 1436 var result = {}, 1437 tmp = []; 1438 1439 window.location.search 1440 .substr(1) 1441 .split("&") 1442 .forEach(function (item) { 1443 1444 tmp = item.split('='); 1445 1446 if (tmp.length > 1) { 1447 result[tmp[0]] = tmp[1]; 1448 } 1449 1450 }); 1451 1452 return result; 1453 1454 } catch (e) { 1455 console.error(e); 1456 return {}; 1457 } 1458 1459 } 1460 1461 function getLandingPageValue() { 1462 let name = "pys_landing_page" 1463 if(options.visit_data_model === "last_visit") { 1464 name = "last_pys_landing_page" 1465 } 1466 if(Cookies.get(name) && Cookies.get(name) !== "undefined") { 1467 return Cookies.get(name); 1468 } 1469 // No cookie of our own. The dynamic-options endpoint may still know: 1470 // it answers per visitor and is never cached, so it is allowed to carry 1471 // the PHP session copy â which is all a visitor whose cookies a consent 1472 // manager has blocked ever has. In the HTML this value is always empty, 1473 // and an empty value means "nothing to say", so this branch is simply 1474 // skipped unless the endpoint filled it in. 1475 if ( options.hasOwnProperty( "tracking_analytics" ) 1476 && options.tracking_analytics 1477 && options.tracking_analytics.TrafficLanding ) { 1478 return options.tracking_analytics.TrafficLanding; 1479 } 1480 1481 // Nobody knows better than the current URL: it is what the cookie would 1482 // have been set to a moment ago. 1483 return window.location.href; 1484 } 1485 function getTrafficSourceValue() { 1486 let name = "pysTrafficSource" 1487 if(options.visit_data_model === "last_visit") { 1488 name = "last_pysTrafficSource" 1489 } 1490 if(Cookies.get(name) && Cookies.get(name) !== "undefined") { 1491 return Cookies.get(name); 1492 } 1493 // See getLandingPageValue(): filled by the dynamic-options endpoint only. 1494 if ( options.hasOwnProperty( "tracking_analytics" ) 1495 && options.tracking_analytics 1496 && options.tracking_analytics.TrafficSource ) { 1497 return options.tracking_analytics.TrafficSource; 1498 } 1499 return ""; 1500 } 1501 1502 /** 1503 * Handle alternative GCLID parameter for Safari compatibility 1504 * Creates _gcl_aw cookie when alternative parameter is detected and _gcl_aw doesn't exist 1505 */ 1506 function handleAlternativeGclid() { 1507 // Check if alternative GCLID is enabled 1508 if (!options.GATags || !options.GATags.gclid_alternative_enabled || !options.GATags.gclid_alternative_param) { 1509 return; 1510 } 1511 1512 // GDPR consent checks - stop if any consent filter blocks it 1513 if (options.gdpr.all_disabled_by_api) { 1514 return; 1515 } 1516 if (options.gdpr.analytics_disabled_by_api) { 1517 return; 1518 } 1519 if (options.cookie.disabled_all_cookie) { 1520 return; 1521 } 1522 if (options.cookie.disabled_google_alternative_id) { 1523 return; 1524 } 1525 1526 const alternativeParam = options.GATags.gclid_alternative_param; 1527 const queryVars = getQueryVars(); 1528 1529 // Check if alternative parameter exists in URL 1530 if (queryVars[alternativeParam]) { 1531 const gclidValue = queryVars[alternativeParam]; 1532 1533 // Only create _gcl_aw cookie if it doesn't exist 1534 if (!Cookies.get('_gcl_aw')) { 1535 // Create _gcl_aw cookie with the alternative parameter value in Google's format 1536 // Format: GCL.{timestamp}.{gclid_value} 1537 const timestamp = Math.floor(Date.now() / 1000); 1538 const cookieValue = 'GCL.' + timestamp + '.' + gclidValue; 1539 1540 // Set cookie for 90 days (Google's standard) 1541 const expires = new Date(); 1542 expires.setTime(expires.getTime() + (90 * 24 * 60 * 60 * 1000)); 1543 1544 Cookies.set('_gcl_aw', cookieValue, { 1545 expires: expires, 1546 path: '/', 1547 domain: cookieDomain() 1548 }); 1549 1550 if (options.debug) { 1551 console.log('PYS: Created _gcl_aw cookie from alternative parameter:', alternativeParam, '=', gclidValue); 1552 } 1553 } 1554 } 1555 } 1556 1557 function getUTMId(useLast = false) { 1558 try { 1559 let cookiePrefix = 'pys_' 1560 let terms = []; 1561 if (useLast) { 1562 cookiePrefix = 'last_pys_' 1563 } 1564 // Fall back to this request's own query string, not to the server 1565 // options: cached HTML carries whatever the visitor who generated 1566 // the cache entry arrived with. 1567 let queryVars = getQueryVars(); 1568 $.each(utmId, function (index, name) { 1569 if (Cookies.get(cookiePrefix + name)) { 1570 terms[name] = Cookies.get(cookiePrefix + name) 1571 } 1572 else if(options.hasOwnProperty("tracking_analytics") 1573 && options.tracking_analytics 1574 && options.tracking_analytics.TrafficUtmsId 1575 && options.tracking_analytics.TrafficUtmsId[name]) { 1576 terms[name] = filterEmails(options.tracking_analytics.TrafficUtmsId[name]) 1577 } 1578 else if(queryVars.hasOwnProperty(name)) { 1579 terms[name] = filterEmails(queryVars[name]) 1580 } 1581 }); 1582 return terms; 1583 } catch (e) { 1584 console.error(e); 1585 return []; 1586 } 1587 } 1588 /** 1589 * Return UTM terms from request query variables or from cookies. 1590 */ 1591 function getUTMs(useLast = false) { 1592 1593 try { 1594 let cookiePrefix = 'pys_' 1595 if(useLast) { 1596 cookiePrefix = 'last_pys_' 1597 } 1598 let terms = []; 1599 // See getUTMId(): the query string, never the server options. 1600 let queryVars = getQueryVars(); 1601 $.each(utmTerms, function (index, name) { 1602 if (Cookies.get(cookiePrefix + name)) { 1603 let value = Cookies.get(cookiePrefix + name); 1604 terms[name] = filterEmails(value); // do not allow email in request params (Issue #70) 1605 } 1606 else if(options.hasOwnProperty("tracking_analytics") 1607 && options.tracking_analytics 1608 && options.tracking_analytics.TrafficUtms 1609 && options.tracking_analytics.TrafficUtms[name]) { 1610 terms[name] = filterEmails(options.tracking_analytics.TrafficUtms[name]) 1611 } 1612 else if(queryVars.hasOwnProperty(name)) { 1613 terms[name] = filterEmails(queryVars[name]) 1614 } 1615 }); 1616 1617 return terms; 1618 1619 } catch (e) { 1620 console.error(e); 1621 return []; 1622 } 1623 1624 } 1625 1626 function getDateTime() { 1627 var dateTime = new Array(); 1628 var date = new Date(), 1629 days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], 1630 months = ['January', 'February', 'March', 'April', 'May', 'June', 1631 'July', 'August', 'September', 'October', 'November', 'December' 1632 ], 1633 hours = ['00-01', '01-02', '02-03', '03-04', '04-05', '05-06', '06-07', '07-08', 1634 '08-09', '09-10', '10-11', '11-12', '12-13', '13-14', '14-15', '15-16', '16-17', 1635 '17-18', '18-19', '19-20', '20-21', '21-22', '22-23', '23-24' 1636 ]; 1637 dateTime.push(hours[date.getHours()]); 1638 dateTime.push(days[date.getDay()]); 1639 dateTime.push(months[date.getMonth()]); 1640 return dateTime; 1641 } 1642 1643 function filterEmails(value) { 1644 return Utils.validateEmail(value) ? undefined : value; 1645 } 1646 1647 /** 1648 * PUBLIC API 1649 */ 1650 return { 1651 1652 PRODUCT_SIMPLE : 0, 1653 PRODUCT_VARIABLE : 1, 1654 PRODUCT_BUNDLE : 2, 1655 PRODUCT_GROUPED : 3, 1656 utmTerms : utmTerms, 1657 utmId : utmId, 1658 isNewSession: checkSession(), 1659 setHidePixelCookie: function(){ 1660 let refresh_after_consent = false;
1661 if ( !firstVisit && options.gdpr.consent_magic_integration_enabled && window.CS_Data !== undefined && window.CS_Data.cs_refresh_after_consent) { 1662 refresh_after_consent = true; 1663 } 1664 if( !refresh_after_consent && !firstVisit) return false; 1665 1666 const url_parts = window.location.href; 1667 const url_params = new URLSearchParams(window.location.search); 1668 const matchingPixels = ["facebook", "ga", "gtm", "google_ads", "bing", "pinterest", "tiktok", "reddit", "openai"]; 1669 $.each(matchingPixels, function (index, slug) { 1670 var module = getPixelBySlag(slug); 1671 if (module && module.isEnabled()) { 1672 const fullUrl = window.location.href; 1673 $.each(module.getHidePixel(), function (index, hide_info) { 1674 for (const item of hide_info.hide_tag_contain) { 1675 if (item && fullUrl.includes(item)) { 1676 let hideTagTimeInMilliseconds = hide_info.hide_tag_time * 60 * 60 * 1000; 1677 let expiresTime = new Date().getTime() + hideTagTimeInMilliseconds; 1678 Cookies.set('hide_tag_' + hide_info.pixel, true, { expires: new Date(expiresTime), path: '/' }); 1679 break; 1680 } 1681 } 1682 }); 1683 } 1684 1685 }); 1686 }, 1687 /** 1688 * Is this Meta pixel opted into Meta-Enabled Conversion API? 1689 */ 1690 isMetaEnabledCapiPixel: function (pixelId) { 1691 var pixels = options.facebook && options.facebook.metaEnabledCapiPixels; 1692 return Array.isArray(pixels) && pixels.indexOf(pixelId) !== -1; 1693 }, 1694 hideMatchingPixel: function(pixelValue, slug) { 1695 if(Cookies.get('hide_tag_'+pixelValue)) 1696 { 1697 return true; 1698 } 1699 return false; 1700 }, 1701 /** 1702 * Whether a send_to value actually points at a destination. 1703 * 1704 * gtag('event', name, { send_to: [] }) has no destination, so gtag.js 1705 * parks the hit until a matching config command appears. Tag Assistant 1706 * reports that as "Some hits will only be sent after a config command 1707 * is issued via gtag('config')". Callers must skip the event instead. 1708 */ 1709 hasSendToTarget: function (target) { 1710 if (Array.isArray(target)) { 1711 return target.length > 0; 1712 } 1713 return typeof target === 'string' && target !== ''; 1714 }, 1715 validateEmail: function (email) { 1716 var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 1717 return re.test(email); 1718 }, 1719 fireEventForAllPixel:function(functionName,events){ 1720 Utils.refreshAddonPixels(); 1721 1722 if (events.hasOwnProperty(Facebook.tag())) 1723 Facebook[functionName](events[Facebook.tag()]); 1724 if (events.hasOwnProperty(Analytics.tag())) 1725 Analytics[functionName](events[Analytics.tag()]); 1726 if (events.hasOwnProperty(GAds.tag())) 1727 GAds[functionName](events[GAds.tag()]); 1728 if (events.hasOwnProperty(Pinterest.tag())) 1729 Pinterest[functionName](events[Pinterest.tag()]); 1730 if (events.hasOwnProperty(Bing.tag())) 1731 Bing[functionName](events[Bing.tag()]); 1732 if (events.hasOwnProperty(TikTok.tag())) 1733 TikTok[functionName](events[TikTok.tag()]); 1734 if (events.hasOwnProperty(Reddit.tag())) 1735 Reddit[functionName](events[Reddit.tag()]); 1736 1737 if (events.hasOwnProperty(GTM.tag())) 1738 GTM[functionName](events[GTM.tag()]); 1739 }, 1740 1741 getQueryValue:function (name){ 1742 return getQueryVars()[name]; 1743 }, 1744 1745 filterEmails: function (value) { 1746 return filterEmails(value); 1747 }, 1748 1749 /** 1750 * Re-read the pixels that live in separate plugins. 1751 */ 1752 refreshAddonPixels: function () { 1753 Pinterest = Utils.setupPinterestObject(); 1754 Bing = Utils.setupBingObject(); 1755 Reddit = Utils.setupRedditObject(); 1756 }, 1757 1758 /** 1759 * Is one of ConsentMagic's switches actually on? 1760 */ 1761 csModeEnabled: function ( value ) { 1762 return Number( value ) === 1; 1763 }, 1764 1765 setupPinterestObject: function () { 1766 Pinterest = window.pys.Pinterest || Pinterest; 1767 return Pinterest; 1768 }, 1769 1770 setupBingObject: function () { 1771 Bing = window.pys.Bing || Bing; 1772 return Bing; 1773 }, 1774 1775 setupRedditObject: function () { 1776 Reddit = window.pys.Reddit || Reddit; 1777 return Reddit; 1778 }, 1779 1780 // Clone all object members to another and return it 1781 copyProperties: function (from, to) { 1782 for (var key in from) { 1783 if("function" == typeof from[key]) { 1784 continue; 1785 } 1786 to[key] = from[key]; 1787 } 1788 return to; 1789 }, 1790 1791 getVideoTypeFromUrl: function (url) { 1792 if (!url) return 'unknown'; 1793 if (url.indexOf('youtube.com') !== -1 || url.indexOf('youtu.be') !== -1) return 'youtube'; 1794 if (url.indexOf('vimeo.com') !== -1) return 'vimeo'; 1795 return 'html5'; 1796 }, 1797 1798 getHtml5VideoTitle: function (url) { 1799 if (!url) return 'Unknown Title'; 1800 var path = url.split('?')[0].split('#')[0]; 1801 var filename = path.split('/').pop(); 1802 if (!filename) return 'Unknown Title'; 1803 var ext = filename.lastIndexOf('.'); 1804 return ext > 0 ? filename.slice(0, ext) : filename; 1805 }, 1806 1807 /** 1808 * Generate unique ID 1809 * 1810 * For purchase events (woo_purchase, edd_purchase): 1811 * - Server eventID has priority (for server-browser deduplication) 1812 * - If server didn't provide eventID, generate one keyed by order_id 1813 * 1814 * For other events: 1815 * - Use existing logic with event type as key 1816 */ 1817 generateUniqueId : function (event) { 1818 var eventsWithHook = [ 1819 'woo_purchase', 1820 'woo_start_trial', 1821 'woo_subscription_created', 1822 'woo_subscription_renewal', 1823 'edd_purchase', 1824 'edd_start_trial', 1825 'edd_subscription_created', 1826 'edd_subscription_renewal', 1827 'edd_license_created', 1828 'edd_license_upgrade', 1829 'edd_license_renewal' 1830 ]; 1831 1832 // For events with server hooks: prioritize server eventID 1833 if ( eventsWithHook.includes( event.e_id ) ) { 1834 // If server provided eventID, use it (for deduplication) 1835 if ( event.eventID && event.eventID.length > 0 ) { 1836 var orderKey = event.e_id; 1837 if ( !uniqueId.hasOwnProperty( orderKey ) ) { 1838 uniqueId[orderKey] = event.eventID; 1839 } 1840 return uniqueId[orderKey]; 1841 } 1842 } 1843 1844 if ( 1845 event.eventID.length == 0 || 1846 ( !event.isVariationTrigger && 1847 ( event.type == "static" && options.ajaxForServerStaticEvent || 1848 event.type !== "static" && options.ajaxForServerEvent ) 1849 ) 1850 ) { 1851 let idKey = event.hasOwnProperty('custom_event_post_id') ? event.custom_event_post_id : event.e_id; 1852 if ( !uniqueId.hasOwnProperty( idKey ) ) { 1853 uniqueId[idKey] = pys_generate_token(); 1854 } 1855 return uniqueId[idKey]; 1856 } 1857 else if ( event.eventID.length !== 0 ) { 1858 return event.eventID; 1859 } 1860 }, 1861 1862 // Flatten object for sendBeacon compatibility 1863 flattenObject: function (obj, prefix = '', res = {}) { 1864 for (const [key, value] of Object.entries(obj)) { 1865 const prefixedKey = prefix 1866 ? (Array.isArray(obj) ? `${prefix}[${key}]` : `${prefix}[${key}]`)
1867 : key; 1868 if (value !== null && typeof value === 'object' && !(value instanceof File)) { 1869 this.flattenObject(value, prefixedKey, res); 1870 } else { 1871 res[prefixedKey] = value; 1872 } 1873 } 1874 return res; 1875 }, 1876 1877 sendServerAjaxRequest: function ( url, data ) { 1878 // Check if we should use REST API for TikTok or Facebook 1879 if (data.action === 'pys_tiktok_api_event' && window.pysTikTokRest) { 1880 // Use TikTok REST API 1881 this.sendRestAPIRequest(data, 'tiktok'); 1882 return; 1883 } 1884 1885 if (data.action === 'pys_openai_api_event' && window.pysOpenAIRest) { 1886 this.sendRestAPIRequest(data, 'openai'); 1887 return; 1888 } 1889 1890 if (data.action === 'pys_api_event' && data.pixel === 'facebook' && window.pysFacebookRest) { 1891 // Use Facebook REST API 1892 this.sendRestAPIRequest(data, 'facebook'); 1893 return; 1894 } 1895 1896 // Check if sendBeacon is enabled and supported 1897 if (options.useSendBeacon && navigator.sendBeacon) { 1898 try { 1899 // Flatten the data object for sendBeacon compatibility 1900 const flattenedData = this.flattenObject(data); 1901 const formData = new URLSearchParams(); 1902 1903 // Convert flattened data to URLSearchParams 1904 for (const [key, value] of Object.entries(flattenedData)) { 1905 if (value !== null && value !== undefined) { 1906 formData.append(key, value); 1907 } 1908 } 1909 1910 // Try to send using sendBeacon 1911 const success = navigator.sendBeacon(url, formData); 1912 if (success) { 1913 return; // Successfully sent via sendBeacon 1914 } 1915 } catch (e) { 1916 // If sendBeacon fails, fall back to jQuery.ajax 1917 if (options.debug) { 1918 console.log('PYS: sendBeacon failed, falling back to jQuery.ajax:', e); 1919 } 1920 } 1921 } 1922 1923 // Fallback to jQuery.ajax 1924 jQuery.ajax( { 1925 type: 'POST', 1926 url: url, 1927 data: data, 1928 headers: { 1929 'Cache-Control': 'no-cache' 1930 }, 1931 success: function () { 1932 }, 1933 } ); 1934 }, 1935 1936 // Send event via REST API (unified function for TikTok, Facebook, and other platforms) 1937 // Replaces the old sendTikTokRestAPIRequest and sendFacebookRestAPIRequest functions 1938 sendRestAPIRequest: function (data, platform) { 1939 let restApiUrl; 1940 1941 // Get platform-specific REST API configuration 1942 switch (platform) { 1943 case 'tiktok': 1944 restApiUrl = window.pysTikTokRest ? window.pysTikTokRest.restApiUrl : '/wp-json/pys-tiktok/v1/event'; 1945 break; 1946 case 'facebook': 1947 restApiUrl = window.pysFacebookRest ? window.pysFacebookRest.restApiUrl : '/wp-json/pys-facebook/v1/event'; 1948 break; 1949 case 'pinterest': 1950 restApiUrl = window.pysPinterestRest ? window.pysPinterestRest.restApiUrl : '/wp-json/pys-pinterest/v1/event'; 1951 break; 1952 case 'openai': 1953 restApiUrl = window.pysOpenAIRest ? window.pysOpenAIRest.restApiUrl : '/wp-json/pys-openai/v1/event'; 1954 break; 1955 default: 1956 console.error('PYS: Unknown platform for REST API:', platform); 1957 this.sendAjaxFallback(data); 1958 return; 1959 } 1960 1961 // Prepare data for REST API 1962 const restApiData = { 1963 event: data.event, 1964 event_slug: data.event_slug, 1965 data: JSON.stringify(data.data || {}), 1966 ids: JSON.stringify(data.ids || []), 1967 eventID: data.event_id || data.eventID || '',
1968 woo_order: data.woo_order || '0', 1969 edd_order: data.edd_order || '0', 1970 order_key: data.order_key || '' 1971 }; 1972 1973 // Facebook-only: forward the per-page referrer so the CAPI 1974 // helper can attach it to custom_data.referrer_url. 1975 if (platform === 'facebook') { 1976 const forwardedReferrer = data.referrer_url || pageReferrer; 1977 if (forwardedReferrer) { 1978 restApiData.referrer_url = forwardedReferrer; 1979 } 1980 } 1981 1982 // Variant A: send the actual page URL where the event happened 1983 // so the server sets event_source_url to this page, not the 1984 // REST endpoint. PHP applies enable_remove_source_url_params. 1985 restApiData.event_source_url = data.url || window.location.href; 1986 1987 // Try to send using sendBeacon first (if enabled) 1988 if (options.useSendBeacon && navigator.sendBeacon) { 1989 try { 1990 const formData = new URLSearchParams(); 1991 for (const [key, value] of Object.entries(restApiData)) { 1992 formData.append(key, value); 1993 } 1994 1995 if (navigator.sendBeacon(restApiUrl, formData)) { 1996 return; 1997 } 1998 } catch (e) { 1999 // sendBeacon failed, continue to fetch 2000 } 2001 } 2002 2003 // Try to send using fetch with REST API 2004 if (window.fetch) { 2005 const headers = { 2006 'Content-Type': 'application/json' 2007 }; 2008 2009 fetch(restApiUrl, { 2010 method: 'POST', 2011 headers: headers, 2012 body: JSON.stringify(restApiData) 2013 }) 2014 .then(response => { 2015 if (!response.ok) { 2016 throw new Error(platform + ' REST API request failed: ' + response.status); 2017 } 2018 }) 2019 .catch(error => { 2020 // Fallback to AJAX if REST API fails 2021 if (options.debug) { 2022 console.log('PYS: ' + platform + ' REST API failed, falling back to AJAX:', error); 2023 } 2024 this.sendAjaxFallback(data); 2025 }); 2026 } else { 2027 // Fallback to AJAX if fetch is not supported 2028 this.sendAjaxFallback(data); 2029 } 2030 }, 2031 2032 2033 2034 // Fallback AJAX method 2035 sendAjaxFallback: function (data) { 2036 jQuery.ajax({ 2037 type: 'POST', 2038 url: options.ajaxUrl, 2039 data: data, 2040 headers: { 2041 'Cache-Control': 'no-cache' 2042 }, 2043 success: function () { 2044 }, 2045 }); 2046 }, 2047 2048 clone: function(obj) { 2049 var copy; 2050 2051 // Handle the 3 simple types, and null or undefined 2052 if (null == obj || "object" != typeof obj) return obj; 2053 2054 // Handle Date 2055 if (obj instanceof Date) { 2056 copy = new Date(); 2057 copy.setTime(obj.getTime()); 2058 return copy; 2059 } 2060 2061 // Handle Array 2062 if (obj instanceof Array) { 2063 copy = []; 2064 for (var i = 0, len = obj.length; i < len; i++) { 2065 if("function" == typeof obj[i]) { 2066 continue; 2067 } 2068 copy[i] = Utils.clone(obj[i]); 2069 } 2070 return copy; 2071 } 2072 2073 // Handle Object 2074 if (obj instanceof Object) { 2075 copy = {}; 2076 for (var attr in obj) { 2077 if (obj.hasOwnProperty(attr)) { 2078 if("function" == typeof obj[attr]) { 2079 continue; 2080 } 2081 copy[attr] = Utils.clone(obj[attr]); 2082 } 2083 } 2084 return copy; 2085 } 2086 2087 return obj; 2088 }, 2089 2090 // Returns array of elements with given tag name 2091 getTagsAsArray: function (tag) { 2092 return [].slice.call(document.getElementsByTagName(tag)); 2093 }, 2094 2095 2096 initializeVideoAPIs: function (options) { 2097 let trigger_has_video_YT, trigger_has_video_Vimeo = false;
2098 if (options.hasOwnProperty('automatic') && options.automatic.enable_video) { 2099 if ( options.triggerEventTypes.hasOwnProperty( "video_view" ) ) { 2100 Object.entries( options.triggerEventTypes.video_view ).forEach( function ( [ trigger_id, triggers ] ) { 2101 triggers.forEach( function ( trigger ) { 2102 if (trigger.type === 'youtube') { 2103 trigger_has_video_YT = true; 2104 } 2105 if(trigger.type === 'vimeo'){ 2106 trigger_has_video_Vimeo = true; 2107 } 2108 }); 2109 }); 2110 } 2111 if ( 2112 options.hasOwnProperty('enable_automatic_events') && 2113 options.enable_automatic_events 2114 ) { 2115 if (options.automatic.enable_youtube || trigger_has_video_YT) { 2116 Utils.initYouTubeAPI(); 2117 } 2118 if (options.automatic.enable_vimeo || trigger_has_video_Vimeo) { 2119 Utils.initVimeoAPI(); 2120 } 2121 initHtml5VideoTracking(); 2122 } 2123 } 2124 2125 // Also init YT/Vimeo APIs for video_speed custom triggers and global VideoSpeed event 2126 var has_video_speed_trigger = options.triggerEventTypes && options.triggerEventTypes.hasOwnProperty('video_speed') && 2127 Object.keys(options.triggerEventTypes.video_speed).length > 0; 2128 var has_global_video_speed = options.dynamicEvents && options.dynamicEvents.hasOwnProperty('automatic_event_video_speed'); 2129 if (has_video_speed_trigger || has_global_video_speed) { 2130 Utils.initYouTubeAPI(); 2131 Utils.initVimeoAPI(); 2132 } 2133 }, 2134 /** 2135 * Load and initialize YouTube API 2136 * 2137 * @link: https://developers.google.com/youtube/iframe_api_reference 2138 */ 2139 initYouTubeAPI: function () { 2140 2141 // maybe load YouTube JS API 2142 if (typeof window.YT === 'undefined') { 2143 var tag = document.createElement('script'); 2144 tag.src = '//www.youtube.com/iframe_api'; 2145 var firstScriptTag = document.getElementsByTagName('script')[0]; 2146 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); 2147 } 2148 2149 // initialize when API is ready 2150 if ( typeof window.onYouTubeIframeAPIReady !== 'function' ) { 2151 2152 window.onYouTubeIframeAPIReady = function () { 2153 2154 // collect all possible YouTube tags 2155 var potentialVideos = Utils.getTagsAsArray( 'iframe' ).concat(
2155Utils.getTagsAsArray( 'embed' ) ); 2156 2157 // turn videos into trackable videos with events 2158 for ( var i = 0; i < potentialVideos.length; i++ ) { 2159 var video = potentialVideos[ i ]; 2160 if ( tagIsYouTubeVideo( video ) ) { 2161 var iframe = normalizeYouTubeIframe( video ); 2162 addYouTubeEvents( iframe ); 2163 } else if ( tagIsYouTubeAsyncVideo( video ) ){ 2164 video.addEventListener( "load", function ( evt ) { 2165 var iframe = normalizeYouTubeIframe( evt.currentTarget ); 2166 addYouTubeEvents( iframe ); 2167 } ); 2168 } 2169 } 2170 2171 var targets = document.querySelectorAll( '.elementor-widget-video .elementor-wrapper' ); 2172 2173 const config = { 2174 attributes: false, 2175 childList: true, 2176 subtree: true 2177 }; 2178 2179 const callback = function ( mutationsList, observer ) { 2180 for ( let mutation of mutationsList ) { 2181 if ( mutation.type === 'childList' ) { 2182 for ( var m = 0; m < mutation.addedNodes.length; m++ ) { 2183 addDynYouTubeVideos( mutation.addedNodes[ m ] ); 2184 } 2185 } 2186 } 2187 }; 2188 // observe elementator widget-video and add event when it add iframe 2189 for ( var i = 0; i < targets.length; i++ ) { 2190 const observer = new MutationObserver( callback ); 2191 observer.observe( targets[ i ], config );//maybe remove before add 2192 } 2193 } 2194 } 2195 }, 2196 2197 /** 2198 * Load and initialize Vimeo API 2199 * 2200 * @link: https://github.com/vimeo/player.js 2201 */ 2202 initVimeoAPI: function () { 2203 2204 2205 $(document).ready(function () { 2206 2207 var potentialVideos = Utils.getTagsAsArray('iframe').concat(Utils.getTagsAsArray('embed')); 2208 2209 for (var i = 0; i < potentialVideos.length; i++) { 2210 var tag = potentialVideos[i]; 2211 if (tagIsVimeoVideo(tag)) { 2212 var nextElementHasClass = $(tag).closest('.elementor-widget-video').find('.elementor-custom-embed-image-overlay'); 2213 if(nextElementHasClass.length > 0) { 2214 tag.addEventListener("load", function() { 2215 attachVimeoPlayerToTag(this); 2216 }); 2217 } 2218 else { 2219 attachVimeoPlayerToTag(tag); 2220 } 2221 2222 2223 } else { 2224 if (tagIsAsincVimeoVideo(tag)) { 2225 tag.addEventListener("load", function(evt) { 2226 attachVimeoPlayerToTag(evt.currentTarget); 2227 }); 2228 } 2229 } 2230 } 2231 2232 }); 2233 2234 }, 2235 2236 /** 2237 * Ask PHP for this browser's external_id (single-flight). 2238 * 2239 * The cookie keeps the id across a network change, so it is 2240 * requested whenever external_id is on -- not only when server 2241 * events go over AJAX. Returns the in-flight jqXHR, or null when no 2242 * request is warranted (feature off, cookie already there, no 2243 * Meta pixel). 2244 */ 2245 requestPbid: function () { 2246 if (!options.send_external_id || Cookies.get('pbid') || !Facebook.isEnabled()) { 2247 return null; 2248 } 2249 2250 if (!pbidRequest) { 2251 pbidRequest = jQuery.ajax({ 2252 url: options.ajaxUrl, 2253 dataType: 'json', 2254 data: { 2255 action: 'pys_get_pbid' 2256 }, 2257 success: function (res) { 2258 if (res.data && res.data.pbid != false) { 2259 if(!(options.cookie.disabled_all_cookie || options.cookie.externalID_disabled_by_api)){ 2260 var expires = parseInt(options.external_id_expire || 180); 2261 Cookies.set('pbid', res.data.pbid, { expires: expires,path: '/',domain: cookieDomain() }); 2262 } 2263 2264 if(options.hasOwnProperty('facebook')) { 2265 options.facebook.advancedMatching = { 2266 ...options.facebook.advancedMatching, // spread current advancedMatching values 2267 external_id: res.data.pbid 2268 }; 2269 } 2270 } 2271 } 2272 }).always(function () { 2273 pbidRequest = null; 2274 }); 2275 } 2276 2277 return pbidRequest; 2278 }, 2279 2280 /** 2281 * Defer `fn` until this browser's external_id is known, or until a 2282 * short ceiling passes. Returns true when `fn` was deferred (it will 2283 * be called back), false when the caller may go ahead right now. 2284 * 2285 * Pixels read external_id when they initialise. On a first visit the 2286 * cookie does not exist yet, and the inline value in the page may 2287 * belong to whoever rendered a cached copy of it; the reply to 2288 * pys_get_pbid is never cached and names this browser, so the first 2289 * PageView waits for it. Sites that fetch identity over REST already 2290 * have the right value by now and do not wait again. 2291 */ 2292 holdForIdentity: function (fn) { 2293 if (pbidSettled || !options.send_external_id || Cookies.get('pbid')) { 2294 return false;
2295 } 2296 2297 if (options.dynamicDataUrl && options.facebook && options.facebook.advancedMatching 2298 && options.facebook.advancedMatching.external_id) { 2299 return false; 2300 } 2301 2302 var request = Utils.requestPbid(); 2303 if (!request) { 2304 return false; 2305 } 2306 2307 var done = false; 2308 var finish = function () { 2309 if (done) { 2310 return; 2311 } 2312 done = true; 2313 pbidSettled = true; 2314 fn(); 2315 }; 2316 2317 request.always(finish); 2318 setTimeout(finish, PBID_WAIT_MS); 2319 2320 return true; 2321 }, 2322 2323 manageCookies: function () { 2324 if ( typeof options.cookie === 'undefined' ) { 2325 return; 2326 } 2327 2328 // Handle alternative GCLID parameter for Safari compatibility 2329 handleAlternativeGclid(); 2330 2331 if (options.gdpr.cookiebot_integration_enabled && typeof Cookiebot !== 'undefined') { 2332 if (Cookiebot.consented === false && !Cookiebot.consent['marketing'] && !Cookiebot.consent['statistics']) { 2333 return; 2334 } 2335 } 2336 2337 let cm_consent_not_expressed = false; 2338 if ( options.gdpr.consent_magic_integration_enabled && window.CS_Data !== undefined && window.CS_Data.cs_refresh_after_consent == 1 ) { 2339 if ( Cookies.get( 'cs_viewed_cookie_policy' ) === undefined ) { 2340 cm_consent_not_expressed = true; 2341 } 2342 } 2343 2344 // Hand PHP the browser-verified cookie domain, under the same 2345 // consent gates as every cookie written below it. 2346 if( !cm_consent_not_expressed && !options.cookie.disabled_all_cookie ) { 2347 reportCookieDomain(cookieDomain()); 2348 } 2349 2350 if( !cm_consent_not_expressed && isNewSession && !options.cookie.disabled_all_cookie && !options.cookie.disabled_start_session_cookie) { 2351 let duration = options.last_visit_duration * 60000 2352 var now = new Date(); 2353 now.setTime(now.getTime() + duration); 2354 Cookies.set('pys_session_limit', true,{ expires: now, path: '/',domain: cookieDomain() }) 2355 Cookies.set('pys_start_session', true,{ path: '/',domain: cookieDomain() }); 2356 Utils.setHidePixelCookie(); 2357 } 2358 2359 if (!Cookies.get('pbid')) { 2360 Utils.requestPbid(); 2361 } else if (Facebook.isEnabled()){ 2362 if(options.hasOwnProperty('facebook')) { 2363 options.facebook.advancedMatching = { 2364 ...options.facebook.advancedMatching, 2365 external_id: Cookies.get('pbid') 2366 }; 2367 } 2368 } 2369 let expires = parseInt(options.cookie_duration); // days 2370 let queryVars = getQueryVars(); 2371 let landing = window.location.href; 2372 try { 2373 // save data for first visit 2374 if(Cookies.get('pys_first_visit') === undefined && (!options.cookie.disabled_all_cookie)) { 2375 2376 if(!options.cookie.disabled_first_visit_cookie) 2377 { 2378 Cookies.set('pys_first_visit', true, { expires: expires, path: '/',domain: cookieDomain() }); 2379 } 2380 else { 2381 Cookies.remove('pys_first_visit', { path: '/',domain: cookieDomain() } ) 2382 } 2383 2384 if(!options.cookie.disabled_trafficsource_cookie) 2385 { 2386 Cookies.set('pysTrafficSource', getTrafficSource(), { expires: expires,path: '/',domain: cookieDomain() }); 2387 } 2388 else { 2389 Cookies.remove('pysTrafficSource', { path: '/',domain: cookieDomain() }) 2390 } 2391 2392 if(!options.cookie.disabled_landing_page_cookie && options.enable_lading_page_param) 2393 { 2394 Cookies.set('pys_landing_page',landing,{ expires: expires,path: '/',domain: cookieDomain() }); 2395 } 2396 else { 2397 Cookies.remove('pys_landing_page', { path: '/',domain: cookieDomain() }) 2398 } 2399 2400 if(!options.cookie.disabled_utmTerms_cookie) 2401 { 2402 $.each(utmTerms, function (index, name) { 2403 if (queryVars.hasOwnProperty(name)) { 2404 Cookies.set('pys_' + name, queryVars[name], { expires: expires,path: '/',domain: cookieDomain() }); 2405 } else { 2406 Cookies.remove('pys_' + name, { path: '/',domain: cookieDomain() }) 2407 } 2408 }); 2409 } 2410 else { 2411 $.each(utmTerms, function (index, name) { 2412 Cookies.remove('pys_' + name, { path: '/',domain: cookieDomain() }) 2413 }); 2414 } 2415 2416 if(!options.cookie.disabled_utmId_cookie) 2417 { 2418 $.each(utmId,function(index,name) { 2419 if (queryVars.hasOwnProperty(name)) { 2420 Cookies.set('pys_' + name, queryVars[name], { expires: expires,path: '/',domain: cookieDomain() }); 2421 } else { 2422 Cookies.remove('pys_' + name, { path: '/',domain: cookieDomain() }) 2423 } 2424 }) 2425 } 2426 else { 2427 $.each(utmId, function (index, name) { 2428 Cookies.remove('pys_' + name, { path: '/',domain: cookieDomain() }) 2429 }); 2430 } 2431 2432 } 2433 // save data for last visit if it new session 2434 if(isNewSession && (!options.cookie.disabled_all_cookie)) { 2435 if(!options.cookie.disabled_trafficsource_cookie) 2436 { 2437 Cookies.set('last_pysTrafficSource', getTrafficSource(), { expires: expires,path: '/',domain: cookieDomain() }); 2438 } 2439 else { 2440 Cookies.remove('last_pysTrafficSource', { path: '/',domain: cookieDomain() }) 2441 } 2442 2443 if(!options.cookie.disabled_landing_page_cookie && options.enable_lading_page_param) 2444 { 2445 Cookies.set('last_pys_landing_page',landing,{ expires: expires,path: '/',domain: cookieDomain() }); 2446 } 2447 else { 2448 Cookies.remove('last_pys_landing_page', { path: '/',domain: cookieDomain() }) 2449 } 2450 2451 if(!options.cookie.disabled_utmTerms_cookie) 2452 { 2453 $.each(utmTerms, function (index, name) { 2454 if (queryVars.hasOwnProperty(name)) { 2455 Cookies.set('last_pys_' + name, queryVars[name], { expires: expires,path: '/',domain: cookieDomain() }); 2456 } 2457 }); 2458 } 2459 else { 2460 $.each(utmTerms, function (index, name) { 2461 Cookies.remove('last_pys_' + name, { path: '/',domain: cookieDomain() }) 2462 }); 2463 } 2464 2465 if(!options.cookie.disabled_utmId_cookie) 2466 { 2467 $.each(utmId,function(index,name) { 2468 if (queryVars.hasOwnProperty(name)) { 2469 Cookies.set('last_pys_' + name, queryVars[name], { expires: expires,path: '/',domain: cookieDomain() }); 2470 } 2471 }) 2472 } 2473 else { 2474 $.each(utmId, function (index, name) { 2475 Cookies.remove('last_pys_' + name, { path: '/',domain: cookieDomain() }) 2476 }); 2477 } 2478 2479 } 2480 if(options.cookie.disabled_start_session_cookie) { 2481 Cookies.remove('pys_start_session', { path: '/',domain: cookieDomain() }) 2482 Cookies.remove('pys_session_limit', { path: '/',domain: cookieDomain() }) 2483 } 2484 if(options.cookie.disabled_all_cookie) 2485 { 2486 Cookies.remove('pys_first_visit', { path: '/',domain: cookieDomain() }) 2487 Cookies.remove('pysTrafficSource', { path: '/',domain: cookieDomain() }) 2488 Cookies.remove('pys_landing_page', { path: '/',domain: cookieDomain() }) 2489 Cookies.remove('last_pys_landing_page', { path: '/',domain: cookieDomain() }) 2490 Cookies.remove('last_pysTrafficSource', { path: '/',domain: cookieDomain() }) 2491 Cookies.remove('pys_start_session', { path: '/',domain: cookieDomain() }) 2492 Cookies.remove('pys_session_limit', { path: '/',domain: cookieDomain() }) 2493 $.each(Utils.utmTerms, function (index, name) { 2494 Cookies.remove('pys_' + name, { path: '/',domain: cookieDomain() }) 2495 }); 2496 $.each(Utils.utmId,function(index,name) { 2497 Cookies.remove('pys_' + name, { path: '/',domain: cookieDomain() }) 2498 }) 2499 $.each(Utils.utmTerms, function (index, name) { 2500 Cookies.remove('last_pys_' + name, { path: '/',domain: cookieDomain() }) 2501 }); 2502 $.each(Utils.utmId,function(index,name) { 2503 Cookies.remove('last_pys_' + name, { path: '/',domain: cookieDomain() }) 2504 }); 2505 } 2506 } catch (e) { 2507 console.error(e); 2508 } 2509 }, 2510
2511 initializeRequestParams: function () { 2512 2513 if (options.trackTrafficSource) { 2514 requestParams.traffic_source = getTrafficSourceValue(); 2515 } 2516 2517 if (options.trackUTMs) { 2518 2519 var utms = getUTMs(options.visit_data_model === "last_visit"); 2520 2521 $.each(utmTerms, function (index, term) { 2522 if (term in utms) { 2523 requestParams[term] = utms[term]; 2524 } 2525 }); 2526 2527 } 2528 2529 2530 var dateTime = getDateTime(); 2531 if(options.enable_event_time_param) { 2532 requestParams.event_hour = dateTime[0]; 2533 } 2534 2535 if(options.enable_event_day_param) { 2536 requestParams.event_day = dateTime[1]; 2537 } 2538 if(options.enable_event_month_param) { 2539 requestParams.event_month = dateTime[2]; 2540 } 2541 2542 if(options.enable_lading_page_param){ 2543 requestParams.landing_page = stripUrlQuery(getLandingPageValue()); 2544 } 2545 }, 2546
2547 getRequestParams: function () { 2548 return requestParams; 2549 }, 2550 2551 /** 2552 * DOWNLOAD DOCS 2553 */ 2554 2555 getLinkExtension: function (link) { 2556 2557 // Remove anchor, query string and everything before last slash 2558 link = link.substring(0, (link.indexOf("#") === -1) ? link.length : link.indexOf("#")); 2559 link = link.substring(0, (link.indexOf("?") === -1) ? link.length : link.indexOf("?")); 2560 link = link.substring(link.lastIndexOf("/") + 1, link.length); 2561 2562 // If there's a period left in the URL, then there's a extension 2563 if (link.length > 0 && link.indexOf('.') !== -1) { 2564 link = link.substring(link.lastIndexOf(".") + 1); // Remove everything but what's after the first period 2565 return link; 2566 } else { 2567 return ""; 2568 } 2569 }, 2570 2571 getLinkFilename: function (link) { 2572 2573 // Remove anchor, query string and everything before last slash 2574 link = link.substring(0, (link.indexOf("#") === -1) ? link.length : link.indexOf("#")); 2575 link = link.substring(0, (link.indexOf("?") === -1) ? link.length : link.indexOf("?")); 2576 link = link.substring(link.lastIndexOf("/") + 1, link.length); 2577 2578 // If there's a period left in the URL, then there's a extension 2579 if (link.length > 0 && link.indexOf('.') !== -1) { 2580 return link; 2581 } else { 2582 return ""; 2583 } 2584 }, 2585 2586 /** 2587 * CUSTOM EVENTS 2588 */ 2589 2590 setupMouseOverClickEvents: function (eventId, triggers, triggerType) { 2591 2592 // Non-default binding used to avoid situations when some code in external js 2593 // stopping events propagation, eg. returns false, and our handler will never called 2594 document.addEventListener('mouseover', function(event) { 2595 var matchedElements = Array.from(document.querySelectorAll(triggers)); 2596 var clickedElement = event.target; 2597 var closestMatch = clickedElement.closest(triggers); 2598 if (matchedElements.includes(clickedElement) || closestMatch){ 2599 if (event.target.classList.contains('pys-mouse-over-' + eventId)) { 2600 return true; 2601 } else { 2602 event.target.classList.add('pys-mouse-over-' + eventId); 2603 } 2604 2605 Utils.handleTriggerFired(eventId, triggerType || 'css_mouseover'); 2606 } 2607 }); 2608 2609 2610 }, 2611 2612 setupFormFieldEvents: function (eventId, triggers, triggerType) { 2613 triggers.forEach(function(trigger) { 2614 let eventType = 'input'; 2615 2616 if (['checkbox', 'radio', 'select'].includes(trigger.type)) { 2617 eventType = 'change'; 2618 } else if (['text', 'email', 'tel'].includes(trigger.type)) { 2619 eventType = 'blur'; 2620 } 2621 2622 const form = document.querySelector(trigger.form); 2623 if (!form) return; 2624 2625 const field = form.querySelector(trigger.field); // search for the field ONLY within the form 2626 if (!field) return; 2627 2628 field.addEventListener(eventType, function (event) { 2629 Utils.handleTriggerFired(eventId, triggerType || 'form_field'); 2630 }, true); 2631 }); 2632 }, 2633 2634 setupCSSClickEvents: function (eventId, triggers, triggerType, clickCount, clickTimeLimit) { 2635 // Non-default binding used to avoid situations when some code in external js 2636 // stopping events propagation, eg. returns false, and our handler will never called
2637 // add event to document to support dyn class 2638 var requiredClicks = (clickCount && clickCount > 1) ? clickCount : 1; 2639 var timeWindow = (clickTimeLimit && clickTimeLimit > 0) ? clickTimeLimit : 0; 2640 var clickCounters = typeof WeakMap !== 'undefined' ? new WeakMap() : null; 2641 2642 document.addEventListener('click', function(event) { 2643 let matchedElements = Array.from(document.querySelectorAll(triggers)), 2644 clickedElement = event.target, 2645 closestMatch = clickedElement.closest(triggers); 2646 2647 if (matchedElements.includes(clickedElement) || closestMatch) { 2648 var target = closestMatch || clickedElement; 2649 2650 if (requiredClicks <= 1) { 2651 Utils.handleTriggerFired(eventId, triggerType || 'css_click'); 2652 return; 2653 } 2654 2655 if (!clickCounters) { 2656 Utils.handleTriggerFired(eventId, triggerType || 'css_click'); 2657 return; 2658 } 2659 2660 var now = Date.now(); 2661 var state = clickCounters.get(target) || { count: 0, last: 0, timer: null }; 2662 2663 // When a time window is set, reset the counter if the gap exceeded it. 2664 if (timeWindow > 0 && now - state.last > timeWindow) { 2665 state.count = 0; 2666 } 2667 state.count++; 2668 state.last = now; 2669 2670 if (state.timer) clearTimeout(state.timer); 2671 // Schedule cleanup to avoid memory leaks.
2672 // With time window: delete entry after window expires. 2673 // Without: quietly clear the timer reference after a long idle period. 2674 state.timer = setTimeout(function() { 2675 if (clickCounters) { 2676 if (timeWindow > 0) { 2677 clickCounters.delete(target); 2678 } else { 2679 var s = clickCounters.get(target); 2680 if (s) { s.timer = null; clickCounters.set(target, s); } 2681 } 2682 } 2683 }, timeWindow > 0 ? timeWindow : 60000); 2684 2685 clickCounters.set(target, state); 2686 2687 if (state.count >= requiredClicks) { 2688 clickCounters.delete(target); 2689 Utils.handleTriggerFired(eventId, triggerType || 'css_click'); 2690 } 2691 } 2692 }, true); 2693 }, 2694 2695 setupCopyElementEvents: function (eventId, selectors) { 2696 var selectorList = Array.isArray(selectors) ? selectors.join(',') : selectors; 2697 if (!selectorList) return; 2698 2699 document.addEventListener('copy', function() { 2700 var selection = window.getSelection(); 2701 if (!selection || selection.isCollapsed) return; 2702 2703 var node = selection.focusNode; 2704 var el = node ? (node.nodeType === 3 ? node.parentElement : node) : null; 2705 if (!el) return; 2706 2707 try { 2708 if (el.closest(selectorList)) { 2709 Utils.handleTriggerFired(eventId, 'copy_element'); 2710 } 2711 } catch (e) {} 2712 }); 2713 }, 2714 2715 setupVideoSpeedEvents: function (eventId, config) { 2716 var urls = Array.isArray(config) ? config : (config.urls || []); 2717 var speedRate = Array.isArray(config) ? 'any' : (config.speed_rate || 'any'); 2718 var triggers = Array.isArray(config) ? [] : (config.triggers || []); 2719 2720 var matchesPage = function() { 2721 if (!urls || urls.length === 0) return false; 2722 var href = window.location.href; 2723 return urls.some(function(u) { 2724 return u === '*' || u === '' || href.indexOf(u) !== -1; 2725 }); 2726 }; 2727 2728 var matchesRate = function(prev, curr) { 2729 if (speedRate === 'any') return curr !== prev; 2730 if (speedRate === 'increase') return curr > prev; 2731 if (speedRate === 'decrease') return curr < prev; 2732 return false; 2733 }; 2734 2735 2736 var matchesVideoId = function(videoId) { 2737 if (!triggers || triggers.length === 0) return true; 2738 var decId; 2739 try { decId = decodeURIComponent(videoId); } catch(e) { decId = videoId; } 2740 return triggers.some(function(t) { 2741 if (t.rule === videoId || t.rule === String(videoId)) return true; 2742 var decRule; 2743 try { decRule = decodeURIComponent(t.rule); } catch(e) { decRule = t.rule; } 2744 return decRule === decId; 2745 }); 2746 }; 2747 2748 if (!matchesPage()) return; 2749 2750 // HTML5 <video> speed tracking 2751 document.addEventListener('ratechange', function(event) { 2752 var video = event.target; 2753 if (!video || video.tagName !== 'VIDEO') return; 2754 var prev = video._pysSpeedRate !== undefined ? video._pysSpeedRate : 1; 2755 var curr = video.playbackRate; 2756 video._pysSpeedRate = curr; 2757 2758 if (matchesRate(prev, curr)) { 2759 var videoId = video.id || video.src || video.currentSrc || ''; 2760 if (matchesVideoId(videoId)) { 2761 Utils.handleTriggerFired(eventId, 'video_speed'); 2762 } 2763 } 2764 }, true); 2765 }, 2766 2767 setupURLClickEvents: function () { 2768 2769 if( !options.triggerEventTypes.hasOwnProperty('url_click') ) { 2770 return; 2771 } 2772 // Non-default binding used to avoid situations when some code in external js 2773 // stopping events propagation, eg. returns false, and our handler will never called 2774 document.addEventListener('click', function(event) { 2775 let anchor = event.target.closest('a'); 2776 2777 if (anchor) { 2778 var url = anchor.getAttribute('href'); 2779 if (url) { 2780 Object.entries(options.triggerEventTypes.url_click).forEac
2780h(function ([eventId, triggers]) { 2781 triggers.forEach(function (trigger) { 2782 if (Utils.compareUrl(url, trigger.value, trigger.rule)) { 2783 Utils.handleTriggerFired(eventId, 'url_click'); 2784 } 2785 }); 2786 }); 2787 } 2788 } 2789 }, true); 2790 2791 }, 2792 2793 removeUrlDomain(url) { 2794 if(url.indexOf("/#") > -1) { 2795 url = url.substring(0, url.indexOf("/#")); 2796 } 2797 return url.replace('http://','') 2798 .replace('https://','') 2799 .replace('www.','') 2800 .trim() 2801 .replace(/^\/+/g, '') 2802 2803 }, 2804 2805 compareUrl: function(base,url,rule){ 2806 2807 if(url == "*" || url == '') return true; 2808 2809 base = Utils.removeUrlDomain(base) 2810 url = Utils.removeUrlDomain(url) 2811 2812 if(rule == 'match') { 2813 return url == base; 2814 } else { 2815 return base.indexOf(url) !== -1 2816 } 2817 2818 }, 2819 2820 /** 2821 * Evaluate the custom-event conditions the server refused to decide. 2822 * 2823 * A condition on the landing page or the traffic source depends on the 2824 * individual visitor, so deciding it in PHP would bake one visitor's 2825 * answer into HTML that a page cache serves to everybody. PHP therefore 2826 * ships the condition itself in the event payload and we answer it here, 2827 * from this visitor's own cookies. See ConditionalEvent:: 2828 * isDeferredToFrontEnd() and CustomEvent::resolveConditions(). 2829 * 2830 * PHP only attaches pendingConditions when the answer can still change 2831 * the outcome, so an event carrying them and failing here must not fire. 2832 * 2833 * @param event one pixel's copy of the event; the payload is the same in all of them 2834 * @returns {boolean} 2835 */ 2836 conditionsPass: function (event) { 2837 2838 if (!event || !event.pendingConditions) return true; 2839 2840 var pending = event.pendingConditions, 2841 conditions = pending.conditions || []; 2842 2843 if (!conditions.length) return true; 2844 2845 var results = []; 2846 2847 for (var i = 0; i < conditions.length; i++) { 2848 results.push(Utils.checkPendingCondition(conditions[i])); 2849 } 2850 2851 if (pending.logic === 'AND') { 2852 return results.indexOf(false) === -1; 2853 } 2854 return results.indexOf(true) !== -1; 2855 }, 2856 2857 /** 2858 * @returns {boolean} 2859 */ 2860 checkPendingCondition: function (condition) { 2861 2862 var rule = condition.rule, 2863 value = condition.value; 2864 2865 // The PHP rule compares rtrim($base, '/') with rtrim($url, '/'), while 2866 // Utils.compareUrl() below compares exactly. That helper is shared with 2867 // trigger matching, so normalise here instead of changing it. Skipped 2868 // when trimming would empty the value, because compareUrl() reads an 2869 // empty needle as "anything matches" and PHP does not. 2870 var matchExact = function (base) { 2871 var needle = String(value == null ? '' : value), 2872 trimmed = needle.replace(/\/+$/, ''); 2873 if (trimmed === '') { 2874 return Utils.compareUrl(base, needle, 'match'); 2875 } 2876 return Utils.compareUrl(String(base == null ? '' : base).replace(/\/+$/, ''), trimmed, 'match'); 2877 }; 2878 2879 switch (condition.type) { 2880 2881 case 'landing_page': 2882 var landing = getLandingPageValue(); 2883 if (rule === 'param_contains' || rule === 'param_match') { 2884 return Utils.compareUrlParams(landing, value, rule); 2885 } 2886 landing = stripUrlQuery(landing); 2887 return rule === 'match' ? matchExact(landing) : Utils.compareUrl(landing, value, rule); 2888 2889 case 'source': 2890 var source = getTrafficSourceValue(); 2891 return rule === 'match' ? matchExact(source) :
2891Utils.compareUrl(source, value, rule); 2892 2893 case 'url_parameters': 2894 // The stored rule is contains/match; PHP prefixes it before 2895 // comparing â compareURLs( $value, '', 'param_' . $rule ) â so 2896 // the comparison always runs against the query string, never 2897 // the path. The query string of *this* request is the point: 2898 // the cached HTML was built for whoever arrived first. 2899 return Utils.compareUrlParams(window.location.href, value, 'param_' + rule); 2900 2901 default: 2902 // A condition type this build cannot judge. PHP deferred it, 2903 // so nobody has decided it â fire rather than swallow the 2904 // event: a missing event is invisible in the reports, a 2905 // superfluous one is not. 2906 return true; 2907 } 2908 }, 2909 2910 /** 2911 * The param_match / param_contains rules of the PHP side, which apply to 2912 * the query string of the landing page rather than to its path. 2913 * 2914 * param_match: any "key=value" pair in the condition matches the URL. 2915 * param_contains: the condition text appears in any parameter name or 2916 * value, case-insensitively. 2917 * 2918 * @returns {boolean} 2919 */ 2920 compareUrlParams: function (url, value, rule) { 2921 2922 value = String(value == null ? '' : value).replace(/\*+$/, ''); 2923 2924 if (value === '' || value === '*') return true; 2925 2926 // PHP parses both sides with parse_str(), which rewrites spaces and 2927 // dots in parameter *names* to underscores. Mirror that, or a 2928 // condition on "a.b" would never meet the "a_b" the server sees. 2929 var normaliseKey = function (raw) { 2930 return decodeURIComponent(String(raw).replace(/\+/g, ' ')).replace(/[ .]/g, '_'); 2931 }; 2932 2933 var query = String(url == null ? '' : url).split('#')[0].split('?')[1] || '', 2934 params = {}; 2935 2936 query.replace(/&/g, '&').split('&').forEach(function (pair) { 2937 if (!pair) return; 2938 var parts = pair.split('='); 2939 params[normaliseKey(parts[0])] = 2940 decodeURIComponent((parts.slice(1).join('=') || '').replace(/\+/g, ' ')); 2941 }); 2942 2943 if (rule === 'param_match') { 2944 var wanted = value.split('&'); 2945 for (var i = 0; i < wanted.length; i++) { 2946 var parts = wanted[i].split('='), 2947 key = normaliseKey(parts[0]), 2948 expected = decodeURIComponent((parts.slice(1).join('=') || '').replace(/\+/g, ' ')); 2949 if (params.hasOwnProperty(key) && params[key] === expected) { 2950 return true; 2951 } 2952 } 2953 return false; 2954 } 2955 2956 var needle = value.toLowerCase(); 2957 for (var name in params) { 2958 if (!params.hasOwnProperty(name)) continue; 2959 if (name.toLowerCase().indexOf(needle) !== -1) return true; 2960 if (String(params[name]).toLowerCase().indexOf(needle) !== -1) return true; 2961 } 2962 return false; 2963 }, 2964 2965 setupScrollPosEvents: function (eventId, triggers, triggerType) { 2966 2967 var scrollPosThresholds = {}, 2968 docHeight = $(document).height() - $(window).height(); 2969 2970 // convert % to absolute positions 2971 $.each(triggers, function (index, scrollPos) { 2972 2973 // convert % to pixels 2974 scrollPos = docHeight * scrollPos / 100; 2975 scrollPos = Math.round(scrollPos); 2976 2977 scrollPosThresholds[scrollPos] = eventId; 2978 2979 }); 2980 2981 $(document).on("scroll",function () { 2982 2983 var scrollPos = $(window).scrollTop(); 2984 2985 $.each(scrollPosThresholds, function (threshold, eventId) { 2986 2987 // position has not reached yes 2988 if (scrollPos < threshold - 0.5) { 2989 return true; 2990 } 2991 2992 // fire event only once 2993 if (eventId === null) { 2994 return true; 2995 } else { 2996 scrollPosThresholds[threshold] = null; 2997 } 2998 2999 Utils.handleTriggerFired(eventId, triggerType || 'scroll_pos'); 3000 }); 3001 3002 }); 3003 3004 }, 3005 setupCommentEvents : function (eventId,triggers) { 3006 $('form.comment-form').on("submit",function () { 3007 Utils.fireTriggerEvent(eventId); 3008 }); 3009 }, 3010 3011 setupEmailLinkEvents: function ( events ) { 3012 3013 $( document ).on( 'click', 'a', function ( event ) { 3014 let anchor = event.target.closest('a'); 3015 3016 if (anchor) { 3017 let url = anchor.getAttribute('href'); 3018 if (url && (url.startsWith('mailto:') || url.startsWith('tel:'))) { 3019 3020 let sendEventIds = [], 3021 disabled_for_pixels = [], 3022 disabled_email_action = false;
3023 3024 $.each(events, function (eventId, trigger_group) { 3025 url = url.replace('mailto:', '').replace('tel:', ''); 3026 trigger_group.forEach(function (triggers) { 3027 triggers.rules.forEach(function (trigger) { 3028 if (Utils.compareUrl(url, trigger.value, trigger.rule)) { 3029 sendEventIds.push(eventId); 3030 if (triggers.hasOwnProperty('disabled_email_link') && triggers.disabled_email_link) { 3031 disabled_email_action = true; 3032 if (options.triggerEvents.hasOwnProperty(eventId)) { 3033 disabled_for_pixels.push(...Object.keys(options.triggerEvents[eventId])); 3034 } 3035 } 3036 } 3037 }); 3038 }) 3039 }) 3040 3041 if (sendEventIds.length > 0) { 3042 sendEventIds.forEach(function (sendEventId) { 3043 Utils.handleTriggerFired(sendEventId, 'email_link'); 3044 }) 3045 } 3046 setupEmailLinks(disabled_for_pixels); 3047 } 3048 } 3049 } ) 3050 }, 3051 3052 /** 3053 * Events 3054 */ 3055 3056 isEventInTimeWindow: function (eventName, event, prefix) { 3057 3058 if(event.hasOwnProperty("hasTimeWindow") && event.hasTimeWindow) { 3059 var cookieName = prefix+"_"+eventName; 3060 var now = new Date().getTime(); 3061 3062 if(Cookies.get(cookieName) !== undefined) { 3063 3064 var lastTimeFire = Cookies.get(cookieName); 3065 var fireTime = event.timeWindow * 60*60*1000; 3066 3067 if( now - lastTimeFire > fireTime) { 3068 Cookies.set(cookieName,now, { expires: event.timeWindow / 24.0, path: '/'} ); 3069 } else { 3070 return false; 3071 } 3072 } else { 3073 Cookies.set(cookieName,now, { expires: event.timeWindow / 24.0, path: '/'} ); 3074 } 3075 } 3076 return true 3077 }, 3078 3079 handleTriggerFired: function (eventId, triggerType) { 3080 if (options.triggerLogic && options.triggerLogic[eventId] === 'AND') { 3081 var state = pysCustomEventsState[eventId]; 3082 if (!state || state.fired) return; 3083 state.triggered[triggerType] = true; 3084 if (Object.keys(state.triggered).length >= state.total) { 3085 state.fired = true; 3086 Utils.fireTriggerEvent(eventId); 3087 } 3088 } else { 3089 Utils.fireTriggerEvent(eventId); 3090 } 3091 }, 3092 3093 fireTriggerEvent: function (eventId) { 3094 3095 if (!options.triggerEvents.hasOwnProperty(eventId)) { 3096 return; 3097 } 3098 3099 if (options.triggerFrequency && options.triggerFrequency[eventId] === 'once') { 3100 if (pysEventFiredOnce[eventId]) return; 3101 pysEventFiredOnce[eventId] = true; 3102 } 3103 3104 var event = {}; 3105 var events = options.triggerEvents[eventId]; 3106 3107 // One gate for every pixel: they all carry the same payload. 3108 for (var pixelSlug in events) { 3109 if (!events.hasOwnProperty(pixelSlug)) continue; 3110 if (!Utils.conditionsPass(events[pixelSlug])) return; 3111 break; 3112 } 3113 3114 if (events.hasOwnProperty('facebook')) { 3115 event = events.facebook;
3116 if(Utils.isEventInTimeWindow(event.name,event,"dyn_facebook_"+eventId)) { 3117 event = Utils.getFormFilledData(event); 3118 Facebook.fireEvent(event.name, event); 3119 } 3120 } 3121 3122 if (events.hasOwnProperty('ga')) { 3123 event = events.ga; 3124 if(Utils.isEventInTimeWindow(event.name,event,"dyn_ga_"+eventId)) { 3125 event = Utils.getFormFilledData(event); 3126 Analytics.fireEvent(event.name, event); 3127 } 3128 } 3129 3130 if (events.hasOwnProperty('google_ads')) { 3131 event = events.google_ads; 3132 if(Utils.isEventInTimeWindow(event.name,event,"dyn_google_ads_"+eventId)) { 3133 event = Utils.getFormFilledData(event); 3134 GAds.fireEvent(event.name, event); 3135 } 3136 } 3137 3138 if (events.hasOwnProperty('pinterest')) { 3139 event = events.pinterest; 3140 if(Utils.isEventInTimeWindow(event.name,event,"dyn_pinterest_"+eventId)) { 3141 event = Utils.getFormFilledData(event); 3142 Pinterest.fireEvent(event.name, event);; 3143 } 3144 } 3145 3146 if (events.hasOwnProperty('bing')) { 3147 event = events.bing; 3148 if(Utils.isEventInTimeWindow(event.name,event,"dyn_bing_"+eventId)) { 3149 event = Utils.getFormFilledData(event); 3150 Bing.fireEvent(event.name, event);; 3151 } 3152 } 3153 if (events.hasOwnProperty('tiktok')) { 3154 event = events.tiktok; 3155 if(Utils.isEventInTimeWindow(event.name,event,"dyn_tiktok_"+eventId)) { 3156 event = Utils.getFormFilledData(event); 3157 TikTok.fireEvent(event.name, event); 3158 } 3159 } 3160 if (events.hasOwnProperty('reddit')) { 3161 event = events.reddit; 3162 if(Utils.isEventInTimeWindow(event.name,event,"dyn_reddit_"+eventId)) { 3163 event = Utils.getFormFilledData(event); 3164 Reddit.fireEvent(event.name, event); 3165 } 3166 } 3167 3168 3169 if (events.hasOwnProperty('openai')) { 3170 event = events.openai; 3171 if(Utils.isEventInTimeWindow(event.name,event,"dyn_openai_"+eventId)) { 3172 event = Utils.getFormFilledData(event); 3173 OpenAI.fireEvent(event.name, event); 3174 } 3175 } 3176 3177 if (events.hasOwnProperty('gtm')) { 3178 event = events.gtm; 3179 if(Utils.isEventInTimeWindow(event.name,event,"dyn_gtm_"+eventId)) { 3180 event = Utils.getFormFilledData(event); 3181 GTM.fireEvent(event.name, event); 3182 } 3183 } 3184 3185 3186 }, 3187 3188 isFirstPurchaseFire: function ($eventName,orderId,pixel) { 3189 3190 if(Cookies.get("pys_"+$eventName+"_order_id_"+pixel) == orderId) { 3191 return false; 3192 } else { 3193 Cookies.set("pys_"+$eventName+"_order_id_"+pixel, orderId, { expires: 1,path: '/' }); 3194 } 3195 return true; 3196 }, 3197 3198 fireStaticEvents: function (pixel, timeout = 0) { 3199 3200 Utils.refreshAddonPixels(); 3201 3202 if (options.staticEvents.hasOwnProperty(pixel)) { 3203 3204 $.each(options.staticEvents[pixel], function (eventId, events) { 3205 3206 //skip purchase event if this order was fired 3207 if( options.woo.hasOwnProperty('woo_purchase_on_transaction') && 3208 options.woo.woo_purchase_on_transaction && 3209 (eventId === "woo_purchase" || eventId === "woo_purchase_category") ) { 3210 if(!Utils.isFirstPurchaseFire(eventId,events[0].woo_order,pixel)) { 3211 return; 3212 } 3213 } 3214 3215 if( options.edd.hasOwnProperty('edd_purchase_on_transaction') && 3216 options.edd.edd_purchase_on_transaction && 3217 (eventId === "edd_purchase" || eventId === "edd_purchase_category") ) { 3218 if(!Utils.isFirstPurchaseFire(eventId,events[0].edd_order,pixel)) { 3219 return; 3220 } 3221 } 3222 3223 3224 $.each(events, function (index, event) { 3225 3226 event.fired = event.fired || false; 3227 3228 if (!event.fired && Utils.conditionsPass(event) && Utils.isEventInTimeWindow(event.
3228name,event,'static_' + pixel+"_")) { 3229 event = Utils.getFormFilledData(event); 3230 3231 var fired = false; 3232 3233 // fire event 3234 if ( pixel === 'tiktok' ) { 3235 setTimeout(function() { 3236 getPixelBySlag(pixel).fireEvent(event.name, event); 3237 }, timeout * 500); 3238 timeout++; 3239 } else { 3240 getPixelBySlag(pixel).fireEvent(event.name, event); 3241 } 3242 3243 // prevent event double event firing 3244 event.fired = fired; 3245 } 3246 }); 3247 }); 3248 } 3249 }, 3250 3251 /** 3252 * Load tag's JS 3253 * 3254 * @link: https://developers.google.com/analytics/devguides/collection/gtagjs/ 3255 * @link: https://developers.google.com/analytics/devguides/collection/gtagjs/custom-dims-mets 3256 */ 3257 loadGoogleTag: function (id) { 3258 3259 if (!gtag_loaded) { 3260 let dataLayerName = this.resolveGaDataLayerName(); 3261 this.dataLayerName = dataLayerName; 3262 (function (window, document, src) { 3263 var a = document.createElement('script'), 3264 m = document.getElementsByTagName('script')[0]; 3265 a.async = 1; 3266 a.src = src; 3267 m.parentNode.insertBefore(a, m); 3268 })(window, document, '//www.googletagmanager.com/gtag/js?id=' + id+'&l='+this.dataLayerName); 3269 3270 window[dataLayerName] = window[dataLayerName] || []; 3271 // Published only so custom client code and third-party snippets 3272 // have a gtag() to call. PYS itself must never route through it: 3273 // the `||` means an earlier definition (Site Kit, a theme, the 3274 // GTM bootstrap) keeps ownership and would point at its own data 3275 // layer. Use Utils.gtagPush() instead. 3276 window.gtag = window.gtag || function gtag() { 3277 window[dataLayerName].push(arguments); 3278 }; 3279 3280 if ( options.google_consent_mode ) { 3281 let data = this.resolveConsentValues(); 3282 3283 // Normally this data layer is ours alone and still empty, so a 3284 // `default` is what belongs here. But the GA and GTM scopes 3285 // collapse into one layer when their names resolve to the same 3286 // value (gtm_dataLayer_name left at 'dataLayerPYS', or the GA 3287 // layer set to "Disable name transformation"), and then the GTM 3288 // <head> snippet has already written a strict `denied` default 3289 // here. Repeating `default` would leave GA denied; `update` is 3290 // the documented transition once a default exists. 3291 this.pushConsent( dataLayerName, this.layerHasConsentDefault( dataLayerName ) ? 'update' : 'default', data ); 3292 } 3293 3294 Utils.gtagPush('js', new Date()); 3295 gtag_loaded = true; 3296 3297 } 3298 3299 }, 3300 3301 /** 3302 * Name of the data layer gtag.js is loaded with, see loadGoogleTag(). 3303 * Pure, so it can be resolved before the tag is loaded. 3304 */ 3305 resolveGaDataLayerName: function () { 3306 if ( options.hasOwnProperty( 'GATags' ) ) { 3307 switch ( options.GATags.ga_datalayer_type ) { 3308 case 'default': 3309 return 'dataLayerPYS'; 3310 case 'custom': 3311 return options.GATags.ga_datalayer_name; 3312 default: 3313 return 'dataLayer'; 3314 } 3315 } 3316 return this.dataLayerName || 'dataLayer'; 3317 }, 3318 3319 /** 3320 * Name of the GTM container's data layer. Mirrors GTM::pys_wp_header_top() 3321 * in modules/google_gtm/gtm.php â the two must agree, or the <head> consent 3322 * default and the footer consent update land in different layers. `??` 3323 * alone is not enough: the option can be saved as an empty string, and 3324 * window[''] is a silent dead end. 3325 */ 3326 resolveGtmDataLayerName: function () { 3327 var name = options.hasOwnProperty( 'gtm' ) ? options.gtm.gtm_dataLayer_name : ''; 3328 return ( typeof name === 'string' && name.length ) ? name : 'dataLayer'; 3329 }, 3330 3331 /** 3332 * Sends a gtag command (js/config/set/event/consent) to the GA data layer. 3333 * 3334 * Deliberately does NOT call window.gtag. That global is created with 3335 * `window.gtag = window.gtag || ...`, so whoever defined it first owns it: 3336 * if a theme, Site Kit or the GTM bootstrap got there first, our config 3337 * and event commands would silently land in their data layer while the 3338 * gtag.js instance we loaded (&l=<GA layer>) never receives a config â 3339 * Tag Assistant then reports "Some hits will only be sent after a config 3340 * command is issued". Pushing the arguments object is exactly what gtag() 3341 * does internally, so the payload is identical. 3342 */ 3343 gtagPush: function () { 3344 var name = this.dataLayerName || this.resolveGaDataLayerName(); 3345 window[ name ] = window[ name ] || []; 3346 window[ name ].push( arguments ); 3347 }, 3348 3349 /** 3350 * Back-compat alias of the old GA-scope consent sender. Exposed through 3351 * window.pys.Utils, so custom client code may call it. New code uses 3352 * pushConsent() with an explicit data layer name. 3353 */ 3354 loadDefaultConsent: function() { 3355 this.gtagPush.apply( this, arguments ); 3356 }, 3357 3358 /** 3359 * Sends one Consent Mode command ('default' or 'update') to a named data 3360 * layer. The single place that writes consent commands. 3361 * 3362 * The IIFE is not decoration: gtag() pushes its own `arguments` object, 3363 * and gtag.js / GTM read the command by index off that object. Pushing a 3364 * plain array works today but is not what the tag emits, so we reproduce 3365 * the exact shape â same reason gtm.php builds its <head> command this way. 3366 */ 3367 pushConsent: function ( layerName, command, values ) { 3368 if ( !layerName ) { 3369 return; 3370 } 3371 window[ layerName ] = window[ layerName ] || []; 3372 ( function () { 3373 window[ layerName ].push( arguments ); 3374 } )( 'consent', command, values ); 3375 }, 3376 3377 /** 3378 * Whether a Consent Mode `default` has already been written to this data 3379 * layer â by our own <head> snippet, by a consent plugin, or by a third 3380 * party (Site Kit, a theme). Decides `default` vs `update`: Consent Mode 3381 * documents exactly one `default` per layer, and a repeated one is not a 3382 * defined transition. Consent Magic reads the layer the same way 3383 * (cs-public.js, CS_Consent_Mode.setDefaultConsent). 3384 */ 3385 layerHasConsentDefault: function ( layerName ) { 3386 var layer = window[ layerName ]; 3387 if ( !layer || typeof layer.length !== 'number' ) { 3388 return false;
3389 } 3390 for ( var i = 0; i < layer.length; i++ ) { 3391 var item = layer[ i ]; 3392 // Entries pushed by other integrations are plain objects; indexing 3393 // them just yields undefined. 3394 if ( item && item[ 0 ] === 'consent' && item[ 1 ] === 'default' ) { 3395 return true; 3396 } 3397 } 3398 return false; 3399 }, 3400 3401 /** 3402 * The four Consent Mode signals as PYS currently resolves them. `enabled` 3403 * is false when consent mode is off for that signal, in which case Google's 3404 * absence of a value means granted anyway â say so explicitly. 3405 */ 3406 resolveConsentValues: function () { 3407 return { 3408 'analytics_storage': options.gdpr.analytics_storage.enabled ? options.gdpr.analytics_storage.value : 'granted', 3409 'ad_storage': options.gdpr.ad_storage.enabled ? options.gdpr.ad_storage.value : 'granted', 3410 'ad_user_data': options.gdpr.ad_user_data.enabled ? options.gdpr.ad_user_data.value : 'granted', 3411 'ad_personalization': options.gdpr.ad_personalization.enabled ? options.gdpr.ad_personalization.value : 'granted', 3412 }; 3413 }, 3414 3415 loadGTMScript: function (id = '') { 3416 // Strip trailing slash to allow both "domain.com" and "domain.com/" inputs. 3417 const domain = ( options.gtm.gtm_container_domain ?? 'www.googletagmanager.com' ).replace( /\/$/, '' ); 3418 const loader = options.gtm.gtm_container_identifier ?? 'gtm'; 3419 const gtm_auth = options.gtm.gtm_auth ?? ''; 3420 const gtm_preview = options.gtm.gtm_preview ?? ''; 3421 const datalayer_name = this.resolveGtmDataLayerName(); 3422 3423 window[ datalayer_name ] = window[ datalayer_name ] || []; 3424 window.gtag = window.gtag || function gtag() { 3425 window[ datalayer_name ].push( arguments ); 3426 }; 3427 3428 // Send the consent state synchronously before the GTM script tag is 3429 // inserted, so it is in the dataLayer before GTM processes any events. 3430 this.pushGTMConsentState( datalayer_name ); 3431 3432 (function(w, d, s, l, i) { 3433 w[l] = w[l] || []; 3434 w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' }); 3435 const f = d.getElementsByTagName(s)[0]; 3436 const j = d.createElement(s); 3437 const dl = l !== 'dataLayer' ? '&l=' + l : ''; 3438 j.async = true; 3439 3440 // Detect domain format: bare hostname (legacy) vs full URL / relative path (new). 3441 // Legacy "www.googletagmanager.com" â prepend https:// and append .js. 3442 // New format "/metrics" or "https://custom.com" â use as-is. 3443 // Identifier with extension (e.g. "gtm.js") â file: domain/gtm.js?id= 3444 // Identifier without extension (e.g. "g4h25o") â path: domain/g4h25o/?id= 3445 const isNewFormat = domain.startsWith( 'http://' ) 3446 || domain.startsWith( 'https://' ) 3447 || domain.startsWith( '/' ); 3448 3449 if ( !isNewFormat ) { 3450 j.src = 'https://' + domain + '/' + loader + '.js?id=' + i + dl; 3451 } else { 3452 const hasExtension = loader.includes( '.' ); 3453 j.src = domain + '/' + loader + ( hasExtension ? '?' : '/?' ) + 'id=' + i + dl; 3454 } 3455 3456 if (gtm_auth && gtm_preview) { 3457 j.src += '>m_auth=' + gtm_auth + '>m_preview=' + gtm_preview + '>m_cookies_win=x'; 3458 } 3459 f.parentNode.insertBefore(j, f); 3460 })(window, document, 'script', datalayer_name, id); 3461 3462 }, 3463 3464 /** 3465 * Back-compat aliases. Both are reachable as window.pys.Utils.* and may be 3466 * called by custom client code, so they keep working; internally everything 3467 * goes through pushConsent() / pushGTMConsentState() with an explicit layer 3468 * name instead of the implicit this.GTMdataLayerName state. 3469 */ 3470 loadDefaultGTMConsent: function() { 3471 window[ this.GTMdataLayerName ] = window[ this.GTMdataLayerName ] || []; 3472 window[ this.GTMdataLayerName ].push( arguments ); 3473 }, 3474 pushGTMConsentDefaults: function( datalayer_name ) { 3475 this.pushGTMConsentState( datalayer_name ); 3476 }, 3477 3478 /** 3479 * Sends the GTM container's consent state, once per page load. 3480 * 3481 * Called from loadGTMScript() (PYS loads the container itself) and from 3482 * GTM.loadPixel() when gtm_just_data_layer is on, so the state is sent 3483 * whichever path runs â and only once when both do. 3484 * 3485 * Emits `update`, not a second `default`, whenever a default already sits 3486 * in the layer. modules/google_gtm/gtm.php writes a strict all-denied 3487 * default with wait_for_update:500 in <head>, before the container boots; 3488 * that is correct Consent Mode practice, and it is what makes PYS's own 3489 * prior-consent gate work â this function runs downstream of 3490 * Utils.consentGiven('analytics'), so nothing is granted until consent 3491 * actually exists. What was missing is the other half of the contract: 3492 * the update that resolves the wait_for_update window. A repeated 3493 * `default` is not a defined transition and left GTM-managed tags denied. 3494 */ 3495 pushGTMConsentState: function( datalayer_name ) { 3496 if ( !options.google_consent_mode || gtm_consent_state_pushed ) { 3497 return; 3498 } 3499 3500 // A consent management plugin owns the transition: it issues its own 3501 // update from the user's live choice, while ours carries whatever the 3502 // cookies said when the page was rendered. Consent Magic diffs against 3503 // the last consent values in the layer, so a late push from us either 3504 // suppresses its update or overwrites the choice the user just made. 3505 // The <head> default already carries the CMP's resolved values. 3506 if ( options.consent_mode_cmp_active ) { 3507 return; 3508 } 3509 3510 gtm_consent_state_pushed = true; 3511 // Kept in sync for the loadDefaultGTMConsent() back-compat alias. 3512 this.GTMdataLayerName = datalayer_name; 3513 this.pushConsent( 3514 datalayer_name, 3515 this.layerHasConsentDefault( datalayer_name ) ? 'update' : 'default', 3516 this.resolveConsentValues() 3517 ); 3518 }, 3519 3520 /** 3521 * GDPR 3522 */ 3523 3524 loadPixels: function () { 3525 3526 if (options.gdpr.ajax_enabled && !options.gdpr.consent_magic_integration_enabled) { 3527 3528 // retrieves actual PYS GDPR filters values which allow to avoid cache issues 3529 $.get({ 3530 url: options.ajaxUrl, 3531 dataType: 'json', 3532 data: { 3533 action: 'pys_get_gdpr_filters_values' 3534 }, 3535 success: function (res) { 3536 3537 if (res.success) { 3538 3539 options.gdpr.all_disabled_by_api = res.data.all_disabled_by_api; 3540 options.gdpr.facebook_disabled_by_api = res.data.facebook_disabled_by_api; 3541 options.gdpr.tiktok_disabled_by_api = res.data.tiktok_disabled_by_api; 3542 options.gdpr.openai_disabled_by_api = res.data.openai_disabled_by_api; 3543 options.gdpr.analytics_disabled_by_api = res.data.analytics_disabled_by_api; 3544 options.gdpr.google_ads_disabled_by_api = res.data.google_ads_disabled_by_api; 3545 options.gdpr.pinterest_disabled_by_api = res.data.pinterest_disabled_by_api; 3546 options.gdpr.bing_disabled_by_api = res.data.bing_disabled_by_api; 3547 options.gdpr.reddit_disabled_by_api = res.data.reddit_disabled_by_api; 3548 3549 options.cookie.externalID_disabled_by_api = res.data.externalID_disabled_by_api; 3550 options.cookie.disabled_all_cookie = res.data.disabled_all_cookie; 3551 options.cookie.disabled_advanced_form_data_cookie = res.data.disabled_advanced_form_data_cookie; 3552 options.cookie.disabled_landing_page_cookie = res.data.disabled_landing_page_cookie || !options.enable_lading_page_param; 3553 options.cookie.disabled_first_visit_cookie = res.data.disabled_first_visit_cookie; 3554 options.cookie.disabled_trafficsource_cookie = res.data.disabled_trafficsource_cookie; 3555 options.cookie.disabled_utmTerms_cookie = res.data.disabled_utmTerms_cookie; 3556 options.cookie.disabled_utmId_cookie = res.data.disabled_utmId_cookie; 3557 } 3558 loadPixels(); 3559 3560 } 3561 }); 3562 3563 } else { 3564 loadPixels(); 3565 } 3566 3567 }, 3568 3569 consentGiven: function (pixel) { 3570 3571 /** 3572 * Cookiebot 3573 */ 3574 if (options.gdpr.cookiebot_integration_enabled && typeof Cookiebot !== 'undefined') { 3575 3576 var cookiebot_consent_category = options.gdpr['cookiebot_' + pixel + '_consent_category']; 3577 3578 if (options.gdpr[pixel + '_prior_consent_enabled']) { 3579 if (Cookiebot.consented === true || Cookiebot.consent[cookiebot_consent_category]) { 3580 return true; 3581 } 3582 } else { 3583 if (Cookiebot.consent[cookiebot_consent_category]) { 3584 return true; 3585 } 3586 } 3587 return false;
3588 3589 } 3590 3591 /** 3592 * Cookie Notice 3593 */ 3594 if (options.gdpr.cookie_notice_integration_enabled && typeof cnArgs !== 'undefined') { 3595 3596 var cn_cookie = Cookies.get(cnArgs.cookieName); 3597 3598 if (options.gdpr[pixel + '_prior_consent_enabled']) { 3599 if (typeof cn_cookie === 'undefined' || cn_cookie === 'true') { 3600 return true; 3601 } 3602 } else { 3603 if (cn_cookie === 'true') { 3604 return true; 3605 } 3606 } 3607 3608 return false; 3609 3610 } 3611 3612 /** 3613 * Cookie Law Info 3614 */ 3615 3616 if (options.gdpr.cookie_law_info_integration_enabled) { 3617 var cli_cookie = Cookies.get('cookieyes-consent') ?? Cookies.get('wt_consent') ?? Cookies.get('viewed_cookie_policy'); 3618 if (options.gdpr[pixel + '_prior_consent_enabled']) { 3619 if (typeof cli_cookie === 'undefined') return true; 3620 if ( 3621 cli_cookie && 3622 (cli_cookie === Cookies.get('cookieyes-consent') || cli_cookie === Cookies.get('wt_consent')) 3623 ) { 3624 if (getCookieYes('analytics') === 'yes') { 3625 return true; 3626 } 3627 } else if (cli_cookie && cli_cookie === Cookies.get('viewed_cookie_policy')) { 3628 if (Cookies.get('viewed_cookie_policy') === 'yes') { 3629 return true; 3630 } 3631 } 3632 } else { 3633 if ( 3634 cli_cookie && 3635 (cli_cookie === Cookies.get('cookieyes-consent') || cli_cookie === Cookies.get('wt_consent')) 3636 ) { 3637 if (getCookieYes('analytics') === 'yes') { 3638 return true; 3639 } 3640 } else if (cli_cookie && cli_cookie === Cookies.get('viewed_cookie_policy')) { 3641 if (Cookies.get('viewed_cookie_policy') === 'yes') { 3642 return true; 3643 } 3644 } 3645 } 3646 return false; 3647 } 3648 3649 /** 3650 * ConsentMagic 3651 */ 3652 if (options.gdpr.consent_magic_integration_enabled && typeof CS_Data !== "undefined" ) { 3653 3654 let test_prefix = CS_Data.test_prefix; 3655 if ( 3656 ( Utils.csModeEnabled( CS_Data.cs_google_consent_mode_enabled ) && ( pixel == 'analytics' || pixel == 'google_ads' ) ) 3657 || ( Utils.csModeEnabled( CS_Data.cs_meta_ldu_mode ) && pixel == 'facebook' ) 3658 || ( Utils.csModeEnabled( CS_Data.cs_reddit_ldu_mode ) && pixel == 'reddit' ) 3659 || ( Utils.csModeEnabled( CS_Data.cs_openai_opt_out_mode ) && pixel == 'openai' ) 3660 || ( Utils.csModeEnabled( CS_Data.cs_bing_consent_mode?.ad_storage?.enabled ) && pixel == 'bing' ) 3661 ) { 3662 if ( Number( CS_Data.cs_cache_enabled ) === 0 3663 || ( Utils.csModeEnabled( CS_Data.cs_cache_enabled ) && window.CS_Cache && window.CS_Cache.check_status ) ) { 3664 return true; 3665 } else { 3666 return false; 3667 } 3668 } 3669 3670 if( pixel == 'facebook' && ( CS_Data.cs_script_cat.facebook == 0 || CS_Data.cs_script_cat.facebook == CS_Data.cs_necessary_cat_id ) ) { 3671 return true; 3672 } else if( pixel == 'bing' && ( CS_Data.cs_script_cat.bing == 0 || CS_Data.cs_script_cat.bing == CS_Data.cs_necessary_cat_id ) ) { 3673 return true; 3674 } else if( pixel == 'analytics' && ( CS_Data.cs_script_cat.analytics == 0 || CS_Data.cs_script_cat.analytics == CS_Data.cs_necessary_cat_id ) ) { 3675 return true; 3676 } else if( pixel == 'google_ads' && ( CS_Data.cs_script_cat.gads == 0 || CS_Data.cs_script_cat.gads == CS_Data.cs_necessary_cat_id ) ) { 3677 return true; 3678 } else if( pixel == 'pinterest' && ( CS_Data.cs_script_cat.pinterest == 0 || CS_Data.cs_script_cat.pinterest == CS_Data.cs_necessary_cat_id ) ) { 3679 return true; 3680 } else if( pixel == 'tiktok' && ( CS_Data.cs_script_cat.tiktok == 0 || CS_Data.cs_script_cat.tiktok == CS_Data.cs_necessary_cat_id ) ) { 3681 return true; 3682 } else if( pixel == 'reddit' && ( CS_Data.cs_script_cat?.reddit == 0 || CS_Data.cs_script_cat?.reddit == CS_Data.cs_necessary_cat_id ) ) { 3683 return true; 3684 } else if( pixel == 'openai' && ( CS_Data.cs_script_cat?.openai == 0 || CS_Data.cs_script_cat?.openai == CS_Data.cs_necessary_cat_id ) ) { 3685 return true; 3686 } 3687 3688 let substring = "cs_enabled_cookie_term", 3689 theCookies = document.cookie.split( ';' ); 3690 3691 for ( let i = 1; i <= theCookies.length; i++ ) { 3692 if ( theCookies[ i - 1 ].indexOf( substring ) !== -1 ) { 3693 let categoryCookie = theCookies[ i - 1 ].replace( 'cs_enabled_cookie_term' + test_prefix + '_', '' ); 3694 categoryCookie = Number( categoryCookie.replace( /\D+/g, "" ) ); 3695 let cs_cookie_val = Cookies.get( 'cs_enabled_cookie_term' + test_prefix + '_' + categoryCookie ); 3696 3697 if ( categoryCookie === CS_Data.cs_script_cat.facebook && pixel == 'facebook' ) { 3698 return cs_cookie_val == 'yes'; 3699 } else if ( categoryCookie === CS_Data.cs_script_cat.bing && pixel == 'bing' ) { 3700 return cs_cookie_val == 'yes'; 3701 } else if ( categoryCookie === CS_Data.cs_script_cat.analytics && pixel == 'analytics' ) { 3702 return cs_cookie_val == 'yes'; 3703 } else if ( categoryCookie === CS_Data.cs_script_cat.gads && pixel == 'google_ads' ) { 3704 return cs_cookie_val == 'yes'; 3705 } else if ( categoryCookie === CS_Data.cs_script_cat.pinterest && pixel == 'pinterest' ) { 3706 return cs_cookie_val == 'yes'; 3707 } else if ( categoryCookie === CS_Data.cs_script_cat.tiktok && pixel == 'tiktok' ) { 3708 return cs_cookie_val == 'yes'; 3709 } else if ( categoryCookie === CS_Data.cs_script_cat?.reddit && pixel == 'reddit' ) { 3710 return cs_cookie_val == 'yes'; 3711 } else if ( categoryCookie === CS_Data.cs_script_cat?.openai && pixel == 'openai' ) { 3712 return cs_cookie_val == 'yes'; 3713 } 3714 } 3715 } 3716 3717 return false;
3718 } 3719 3720 3721 /** 3722 * Real Cookie Banner 3723 */ 3724 if (options.gdpr.real_cookie_banner_integration_enabled) { 3725 var consentApi = window.consentApi; 3726 if (consentApi) { 3727 switch (pixel) { 3728 case "analytics": 3729 return consentApi.consentSync("http", "_ga", "*").cookieOptIn; 3730 case "facebook": 3731 return consentApi.consentSync("http", "_fbp", "*").cookieOptIn; 3732 case "pinterest": 3733 return consentApi.consentSync("http", "_pinterest_sess", ".pinterest.com").cookieOptIn; 3734 case "bing": 3735 return consentApi.consentSync("http", "_uetsid", "*").cookieOptIn; 3736 case "google_ads": 3737 return consentApi.consentSync("http", "1P_JAR", ".google.com").cookieOptIn; 3738 case 'tiktok': 3739 return consentApi.consentSync("http", "tt_webid_v2", ".tiktok.com").cookieOptIn; 3740 case 'openai': 3741 return consentApi.consentSync("http", "__oppref", "*").cookieOptIn; 3742 default: 3743 return true; 3744 } 3745 } 3746 } 3747 3748 return true; 3749 3750 }, 3751 3752 setupGdprCallbacks: function () { 3753 3754 let isConcent = false; 3755 3756 /** 3757 * Cookiebot 3758 */ 3759 if ( options.gdpr.cookiebot_integration_enabled && typeof Cookiebot !== 'undefined' ) { 3760 isConcent = true; 3761 window.addEventListener( "CookiebotOnConsentReady", function () { 3762 3763 let consent = { 3764 facebook: true, 3765 ga: true, 3766 google_ads: true, 3767 tiktok: true, 3768 openai: true, 3769 bing: true, 3770 pinterest: true, 3771 gtm: true, 3772 }; 3773 Utils.initializeVideoAPIs( options ); 3774 3775 Utils.manageCookies(); 3776 if ( Cookiebot.consent.marketing ) { 3777 Facebook.loadPixel(); 3778 Bing.loadPixel(); 3779 Pinterest.loadPixel(); 3780 GAds.loadPixel(); 3781 TikTok.loadPixel(); 3782 OpenAI.loadPixel(); 3783 3784 consent.facebook = true; 3785 consent.bing = true; 3786 consent.google_ads = true; 3787 consent.pinterest = true; 3788 consent.tiktok = true; 3789 } 3790 if ( Cookiebot.consent.statistics ) { 3791 Analytics.loadPixel(); 3792 3793 consent.ga = true; 3794 consent.gtm = true; 3795 } 3796 if ( !Cookiebot.consent.marketing ) { 3797 Facebook.disable(); 3798 Pinterest.disable(); 3799 Bing.disable() 3800 GAds.disable(); 3801 TikTok.disable(); 3802 OpenAI.disable(); 3803 3804 consent.facebook = false; 3805 consent.bing = false; 3806 consent.google_ads = false; 3807 consent.pinterest = false; 3808 consent.tiktok = false; 3809 } 3810 if ( !Cookiebot.consent.statistics ) { 3811 Analytics.disable(); 3812 3813 consent.ga = false;
3814 consent.gtm = false; 3815 } 3816 3817 Utils.setupGDPRData( consent ); 3818 } ); 3819 } 3820 3821 /** 3822 * Cookie Notice 3823 */ 3824 if ( options.gdpr.cookie_notice_integration_enabled ) { 3825 isConcent = true; 3826 $( document ).onFirst( 'click', '.cn-set-cookie', function () { 3827 3828 let consent = {}; 3829 3830 if ( $( this ).data( 'cookie-set' ) === 'accept' ) { 3831 3832 Utils.initializeVideoAPIs( options ); 3833 3834 Facebook.loadPixel(); 3835 Analytics.loadPixel(); 3836 GAds.loadPixel(); 3837 Pinterest.loadPixel(); 3838 Bing.loadPixel(); 3839 TikTok.loadPixel(); 3840 OpenAI.loadPixel(); 3841 3842 consent = { 3843 facebook: true, 3844 ga: true, 3845 google_ads: true, 3846 tiktok: true, 3847 openai: true, 3848 bing: true, 3849 pinterest: true, 3850 gtm: true, 3851 }; 3852 } else { 3853 Facebook.disable(); 3854 Analytics.disable(); 3855 GAds.disable(); 3856 Pinterest.disable(); 3857 Bing.disable(); 3858 TikTok.disable(); 3859 OpenAI.disable(); 3860 3861 consent = { 3862 facebook: false, 3863 ga: false, 3864 google_ads: false, 3865 tiktok: false, 3866 openai: false, 3867 bing: false, 3868 pinterest: false, 3869 gtm: false, 3870 }; 3871 } 3872 Utils.setupGDPRData( consent ); 3873 3874 } ); 3875 3876 $( document ).onFirst( 'click', '.cn-revoke-cookie', function () { 3877 Facebook.disable(); 3878 Analytics.disable(); 3879 GAds.disable(); 3880 Pinterest.disable(); 3881 Bing.disable(); 3882 TikTok.disable(); 3883 OpenAI.disable(); 3884 3885 let consent = { 3886 facebook: false, 3887 ga: false, 3888 google_ads: false, 3889 tiktok: false, 3890 openai: false, 3891 bing: false, 3892 pinterest: false, 3893 gtm: false, 3894 }; 3895 Utils.setupGDPRData( consent ); 3896 3897 } ); 3898 } 3899 3900 /** 3901 * Cookie Law Info 3902 */ 3903 if ( options.gdpr.cookie_law_info_integration_enabled ) { 3904 isConcent = true; 3905 $( document ).onFirst( 'click', '#wt-cli-accept-all-btn,#cookie_action_close_header, .cky-btn-accept', function () { 3906 Utils.initializeVideoAPIs( options ); 3907 setTimeout( function () { 3908 let cli_cookie = Cookies.get( 'cookieyes-consent' ) ?? Cookies.get( 'wt_consent' ) ?? Cookies.get( 'viewed_cookie_policy' ); 3909 if ( typeof cli_cookie !== 'undefined' ) { 3910 if ( 3911 cli_cookie && 3912 (cli_cookie === Cookies.get('cookieyes-consent') || cli_cookie === Cookies.get('wt_consent')) 3913 ) { 3914 if (getCookieYes('analytics') === 'yes') { 3915 Utils.manageCookies(); 3916 } 3917 }
3917 else if ( cli_cookie === Cookies.get( 'viewed_cookie_policy' ) && cli_cookie == 'yes' ) { 3918 Utils.manageCookies(); 3919 } 3920 } 3921 }, 1000 ) 3922 Facebook.loadPixel(); 3923 Analytics.loadPixel(); 3924 GAds.loadPixel(); 3925 Pinterest.loadPixel(); 3926 Bing.loadPixel(); 3927 TikTok.loadPixel(); 3928 OpenAI.loadPixel(); 3929 3930 let consent = { 3931 facebook: true, 3932 ga: true, 3933 google_ads: true, 3934 tiktok: true, 3935 openai: true, 3936 bing: true, 3937 pinterest: true, 3938 gtm: true, 3939 }; 3940 Utils.setupGDPRData( consent ); 3941 } ); 3942 3943 $( document ).onFirst( 'click', '#cookie_action_close_header_reject, .cky-btn-reject', function () { 3944 Facebook.disable(); 3945 Analytics.disable(); 3946 GAds.disable(); 3947 Pinterest.disable(); 3948 Bing.disable(); 3949 TikTok.disable(); 3950 OpenAI.disable(); 3951 3952 let consent = { 3953 facebook: false, 3954 ga: false, 3955 google_ads: false, 3956 tiktok: false, 3957 openai: false, 3958 bing: false, 3959 pinterest: false, 3960 gtm: false, 3961 }; 3962 Utils.setupGDPRData( consent ); 3963 } ); 3964 } 3965 3966 /** 3967 * ConsentMagic 3968 */ 3969 if ( options.gdpr.consent_magic_integration_enabled && typeof CS_Data !== "undefined" ) { 3970 isConcent = true; 3971 let test_prefix = CS_Data.test_prefix, 3972 cs_refresh_after_consent = false, 3973 substring = "cs_enabled_cookie_term"; 3974 3975 if ( CS_Data.cs_refresh_after_consent == 1 ) { 3976 cs_refresh_after_consent = CS_Data.cs_refresh_after_consent; 3977 } 3978 3979 let consent_actions = function () { 3980 let theCookies = document.cookie.split( ';' ); 3981 3982 let consent = { 3983 facebook: true, 3984 ga: true, 3985 google_ads: true, 3986 tiktok: true, 3987 openai: true, 3988 bing: true, 3989 pinterest: true, 3990 gtm: true, 3991 reddit: true, 3992 }; 3993 3994 for ( let i = 1; i <= theCookies.length; i++ ) { 3995 if ( theCookies[ i - 1 ].indexOf( substring ) !== -1 ) { 3996 let categoryCookie = theCookies[ i - 1 ].replace( 'cs_enabled_cookie_term' + test_prefix + '_', '' ); 3997 categoryCookie = Number( categoryCookie.replace( /\D+/g, "" ) ); 3998 let cs_cookie_val = Cookies.get( 'cs_enabled_cookie_term' + test_prefix + '_' + categoryCookie ); 3999 if ( cs_cookie_val == 'yes' ) { 4000 4001 Utils.initializeVideoAPIs( options ); 4002 4003 if ( ( categoryCookie === CS_Data.cs_script_cat.facebook ) || Utils.csModeEnabled( CS_Data.cs_meta_ldu_mode ) ) { 4004 Facebook.loadPixel(); 4005 } 4006 4007 if ( categoryCookie === CS_Data.cs_script_cat.bing || Utils.csModeEnabled( CS_Data.cs_bing_consent_mode?.ad_storage?.enabled ) ) { 4008 Bing.loadPixel(); 4009 } 4010 4011 if ( categoryCookie === CS_Data.cs_script_cat.analytics || ( typeof CS_Data.cs_google_analytics_consent_mode !== "undefined" && CS_Data.cs_google_analytics_consent_mode == 1 ) ) { 4012 Analytics.loadPixel(); 4013 } 4014 if ( categoryCookie === CS_Data.cs_script_cat.gads || ( typeof CS_Data.cs_google_ads_consent_mode !== "undefined" && CS_Data.cs_google_ads_consent_mode == 1 ) ) { 4015 GAds.loadPixel(); 4016 } 4017 4018 if ( categoryCookie === CS_Data.cs_script_cat.pinterest ) { 4019 Pinterest.loadPixel(); 4020 } 4021 4022 if ( categoryCookie === CS_Data.cs_script_cat.tiktok ) { 4023 TikTok.loadPixel(); 4024 } 4025 4026 if ( ( categoryCookie === CS_Data.cs_script_cat.reddit ) || Utils.csModeEnabled( CS_Data.cs_reddit_ldu_mode ) ) { 4027 Reddit.loadPixel(); 4028 } 4029 4030 if ( ( categoryCookie === CS_Data.cs_script_cat.openai ) || Utils.csModeEnabled( CS_Data.cs_openai_opt_out_mode ) ) { 4031 OpenAI.loadPixel(); 4032 } 4033 } else { 4034 if ( ( categoryCookie === CS_Data.cs_script_cat.facebook ) && ! Utils.csModeEnabled( CS_Data.cs_meta_ldu_mode ) ) { 4035 Facebook.disable(); 4036 consent.facebook = false;
4037 } 4038 4039 if ( categoryCookie === CS_Data.cs_script_cat.bing && ! Utils.csModeEnabled( CS_Data.cs_bing_consent_mode?.ad_storage?.enabled ) ) { 4040 Bing.disable(); 4041 consent.bing = false; 4042 } 4043 4044 if ( categoryCookie === CS_Data.cs_script_cat.analytics && ( typeof CS_Data.cs_google_analytics_consent_mode == "undefined" || CS_Data.cs_google_analytics_consent_mode == 0 ) ) { 4045 Analytics.disable(); 4046 consent.ga = false; 4047 consent.gtm = false; 4048 } 4049 4050 if ( categoryCookie === CS_Data.cs_script_cat.gads && ( typeof CS_Data.cs_google_ads_consent_mode == "undefined" || CS_Data.cs_google_ads_consent_mode == 0 ) ) { 4051 GAds.disable(); 4052 consent.google_ads = false; 4053 } 4054 4055 if ( categoryCookie === CS_Data.cs_script_cat.pinterest ) { 4056 Pinterest.disable(); 4057 consent.pinterest = false; 4058 } 4059 4060 if ( categoryCookie === CS_Data.cs_script_cat.tiktok ) { 4061 TikTok.disable(); 4062 consent.tiktok = false; 4063 } 4064 4065 if ( ( categoryCookie === CS_Data.cs_script_cat.reddit ) && ! Utils.csModeEnabled( CS_Data.cs_reddit_ldu_mode ) ) { 4066 Reddit.disable(); 4067 consent.reddit = false; 4068 } 4069 4070 if ( ( categoryCookie === CS_Data.cs_script_cat.openai ) && ! Utils.csModeEnabled( CS_Data.cs_openai_opt_out_mode ) ) { 4071 OpenAI.disable(); 4072 consent.openai = false; 4073 } 4074 } 4075 if ( Cookies.get( 'cs_enabled_advanced_matching' ) == 'yes' ) { 4076 Facebook.loadPixel(); 4077 } 4078 } 4079 } 4080 4081 Utils.setupGDPRData( consent ); 4082 } 4083 4084 if ( !cs_refresh_after_consent ) { 4085 consent_actions(); 4086 4087 $( document ).on( 'click', '.cs_action_btn', function ( e ) { 4088 e.preventDefault(); 4089 4090 let consent = { 4091 facebook: true, 4092 ga: true, 4093 google_ads: true, 4094 tiktok: true, 4095 openai: true, 4096 bing: true, 4097 pinterest: true, 4098 gtm: true, 4099 reddit: true, 4100 }; 4101 4102 let elm = $( this ), 4103 button_action = elm.attr( 'data-cs_action' ); 4104 4105 if ( button_action === 'allow_all' ) { 4106 4107 Utils.initializeVideoAPIs( options ); 4108 4109 Facebook.loadPixel(); 4110 Bing.loadPixel(); 4111 Analytics.loadPixel(); 4112 GAds.loadPixel(); 4113 Pinterest.loadPixel(); 4114 TikTok.loadPixel(); 4115 OpenAI.loadPixel(); 4116 Reddit.loadPixel(); 4117 4118 consent.facebook = true; 4119 consent.bing = true; 4120 consent.ga = true; 4121 consent.google_ads = true; 4122 consent.pinterest = true; 4123 consent.tiktok = true; 4124 consent.reddit = true; 4125 consent.gtm = true; 4126 4127 Utils.setupGDPRData( consent ); 4128 4129 } else if ( button_action === 'disable_all' ) { 4130 4131 if ( ! Utils.csModeEnabled( CS_Data.cs_meta_ldu_mode ) ) { 4132 Facebook.disable(); 4133 consent.facebook = false;
4134 } 4135 4136 if ( ! Utils.csModeEnabled( CS_Data.cs_bing_consent_mode?.ad_storage?.enabled ) ) { 4137 Bing.disable(); 4138 consent.bing = false; 4139 } 4140 4141 if ( typeof CS_Data.cs_google_analytics_consent_mode == "undefined" || CS_Data.cs_google_analytics_consent_mode == 0 ) { 4142 Analytics.disable(); 4143 consent.ga = false; 4144 consent.gtm = false; 4145 } 4146 4147 if ( typeof CS_Data.cs_google_ads_consent_mode == "undefined" || CS_Data.cs_google_ads_consent_mode == 0 ) { 4148 GAds.disable(); 4149 consent.google_ads = false; 4150 } 4151 4152 if ( ! Utils.csModeEnabled( CS_Data.cs_reddit_ldu_mode ) ) { 4153 Reddit.disable(); 4154 consent.reddit = false; 4155 } 4156 4157 Pinterest.disable(); 4158 TikTok.disable(); 4159 OpenAI.disable(); 4160 4161 consent.pinterest = false; 4162 consent.tiktok = false; 4163 consent.openai = false; 4164 4165 Utils.setupGDPRData( consent ); 4166 } else if ( button_action === 'cs_confirm' ) { 4167 consent_actions(); 4168 } 4169 } ); 4170 } 4171 } 4172 4173 /** 4174 * Real Cookie Banner 4175 */ 4176 if ( options.gdpr.real_cookie_banner_integration_enabled ) { 4177 isConcent = true; 4178 let consentApi = window.consentApi; 4179 if ( consentApi ) { 4180 consentApi.consent( "http", "_ga", "*" ) 4181 .then( Analytics.loadPixel.bind( Analytics ), Analytics.disable.bind( Analytics ) ); 4182 consentApi.consent( "http", "1P_JAR", ".google.com" ) 4183 .then( GAds.loadPixel.bind( GAds ), GAds.disable.bind( GAds ) ); 4184 consentApi.consent( "http", "_fbp", "*" ) 4185 .then( Facebook.loadPixel.bind( Facebook ), Facebook.disable.bind( Facebook ) ); 4186 consentApi.consent( "http", "_pinterest_sess", ".pinterest.com" ) 4187 .then( Pinterest.loadPixel.bind( Pinterest ), Pinterest.disable.bind( Pinterest ) ); 4188 consentApi.consent( "http", "_uetsid", "*" ) 4189 .then( Bing.loadPixel.bind( Bing ), Bing.disable.bind( Bing ) ); 4190 consentApi.consent( "http", "tt_webid_v2", ".tiktok.com" ) 4191 .then( TikTok.loadPixel.bind( TikTok ), TikTok.disable.bind( TikTok ) ); 4192 consentApi.consent( "http", "__oppref", "*" ) 4193 .then( OpenAI.loadPixel.bind( OpenAI ), OpenAI.disable.bind( OpenAI ) ); 4194 4195 let consent = { 4196 facebook: true, 4197 ga: true, 4198 google_ads: true, 4199 tiktok: true, 4200 openai: true, 4201 bing: true, 4202 pinterest: true, 4203 gtm: true, 4204 }; 4205 4206 if (!consentApi.consentSync("http", "_ga", "*").cookieOptIn) { 4207 consent.ga = false; 4208 consent.gtm = false; 4209 } 4210 if (!consentApi.consentSync("http", "_fbp", "*").cookieOptIn) { 4211 consent.facebook = false; 4212 } 4213 if (!consentApi.consentSync("http", "_pinterest_sess", ".pinterest.com").cookieOptIn) { 4214 consent.pinterest = false;
4215 } 4216 if (!consentApi.consentSync("http", "_uetsid", "*").cookieOptIn) { 4217 consent.bing = false; 4218 } 4219 if (!consentApi.consentSync("http", "1P_JAR", ".google.com").cookieOptIn) { 4220 consent.google_ads = false; 4221 } 4222 if (!consentApi.consentSync("http", "tt_webid_v2", ".tiktok.com").cookieOptIn) { 4223 consent.tiktok = false; 4224 } 4225 if (!consentApi.consentSync("http", "__oppref", "*").cookieOptIn) { 4226 consent.openai = false; 4227 } 4228 Utils.setupGDPRData( consent ); 4229 4230 // load WatchVideo event APIs for Auto 4231 /** 4232 * Real Cookie Banner. 4233 */ 4234 if ( options.hasOwnProperty( 'automatic' ) && options.automatic.enable_video && options.automatic.enable_youtube ) { 4235 consentApi.consent( "http", "CONSENT", ".youtube.com" ).then( Utils.initYouTubeAPI ); 4236 } 4237 4238 if ( options.hasOwnProperty( 'automatic' ) && options.automatic.enable_video && options.automatic.enable_vimeo ) { 4239 consentApi.consent( "http", "player", ".vimeo.com" ).then( Utils.initVimeoAPI ); 4240 } 4241 } 4242 } 4243 4244 if ( !isConcent ) { 4245 Utils.initializeVideoAPIs( options ); 4246 } 4247 }, 4248 4249 setupGDPRData: function ( consent ) { 4250 consent = window.btoa( JSON.stringify( consent ) ); 4251 Cookies.set( 'pys_consent', consent, { expires: 365, path: '/', domain: cookieDomain() } ); 4252 }, 4253 4254 /** 4255 * Enrich 4256 */ 4257 isCheckoutPage: function () { 4258 return $('body').hasClass('woocommerce-checkout') || document.querySelector('.woocommerce-checkout') || 4259 $('body').hasClass('edd-checkout'); 4260 }, 4261 addCheckoutFields : function() { 4262 var utm = ""; 4263 var utms = getUTMs() 4264 $.each(utmTerms, function (index, name) { 4265 if(index > 0) { 4266 utm+="|"; 4267 } 4268 utm+=name+":"+utms[name]; 4269 }); 4270 var utmIdList = ""; 4271 var utmsIds = getUTMId() 4272 $.each(utmId, function (index, name) { 4273 if(index > 0) { 4274 utmIdList+="|"; 4275 } 4276 utmIdList+=name+":"+utmsIds[name]; 4277 }); 4278 var utmIdListLast = ""; 4279 var utmsIdsLast = getUTMId(true) 4280 $.each(utmId, function (index, name) { 4281 if(index > 0) { 4282 utmIdListLast+="|"; 4283 } 4284 utmIdListLast+=name+":"+utmsIdsLast[name]; 4285 }); 4286 4287 4288 var utmLast = ""; 4289 var utmsLast = getUTMs(true) 4290 $.each(utmTerms, function (index, name) { 4291 if(index > 0) { 4292 utmLast+="|"; 4293 } 4294 utmLast+=name+":"+utmsLast[name]; 4295 }); 4296 4297 var dateTime = getDateTime(); 4298 var landing = Cookies.get('pys_landing_page'); 4299 var lastLanding = Cookies.get('last_pys_landing_page'); 4300 var trafic = Cookies.get('pysTrafficSource'); 4301 var lastTrafic = Cookies.get('last_pysTrafficSource'); 4302 4303 var $form = null; 4304 if($('body').hasClass('woocommerce-checkout')) { 4305 $form = $("form.woocommerce-checkout"); 4306 } else { 4307 $form = $("#edd_purchase_form"); 4308 } 4309 var inputs = {'pys_utm':utm, 4310 'pys_utm_id':utmIdList, 4311 'pys_browser_time':dateTime.join("|"), 4312 'pys_landing':landing, 4313 'pys_source':trafic, 4314 'pys_order_type': $(".wcf-optin-form").length > 0 ? "wcf-optin" : "normal", 4315 4316 'last_pys_landing':lastLanding, 4317 'last_pys_source':lastTrafic, 4318 'last_pys_utm':utmLast, 4319 'last_pys_utm_id':utmIdListLast, 4320 } 4321
4322 Object.keys(inputs).forEach(function(key,index) { 4323 $form.append("<input type='hidden' name='"+key+"' value='"+inputs[key]+"' /> "); 4324 }); 4325 4326 4327 }, 4328 4329 /** 4330 * Whether we may capture what the visitor typed into a form. 4331 * 4332 * This mirrors the consent checks that already guard manageCookies() 4333 * further down. It is evaluated per capture rather than once at 4334 * bind time, so granting consent mid-page starts capture straight 4335 * away without having to rebind the listeners. 4336 * 4337 * With no consent-manager integration configured the answer is yes, 4338 * which leaves behaviour on such sites unchanged. 4339 */ 4340 advancedFormDataConsentGiven: function () { 4341 4342 if ( typeof options.gdpr === 'undefined' ) { 4343 return true; 4344 } 4345 4346 // Cookiebot 4347 if ( options.gdpr.cookiebot_integration_enabled && typeof Cookiebot !== 'undefined' ) { 4348 return Cookiebot.consented === true 4349 || !!Cookiebot.consent[ 'marketing' ] 4350 || !!Cookiebot.consent[ 'statistics' ]; 4351 } 4352 4353 // ConsentMagic 4354 if ( options.gdpr.consent_magic_integration_enabled && typeof CS_Data !== 'undefined' ) { 4355 if ( CS_Data.cs_script_cat.pys == CS_Data.cs_necessary_cat_id || CS_Data.cs_script_cat.pys == 0 ) { 4356 return true; 4357 } 4358 return Cookies.get( 'cs_enabled_cookie_term' + CS_Data.test_prefix + '_' + CS_Data.cs_script_cat.pys ) === 'yes'; 4359 } 4360 4361 // Cookie Law Info / CookieYes 4362 if ( options.gdpr.cookie_law_info_integration_enabled ) { 4363 let cli_cookie = Cookies.get( 'cookieyes-consent' ) ?? Cookies.get( 'wt_consent' ) ?? Cookies.get( 'viewed_cookie_policy' ); 4364 if ( typeof cli_cookie === 'undefined' ) { 4365 return false; 4366 } 4367 if ( cli_cookie === Cookies.get( 'cookieyes-consent' ) || cli_cookie === Cookies.get( 'wt_consent' ) ) { 4368 return getCookieYes( 'analytics' ) === 'yes'; 4369 } 4370 return cli_cookie === 'yes'; 4371 } 4372 4373 // Cookie Notice 4374 if ( options.gdpr.cookie_notice_integration_enabled && typeof cnArgs !== 'undefined' ) { 4375 return Cookies.get( cnArgs.cookieName ) === 'true'; 4376 } 4377 4378 return true; 4379 }, 4380 4381 /** 4382 * js-cookie 2.1.3 builds the cookie string from a fixed list of 4383 * attributes - expires, path, domain, secure - so a sameSite passed 4384 * to Cookies.set() is silently dropped. This cookie holds what the 4385 * visitor typed into a form, so it is written by hand to get both 4386 * SameSite and Secure onto it. 4387 * 4388 * HttpOnly is not available here: Meta's and TikTok's browser pixels 4389 * are fed from this value, so the page has to be able to read it. 4390 */ 4391 setAdvancedFormDataCookie: function ( data ) { 4392 4393 let days = parseInt( options.cookie && options.cookie.advanced_form_data_expire ) || 30; 4394 let expires = new Date(); 4395 expires.setTime( expires.getTime() + days * 864e5 ); 4396 // '' means host-only, which is what every other writer here gets too. 4397 let scope = cookieDomain(); 4398 4399 let cookie = 'pys_advanced_form_data=' + encodeURIComponent( JSON.stringify( data ) ) 4400 + '; expires=' + expires.toUTCString() 4401 + '; path=/' 4402 + ( scope ? '; domain=' + scope : '' ) 4403 + '; samesite=lax'; 4404 4405 // Secure would make the cookie unwritable on a plain-HTTP site, 4406 // which would silently break matching on local and dev installs. 4407 if ( window.location.protocol === 'https:' ) { 4408 cookie += '; secure'; 4409 } 4410 4411 document.cookie = cookie; 4412 4413 // A host-only copy written while the domain was still unknown 4414 // would shadow this one: the older cookie is listed first, and 4415 // both js-cookie and PHP read the first match. 4416 if ( scope ) { 4417 document.cookie = 'pys_advanced_form_data=; path=/; max-age=0'; 4418 } 4419 }, 4420 4421 /** 4422 * Advanced From Data 4423 */ 4424 saveAdvancedFormData: function ( email, phone, firstName, lastName, override = true ) { 4425 4426 if ( !Utils.advancedFormDataConsentGiven() ) { 4427 return; 4428 } 4429 4430 if ( !options.cookie.disabled_advanced_form_data_cookie ) { 4431 let data = Utils.getAdvancedFormData(); 4432 // Ensure data["address"] is an object 4433 4434 if ( email != null ) { 4435 if ( !override ) { 4436 if ( typeof data[ "email" ] === 'undefined' || !data[ "email" ] ) { 4437 data[ "email" ] = email; 4438 } 4439 } else { 4440 data[ "email" ] = email; 4441 } 4442 4443 if ( typeof data[ "emails" ] === 'object' ) { 4444 if ( override ) { 4445 data[ "emails" ] = [ email, ...Object.values( data[ "emails" ] ) ]; 4446 } else { 4447 data[ "emails" ] = [ ...Object.values( data[ "emails" ] ), email ]; 4448 } 4449 } else { 4450 data[ "emails" ] = [ email ]; 4451 } 4452 data[ "emails" ] = [ ...new Set( data[ "emails" ] ) ]; 4453 data[ "emails" ] = data[ "emails" ].slice( 0, 3 ); 4454 } 4455 4456 if ( phone != null ) { 4457 if ( !override ) { 4458 if ( typeof data[ "phone" ] === 'undefined' || !data[ "phone" ] ) { 4459 data[ "phone" ] = phone; 4460 } 4461 } else { 4462 data[ "phone" ] = phone; 4463 } 4464 4465 if ( typeof data[ "phones" ] === 'object' ) { 4466 if ( override ) { 4467 data[ "phones" ] = [ phone, ...Object.values( data[ "phones" ] ) ]; 4468 } else { 4469 data[ "phones" ] = [ ...Object.values( data[ "phones" ] ), phone ]; 4470 } 4471 } else { 4472 data[ "phones" ] = [ phone ]; 4473 } 4474 data[ "phones" ] = [ ...new Set( data[ "phones" ] ) ]; 4475 data[ "phones" ] = data[ "phones" ].slice( 0, 3 ); 4476 } 4477 4478 if ( firstName != null ) { 4479 if ( !override ) { 4480 if ( typeof data[ "first_name" ] === 'undefined' || !data[ "first_name" ] ) { 4481 data[ "first_name" ] = firstName; 4482 } 4483 } else { 4484 data[ "first_name" ] = firstName; 4485 } 4486 4487 if ( typeof data[ "fns" ] === 'object' ) { 4488 if ( override ) { 4489 data[ "fns" ] = [ firstName, ...Object.values( data[ "fns" ] ) ]; 4490 } else { 4491 data[ "fns" ] = [ ...Object.values( data[ "fns" ] ), firstName ]; 4492 } 4493 } else { 4494 data[ "fns" ] = [ firstName ]; 4495 } 4496 data[ "fns" ] = [ ...new Set( data[ "fns" ] ) ]; 4497 data[ "fns" ] = data[ "fns" ].slice( 0, 2 ); 4498 } 4499 4500 if ( lastName != null ) { 4501 if ( !override ) { 4502 if ( typeof data[ "last_name" ] === 'undefined' || !data[ "last_name" ] ) { 4503 data[ "last_name" ] = lastName; 4504 } 4505 } else { 4506 data[ "last_name" ] = lastName; 4507 } 4508
4509 if ( typeof data[ "lns" ] === 'object' ) { 4510 if ( override ) { 4511 data[ "lns" ] = [ lastName, ...Object.values( data[ "lns" ] ) ]; 4512 } else { 4513 data[ "lns" ] = [ ...Object.values( data[ "fns" ] ), lastName ]; 4514 } 4515 } else { 4516 data[ "lns" ] = [ lastName ]; 4517 } 4518 data[ "lns" ] = [ ...new Set( data[ "lns" ] ) ]; 4519 data[ "lns" ] = data[ "lns" ].slice( 0, 2 ); 4520 } 4521 4522 Utils.setAdvancedFormDataCookie( data ); 4523 } else { 4524 // Same cookie, same two disagreeing writers, so the same pair of 4525 // deletes -- see the long note at the disabled_advanced_form_data_cookie 4526 // branch in pysNormalInit(). `domain: ''` means host-only. 4527 Cookies.remove( 'pys_advanced_form_data', { path: '/',domain: cookieDomain() } ) 4528 Cookies.remove( 'pys_advanced_form_data', { path: '/',domain: '' } ) 4529 } 4530 if(GTM.isEnabled()){ 4531 GTM.updateEnhancedConversionData(); 4532 } 4533 if(Analytics.isEnabled()){ 4534 Analytics.updateEnhancedConversionData(); 4535 } else if ( GAds.isEnabled() ) { 4536 GAds.updateEnhancedConversionData(); 4537 } 4538 if ( Bing.isEnabled() && typeof Bing.updateEnhancedConversionData === 'function' ) { 4539 Bing.updateEnhancedConversionData(); 4540 } 4541 }, 4542 getAdvancedMergeFormData: function (allways_provided_data = false) { 4543 const advanced = Utils.getAdvancedFormData(); 4544 let mergedData = {}, 4545 limit = options.tracking_analytics.use_multiple_provided_data || allways_provided_data ? 3 : 1, 4546 address_limit = options.tracking_analytics.use_multiple_provided_data || allways_provided_data ? 2 : 1; 4547 4548 // These values come partly from a cookie PHP also writes, so any of them 4549 // can be missing, null or empty. sha256() throws on anything but a 4550 // string, and hashing '' would send Google the hash of nothing. 4551 const hash = ( value ) => ( typeof value === 'string' && value !== '' ) || typeof value === 'number' 4552 ? sha256( String( value ) ) 4553 : ''; 4554 4555 if ( options.tracking_analytics.use_encoding_provided_data || allways_provided_data) { 4556 mergedData.sha256_email_address = []; 4557 mergedData.sha256_phone_number = []; 4558 mergedData.address = []; 4559 if ( options.tracking_analytics.hasOwnProperty( "userData" ) ) { 4560 if ( options.tracking_analytics.userData.hasOwnProperty( "emails" ) && Object.keys( options.tracking_analytics.userData.emails ).length > 0 ) { 4561 Object.keys( options.tracking_analytics.userData.emails ).forEach( ( key ) => { 4562 mergedData.sha256_email_address.push( hash( options.tracking_analytics.userData.emails[ key ] ) ); 4563 } ) 4564 } 4565 if ( options.tracking_analytics.userData.hasOwnProperty( "phones" ) && Object.keys( options.tracking_analytics.userData.phones ).length > 0 ) { 4566 Object.keys( options.tracking_analytics.userData.phones ).forEach( ( key ) => { 4567 mergedData.sha256_phone_number.push( hash( options.tracking_analytics.userData.phones[ key ] ) ); 4568 } ) 4569 } 4570 if ( options.tracking_analytics.userData.hasOwnProperty( "addresses" ) && Object.keys( options.tracking_analytics.userData.addresses ).length > 0 ) { 4571 Object.keys( options.tracking_analytics.userData.addresses ).forEach( ( ke
4571y ) => { 4572 let address = Object.assign( {}, options.tracking_analytics.userData.addresses[ key ] ); 4573 4574 if ( address.first_name ) { 4575 address.sha256_first_name = hash( address.first_name ); 4576 delete address.first_name; 4577 } 4578 if ( address.last_name ) { 4579 address.sha256_last_name = hash( address.last_name ); 4580 delete address.last_name; 4581 } 4582 mergedData.address.push( address ); 4583 } ) 4584 } 4585 } 4586 4587 let data_persistency = options.data_persistency; 4588 4589 //emails 4590 if ( advanced.emails && Object.keys( advanced.emails ).length > 0 ) { 4591 Object.keys( advanced.emails ).forEach( ( key ) => { 4592 if ( data_persistency === 'recent_data' ) { 4593 mergedData.sha256_email_address.push( hash( advanced.emails[ key ] ) ); 4594 } else { 4595 mergedData.sha256_email_address.unshift( hash( advanced.emails[ key ] ) ); 4596 } 4597 } ) 4598 } 4599 if ( advanced.email && advanced.email !== '' ) { 4600 if ( data_persistency === 'recent_data' ) { 4601 mergedData.sha256_email_address.push( hash( advanced.email ) ); 4602 } else { 4603 mergedData.sha256_email_address.unshift( hash( advanced.email ) ); 4604 } 4605 } 4606 mergedData.sha256_email_address = [ ...new Set( mergedData.sha256_email_address.filter( Boolean ) ) ]; 4607 mergedData.sha256_email_address = mergedData.sha256_email_address.slice( 0, limit ); 4608 4609 //phones 4610 if ( advanced.phones && Object.keys( advanced.phones ).length > 0 ) { 4611 Object.keys( advanced.phones ).forEach( ( key ) => { 4612 if ( data_persistency === 'recent_data' ) { 4613 mergedData.sha256_phone_number.push( hash( advanced.phones[ key ] ) ); 4614 } else { 4615 mergedData.sha256_phone_number.unshift( hash( advanced.phones[ key ] ) ); 4616 } 4617 } ) 4618 } 4619 if ( advanced.phone && advanced.phone !== '' ) { 4620 if ( data_persistency === 'recent_data' ) { 4621 mergedData.sha256_phone_number.push( hash( advanced.phone ) ); 4622 } else { 4623 mergedData.sha256_phone_number.unshift( hash( advanced.phone ) ); 4624 } 4625 } 4626 mergedData.sha256_phone_number = [ ...new Set( mergedData.sha256_phone_number.filter( Boolean ) ) ]; 4627 mergedData.sha256_phone_number = mergedData.sha256_phone_number.slice( 0, limit ); 4628 4629 //first and last names 4630 if ( advanced.fns && Object.keys( advanced.fns ).length > 0 ) { 4631 Object.entries( advanced.fns ).forEach( ( item ) => { 4632 if ( mergedData.address.length < address_limit ) { 4633 let first_name = item[ 1 ]; 4634 let last_name = ( advanced.lns && Object.keys( advanced.lns ).length > 0 ) ? ( advanced.lns[ item[ 0 ] ] ? advanced.lns[ item[ 0 ] ] : advanced.lns[ 0 ] ) : ''; 4635 let address = { 4636 sha256_first_name: hash( first_name ), 4637 sha256_last_name: hash( last_name ), 4638 // Add other address properties here if they exist in advanced 4639 }; 4640 if ( address.sha256_first_name || address.sha256_last_name ) { 4641 mergedData.address.push( address ); 4642 } 4643 } 4644 } ) 4645 } 4646 4647 if ( advanced.first_name || advanced.last_name ) { 4648 if ( mergedData.address.length < address_limit ) { 4649 let address = { 4650 sha256_first_name: hash( advanced.first_name ),
4651 sha256_last_name: hash( advanced.last_name ), 4652 // Add other address properties here if they exist in advanced 4653 }; 4654 if ( address.sha256_first_name || address.sha256_last_name ) { 4655 mergedData.address.push( address ); 4656 } 4657 } 4658 } 4659 } else { 4660 mergedData.email = []; 4661 mergedData.phone_number = []; 4662 mergedData.address = []; 4663 if ( options.tracking_analytics.hasOwnProperty( "userData" ) ) { 4664 if ( options.tracking_analytics.userData.hasOwnProperty( "emails" ) && Object.keys( options.tracking_analytics.userData.emails ).length > 0 ) { 4665 Object.keys( options.tracking_analytics.userData.emails ).forEach( ( key ) => { 4666 mergedData.email.push( options.tracking_analytics.userData.emails[ key ] ); 4667 } ) 4668 } 4669 if ( options.tracking_analytics.userData.hasOwnProperty( "phones" ) && Object.keys( options.tracking_analytics.userData.phones ).length > 0 ) { 4670 Object.keys( options.tracking_analytics.userData.phones ).forEach( ( key ) => { 4671 mergedData.phone_number.push( options.tracking_analytics.userData.phones[ key ] ); 4672 } ) 4673 } 4674 if ( options.tracking_analytics.userData.hasOwnProperty( "addresses" ) && Object.keys( options.tracking_analytics.userData.addresses ).length > 0 ) { 4675 Object.keys( options.tracking_analytics.userData.addresses ).forEach( ( key ) => { 4676 mergedData.address.push( options.tracking_analytics.userData.addresses[ key ] ); 4677 } ) 4678 } 4679 } 4680 4681 let data_persistency = options.data_persistency; 4682 4683 //emails 4684 if ( advanced.emails && Object.keys( advanced.emails ).length > 0 ) { 4685 Object.keys( advanced.emails ).forEach( ( key ) => { 4686 if ( data_persistency === 'recent_data' ) { 4687 mergedData.email.push( advanced.emails[ key ] ); 4688 } else { 4689 mergedData.email.unshift( advanced.emails[ key ] ); 4690 } 4691 } ) 4692 } 4693 if ( advanced.email && advanced.email !== '' ) { 4694 if ( data_persistency === 'recent_data' ) { 4695 mergedData.email.push( advanced.email ); 4696 } else { 4697 mergedData.email.unshift( advanced.email ); 4698 } 4699 } 4700 mergedData.email = [ ...new Set( mergedData.email ) ]; 4701 mergedData.email = mergedData.email.slice( 0, limit ); 4702 4703 //phones 4704 if ( advanced.phones && Object.keys( advanced.phones ).length > 0 ) { 4705 Object.keys( advanced.phones ).forEach( ( key ) => { 4706 if ( data_persistency === 'recent_data' ) { 4707 mergedData.phone_number.push( advanced.phones[ key ] ); 4708 } else { 4709 mergedData.phone_number.unshift( advanced.phones[ key ] ); 4710 } 4711 } ) 4712 } 4713 if ( advanced.phone && advanced.phone !== '' ) { 4714 if ( data_persistency === 'recent_data' ) { 4715 mergedData.phone_number.push( advanced.phone ); 4716 } else { 4717 mergedData.phone_number.unshift( advanced.phone ); 4718 } 4719 } 4720 mergedData.phone_number = [ ...new Set( mergedData.phone_number ) ]; 4721 mergedData.phone_number = mergedData.phone_number.slice( 0, limit ); 4722 4723 //first and last names 4724 if ( advanced.fns && Object.keys( advanced.fns ).length > 0 ) { 4725 Object.entries( advanced.fns ).forEach( ( item ) => { 4726 if ( mergedData.address.length < address_limit ) { 4727 let first_name = item[ 1 ]; 4728 let last_name = ( advanced.lns && Object.keys( advanced.lns ).length > 0 ) ? ( advanced.lns[ item[ 0 ] ] ? advanced.lns[ item[ 0 ] ] : advanced.lns[ 0 ] ) : ''; 4729 let address = { 4730 first_name: first_name || '', 4731 last_name: last_name || '', 4732 // Add other address properties here if they exist in advanced 4733 }; 4734 mergedData.address.push( address ); 4735 } 4736 } ) 4737 } 4738 4739 if ( advanced.first_name || advanced.last_name ) { 4740 if ( mergedData.address.length < address_limit ) { 4741 let address = { 4742 first_name: advanced.first_name || '', 4743 last_name: advanced.last_name || '', 4744 // Add other address properties here if they exist in advanced 4745 }; 4746 mergedData.address.push( address ); 4747 } 4748 } 4749 } 4750 4751 Utils.removeEmptyProperties( mergedData ); 4752 return mergedData; 4753 }, 4754 removeEmptyProperties : function(obj) { 4755 4756 if (obj.hasOwnProperty('address')) { 4757 // Iterate over each array in 'address' 4758 for (let i = 0; i < obj['address'].length; i++) { 4759 let address1 = obj['address'][i]; 4760 4761 // Compare with every other array in 'address' 4762 for (let j = i + 1; j < obj['address'].length; j++) { 4763 let address2 = obj['address'][j]; 4764 4765 // If duplicate properties are found, remove them from address2 4766 for (let prop in address1) { 4767 if (address1[prop] === address2[prop]) { 4768 delete address2[prop]; 4769 } 4770 } 4771 } 4772 } 4773 } 4774 4775 for (let key in obj) { 4776 if (obj[key] === null || obj[key] === undefined || obj[key] === '') { 4777 delete obj[key]; 4778 } else if (Array.isArray(obj[key])) { 4779 obj[key] = [...new Set(obj[key])]; // Remove duplicates from the array 4780 obj[key] = obj[key].filter(item => item !== null && item !== undefined && item !== ''); 4781 // Process objects inside arrays 4782 obj[key] = obj[key].map(item => { 4783 if (item === null) { 4784 return; // Skip null elements 4785 } else if (Array.isArray(item)) { 4786 return this.removeEmptyProperties(item); 4787 } else if (typeof item === 'object') { 4788 let cleanedItem = this.removeEmptyProperties(item); 4789 if (Object.keys(cleanedItem).length > 0) { 4790 return cleanedItem; 4791 } 4792 } else { 4793 return item; 4794 } 4795 }); 4796 // Remove empty objects and null values from the array 4797 obj[key] = obj[key].filter(item => item !== undefined && !(typeof item === 'object' && Object.keys(item).length === 0)); 4798 if (obj[key].length === 0) { 4799 delete obj[key]; 4800 } 4801 } else if (typeof obj[key] === 'object') { 4802 this.removeEmptyProperties(obj[key]); 4803 if (Object.keys(obj[key]).length === 0) { 4804 delete obj[key]; 4805 } 4806 } 4807 } 4808 return obj; 4809 }, 4810 getAdvancedFormData: function () { 4811 let dataStr = Cookies.get("pys_advanced_form_data"); 4812 if(dataStr === undefined) { 4813 return {'first_name':"",'last_name':"",'email':"",'phone':"",'fns':[],'lns':[],'emails':[],'phones':[]}; 4814 } else { 4815 return JSON.parse(dataStr); 4816 } 4817 }, 4818 4819 /** 4820 * Advanced Matching that the server is no longer allowed to render. 4821 * 4822 * Page HTML can be served out of a shared cache, so the server 4823 * leaves the visitor's own email, phone and name out of pysOptions. 4824 * The values are on this device in pys_advanced_form_data, so the 4825 * browser puts them back just before each pixel initialises. 4826 * Normalisation mirrors the PHP side field for field -- if one 4827 * moves, both move. 4828 */ 4829 AdvancedMatching: { 4830 4831 normalizeEmail: function (value) { 4832 if (typeof value !== 'string') { 4833 return ''; 4834 } 4835 var email = value.trim().toLowerCase(); 4836 if (email.length === 0 || email.length > 254) { 4837 return ''; 4838 } 4839 return /^[^\s@]+@[^\s@.]+(\.[^\s@.]+)+$/.test(email) ? email : ''; 4840 }, 4841 4842 normalizePhone: function (value) { 4843 if (typeof value !== 'string') { 4844 return ''; 4845 } 4846 var digits = value.replace(/[^0-9]/g, ''); 4847 return digits.length > 0 ? digits : ''; 4848 }, 4849 4850 normalizeName: function (value) { 4851 if (typeof value !== 'string' || value.length > 1024) { 4852 return ''; 4853 } 4854 return value.trim().toLowerCase(); 4855 }, 4856 4857 /** 4858 * The identity this device carries, normalised and possibly 4859 * all-empty. Never throws when the cookie is absent or corrupt. 4860 */ 4861 cookieIdentity: function () { 4862 var data; 4863 try { 4864 data = Utils.getAdvancedFormData() || {}; 4865 } catch (e) { 4866 data = {}; 4867 } 4868 return { 4869 email: this.normalizeEmail(data.email || ''), 4870 phone: this.normalizePhone(data.phone || ''), 4871 first_name: this.normalizeName(data.first_name || ''), 4872 last_name: this.normalizeName(data.last_name || '') 4873 }; 4874 }, 4875 4876 /** 4877 * @param {object} target The pixel's advanced matching object, edited in place. 4878 * @param {object} mapping {targetKey: {field, hash}} 4879 * @param {object} identity Optional; defaults to cookieIdentity(). 4880 * @param {string} persistency Optional; defaults to options.data_persistency. 4881 */ 4882 merge: function (target, mapping, identity, persistency) { 4883 4884 if (!target || !mapping) { 4885 return target; 4886 } 4887 4888 identity = identity || this.cookieIdentity(); 4889 4890 if (typeof persistency === 'undefined') { 4891 persistency = typeof options !== 'undefined' ? options.data_persistency : ''; 4892 } 4893 4894 var keepData = persistency === 'keep_data'; 4895 4896 Object.keys(mapping).forEach(function (targetKey) { 4897 4898 var rule = mapping[targetKey]; 4899 var value = identity[rule.field]; 4900 4901 if (typeof value !== 'string' || value.length === 0) { 4902 return; 4903 } 4904 4905 var hasServerValue = typeof target[targetKey] === 'string' 4906 && target[targetKey].length > 0; 4907 4908 if (hasServerValue && !keepData) { 4909 return; 4910 } 4911 4912 if (rule.hash) { 4913 if (typeof sha256 !== 'function') { 4914 return; 4915 } 4916 target[targetKey] = sha256(value); 4917 } else { 4918 target[targetKey] = value; 4919 } 4920 }); 4921 4922 return target; 4923 } 4924 }, 4925 4926 getFormFilledData: function ( event ) { 4927 // First, resolve static/dynamic parameters 4928 if (event.params && Object.keys(event.params).length > 0) {
4929 Object.entries(event.params).forEach(([key, value]) => { 4930 // Resolve parameter value (static or dynamic) 4931 const resolvedValue = resolveParamValue(value, key); 4932 if (resolvedValue !== null) { 4933 event.params[key] = resolvedValue; 4934 } 4935 else { 4936 delete event.params[key]; 4937 } 4938 }); 4939 } 4940 4941 // Then, handle dynamic fields from cookies (existing logic) 4942 4943 if ( Object.keys(options.track_dynamic_fields).length > 0 && Object.keys(event.params).length > 0 ) { 4944 Object.entries(event.params).forEach((item) => { 4945 4946 if ( options.track_dynamic_fields.hasOwnProperty(item[0]) ) { 4947 let fieldData = Cookies.get('pys_dyn_field_' + item[1] ); 4948 4949 if(fieldData !== undefined && fieldData !== '') { 4950 event.params[item[0]] = fieldData; 4951 } else { 4952 delete event.params[item[0]]; 4953 } 4954 } 4955 }) 4956 } 4957 return event; 4958 }, 4959 4960 registerGadsPhoneConversion( conversion_id ) { 4961 // Ads phone call conversion 4962 if ( options.triggerEventTypes.hasOwnProperty( 'gads_phone_conversion' ) ) { 4963 Object.entries( options.triggerEventTypes.gads_phone_conversion ).forEach( function ( [ trigger_id, data ] ) { 4964 if ( data.enabled && Object.entries( data.numbers ).length > 0 ) { 4965 4966 let allowed_pixels = []; 4967 Object.entries( options.triggerEvents[ trigger_id ] ).forEach( function ( [ pixel, pixel_data ] ) { 4968 if ( pixel !== 'google_ads' ) { 4969 return; 4970 } 4971 4972 allowed_pixels.push( ...pixel_data.conversion_ids ); 4973 } ) 4974 4975 if ( !allowed_pixels.includes( conversion_id ) ) { 4976 return; 4977 } 4978 4979 data.numbers.forEach( function ( pair ) { 4980 if ( !pair.number || !pair.label ) { 4981 return; 4982 } 4983 4984 let phoneCb = function () { 4985 Utils.fireTriggerEvent( trigger_id ) 4986 }; 4987 4988 Utils.gtagPush( 'config', conversion_id + '/' + pair.label, { 4989 phone_conversion_number: pair.number, 4990 phone_conversion_callback: phoneCb 4991 } ); 4992 } ) 4993 } 4994 } ); 4995 } 4996 } 4997 } 4998 4999 5000 }(options); 5001 5002 var TikTok = function (options) { 5003 5004 var initialized = false; 5005 5006 // TikTok appends its click id to the ad landing page URL. Unlike Meta's 5007 // fbc it is sent to the Events API raw, with no 'tt.1.<timestamp>.' 5008 // style prefix, so no formatting is applied here. 5009 var TTCLID_COOKIE = 'pys_ttclid'; 5010 5011 // Click id seen on this page load, kept so a brand new ad click beats a 5012 // cookie left over from an earlier one. Mirrors the same memo in 5013 // TikTokServer, and means the URL is only ever read from storeTtclid(), 5014 // behind the consent check in loadPixel(). 5015 var freshTtclid = ''; 5016 5017 // Persist the click id so it is still around at checkout, which is where 5018 // it matters most for match rate. Deliberately writes our own cookie and 5019 // never TikTok's: the format of theirs is theirs to change. 5020 var storeTtclid = function () { 5021 var ttclid = getUrlParameter('ttclid'); 5022 5023 if (!ttclid) { 5024 return; 5025 } 5026 5027 freshTtclid = ttclid; 5028 5029 if (!options.cookie || options.cookie.disabled_all_cookie) { 5030 return; 5031 } 5032 5033 Cookies.set(TTCLID_COOKIE, ttclid, { 5034 expires: parseInt(options.cookie_duration), 5035 path: '/', 5036 domain: cookieDomain() 5037 }); 5038 }; 5039 5040 // Read the click id the same way PHP does: this page load's value first, 5041 // then the cookie the TikTok SDK writes itself, then ours. 5042 var getTtclid = function () { 5043 return freshTtclid 5044 || Cookies.get('ttclid') 5045 || Cookies.get(TTCLID_COOKIE) 5046 || ''; 5047 }; 5048 5049 function fireEvent(name, event) { 5050 5051 if(typeof window.pys_event_data_filter === "function" && window.pys_disable_event_filter(name,'tiktok')) { 5052 return; 5053 } 5054 5055 var data = event.params; 5056 5057 var ids = event.pixelIds.filter(function (pixelId) { 5058 return !Utils.hideMatchingPixel(pixelId, 'tiktok'); 5059 }) 5060 var params = {}; 5061 Utils.copyProperties(data, params); 5062 5063 params.eventID = Utils.generateUniqueId(event); 5064 5065 if(ids.length > 0){ 5066 TikTok.fireEventAPI(name, event, params); 5067 } 5068 ids.forEach(function(pixelId){ 5069 if (options.debug) { 5070 console.log('[TikTok] ' + name, params,"pixel_id",pixelId); 5071 } 5072 5073 ttq.instance(pixelId).track(name,params) 5074 5075 }); 5076 5077 var customEvent = new CustomEvent('tiktok_event_sent', { 5078 detail: { 5079 eventName: name, 5080 params: params, 5081 pixel_ids: ids 5082 } 5083 }); 5084 window.dispatchEvent(customEvent); 5085 } 5086 5087 return { 5088 tag: function() { 5089 return "tiktok"; 5090 }, 5091 isEnabled: function () { 5092 return options.hasOwnProperty('tiktok'); 5093 }, 5094 getHidePixel: function(){ 5095 if(this.isEnabled() && options.tiktok.hasOwnProperty('hide_pixels')) 5096 { 5097 return options.tiktok.hide_pixels; 5098 } 5099 return []; 5100 }, 5101 disable: function () { 5102 initialized = false;
5103 }, 5104 5105 loadPixel:function () { 5106 if (initialized || !this.isEnabled() || !Utils.consentGiven('tiktok')) { 5107 return; 5108 } 5109 5110 // Same first-visit hold as Facebook.loadPixel(): TikTok's 5111 // advanced matching carries external_id too. 5112 if (Utils.holdForIdentity(function () { TikTok.loadPixel(); })) { 5113 return; 5114 } 5115 for (var i = 0; i < options.tiktok.pixelIds.length; i++) { 5116 var trackingId = options.tiktok.pixelIds[i]; 5117 if (!Utils.hideMatchingPixel(trackingId, 'tiktok')) { 5118 !function (w, d, t) { 5119 w.TiktokAnalyticsObject=t; 5120 var ttq=w[t]=w[t]||[]; 5121 ttq.methods=["page","track","identify","instances","debug","on","off","once","ready","alias","group","enableCookie","disableCookie"]; 5122 ttq.setAndDefer=function(t,e){t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}; 5123 for(var i=0;i<ttq.methods.length;i++) 5124 ttq.setAndDefer(ttq,ttq.methods[i]); 5125 ttq.instance=function(t){for(var e=ttq._i[t]||[],n=0;n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e}; 5126 ttq.load=function(e,n){var i="https://analytics.tiktok.com/i18n/pixel/events.js";ttq._i=ttq._i||{},ttq._i[e]=[],ttq._i[e]._u=i,ttq._t=ttq._t||{},ttq._t[e]=+new Date,ttq._o=ttq._o||{},ttq._o[e]=n||{};var o=document.createElement("script");o.type="text/javascript",o.async=!0,o.src=i+"?sdkid="+e+"&lib="+t;var a=document.getElementsByTagName("script")[0];a.parentNode.insertBefore(o,a)}; 5127 5128 //ttq.load('C60QSCQRVDG9JAKNPK2G'); 5129 //ttq.page(); 5130 }(window, document, 'ttq'); 5131 break; 5132 } 5133 } 5134 5135 // Consent for TikTok is confirmed by the guard above, so the 5136 // click id may be persisted now. 5137 storeTtclid(); 5138 5139 var ids = options.tiktok.pixelIds.filter(function (pixelId) { 5140 return !Utils.hideMatchingPixel(pixelId, 'tiktok'); 5141 }) 5142 ids.forEach(function (pixelId) { 5143 5144 ttq.load(pixelId); 5145 ttq.page(); 5146 let advancedMatching = {}; 5147 5148 if (options.tiktok.hasOwnProperty('advanced_matching')) { 5149 advancedMatching = Object.assign({}, options.tiktok.advanced_matching || {}); 5150 } 5151 5152 // The server leaves the visitor's own email and phone out of 5153 // the page so a shared cache cannot replay them. TikTok hashes 5154 // the same way on both sides. 5155 Utils.AdvancedMatching.merge(advancedMatching, { 5156 sha256_email: { field: 'email', hash: true }, 5157 sha256_phone_number: { field: 'phone', hash: true } 5158 }); 5159 5160 if (!advancedMatching.hasOwnProperty("external_id")) { 5161 if (Cookies.get('pbid')) { 5162 advancedMatching["external_id"] = Cookies.get('pbid'); 5163 } 5164 } else if (advancedMatching.external_id != Cookies.get('pbid')) { 5165 advancedMatching["external_id"] = Cookies.get('pbid') ? Cookies.get('pbid') : advancedMatching.external_id; 5166 } 5167 5168 if (Object.keys(advancedMatching).length > 0) { 5169 ttq.instance(pixelId).identify(advancedMatching) 5170 } 5171 }); 5172 initialized = true; 5173 5174 if (options.staticEvents.hasOwnProperty('tiktok')) { 5175 var timeout = 1; 5176 setTimeout(function (timeout) { 5177 Utils.fireStaticEvents('tiktok', timeout); 5178 }, 1500, timeout) 5179 } 5180 }, 5181 5182 fireEvent: function (name, data) { 5183 if (!initialized || !this.isEnabled()) { 5184 return false;
5185 } 5186 data.delay = data.delay || 0; 5187 5188 if (data.delay === 0) { 5189 fireEvent(name, data); 5190 } else { 5191 setTimeout(function (name, params) { 5192 fireEvent(name, params); 5193 }, data.delay * 1000, name, data); 5194 } 5195 return true; 5196 }, 5197 5198 fireEventAPI: function (name, event, params) { 5199 5200 var ids = event.pixelIds.filter(function (pixelId) { 5201 return !Utils.hideMatchingPixel(pixelId, 'tiktok'); 5202 }) 5203 var notCachedEventsIds = new Array(); 5204 var isAddToCartFromJs = options.woo.hasOwnProperty("addToCartCatchMethod") 5205 && options.woo.addToCartCatchMethod === "add_cart_js"; 5206 if(!isAddToCartFromJs) { 5207 notCachedEventsIds.push('woo_add_to_cart_on_button_click') 5208 } 5209 5210 if(options.tiktok.serverApiEnabled) { 5211 if(!notCachedEventsIds.includes(event.e_id)) { 5212 5213 var isApiDisabled = options.gdpr.all_disabled_by_api || 5214 options.gdpr.tiktok_disabled_by_api || 5215 options.gdpr.cookiebot_integration_enabled || 5216 options.gdpr.cookie_notice_integration_enabled || 5217 options.gdpr.consent_magic_integration_enabled || 5218 options.gdpr.cookie_law_info_integration_enabled; 5219 // Update eventID 5220 5221 // send event from server if they were block by gdpr or need send with delay 5222 if( options.ajaxForServerEvent || isApiDisabled || event.delay > 0 || event.type !== "static" ){ 5223 5224 // Fallback for the case where the landing page was 5225 // served from a cache and no cookie ever reached 5226 // PHP. The click id goes into a copy of the params: 5227 // this same object is handed to ttq.track(), and the 5228 // browser pixel reads the click id by itself. 5229 var apiParams = {}; 5230 Utils.copyProperties(params, apiParams); 5231 5232 var ttclid = getTtclid(); 5233 if (ttclid) { 5234 apiParams.ttclid = ttclid; 5235 } 5236 5237 var json = { 5238 action: 'pys_tiktok_api_event', 5239 pixel: TikTok.tag(), 5240 event: name, 5241 event_slug: event.e_id, 5242 ids: ids, 5243 data:apiParams, 5244 url:window.location.href, 5245 event_id:params.eventID, 5246 ajax_event:options.ajax_event 5247 }; 5248 5249 let delay = 0; 5250 try { 5251 if (sessionStorage) { 5252 Object.keys(sessionStorage) 5253 .filter(key => key.startsWith('wc_fragments_')) 5254 .forEach((key) => { 5255 let value = sessionStorage.getItem(key); 5256 if (event.e_id === 'woo_add_to_cart_on_button_click') { 5257 delay = 2000; 5258 } 5259 }); 5260 } 5261 } catch (error) { 5262 console.error('sessionStorage is not accessible:', error); 5263 } 5264 5265 5266 if(event.hasOwnProperty('woo_order')) { 5267 json['woo_order'] = event.woo_order; 5268 } 5269 5270 if(event.hasOwnProperty('edd_order')) { 5271 json['edd_order'] = event.edd_order; 5272 } 5273 5274 // Forward the order key so the server can bind the 5275 // purchase event to its specific order (RestPurchaseGuard). 5276 if(event.hasOwnProperty('order_key') && event.order_key) { 5277 json['order_key'] = event.order_key; 5278 } 5279 if (event.e_id === "automatic_event_internal_link" || event.e_id === "automatic_event_outbound_link" || name == 'PageView') {
5280 delay = delay === 0 ? 500 : delay; 5281 setTimeout(() => Utils.sendServerAjaxRequest(options.ajaxUrl, json), delay); 5282 } else if (event.type != 'static') { 5283 setTimeout(() => Utils.sendServerAjaxRequest(options.ajaxUrl, json), delay); 5284 } 5285 5286 if ( ( event.type == 'static' && options.ajaxForServerStaticEvent ) || ( event.hasOwnProperty( 'ajaxFire' ) && event.ajaxFire ) ) { 5287 setTimeout( () => Utils.sendServerAjaxRequest( options.ajaxUrl, json ), delay ); 5288 } 5289 } 5290 } 5291 } 5292 }, 5293 5294 onClickEvent: function (event) { 5295 this.fireEvent(event.name, event); 5296 }, 5297 5298 onWooAddToCartOnSingleEvent: function (product_id, qty, product_type, is_external, $form) { 5299 5300 window.pysWooProductData = window.pysWooProductData || []; 5301 if (!options.dynamicEvents.woo_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 5302 return; 5303 5304 if (window.pysWooProductData.hasOwnProperty(product_id)) { 5305 if (window.pysWooProductData[product_id].hasOwnProperty(this.tag())) { 5306 5307 var event = Utils.clone(options.dynamicEvents.woo_add_to_cart_on_button_click[this.tag()]); 5308 5309 Utils.copyProperties(window.pysWooProductData[product_id][this.tag()]['params'], event.params); 5310 5311 // maybe customize value option 5312 if (options.woo.addToCartOnButtonValueEnabled && options.woo.addToCartOnButtonValueOption !== 'global') { 5313 5314 if (product_type === Utils.PRODUCT_BUNDLE) { 5315 var data = $(".bundle_form .bundle_data").data("bundle_form_data"); 5316 var items_sum = getBundlePriceOnSingleProduct(data); 5317 event.params.value = (parseFloat(data.base_price) + items_sum) * qty; 5318 } else { 5319 event.params.value = event.params.value * qty; 5320 } 5321 } 5322 5323 event.params.quantity = qty; 5324 5325 this.fireEvent(event.name, event); 5326 5327 } 5328 } 5329 }, 5330 5331 onWooAddToCartOnButtonEvent: function (product_id) { 5332 if(!options.dynamicEvents.woo_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 5333 return; 5334 5335 if (window.pysWooProductData.hasOwnProperty(product_id)) { 5336 if (window.pysWooProductData[product_id].hasOwnProperty(this.tag())) { 5337 var productData = window.pysWooProductData[product_id][this.tag()] 5338 var event = Utils.clone(options.dynamicEvents.woo_add_to_cart_on_button_click[this.tag()]) 5339 5340 Utils.copyProperties(productData['params'], event.params) 5341 event.pixelIds = productData['pixelIds']; 5342 this.fireEvent(event.name, event); 5343 } 5344 } 5345 }, 5346 5347 onEddAddToCartOnButtonEvent : function (download_id, price_index, qty) { 5348 if(!options.dynamicEvents.edd_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 5349 return; 5350 var event = Utils.clone(options.dynamicEvents.edd_add_to_cart_on_button_click[this.tag()]); 5351 5352 5353 if (window.pysEddProductData && window.pysEddProductData.hasOwnProperty(download_id)) { 5354 5355 var index; 5356 5357 if (price_index) { 5358 index = download_id + '_' + price_index; 5359 } else { 5360 index = download_id; 5361 } 5362 5363 if (window.pysEddProductData[download_id].hasOwnProperty(index)) { 5364 if (window.pysEddProductData[download_id][index].hasOwnProperty(this.tag())) { 5365 5366 Utils.copyProperties(window.pysEddProductData[download_id][index][this.tag()].params, event.params); 5367 this.fireEvent(event.name,event); 5368 5369 } 5370 } 5371 5372 } 5373 }, 5374 5375 onPageScroll: function (event) { 5376 if (initialized && this.isEnabled()) { 5377 this.fireEvent(event.name, event); 5378 } 5379 }, 5380 5381 onWatchVideo: function (event) { 5382 if (initialized && this.isEnabled() && !event.fired) { 5383 this.fireEvent(event.name, event); 5384 } 5385 }, 5386 5387 onCommentEvent: function (event) { 5388 if (initialized && this.isEnabled()) { 5389 this.fireEvent(event.name, event); 5390 } 5391 }, 5392 5393 onAdSenseEvent: function (event) { 5394 if (initialized && this.isEnabled()) { 5395 this.fireEvent(event.name, event); 5396 } 5397 }, 5398 5399 onTime: function (event) { 5400 if (initialized && this.isEnabled()) { 5401 this.fireEvent(event.name, event); 5402 } 5403 }, 5404 } 5405 }(options); 5406 5407 var OpenAI = function (options) { 5408 5409 let initialized = false, 5410 sentIdentity = null, 5411 identityWaitTimer = null, 5412 identityWaitExpired = false;
5413 5414 function visiblePixelIds(ids) { 5415 return (ids || []).filter(function (pixelId) { 5416 return !Utils.hideMatchingPixel(pixelId, 'openai'); 5417 }); 5418 } 5419 /** 5420 * Is the pixel in OpenAI's restricted mode? 5421 */ 5422 function optOutMode() { 5423 5424 if ( options.openai && options.openai.opt_out ) { 5425 return true; 5426 } 5427 5428 return !! ( options.gdpr 5429 && options.gdpr.consent_magic_integration_enabled 5430 && typeof CS_Data !== 'undefined' 5431 && Utils.csModeEnabled( CS_Data.cs_openai_opt_out_mode ) ); 5432 } 5433 5434 5435 const SDK_DATA_FIELDS = { 5436 contents: ['type', 'amount', 'currency', 'contents'], 5437 custom: ['type', 'plan_id', 'amount', 'currency', 'contents'], 5438 customer_action: ['type', 'amount', 'currency'], 5439 plan_enrollment: ['type', 'plan_id', 'amount', 'currency', 'contents'] 5440 }; 5441 5442 const SDK_ITEM_FIELDS = ['id', 'name', 'content_type', 'quantity', 'amount', 'currency']; 5443 5444 function sdkInteger(value) { 5445 let number = typeof value === 'string' ? Number(value.trim()) : value; 5446 5447 return typeof number === 'number' && isFinite(number) ? Math.round(number) : null; 5448 } 5449 5450 function sdkCurrency(value) { 5451 return typeof value === 'string' && /^[A-Za-z]{3}$/.test(value) ? value : null; 5452 } 5453 5454 function sdkText(value) { 5455 if (typeof value === 'string') { 5456 return value === '' ? null : value; 5457 } 5458 5459 return typeof value === 'number' && isFinite(value) ? String(value) : null; 5460 } 5461 5462 /** 5463 * One contents[] item, with only the keys the SDK documents. 5464 * 5465 * @param item the item as configured or collected 5466 * @param eventHasCurrency true when data.currency is set: an item amount 5467 * needs a currency, its own or the event's 5468 */ 5469 function sdkSafeItem(item, eventHasCurrency) { 5470 5471 if (!item || typeof item !== 'object' || Array.isArray(item)) { 5472 return null; 5473 } 5474 5475 let safe = {}; 5476 5477 SDK_ITEM_FIELDS.forEach(function (field) { 5478 if (item[field] === undefined || item[field] === null || item[field] === '') { 5479 return; 5480 } 5481 5482 if (field === 'quantity' || field === 'amount') { 5483 let number = sdkInteger(item[field]); 5484 5485 if (number !== null) { 5486 safe[field] = number; 5487 } 5488 5489 return; 5490 } 5491 5492 if (field === 'currency') { 5493 let currency = sdkCurrency(item.currency); 5494 5495 if (currency !== null) { 5496 safe.currency = currency; 5497 } 5498 5499 return; 5500 } 5501 5502 let text = sdkText(item[field]); 5503 5504 if (text !== null) { 5505 safe[field] = text; 5506 } 5507 }); 5508 5509 if (safe.hasOwnProperty('amount') && !safe.hasOwnProperty('currency') && !eventHasCurrency) { 5510 delete safe.amount; 5511 } 5512 5513 return Object.keys(safe).length ? safe : null; 5514 } 5515 5516 /** 5517 * The event data, with only the fields the SDK documents for its shape. 5518 */ 5519 function sdkSafeData(data) { 5520 5521 if (!data || typeof data !== 'object') { 5522 return {}; 5523 } 5524 5525 let allowed = SDK_DATA_FIELDS[data.type]; 5526 5527 if (!allowed) { 5528 return data; 5529 } 5530 5531 let safe = {}; 5532 5533 allowed.forEach(function (field) { 5534 if (data[field] !== undefined && data[field] !== null && data[field] !== '') { 5535 safe[field] = data[field]; 5536 } 5537 }); 5538 5539 if (safe.hasOwnProperty('plan_id')) { 5540 let planId = sdkText(safe.plan_id); 5541 5542 if (planId === null) { 5543 delete safe.plan_id; 5544 } else { 5545 safe.plan_id = planId; 5546 } 5547 } 5548 5549 if (safe.hasOwnProperty('currency')) { 5550 let currency = sdkCurrency(safe.currency); 5551 5552 if (currency === null) { 5553 delete safe.currency; 5554 } else { 5555 safe.currency = currency; 5556 } 5557 } 5558 5559 if (safe.hasOwnProperty('amount')) { 5560 let amount = sdkInteger(safe.amount); 5561 5562 // The SDK refuses an amount with no currency beside it. 5563 if (amount === null || !safe.hasOwnProperty('currency')) { 5564 delete safe.amount; 5565 } else { 5566 safe.amount = amount; 5567 } 5568 } 5569 5570 if (safe.hasOwnProperty('contents')) { 5571 let items = Array.isArray(safe.contents) 5572 ? safe.contents 5573 .map(function (item) { return sdkSafeItem(item, safe.hasOwnProperty('currency')); }) 5574 .filter(function (item) { return item !== null; }) 5575 : []; 5576 5577 if (items.length) { 5578 safe.contents = items; 5579 } else { 5580 delete safe.contents; 5581 } 5582 } 5583 5584 return safe; 5585 } 5586 5587 function fireEvent(name, event) { 5588 5589 if (typeof window.pys_event_data_filter === "function" && window.pys_disable_event_filter(name, 'openai')) { 5590 return; 5591 } 5592 5593 if (typeof window.oaiq !== 'function') { 5594 return; 5595 } 5596 5597 let ids = visiblePixelIds(event.pixelIds); 5598 5599 if (ids.length === 0) { 5600 return; 5601 } 5602 5603 let data = (event.params && event.params.data) ? Utils.clone(event.params.data) : {}; 5604 5605 let eventOptions = {}, 5606 eventId = Utils.generateUniqueId(event); 5607 5608 if (eventId) { 5609 eventOptions.event_id = eventId; 5610 } 5611 5612 if (event.params && event.params.custom_event_name) { 5613 eventOptions.custom_event_name = event.params.custom_event_name; 5614 } 5615 5616 OpenAI.fireEventAPI(name, event, data, eventId, ids); 5617 5618 let browserData = sdkSafeData(data); 5619 5620 if (optOutMode()) { 5621 eventOptions.opt_out = true; 5622 } 5623 5624 if (options.debug) { 5625 console.log('[OpenAI] ' + name, browserData, eventOptions, 'pixel_ids', ids); 5626 } 5627 5628 ids.forEach(function (pixelId) { 5629 oaiq('measureSingle', pixelId, name, browserData, eventOptions); 5630 }); 5631 5632 let customEvent = new CustomEvent('openai_event_sent', { 5633 detail: { 5634 eventName: name, 5635 data: data, 5636 options: eventOptions, 5637 pixel_ids: ids 5638 } 5639 }); 5640 window.dispatchEvent(customEvent); 5641 } 5642 5643 /** 5644 * True while the visitor's identity is still in flight. 5645 */ 5646 function identityPending() { 5647 return ! identityWaitExpired 5648 && ! options.dynamicDataApplied 5649 && !! options.dynamicDataUrl; 5650 } 5651 5652 5653 const OPENAI_USER_FIELDS = [ 5654 'email_sha256', 5655 'phone_number_sha256', 5656 'external_id_sha256', 5657 'first_name_sha256', 5658 'last_name_sha256', 5659 'country', 5660 'city', 5661 'region', 5662 'postal_code' 5663 ]; 5664 5665 /** 5666 * OpenAI normalises more aggressively than the other pixels: the phone 5667 * is reduced to its national digit form (leading +, 00 and zeros 5668 * stripped, then length-validated), and names lose every punctuation 5669 * and whitespace character. Mirrors pys_openai_normalize_* in 5670 * modules/openai/function_helpers.php. 5671 */ 5672 function openaiCookieIdentity() { 5673 5674 let raw; 5675 5676 try { 5677 raw = Utils.getAdvancedFormData() || {}; 5678 } catch (e) { 5679 raw = {}; 5680 } 5681 5682 let phone = typeof raw.phone === 'string' 5683 ? raw.phone.trim().replace(/[\s().-]+/g, '') 5684 : ''; 5685 5686 if (!/^\+?[0-9]+$/.test(phone) || phone.indexOf('+0') === 0) { 5687 phone = ''; 5688 } else { 5689 let international = phone.indexOf('+') === 0 || phone.indexOf('00') === 0; 5690 let national = phone.replace(/^\+?0*/, ''); 5691 5692 if (international && national.indexOf('1') === 0 && national.length !== 11) { 5693 national = ''; 5694 } 5695 5696 phone = /^[1-9][0-9]{7,14}$/.test(national) ? national : ''; 5697 } 5698 5699 let normalizeName = function (value) { 5700 if (typeof value !== 'string' || value.length > 1024) { 5701 return ''; 5702 } 5703 let stripped = value.replace(/[!-\/:-@\[-`{-~\s]+/g, '').toLowerCase(); 5704 return /\D/u.test(stripped) ? stripped : ''; 5705 }; 5706 5707 return {
5708 email: Utils.AdvancedMatching.normalizeEmail(raw.email || ''), 5709 phone: phone, 5710 first_name: normalizeName(raw.first_name || ''), 5711 last_name: normalizeName(raw.last_name || '') 5712 }; 5713 } 5714 5715 /** 5716 * The identity to hand the SDK, built from whatever is known right now. 5717 */ 5718 function buildAdvancedMatchingUser() { 5719 5720 let user = {}; 5721 5722 if (options.openai.hasOwnProperty('advanced_matching')) { 5723 5724 let matching = options.openai.advanced_matching; 5725 5726 OPENAI_USER_FIELDS.forEach(function (field) { 5727 let value = matching[field]; 5728 5729 if (typeof value === 'string' && value.length > 0) { 5730 user[field] = value; 5731 } 5732 }); 5733 } 5734 5735 Utils.AdvancedMatching.merge(user, { 5736 email_sha256: { field: 'email', hash: true }, 5737 phone_number_sha256: { field: 'phone', hash: true }, 5738 first_name_sha256: { field: 'first_name', hash: true }, 5739 last_name_sha256: { field: 'last_name', hash: true } 5740 }, openaiCookieIdentity()); 5741 5742 let externalIdAllowed = options.send_external_id 5743 && ! ( options.gdpr && options.gdpr.externalID_disabled_by_api ) 5744 && ! ( options.cookie 5745 && ( options.cookie.disabled_all_cookie 5746 || options.cookie.externalID_disabled_by_api ) ); 5747 5748 if (!user.hasOwnProperty('external_id_sha256') 5749 && externalIdAllowed 5750 && typeof sha256 === 'function' 5751 && Cookies.get('pbid')) { 5752 user.external_id_sha256 = sha256(Cookies.get('pbid')); 5753 } 5754 5755 return user; 5756 } 5757 5758 return { 5759 tag: function () { 5760 return "openai"; 5761 }, 5762 5763 fireEventAPI: function (name, event, data, eventId, ids) { 5764 5765 if (!this.isEnabled() || !options.openai.serverApiEnabled) { 5766 return; 5767 } 5768 5769 let notCachedEventsIds = [], 5770 isAddToCartFromJs = options.hasOwnProperty('woo') 5771 && options.woo.hasOwnProperty('addToCartCatchMethod') 5772 && options.woo.addToCartCatchMethod === 'add_cart_js'; 5773 5774 if (!isAddToCartFromJs) { 5775 notCachedEventsIds.push('woo_add_to_cart_on_button_click'); 5776 } 5777 5778 if (notCachedEventsIds.indexOf(event.e_id) !== -1) { 5779 return; 5780 } 5781 5782 let isApiDisabled = options.gdpr.all_disabled_by_api || 5783 options.gdpr.openai_disabled_by_api || 5784 options.gdpr.cookiebot_integration_enabled || 5785 options.gdpr.cookie_notice_integration_enabled || 5786 options.gdpr.consent_magic_integration_enabled || 5787 options.gdpr.cookie_law_info_integration_enabled; 5788 5789 let forcedToBrowser = ( event.hasOwnProperty( 'ajaxFire' ) && event.ajaxFire ) 5790 || isApiDisabled 5791 || event.delay > 0; 5792 5793 if (event.type === "static" && !forcedToBrowser 5794 && !(options.ajaxForServerEvent && options.ajaxForServerStaticEvent)) { 5795 return; 5796 } 5797 5798 if (!(options.ajaxForServerEvent || forcedToBrowser || event.type !== "static")) { 5799 return; 5800 } 5801 5802 let params = {data: data}; 5803 5804 if (event.params && event.params.custom_event_name) { 5805 params.custom_event_name = event.params.custom_event_name; 5806 } 5807 5808 let json = { 5809 action: 'pys_openai_api_event', 5810 pixel: OpenAI.tag(), 5811 event: name, 5812 event_slug: event.e_id, 5813 ids: ids, 5814 data: params, 5815 url: window.location.href, 5816 event_id: eventId, 5817 ajax_event: options.ajax_event 5818 }; 5819 5820 if (event.hasOwnProperty('woo_order')) { 5821 json['woo_order'] = event.woo_order; 5822 } 5823 5824 if (event.hasOwnProperty('edd_order')) { 5825 json['edd_order'] = event.edd_order; 5826 } 5827 5828 if (event.hasOwnProperty('order_key') && event.order_key) { 5829 json['order_key'] = event.order_key; 5830 } 5831
5832 var delay = 0; 5833 5834 if (name === 'page_viewed') { 5835 delay = 500; 5836 } 5837 5838 setTimeout(function () { 5839 Utils.sendServerAjaxRequest(options.ajaxUrl, json); 5840 }, delay); 5841 }, 5842 5843 isEnabled: function () { 5844 return options.hasOwnProperty('openai'); 5845 }, 5846 getHidePixel: function () { 5847 if (this.isEnabled() && options.openai.hasOwnProperty('hide_pixels')) { 5848 return options.openai.hide_pixels; 5849 } 5850 return []; 5851 }, 5852 disable: function () { 5853 initialized = false; 5854 5855 if (typeof window.oaiq === 'function') { 5856 oaiq('consent', false); 5857 } 5858 }, 5859 5860 loadPixel: function () { 5861 5862 if (initialized || !this.isEnabled() || !Utils.consentGiven('openai')) { 5863 return; 5864 } 5865 5866 let ids = visiblePixelIds(options.openai.pixelIds); 5867 5868 if (ids.length === 0) { 5869 return; 5870 } 5871 5872 if (identityPending()) { 5873 5874 if (identityWaitTimer === null) { 5875 identityWaitTimer = setTimeout(function () { 5876 identityWaitExpired = true; 5877 5878 if (window.pys && window.pys.OpenAI) { 5879 window.pys.OpenAI.loadPixel(); 5880 } 5881 }, 3000); 5882 } 5883 5884 return; 5885 } 5886 5887 if (identityWaitTimer !== null) { 5888 clearTimeout(identityWaitTimer); 5889 identityWaitTimer = null; 5890 } 5891 5892 !function (w, d, s, u) { 5893 if (w.oaiq) return; 5894 var q = function () { 5895 q.q.push(arguments); 5896 }; 5897 q.q = []; 5898 w.oaiq = q; 5899 var js = d.createElement(s); 5900 js.async = true; 5901 js.src = u; 5902 var f = d.getElementsByTagName(s)[0]; 5903 f.parentNode.insertBefore(js, f); 5904 }(window, document, 'script', 'https://bzrcdn.openai.com/sdk/oaiq.min.js'); 5905 5906 let user = buildAdvancedMatchingUser(); 5907 5908 if (Object.keys(user).length > 0) { 5909 sentIdentity = JSON.stringify(user); 5910 } 5911 5912 ids.forEach(function (pixelId) { 5913 let initOptions = { 5914 pixelId: pixelId 5915 }; 5916 5917 if (options.openai.debug) { 5918 initOptions.debug = true; 5919 } 5920 5921 if (Object.keys(user).length > 0) { 5922 initOptions.user = user; 5923 } 5924 5925 oaiq('init', initOptions); 5926 }); 5927 5928 oaiq('consent', true); 5929 5930 initialized = true; 5931 5932 if (options.staticEvents.hasOwnProperty('openai')) { 5933 Utils.fireStaticEvents('openai'); 5934 } 5935 }, 5936 5937 refreshAdvancedMatching: function () { 5938 5939 if (!initialized || !this.isEnabled() || typeof window.oaiq !== 'function') { 5940 return; 5941 } 5942 5943 let user = buildAdvancedMatchingUser(); 5944 5945 if (Object.keys(user).length === 0) { 5946 return; 5947 } 5948 5949 let identity = JSON.stringify(user); 5950 5951 if (identity === sentIdentity) { 5952 return; 5953 } 5954 5955 sentIdentity = identity; 5956 5957 (options.openai.pixelIds || []).forEach(function (pixelId) { 5958 oaiq('init', {pixelId: pixelId, user: user}); 5959 }); 5960 }, 5961 5962 fireEvent: function (name, data) { 5963 if (!initialized || !this.isEnabled()) { 5964 return false;
5965 } 5966 data.delay = data.delay || 0; 5967 5968 if (data.delay === 0) { 5969 fireEvent(name, data); 5970 } else { 5971 setTimeout(function (name, params) { 5972 fireEvent(name, params); 5973 }, data.delay * 1000, name, data); 5974 } 5975 return true; 5976 }, 5977 5978 onClickEvent: function (event) { 5979 this.fireEvent(event.name, event); 5980 }, 5981 5982 onWooAddToCartOnButtonEvent: function (product_id) { 5983 if (!options.dynamicEvents.woo_add_to_cart_on_button_click.hasOwnProperty(this.tag())) { 5984 return; 5985 } 5986 if (!window.pysWooProductData || !window.pysWooProductData.hasOwnProperty(product_id)) { 5987 return; 5988 } 5989 if (!window.pysWooProductData[product_id].hasOwnProperty(this.tag())) { 5990 return; 5991 } 5992 5993 var productData = window.pysWooProductData[product_id][this.tag()]; 5994 var event = Utils.clone(options.dynamicEvents.woo_add_to_cart_on_button_click[this.tag()]); 5995 5996 Utils.copyProperties(productData['params'], event.params); 5997 5998 if (productData.hasOwnProperty('pixelIds')) { 5999 event.pixelIds = productData['pixelIds']; 6000 } 6001 6002 this.fireEvent(event.name, event); 6003 }, 6004 6005 onWooAddToCartOnSingleEvent: function (product_id, qty, product_type, is_external, $form) { 6006 if (!options.dynamicEvents.woo_add_to_cart_on_button_click.hasOwnProperty(this.tag())) { 6007 return; 6008 } 6009 if (!window.pysWooProductData || !window.pysWooProductData.hasOwnProperty(product_id)) { 6010 return; 6011 } 6012 if (!window.pysWooProductData[product_id].hasOwnProperty(this.tag())) { 6013 return; 6014 } 6015 6016 var event = Utils.clone(options.dynamicEvents.woo_add_to_cart_on_button_click[this.tag()]); 6017 6018 Utils.copyProperties(window.pysWooProductData[product_id][this.tag()]['params'], event.params); 6019 6020 // The server priced one unit; scale the quantity and the money that 6021 // depends on it. Amounts are already integer minor units, so the 6022 // multiplication stays integral. 6023 if (event.params && event.params.data) { 6024 var data = event.params.data; 6025 6026 if (Array.isArray(data.contents)) { 6027 data.contents.forEach(function (item) { 6028 item.quantity = qty; 6029 }); 6030 } 6031 } 6032 6033 this.fireEvent(event.name, event); 6034 }, 6035 6036 onEddAddToCartOnButtonEvent: function (download_id, price_index, qty) { 6037 if (!options.dynamicEvents.edd_add_to_cart_on_button_click.hasOwnProperty(this.tag())) { 6038 return; 6039 } 6040 if (!window.pysEddProductData || !window.pysEddProductData.hasOwnProperty(download_id)) { 6041 return; 6042 } 6043 6044 var index = price_index ? download_id + '_' + price_index : download_id; 6045 6046 if (!window.pysEddProductData[download_id].hasOwnProperty(index)) { 6047 return; 6048 } 6049 if (!window.pysEddProductData[download_id][index].hasOwnProperty(this.tag())) { 6050 return; 6051 } 6052 6053 var event = Utils.clone(options.dynamicEvents.edd_add_to_cart_on_button_click[this.tag()]); 6054 6055 Utils.copyProperties(window.pysEddProductData[download_id][index][this.tag()].params, event.params); 6056 6057 if (qty && event.params && event.params.data && Array.isArray(event.params.data.contents)) { 6058 event.params.data.contents.forEach(function (item) { 6059 item.quantity = qty; 6060 }); 6061 } 6062 6063 this.fireEvent(event.name, event); 6064 }, 6065 } 6066 }(options); 6067 6068 var Facebook = function (options) { 6069 6070 var defaultEventTypes = [ 6071 'PageView', 6072 'ViewContent', 6073 'Search', 6074 'AddToCart', 6075 'AddToWishlist', 6076 'InitiateCheckout', 6077 'AddPaymentInfo', 6078 'Purchase', 6079 'Lead', 6080 6081 'Subscribe', 6082 'CustomizeProduct', 6083 'FindLocation', 6084 'StartTrial', 6085 'SubmitApplication', 6086 'Schedule', 6087 'Contact', 6088 'Donate' 6089 ]; 6090 6091 var notCachedEventsIds = new Array(); 6092 var isAddToCartFromJs = options.woo.hasOwnProperty("addToCartCatchMethod") 6093 && options.woo.addToCartCatchMethod === "add_cart_js"; 6094 if(!isAddToCartFromJs) { 6095 notCachedEventsIds.push('woo_add_to_cart_on_button_click') 6096 } 6097 6098 var initialized = false;
6099 6100 /** 6101 * The `version.subdomainIndex.` prefix Meta's format calls for. 6102 * subdomainIndex is how many labels the cookie's domain has below 6103 * the public suffix (com=0, example.com=1, www.example.com=2), so it 6104 * must track cookieDomain()/probeCookieDomain() rather than a 6105 * hardcoded `fb.1.`. Only new values are affected â an existing 6106 * cookie is returned untouched. 6107 */ 6108 var fbValuePrefix = function (){ 6109 var resolved = cookieDomain(); 6110 var scope = resolved ? String(resolved).replace(/^\./, '') : ''; 6111 if (!scope) { 6112 try { scope = String(location.hostname || ''); } catch (e) { scope = ''; } 6113 } 6114 var dots = scope ? scope.split('.').length - 1 : 0; 6115 return 'fb.' + dots + '.' + Date.now() + '.'; 6116 }; 6117 var genereateFbp = function (){ 6118 return !Cookies.get('_fbp') ? fbValuePrefix()+Math.floor(1000000000 + Math.random() * 9000000000) : Cookies.get('_fbp'); 6119 }; 6120 var genereateFbc = function (){ 6121 return getUrlParameter('fbclid') ? fbValuePrefix()+getUrlParameter('fbclid') : '' 6122 }; 6123 6124 var configuredPixels = new Array(); 6125 function fireEvent(name, event) { 6126 6127 6128 if(typeof window.pys_event_data_filter === "function" && window.pys_disable_event_filter(name,'facebook')) { 6129 return; 6130 } 6131 6132 var data = event.params; 6133 var ids = event.pixelIds.filter(function (pixelId) { 6134 return !Utils.hideMatchingPixel(pixelId, 'facebook'); 6135 }); 6136 6137 var actionType = defaultEventTypes.includes(name) ? 'trackSingle' : 'trackSingleCustom'; 6138 6139 var params = {}; 6140 6141 Utils.copyProperties(data, params);
6142 Utils.copyProperties(Utils.getRequestParams(), params); 6143 Utils.copyProperties(Utils.getRequestParams(), data); 6144 6145 params = Facebook.filterParams(params); 6146 6147 6148 // sendEventId is true when the token-based Conversion API is on OR 6149 // when Meta-Enabled CAPI is on for at least one of the pixels on the 6150 // page: in both cases Meta receives a second, API-side copy of this 6151 // event and dedupes it by eventID. Only the token-based API needs an 6152 // actual request from us, so the hook/AJAX branches below stay gated 6153 // on serverApiEnabled. 6154 if(options.facebook.sendEventId) { 6155 if(options.facebook.serverApiEnabled && event.e_id === "woo_remove_from_cart" ) { 6156 Facebook.updateEventId(event.name); 6157 event.eventID = Facebook.getEventId(event.name); 6158 } else if(!notCachedEventsIds.includes(event.e_id)) { 6159 var isApiDisabled = options.gdpr.all_disabled_by_api || 6160 options.gdpr.facebook_disabled_by_api || 6161 options.gdpr.cookiebot_integration_enabled || 6162 options.gdpr.cookie_notice_integration_enabled || 6163 options.gdpr.consent_magic_integration_enabled || 6164 options.gdpr.cookie_law_info_integration_enabled; 6165 // Update eventID 6166 6167 event.eventID = Utils.generateUniqueId(event); 6168 6169 if(Cookies.get('_fbp')){ 6170 params._fbp = Cookies.get('_fbp'); 6171 } 6172 if(Cookies.get('_fbc')){ 6173 params._fbc = Cookies.get('_fbc'); 6174 } 6175 // send event from server if they was bloc by gdpr or need send with delay 6176 if( options.facebook.serverApiEnabled && ( options.ajaxForServerEvent || isApiDisabled ) ){ 6177 var json = { 6178 action: 'pys_api_event', 6179 pixel: 'facebook', 6180 event: name, 6181 event_slug: event.e_id, 6182 ids: ids, 6183 data:params, 6184 url:window.location.href, 6185 eventID:event.eventID, 6186 ajax_event:options.ajax_event 6187 }; 6188 6189 if(event.hasOwnProperty('woo_order')) { 6190 json['woo_order'] = event.woo_order; 6191 } 6192 6193 if(event.hasOwnProperty('edd_order')) { 6194 json['edd_order'] = event.edd_order; 6195 } 6196 6197 // Forward the order key so the server can bind the 6198 // purchase event to its specific order (RestPurchaseGuard). 6199 if(event.hasOwnProperty('order_key') && event.order_key) { 6200 json['order_key'] = event.order_key; 6201 } 6202 6203 if (pageReferrer) { 6204 json.referrer_url = pageReferrer; 6205 } 6206 6207 let delay = 0; 6208 try { 6209 if (sessionStorage) { 6210 Object.keys(sessionStorage) 6211 .filter(key => key.startsWith('wc_fragments_')) 6212 .forEach((key) => { 6213 let value = sessionStorage.getItem(key); 6214 if (event.e_id === 'woo_add_to_cart_on_button_click') { 6215 delay = 2000; 6216 } 6217 }); 6218 } 6219 } catch (error) { 6220 console.error('sessionStorage is not accessible:', error); 6221 } 6222 6223 if (event.e_id === "automatic_event_internal_link" || event.e_id === "automatic_event_outbound_link") {
6224 delay = delay === 0 ? 500 : delay; 6225 setTimeout(() => Utils.sendServerAjaxRequest(options.ajaxUrl, json), delay); 6226 } else if (event.type != 'static') { 6227 setTimeout(() => Utils.sendServerAjaxRequest(options.ajaxUrl, json), delay); 6228 } 6229 6230 if ( ( event.type == 'static' && options.ajaxForServerStaticEvent ) || ( event.hasOwnProperty( 'ajaxFire' ) && event.ajaxFire ) ) { 6231 setTimeout( () => Utils.sendServerAjaxRequest( options.ajaxUrl, json ), delay ); 6232 } 6233 } 6234 6235 if( options.facebook.serverApiEnabled && event.e_id !== "automatic_event_signup" && name == "CompleteRegistration" && options.facebook.wooCRSendFromServer ) { 6236 return; 6237 } 6238 } 6239 6240 } 6241 delete params._fbp; 6242 delete params._fbc; 6243 if (options.debug) { 6244 console.log('[Facebook] ' + name, params,"pixel_ids",ids,"eventID",event.eventID); 6245 } 6246 // fire event for each pixel id 6247 ids.forEach(function (pixelId) { 6248 // add eventID for deduplicate events @see https://developers.facebook.com/docs/marketing-api/conversions-api/deduplicate-pixel-and-server-events/ 6249 var args = {}; 6250 if(options.facebook.sendEventId && event.hasOwnProperty('eventID')) { 6251 args.eventID = event.eventID; 6252 } 6253 Facebook.maybeInitPixel(pixelId); 6254 fbq(actionType,pixelId, name, params,args); 6255 }); 6256 var customEvent = new CustomEvent('facebook_event_sent', { 6257 detail: { 6258 eventName: name, 6259 params: params, 6260 pixel_ids: ids 6261 } 6262 }); 6263 window.dispatchEvent(customEvent); 6264 } 6265 6266 /** 6267 * Public API 6268 */ 6269 return { 6270 tag: function() { 6271 return "facebook"; 6272 }, 6273 isEnabled: function () { 6274 return options.hasOwnProperty('facebook'); 6275 }, 6276 getHidePixel: function(){ 6277 if(this.isEnabled() && options.facebook.hasOwnProperty('hide_pixels')) 6278 { 6279 return options.facebook.hide_pixels; 6280 } 6281 return []; 6282 }, 6283 initEventIdCookies: function (key) { 6284 var ids = {}; 6285 ids[key] = pys_generate_token(36) 6286 Cookies.set('pys_fb_event_id', JSON.stringify(ids), {path: '/',domain: cookieDomain()}); 6287 }, 6288 6289 updateEventId:function(key) { 6290 var cooData = Cookies.get("pys_fb_event_id") 6291 if(cooData === undefined) { 6292 this.initEventIdCookies(key); 6293 } else { 6294 var data = JSON.parse(cooData); 6295 data[key] = pys_generate_token(36); 6296 Cookies.set('pys_fb_event_id', JSON.stringify(data), {path: '/',domain: cookieDomain()} ); 6297 } 6298 }, 6299 6300 getEventId:function (key) { 6301 var data = Cookies.get("pys_fb_event_id"); 6302 if(data === undefined) { 6303 this.initEventIdCookies(key); 6304 data = Cookies.get("pys_fb_event_id"); 6305 } 6306 return JSON.parse(data)[key]; 6307 }, 6308 6309 disable: function () { 6310 initialized = false; 6311 }, 6312 6313 filterParams: function (params) { 6314 var filteredParams = {}; 6315 6316 if (options.facebook.hasOwnProperty('enabled_medical') && options.facebook.enabled_medical && options.facebook.hasOwnProperty('do_not_track_medical_param')) { 6317 // Remove empty strings from the array 6318 options.facebook.do_not_track_medical_param = options.facebook.do_not_track_medical_param.filter(Boolean); 6319 for (const key in params) { 6320 if (Array.isArray(options.facebook.do_not_track_medical_param)) { 6321 if (!options.facebook.do_not_track_medical_param.includes(key)) { 6322 filteredParams[key] = params[key]; 6323 } 6324 } 6325 } 6326 } 6327 else { 6328 Utils.copyProperties(params, filteredParams); 6329 } 6330 return filteredParams; 6331 }, 6332 6333 /** 6334 * Load pixel's JS 6335 */ 6336 loadPixel: function () { 6337 if (initialized || !this.isEnabled() || !Utils.consentGiven('facebook')) { 6338 return; 6339 } 6340 6341 // First visit: wait (briefly) for this browser's external_id, so 6342 // the very first PageView is not sent under the inline value. 6343 if (Utils.holdForIdentity(function () { Facebook.loadPixel(); })) { 6344 return; 6345 } 6346 6347 ! function (f, b, e, v, n, t, s) { 6348 if (f.fbq) return; 6349 n = f.fbq = function () { 6350 n.callMethod ? 6351 n.callMethod.apply(n, arguments) : n.queue.push(arguments) 6352 }; 6353 if (!f._fbq) f._fbq = n; 6354 n.push = n; 6355 n.loaded = !0; 6356 n.version = '2.0'; 6357 n.agent = 'dvpixelyoursite'; 6358 n.queue = []; 6359 t = b.createElement(e); 6360 t.async = !0; 6361 t.src = v; 6362 s = b.getElementsByTagName(e)[0]; 6363 s.parentNode.insertBefore(t, s) 6364 }(window, 6365 document, 'script', 'https://connect.facebook.net/en_US/fbevents.js'); 6366 // Report the cookie domain here too, not just from 6367 // manageCookies(): this runs under the `facebook` pixel's own 6368 // consent, which can differ from and outlast the `pys` 6369 // script's consent (e.g. under ConsentMagic's separate 6370 // category assignments), so PHP might otherwise never learn 6371 // the scope for a visitor who only accepted this pixel. 6372 // 6373 // `pys_cd` only ever appears here alongside a cookie we are 6374 // already writing at that scope, so it can never be the first 6375 // cookie a visitor receives. disabled_all_cookie is still 6376 // honoured, matching the call in manageCookies(). 6377 if (!options.cookie || !options.cookie.disabled_all_cookie) { 6378 reportCookieDomain(cookieDomain()); 6379 } 6380 6381 let expires = parseInt(options.cookie_duration); 6382 if(!Cookies.get('_fbp')) { 6383 Cookies.set('_fbp',genereateFbp(), { expires: expires,path: '/',domain: cookieDomain() }); 6384 } 6385 6386 if(getUrlParameter('fbclid')) { 6387 Cookies.set('_fbc',genereateFbc(), { expires: expires,path: '/',domain: cookieDomain() }); 6388 } 6389 6390 // Now that Facebook consent is confirmed, mirror the session 6391 // entry referrer (if any) into the session cookie so PHP can 6392 // use it as a fallback for orphan server-only events. 6393 getOrInitSessionEntryReferrer(true); 6394 6395 // Refresh the per-page event referrer cookie so checkout 6396 // hooks can snapshot the buyer's prior page. 6397 writeEventReferrerCookie(); 6398 6399 var ids = options.facebook.pixelIds.filter(function (pixelId) { 6400 return !Utils.hideMatchingPixel(pixelId, 'facebook'); 6401 }); 6402 // initialize default pixel 6403 ids.forEach(function (pixelId) { 6404 Facebook.maybeInitPixel(pixelId); 6405 }); 6406 initialized = true; 6407 6408 Utils.fireStaticEvents('facebook'); 6409 6410 }, 6411 advancedMatching: function () { 6412 if(options.facebook.advancedMatchingEnabled) { 6413 let advancedMatching = {}; 6414 if(Object.keys(options.facebook.advancedMatching).length > 0) { 6415 advancedMatching = options.facebook.advancedMatching; 6416 } 6417 6418 Utils.AdvancedMatching.merge(advancedMatching, { 6419 em: { field: 'email', hash: false }, 6420 ph: { field: 'phone', hash: false }, 6421 fn: { field: 'first_name', hash: false }, 6422 ln: { field: 'last_name', hash: false } 6423 }); 6424 6425 if(!advancedMatching.hasOwnProperty("external_id")){ 6426 if (Cookies.get('pbid')) { 6427 advancedMatching["external_id"] = Cookies.get('pbid'); 6428 } 6429 } 6430 else if(advancedMatching.hasOwnProperty("external_id") && advancedMatching.external_id != Cookies.get('pbid')) 6431 { 6432 advancedMatching["external_id"] = Cookies.get('pbid') ? Cookies.get('pbid') : advancedMatching.external_id; 6433 } 6434 6435 if(Object.keys(advancedMatching).length > 0) { 6436 return advancedMatching; 6437 } 6438 } 6439 return false 6440 }, 6441 maybeInitPixel: function(pixelId) { 6442 6443 if(configuredPixels.includes(pixelId)) return; 6444 6445 if (options.facebook.removeMetadata || Utils.hideMatchingPixel(pixelId, this.tag())) { 6446 fbq('set', 'autoConfig', false, pixelId); 6447 } 6448 let advancedMatching = Facebook.advancedMatching(); 6449 6450 if ( +options.facebook.meta_ldu === 1 ) { 6451 fbq( 'dataProcessingOptions', [ 'LDU' ], 0, 0 ); 6452 } 6453 6454 // Meta-Enabled Conversion API: let Meta build its own API events 6455 // out of this pixel's data. Must run right before init, and the 6456 // opt-in is per pixel id. 6457 if (Utils.isMetaEnabledCapiPixel(pixelId)) { 6458 fbq('optinMetaEnabledCapi', pixelId); 6459 } 6460 6461 if (options.gdpr.consent_magic_integration_enabled && typeof CS_Data !== "undefined") { 6462 if(!advancedMatching) { 6463 fbq('init', pixelId); 6464 } else { 6465 var test_prefix = CS_Data.test_prefix; 6466 var cs_advanced_matching = Cookies.get('cs_enabled_advanced_matching'+test_prefix); 6467 if (jQuery('#cs_enabled_advanced_matching'+test_prefix).length > 0) { 6468 if (cs_advanced_matching == 'yes') { 6469 fbq('init', pixelId, advancedMatching); 6470 } else { 6471 fbq('init', pixelId); 6472 } 6473 } else { 6474 fbq('init', pixelId, advancedMatching); 6475 } 6476 } 6477 } else { 6478 if(!advancedMatching) { 6479 fbq('init', pixelId); 6480 } else { 6481 fbq('init', pixelId, advancedMatching); 6482 } 6483 } 6484 configuredPixels.push(pixelId); 6485 }, 6486 6487 fireEvent: function (name, data) { 6488 6489 if (!initialized || !this.isEnabled()) { 6490 return false;
6491 } 6492 6493 data.delay = data.delay || 0; 6494 data.params = data.params || {}; 6495 6496 if (data.delay === 0) { 6497 6498 fireEvent(name, data); 6499 6500 } else { 6501 6502 setTimeout(function (name, params) { 6503 fireEvent(name, params); 6504 }, data.delay * 1000, name, data); 6505 6506 } 6507 6508 return true; 6509 6510 }, 6511 6512 onAdSenseEvent: function (event) { 6513 this.fireEvent(event.name, event); 6514 }, 6515 6516 onClickEvent: function (event) { 6517 this.fireEvent(event.name, event); 6518 }, 6519 6520 onWatchVideo: function (event) { 6521 this.fireEvent(event.name, event); 6522 }, 6523 6524 onCommentEvent: function (event) { 6525 this.fireEvent(event.name, event); 6526 }, 6527 6528 onFormEvent: function (event) { 6529 this.fireEvent(event.name, event); 6530 }, 6531 6532 onDownloadEvent: function (event) { 6533 this.fireEvent(event.name, event); 6534 6535 }, 6536 6537 6538 onWooAddToCartOnButtonEvent: function (product_id) { 6539 6540 window.pysWooProductData = window.pysWooProductData || []; 6541 if(!options.dynamicEvents.woo_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 6542 return; 6543 6544 if (window.pysWooProductData.hasOwnProperty(product_id)) { 6545 if (window.pysWooProductData[product_id].hasOwnProperty('facebook')) { 6546 6547 var event = Utils.clone(options.dynamicEvents.woo_add_to_cart_on_button_click[this.tag()]) 6548 6549 Utils.copyProperties(window.pysWooProductData[product_id]['facebook']['params'], event.params) 6550 event.pixelIds = window.pysWooProductData[product_id]['facebook']['pixelIds']; 6551 this.fireEvent(event.name, event); 6552 } 6553 } 6554 }, 6555 6556 onWooAddToCartOnSingleEvent: function (product_id, qty, product_type, is_external, $form) { 6557 6558 window.pysWooProductData = window.pysWooProductData || []; 6559 if(!options.dynamicEvents.woo_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 6560 return; 6561 6562 if (product_type === Utils.PRODUCT_VARIABLE && !options.facebook.wooVariableAsSimple) { 6563 product_id = parseInt($form.find('input[name="variation_id"]').val()); 6564 } 6565 6566 if (window.pysWooProductData.hasOwnProperty(product_id)) { 6567 if (window.pysWooProductData[product_id].hasOwnProperty('facebook')) { 6568 6569 var event = Utils.clone(options.dynamicEvents.woo_add_to_cart_on_button_click.facebook); 6570 6571 6572 Utils.copyProperties(window.pysWooProductData[product_id]['facebook']['params'], event.params); 6573 6574 var groupValue = 0; 6575 if(product_type === Utils.PRODUCT_GROUPED ) { 6576 $form.find(".woocommerce-grouped-product-list .qty").each(function(index){ 6577 var childId = $(this).attr('name').replaceAll("quantity[","").replaceAll("]",""); 6578 var quantity = parseInt($(this).val()); 6579 if(isNaN(quantity)) { 6580 quantity = 0; 6581 } 6582 var childItem = window.pysWooProductData[product_id]['facebook'].grouped[childId]; 6583 6584 if(quantity == 0) { 6585 event.params.content_ids.forEach(function(el,index,array) { 6586 if(el == childItem.content_id) { 6587 array.splice(index, 1); 6588 } 6589 }); 6590 } 6591 6592 if(event.params.hasOwnProperty('contents')) { 6593 event.params.contents.forEac
6593h(function(el,index,array) { 6594 if(el.id == childItem.content_id) { 6595 if(quantity > 0){ 6596 el.quantity = quantity; 6597 } else { 6598 array.splice(index, 1); 6599 } 6600 } 6601 }); 6602 } 6603 6604 6605 groupValue += childItem.price * quantity; 6606 }); 6607 if(groupValue == 0) return; // skip if no items selected 6608 } 6609 6610 6611 6612 // maybe customize value option 6613 if (options.woo.addToCartOnButtonValueEnabled && options.woo.addToCartOnButtonValueOption !== 'global') { 6614 6615 if(product_type === Utils.PRODUCT_GROUPED) { 6616 event.params.value = groupValue; 6617 } else if(product_type === Utils.PRODUCT_BUNDLE) { 6618 var data = $(".bundle_form .bundle_data").data("bundle_form_data"); 6619 var items_sum = getBundlePriceOnSingleProduct(data); 6620 event.params.value = (parseFloat(data.base_price) + items_sum )* qty; 6621 } else { 6622 event.params.value = event.params.value * qty; 6623 } 6624 } 6625 6626 // only when non Facebook for WooCommerce logic used 6627 if (event.params.hasOwnProperty('contents') && product_type !== Utils.PRODUCT_GROUPED) { 6628 event.params.contents[0].quantity = qty; 6629 6630 } 6631 6632 var event_name = is_external ? options.woo.affiliateEventName : event.name; 6633 6634 this.fireEvent(event_name, event); 6635 6636 } 6637 } 6638 6639 }, 6640 6641 onWooRemoveFromCartEvent: function (event) { 6642 this.fireEvent(event.name, event); 6643 }, 6644 6645 onWooAffiliateEvent: function (product_id) { 6646 if(!options.dynamicEvents.woo_affiliate.hasOwnProperty(this.tag())) 6647 return; 6648 var event = Utils.clone(options.dynamicEvents.woo_affiliate[this.tag()]); 6649 6650 6651 if (window.pysWooProductData.hasOwnProperty(product_id)) { 6652 if (window.pysWooProductData[product_id].hasOwnProperty('facebook')) { 6653 6654 Utils.copyProperties(window.pysWooProductData[product_id][this.tag()].params, event.params) 6655 this.fireEvent(options.woo.affiliateEventName, event); 6656 6657 } 6658 } 6659 6660 }, 6661 6662 onWooPayPalEvent: function (event) { 6663 this.fireEvent(event.name, event); 6664 }, 6665 6666 onEddAddToCartOnButtonEvent: function (download_id, price_index, qty) { 6667 if(!options.dynamicEvents.edd_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 6668 return; 6669 var event = Utils.clone(options.dynamicEvents.edd_add_to_cart_on_button_click[this.tag()]); 6670 6671 if (window.pysEddProductData && window.pysEddProductData.hasOwnProperty(download_id)) { 6672 6673 var index; 6674 6675 if (price_index) { 6676 index = download_id + '_' + price_index; 6677 } else { 6678 index = download_id; 6679 } 6680 6681 if (window.pysEddProductData[download_id].hasOwnProperty(index)) { 6682 if (window.pysEddProductData[download_id][index].hasOwnProperty('facebook')) { 6683 6684 Utils.copyProperties(window.pysEddProductData[download_id][index]['facebook']["params"], event.params) 6685 6686 // maybe customize value option 6687 if (options.edd.addToCartOnButtonValueEnabled && options.edd.addToCartOnButtonValueOption !== 'global') { 6688 event.params.value = event.params.value * qty; 6689 } 6690 6691 // update contents qty param 6692 var contents = event.params.contents; 6693 contents[0].quantity = qty; 6694 event.params.contents = contents; 6695 6696 this.fireEvent(event.name,event); 6697 6698 } 6699 } 6700 6701 } 6702 6703 }, 6704 6705 onEddRemoveFromCartEvent: function (event) { 6706 this.fireEvent(event.name, event); 6707 }, 6708 onPageScroll: function (event) { 6709 this.fireEvent(event.name, event); 6710 }, 6711 onTime: function (event) { 6712 this.fireEvent(event.name, event); 6713 }, 6714 }; 6715 6716 }(options); 6717 6718 var Analytics = function (options) { 6719 6720 var initialized = false;
6721 var isAllowEnhancedConversions = false; 6722 6723 /** 6724 * Fires event 6725 * 6726 * @link: https://developers.google.com/analytics/devguides/collection/gtagjs/sending-data 6727 * @link: https://developers.google.com/analytics/devguides/collection/gtagjs/events 6728 * @link: https://developers.google.com/gtagjs/reference/event 6729 * @link: https://developers.google.com/gtagjs/reference/parameter 6730 * 6731 * @link: https://developers.google.com/analytics/devguides/collection/gtagjs/custom-dims-mets 6732 * 6733 * @param name 6734 * @param data 6735 */ 6736 function fireEvent(name, event) { 6737 if(typeof window.pys_event_data_filter === "function" && window.pys_disable_event_filter(name,'ga')) { 6738 return; 6739 } 6740 6741 var eventParams = event.params; 6742 var valuesArray = Object.values(event.trackingIds); 6743 var ids = valuesArray.filter(function (pixelId) { 6744 return loadTags.some(loadTag => pixelId.startsWith(loadTag)) && !Utils.hideMatchingPixel(pixelId, 'ga') && !Utils.hideMatchingPixel(pixelId, 'google_ads'); 6745 }) 6746 6747 // Generate unique event_id for deduplication 6748 eventParams.event_id = Utils.generateUniqueId(event); 6749 event.eventID = Utils.generateUniqueId(event); 6750
6751 Utils.copyProperties(Utils.getRequestParams(), eventParams); 6752 6753 var copyParams = Utils.copyProperties(eventParams, {}); // copy params because mapParamsTov4 can modify it 6754 var params = mapParamsTov4(ids,name,copyParams) 6755 6756 var _fireEvent = function (tracking_ids,name,params) { 6757 6758 // Firing without a destination would park the hit inside gtag.js 6759 // instead of sending it. Skip, but keep the reason visible: an 6760 // empty list means the event's trackingIds and the tags actually 6761 // configured on this page went out of sync. 6762 if (!Utils.hasSendToTarget(tracking_ids)) { 6763 if (options.debug) { 6764 console.warn('[Google Analytics] ' + name + ' skipped: no tracking ID left after filtering', { 6765 eventTrackingIds: Object.values(event.trackingIds), 6766 loadedTags: loadTags 6767 }); 6768 } 6769 return; 6770 } 6771 6772 params['send_to'] = tracking_ids; 6773 6774 if (options.debug) { 6775 console.log('[Google Analytics #' + tracking_ids + '] ' + name, params); 6776 } 6777 6778 // Send via gtag 6779 Utils.gtagPush('event', name, params); 6780 6781 var customEvent = new CustomEvent('gtag_event_sent', { 6782 detail: { 6783 eventName: name, 6784 params: params, 6785 pixel_ids: tracking_ids 6786 } 6787 }); 6788 window.dispatchEvent(customEvent); 6789 }; 6790 6791 6792 delete params.analytics_storage; 6793 delete params.ad_storage; 6794 delete params.ad_user_data; 6795 delete params.ad_personalization; 6796 6797 if(options.hasOwnProperty('google_ads')){ 6798 var conversion_event_name = event.e_id; 6799 switch ( conversion_event_name ) { 6800 case "woo_add_to_cart_on_cart_page": 6801 case "woo_add_to_cart_on_checkout_page": 6802 case "woo_add_to_cart_on_button_click": 6803 conversion_event_name = 'woo_add_to_cart'; 6804 break; 6805 case "edd_add_to_cart_on_cart_page": 6806 case "edd_add_to_cart_on_checkout_page": 6807 case "edd_add_to_cart_on_button_click": 6808 conversion_event_name = 'edd_add_to_cart'; 6809 break; 6810 case "automatic_event_adsense": 6811 case "automatic_event_comment": 6812 case "automatic_event_download": 6813 case "automatic_event_email_link": 6814 case "automatic_event_form": 6815 case "automatic_event_internal_link": 6816 case "automatic_event_outbound_link": 6817 case "automatic_event_scroll": 6818 case "automatic_event_tel_link": 6819 case "automatic_event_time_on_page": 6820 case "automatic_event_video": 6821 conversion_event_name = 'automatic_event'; 6822 break; 6823 } 6824 6825 if (conversion_event_name !== 'automatic_event') { 6826 // We replace the event name for the settings key if it is woo_view_item_list 6827 const trackEventName = conversion_event_name === 'woo_view_item_list' 6828 ? 'woo_view_category' 6829 : conversion_event_name; 6830 6831 if (typeof options.google_ads[trackEventName + '_conversion_track'] !== 'undefined') { 6832 const conversionTrack = options.google_ads[trackEventName + '_conversion_track']; 6833 6834 if (conversionTrack === 'conversion') { 6835 ids = ids.map(id => id.split('/')[0]); 6836 } 6837 } 6838 } 6839 } 6840 6841 6842 _fireEvent(ids, name, params); 6843 if(event.hasOwnProperty('unify') && event.unify){ 6844 isTrackEventForGA.push(name); 6845 } 6846 } 6847 6848 function normalizeEventName(eventName) { 6849 6850 var matches = { 6851 ViewContent: 'view_item', 6852 AddToCart: 'add_to_cart', 6853 AddToWishList: 'add_to_wishlist', 6854 InitiateCheckout: 'begin_checkout', 6855 Purchase: 'purchase', 6856 Lead: 'generate_lead', 6857 CompleteRegistration: 'sign_up', 6858 AddPaymentInfo: 'set_checkout_option' 6859 }; 6860 6861 return matches.hasOwnProperty(eventName) ? matches[eventName] : eventName; 6862 6863 } 6864 6865 function mapParamsTov4(tag,name,param) { 6866 //GA4 automatically collects a number of parameters for all events 6867 var hasGA4Tag = false;
6868 6869 // end 6870 if (Array.isArray(tag)) { 6871 hasGA4Tag = tag.some(function (element) { 6872 return isv4(element); 6873 }); 6874 } else if(isv4(tag)) { 6875 // tag is a string and matches GA4 6876 hasGA4Tag = true; 6877 } 6878 if(hasGA4Tag) { 6879 delete param.event_category; 6880 delete param.event_label; 6881 delete param.ecomm_prodid; 6882 delete param.ecomm_pagetype; 6883 delete param.ecomm_totalvalue; 6884 if(name === 'search') { 6885 param['search'] = param.search_term; 6886 delete param.search_term; 6887 delete param.dynx_itemid; 6888 delete param.dynx_pagetype; 6889 delete param.dynx_totalvalue; 6890 } 6891 } 6892 return param; 6893 } 6894 6895 function isv4(tag) { 6896 return tag.indexOf('G') === 0; 6897 } 6898 6899 /** 6900 * Public API 6901 */ 6902 return { 6903 tag: function() { 6904 return "ga"; 6905 }, 6906 isEnabled: function () { 6907 return options.hasOwnProperty('ga'); 6908 }, 6909 getHidePixel: function(){ 6910 if(this.isEnabled() && options.ga.hasOwnProperty('hide_pixels')) 6911 { 6912 return options.ga.hide_pixels; 6913 } 6914 return []; 6915 }, 6916 disable: function () { 6917 initialized = false; 6918 }, 6919 updateEnhancedConversionData : function () { 6920 if (!initialized || !this.isEnabled()) { 6921 return; 6922 } 6923 if(options.hasOwnProperty("tracking_analytics") && options.tracking_analytics.hasOwnProperty("userDataEnable") && options.tracking_analytics.userDataEnable) { 6924 var advanced = Utils.getAdvancedMergeFormData(); 6925 if (Object.keys(advanced).length > 0) { 6926 Utils.gtagPush('set', 'user_data', advanced); 6927 } 6928 } 6929 }, 6930 loadPixel: function () { 6931 if (initialized || !this.isEnabled() || !Utils.consentGiven('analytics')) { 6932 return; 6933 } 6934 6935 6936 for (var i = 0; i < options.ga.trackingIds.length; i++) { 6937 var trackingId = options.ga.trackingIds[i]; 6938 if (!Utils.hideMatchingPixel(trackingId, 'ga')) { 6939 Utils.loadGoogleTag(trackingId); 6940 break; 6941 } 6942 } 6943 6944 6945 if(options.hasOwnProperty("tracking_analytics") && options.tracking_analytics.hasOwnProperty("userDataEnable") && options.tracking_analytics.userDataEnable){ 6946 var advanced = Utils.getAdvancedMergeFormData(); 6947 if(Object.keys(advanced).length > 0){ 6948 Utils.gtagPush('set', 'user_data', advanced); 6949 } 6950 } 6951 6952 var config = {}; 6953 6954 if(options.user_id && options.user_id != 0) { 6955 config.user_id = options.user_id; 6956 } 6957 config.url_passthrough = options.ga.url_passthrough; 6958 // Cross-Domain tracking 6959 if (options.ga.crossDomainEnabled) { 6960 config.linker = { 6961 accept_incoming: options.ga.crossDomainAcceptIncoming, 6962 domains: options.ga.crossDomainDomains 6963 }; 6964 } 6965 6966 var ids = options.ga.trackingIds.filter(function (pixelId) { 6967 return !Utils.hideMatchingPixel(pixelId, 'ga'); 6968 }); 6969 loadTags.push(...ids); 6970 ids.forEach(function (trackingId,index) { 6971 6972 var obj = options.ga.isDebugEnabled; 6973 var searchValue = "index_"+index; 6974 var config_for_tag = Object.assign({}, config); 6975 config_for_tag.debug_mode = false;
6976 config_for_tag.send_page_view = !options.ga.custom_page_view_event; 6977 for (var key in obj) { 6978 if (obj[key] === searchValue) { 6979 config_for_tag.debug_mode = true; 6980 break; 6981 } 6982 } 6983 if(!config_for_tag.debug_mode) 6984 { 6985 delete config_for_tag.debug_mode; 6986 } 6987 6988 if(isv4(trackingId)) { 6989 if(options.ga.disableAdvertisingPersonalization) { 6990 config_for_tag.allow_ad_personalization_signals = false 6991 } 6992 } 6993 if(options.ga.hasOwnProperty('additionalConfig')){ 6994 if(options.ga.additionalConfig.hasOwnProperty(trackingId) && options.ga.additionalConfig[trackingId]){ 6995 config_for_tag.first_party_collection = options.ga.additionalConfig[trackingId].first_party_collection; 6996 } 6997 6998 } 6999 if(options.ga.hasOwnProperty('serverContainerUrls')){ 7000 if(options.ga.serverContainerUrls.hasOwnProperty(trackingId) && options.ga.serverContainerUrls[trackingId].enable_server_container != false){ 7001 if(options.ga.serverContainerUrls[trackingId].server_container_url != ''){ 7002 config_for_tag.server_container_url = options.ga.serverContainerUrls[trackingId].server_container_url; 7003 } 7004 if(options.ga.serverContainerUrls[trackingId].transport_url != ''){ 7005 config_for_tag.transport_url = options.ga.serverContainerUrls[trackingId].transport_url; 7006 } 7007 } 7008 } 7009 if (options.gdpr.cookiebot_integration_enabled && typeof Cookiebot !== 'undefined') { 7010 var cookiebot_consent_category = options.gdpr['cookiebot_analytics_consent_category']; 7011 if (options.gdpr['analytics_prior_consent_enabled']) { 7012 if (Cookiebot.consented === true && Cookiebot.consent[cookiebot_consent_category]) { 7013 Utils.gtagPush('config', trackingId, config_for_tag); 7014 } 7015 } else { 7016 if (Cookiebot.consent[cookiebot_consent_category]) { 7017 Utils.gtagPush('config', trackingId, config_for_tag); 7018 } 7019 } 7020 } 7021 else 7022 { 7023 Utils.gtagPush('config', trackingId, config_for_tag); 7024 } 7025 }); 7026 if(!isAdsLoad && GAds.isEnabled() && options.google_ads.conversion_ids.length > 0 && Utils.consentGiven('google_ads')) { 7027 for (var i = 0; i < options.google_ads.conversion_ids.length; i++) { 7028 var trackingId = options.google_ads.conversion_ids[i]; 7029 if (!Utils.hideMatchingPixel(trackingId, 'google_ads')) { 7030 Utils.loadGoogleTag(trackingId); 7031 break; 7032 } 7033 } 7034 var ids = options.google_ads.conversion_ids.filter(function (pixelId) { 7035 return !Utils.hideMatchingPixel(pixelId, 'google_ads'); 7036 }); 7037 loadTags.push(...ids); 7038 // configure conversion ids 7039 ids.forEach(function (conversion_id,index) { 7040 7041 Utils.gtagPush('config', conversion_id); 7042 7043 if(options.google_ads.enhanced_conversion.includes("index_"+index)) { 7044 isAllowEnhancedConversions = true 7045 Utils.gtagPush('config', conversion_id,{ 'allow_enhanced_conversions':true }); 7046 } 7047 7048 Utils.registerGadsPhoneConversion( conversion_id ); 7049 7050 }); 7051 isAdsLoad = true; 7052 } 7053 7054 initialized = true; 7055 7056 Utils.fireStaticEvents('ga'); 7057 $( document).trigger( "analytics_initialized") 7058 }, 7059 7060 fireEvent: function (name, data) { 7061 7062 if (!initialized || !this.isEnabled()) { 7063 return false;
7064 } 7065 7066 data.delay = data.delay || 0; 7067 data.params = data.params || {}; 7068 7069 if (data.delay === 0) { 7070 7071 fireEvent(name, data); 7072 7073 } else { 7074 7075 setTimeout(function (name, params) { 7076 fireEvent(name, params); 7077 }, data.delay * 1000, name, data); 7078 7079 } 7080 7081 return true; 7082 7083 }, 7084 7085 onAdSenseEvent: function (event) { 7086 this.fireEvent(event.name, event); 7087 }, 7088 7089 onClickEvent: function (event) { 7090 this.fireEvent(event.name, event); 7091 }, 7092 7093 onWatchVideo: function (event) { 7094 if(!event.hasOwnProperty("youtube_disabled") 7095 || !event.youtube_disabled 7096 || event.params.video_type !== "youtube") { 7097 this.fireEvent(event.name, event); 7098 } 7099 }, 7100 7101 onCommentEvent: function (event) { 7102 7103 this.fireEvent(event.name, event); 7104 7105 }, 7106 7107 onFormEvent: function (event) { 7108 7109 this.fireEvent(event.name, event); 7110 7111 }, 7112 7113 onDownloadEvent: function (event) { 7114 7115 this.fireEvent(event.name, event); 7116 7117 }, 7118 7119 onWooAddToCartOnButtonEvent: function (product_id, prod_info = null) { 7120 if(!options.dynamicEvents.woo_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 7121 return; 7122 7123 if (window.pysWooProductData.hasOwnProperty(product_id)) { 7124 if (window.pysWooProductData[product_id].hasOwnProperty('ga')) { 7125 var event = Utils.clone(options.dynamicEvents.woo_add_to_cart_on_button_click[this.tag()]); 7126 Utils.copyProperties(window.pysWooProductData[product_id]['ga'].params, event.params) 7127 event.trackingIds = window.pysWooProductData[product_id]['ga']['trackingIds']; 7128 if(prod_info) 7129 { 7130 if(prod_info['pys_list_name_productlist_id']) 7131 { 7132 event.params.items[0]['item_list_id'] = prod_info['pys_list_name_productlist_id'] 7133 } 7134 if(prod_info['pys_list_name_productlist_name']) 7135 { 7136 event.params.items[0]['item_list_name'] =prod_info['pys_list_name_productlist_name'] 7137 } 7138 } 7139 this.fireEvent(event.name, event); 7140 } 7141 } 7142 7143 }, 7144 7145 onWooAddToCartOnSingleEvent: function (product_id, qty, product_type, is_external, $form, prod_info) { 7146 7147 window.pysWooProductData = window.pysWooProductData || []; 7148 7149 if(!options.dynamicEvents.woo_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 7150 return; 7151 var event = Utils.clone(options.dynamicEvents.woo_add_to_cart_on_button_click[this.tag()]); 7152 7153 if (product_type === Utils.PRODUCT_VARIABLE && !options.ga.wooVariableAsSimple) { 7154 product_id = parseInt($form.find('input[name="variation_id"]').val()); 7155 } 7156 7157 if (window.pysWooProductData.hasOwnProperty(product_id)) { 7158 if (window.pysWooProductData[product_id].hasOwnProperty('ga')) { 7159 7160 Utils.copyProperties(window.pysWooProductData[product_id]['ga'].params, event.params); 7161 7162 7163 if(product_type === Utils.PRODUCT_GROUPED ) { 7164 var groupValue = 0; 7165 $form.find(".woocommerce-grouped-product-list .qty").each(function(index){ 7166 var childId = $(this).attr('name').replaceAll("quantity[","").replaceAll("]",""); 7167 var quantity = parseInt($(this).val()); 7168 if(isNaN(quantity)) { 7169 quantity = 0; 7170 } 7171 var childItem = window.pysWooProductData[product_id]['ga'].grouped[childId];
7172 event.params.items.forEach(function(el,index,array) { 7173 if(el.id == childItem.content_id) { 7174 if(quantity > 0){ 7175 el.quantity = quantity; 7176 el.price = childItem.price; 7177 } else { 7178 array.splice(index, 1); 7179 } 7180 } 7181 }); 7182 groupValue += childItem.price * quantity; 7183 }); 7184 7185 if(options.woo.addToCartOnButtonValueEnabled && 7186 options.woo.addToCartOnButtonValueOption !== 'global' && 7187 event.params.hasOwnProperty('value')) { 7188 event.params.value = groupValue; 7189 } 7190 7191 if(groupValue == 0) return; // skip if no items selected 7192 } else { 7193 // update items qty param 7194 event.params.items[0].quantity = qty; 7195 } 7196 7197 // maybe customize value option 7198 if (options.woo.addToCartOnButtonValueEnabled && 7199 options.woo.addToCartOnButtonValueOption !== 'global' && 7200 product_type !== Utils.PRODUCT_GROUPED) 7201 { 7202 if(event.params.hasOwnProperty('value')) { 7203 event.params.value = event.params.items[0].price * qty; 7204 } 7205 } 7206 7207 if(prod_info) 7208 { 7209 if(prod_info['pys_list_name_productlist_id']) 7210 { 7211 event.params.items[0]['item_list_id'] = prod_info['pys_list_name_productlist_id'] 7212 } 7213 if(prod_info['pys_list_name_productlist_name']) 7214 { 7215 event.params.items[0]['item_list_name'] =prod_info['pys_list_name_productlist_name'] 7216 } 7217 } 7218 7219 var eventName = is_external ? options.woo.affiliateEventName : event.name; 7220 eventName = normalizeEventName(eventName); 7221 7222 this.fireEvent(eventName, event); 7223 7224 } 7225 } 7226 7227 }, 7228 7229 onWooCheckoutProgressStep: function (event) { 7230 this.fireEvent(event.name, event); 7231 }, 7232 7233 onWooSelectContent: function (event) { 7234 const select_prod_list = {}; 7235 7236 if (event.params.items[0].item_list_name !== undefined) { 7237 select_prod_list.list_name = event.params.items[0].item_list_name; 7238 } 7239 7240 if (event.params.items[0].item_list_id !== undefined) { 7241 select_prod_list.list_id = event.params.items[0].item_list_id; 7242 } 7243 const url = new URL(window.location.href); 7244 select_prod_list.url = url.origin + url.pathname; 7245 Cookies.set('select_prod_list', JSON.stringify(select_prod_list), { expires: 1,path: '/' }); 7246 this.fireEvent(event.name, event); 7247 }, 7248 7249 onWooRemoveFromCartEvent: function (event) { 7250 this.fireEvent(event.name, event); 7251 }, 7252 7253 onWooAffiliateEvent: function (product_id) { 7254 if(!options.dynamicEvents.woo_affiliate.hasOwnProperty(this.tag())) 7255 return; 7256 var event = options.dynamicEvents.woo_affiliate[this.tag()]; 7257 7258 if (window.pysWooProductData.hasOwnProperty(product_id)) { 7259 if (window.pysWooProductData[product_id].hasOwnProperty('ga')) { 7260 7261 event = Utils.clone(event ); 7262 Utils.copyProperties(window.pysWooProductData[product_id][this.tag()], event.params) 7263 this.fireEvent(normalizeEventName(options.woo.affiliateEventName), event); 7264 7265 } 7266 } 7267 7268 }, 7269 7270 onWooPayPalEvent: function (event) { 7271 this.fireEvent(event.name, event); 7272 }, 7273 7274 onEddAddToCartOnButtonEvent: function (download_id, price_index, qty) { 7275 if(!options.dynamicEvents.edd_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 7276 return; 7277 var event = Utils.clone(options.dynamicEvents.edd_add_to_cart_on_button_click[this.tag()]); 7278 7279 7280 if (window.pysEddProductData && window.pysEddProductData.hasOwnProperty(download_id)) { 7281 7282 var index; 7283 7284 if (price_index) { 7285 index = download_id + '_' + price_index; 7286 } else { 7287 index = download_id; 7288 } 7289 7290 if (window.pysEddProductData[download_id].hasOwnProperty(index)) { 7291 if (window.pysEddProductData[download_id][index].hasOwnProperty('ga')) { 7292 7293 Utils.copyProperties(window.pysEddProductData[download_id][index]['ga'].params, event.params); 7294 7295 // update items qty param 7296 event.params.items[0].quantity = qty; 7297 7298 this.fireEvent(event.name,event); 7299 7300 } 7301 } 7302 7303 } 7304 7305 }, 7306 7307 onEddRemoveFromCartEvent: function (event) { 7308 this.fireEvent(event.name, event); 7309 }, 7310 7311 onPageScroll: function (event) { 7312 if (initialized && this.isEnabled()) { 7313 this.fireEvent(event.name, event); 7314 } 7315 }, 7316 onTime: function (event) { 7317 if (initialized && this.isEnabled()) { 7318 this.fireEvent(event.name, event); 7319 } 7320 }, 7321 }; 7322 7323 }(options); 7324 7325 var GAds = function (options) { 7326 7327 var initialized = false;
7328 var isAllowEnhancedConversions = false 7329 /** 7330 * Fires event 7331 * 7332 * @link: https://developers.google.com/analytics/devguides/collection/gtagjs/sending-data 7333 * @link: https://developers.google.com/analytics/devguides/collection/gtagjs/events 7334 * @link: https://developers.google.com/gtagjs/reference/event 7335 * @link: https://developers.google.com/gtagjs/reference/parameter 7336 */ 7337 function fireEvent(name, data) { 7338 if(typeof window.pys_event_data_filter === "function" && window.pys_disable_event_filter(event_name,'google_ads')) { 7339 return; 7340 } 7341 var _params = Utils.copyProperties(data.params,{}); 7342 7343 _params.event_id = Utils.generateUniqueId(data); 7344 7345 var ids = data.ids.filter(function (pixelId) { 7346 return !Utils.hideMatchingPixel(pixelId, 'google_ads'); 7347 }); 7348 7349 var conversionIds = data.hasOwnProperty('conversion_ids') ? data.conversion_ids.filter(function (conversion_id) { 7350 return !Utils.hideMatchingPixel(conversion_id, 'google_ads'); 7351 }) : []; 7352 var conversion_labels = data.hasOwnProperty('conversion_labels') ? data.conversion_labels.filter(function (conversion_label) { 7353 conversion_id = conversion_label.split('/')[0]; 7354 return !Utils.hideMatchingPixel(conversion_id, 'google_ads'); 7355 }) : []; 7356
7357 Utils.copyProperties(Utils.getRequestParams(), _params); 7358 var _fireEvent = function (conversion_id,event_name) { 7359 7360 // Same rule as in the Analytics module: no destination means the 7361 // hit is parked inside gtag.js, not sent. Reaching here with an 7362 // empty list means every configured conversion ID was filtered 7363 // out (hidden pixel, or the conversionIds fallback was empty). 7364 if (!Utils.hasSendToTarget(conversion_id)) { 7365 if (options.debug) { 7366 console.warn('[Google Ads] ' + event_name + ' skipped: no conversion ID left after filtering', { 7367 ids: data.ids, 7368 conversionIds: data.conversion_ids, 7369 conversionLabels: data.conversion_labels 7370 }); 7371 } 7372 return; 7373 } 7374 7375 params = Utils.copyProperties(_params, { send_to: conversion_id }); 7376 7377 if("conversion" === event_name) { 7378 delete params.items; 7379 delete params.ecomm_pagetype; 7380 delete params.ecomm_prodid; 7381 delete params.ecomm_totalvalue; 7382 } 7383 7384 if (options.debug) { 7385 console.log('[Google Ads #' + conversion_id + '] ' + event_name, params); 7386 } 7387 Utils.gtagPush('event', event_name, params); 7388 var customEvent = new CustomEvent('gtag_event_sent', { 7389 detail: { 7390 eventName: event_name, 7391 params: params, 7392 pixel_ids: conversion_id 7393 } 7394 }); 7395 window.dispatchEvent(customEvent); 7396 7397 }; 7398 7399 7400 if ( conversion_labels.length > 0 ) { 7401 ids = conversion_labels; 7402 if ( !isTrackEventForGA.includes( name ) ) { 7403 _fireEvent( ids, name ); 7404 } 7405 } else { 7406 var conversion_event_name = data.e_id; 7407 switch ( conversion_event_name ) { 7408 case "woo_add_to_cart_on_cart_page": 7409 case "woo_add_to_cart_on_checkout_page": 7410 case "woo_add_to_cart_on_button_click": 7411 conversion_event_name = 'woo_add_to_cart'; 7412 break; 7413 case "edd_add_to_cart_on_cart_page": 7414 case "edd_add_to_cart_on_checkout_page": 7415 case "edd_add_to_cart_on_button_click": 7416 conversion_event_name = 'edd_add_to_cart'; 7417 break; 7418 case "automatic_event_adsense": 7419 case "automatic_event_comment": 7420 case "automatic_event_download": 7421 case "automatic_event_email_link": 7422 case "automatic_event_form": 7423 case "automatic_event_internal_link": 7424 case "automatic_event_outbound_link": 7425 case "automatic_event_scroll": 7426 case "automatic_event_tel_link": 7427 case "automatic_event_time_on_page": 7428 case "automatic_event_video": 7429 conversion_event_name = 'automatic_event'; 7430 break; 7431 } 7432 7433 if (ids.length) { 7434 if (conversion_event_name !== 'automatic_event' && options.hasOwnProperty('google_ads')) { 7435 // In the same way, we determine the correct name for the settings key. 7436 const trackEventName = conversion_event_name === 'woo_view_item_list' 7437 ? 'woo_view_category' 7438 : conversion_event_name; 7439 7440 const conversionTrack = options.google_ads[trackEventName + '_conversion_track']; 7441 7442 if (conversionTrack === 'conversion') { 7443 _fireEvent(ids, "conversion"); 7444 ids = conversionIds; 7445 } 7446 } 7447 } else { 7448 ids = conversionIds; 7449 } 7450
7451 if ( !isTrackEventForGA.includes( name ) ) { 7452 _fireEvent( ids, name ); 7453 } 7454 } 7455 } 7456 7457 function normalizeEventName(eventName) { 7458 7459 var matches = { 7460 ViewContent: 'view_item', 7461 AddToCart: 'add_to_cart', 7462 AddToWishList: 'add_to_wishlist', 7463 InitiateCheckout: 'begin_checkout', 7464 Purchase: 'purchase', 7465 Lead: 'generate_lead', 7466 CompleteRegistration: 'sign_up', 7467 AddPaymentInfo: 'set_checkout_option' 7468 }; 7469 7470 return matches.hasOwnProperty(eventName) ? matches[eventName] : eventName; 7471 7472 } 7473 7474 /** 7475 * Public API 7476 */ 7477 return { 7478 tag: function() { 7479 return "google_ads"; 7480 }, 7481 isEnabled: function () { 7482 return options.hasOwnProperty('google_ads'); 7483 }, 7484 getHidePixel: function(){ 7485 if(this.isEnabled() && options.google_ads.hasOwnProperty('hide_pixels')) 7486 { 7487 return options.google_ads.hide_pixels; 7488 } 7489 return []; 7490 }, 7491 disable: function () { 7492 initialized = false; 7493 }, 7494 7495 updateEnhancedConversionData : function () { 7496 7497 if(options.hasOwnProperty("tracking_analytics") && options.tracking_analytics.hasOwnProperty("userDataEnable") && options.tracking_analytics.userDataEnable){ 7498 var advanced = Utils.getAdvancedMergeFormData() 7499 Utils.gtagPush('set', 'user_data', advanced); 7500 } 7501 }, 7502 7503 loadPixel: function () { 7504 7505 if (initialized || !this.isEnabled() || !Utils.consentGiven('google_ads')) { 7506 return; 7507 } 7508 var ids = options.google_ads.conversion_ids.filter(function (pixelId) { 7509 return !Utils.hideMatchingPixel(pixelId, 'google_ads'); 7510 }); 7511 loadTags.push(...ids); 7512 if(!isAdsLoad && options.google_ads.conversion_ids.length > 0){ 7513 for (var i = 0; i < options.google_ads.conversion_ids.length; i++) { 7514 var trackingId = options.google_ads.conversion_ids[i]; 7515 if (!Utils.hideMatchingPixel(trackingId, 'google_ads')) { 7516 Utils.loadGoogleTag(trackingId); 7517 break; 7518 } 7519 } 7520 7521 // configure conversion ids 7522 ids.forEach(function (conversion_id,index) { 7523 7524 let config = {}; 7525 7526 if(options.google_ads.enhanced_conversion.includes("index_"+index)) { 7527 isAllowEnhancedConversions = true; 7528 config.allow_enhanced_conversions = true 7529 } 7530 7531 // Cross-Domain tracking 7532 if (options.google_ads.crossDomainEnabled) { 7533 config.linker = { 7534 accept_incoming: options.google_ads.crossDomainAcceptIncoming, 7535 domains: options.google_ads.crossDomainDomains 7536 }; 7537 } 7538 7539 Utils.gtagPush('config', conversion_id, config ); 7540 7541 Utils.registerGadsPhoneConversion( conversion_id ); 7542 7543 if(options.hasOwnProperty("tracking_analytics") && options.tracking_analytics.hasOwnProperty("userDataEnable") && options.tracking_analytics.userDataEnableuserDataEnable){ 7544 var advanced = Utils.getAdvancedMergeFormData() 7545 Utils.gtagPush('set', 'user_data', advanced); 7546 } 7547 7548 }); 7549 isAdsLoad = true; 7550 } 7551 7552 initialized = true; 7553 7554 Utils.fireStaticEvents('google_ads'); 7555 7556 }, 7557 7558 fireEvent: function (name, data) { 7559 7560 if (!initialized || !this.isEnabled()) { 7561 return false;
7562 } 7563 7564 data.delay = data.delay || 0; 7565 data.params = data.params || {}; 7566 data.ids = data.ids || []; 7567 data.conversion_labels = data.conversion_labels || []; 7568 7569 if (data.delay === 0) { 7570 fireEvent(name, data); 7571 } else { 7572 setTimeout(function (name, data) { 7573 fireEvent(name, data); 7574 }, data.delay * 1000, name, data); 7575 } 7576 7577 return true; 7578 7579 }, 7580 7581 onAdSenseEvent: function (event) { 7582 this.fireEvent(event.name, event); 7583 }, 7584 7585 onClickEvent: function (action, params) { 7586 //disabled 7587 }, 7588 7589 onWatchVideo: function (event) { 7590 this.fireEvent(event.name, event); 7591 }, 7592 7593 onCommentEvent: function (event) { 7594 this.fireEvent(event.name, event); 7595 }, 7596 7597 onFormEvent: function (event) { 7598 this.fireEvent(event.name, event); 7599 }, 7600 7601 onDownloadEvent: function (event) { 7602 this.fireEvent(event.name, event); 7603 }, 7604 7605 7606 7607 7608 onWooAddToCartOnButtonEvent: function (product_id) { 7609 7610 if(!options.dynamicEvents.woo_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 7611 return; 7612 7613 if (window.pysWooProductData.hasOwnProperty(product_id)) { 7614 if (window.pysWooProductData[product_id].hasOwnProperty('google_ads')) { 7615 7616 var event = Utils.clone(options.dynamicEvents.woo_add_to_cart_on_button_click[this.tag()]) 7617 Utils.copyProperties(window.pysWooProductData[product_id]['google_ads']['params'].params, event.params) 7618 event["ids"] = window.pysWooProductData[product_id]['google_ads']['ids'] 7619 event["conversion_labels"] = window.pysWooProductData[product_id]['google_ads']['conversion_labels'] 7620 this.fireEvent(event.name, event); 7621 } 7622 } 7623 7624 }, 7625 7626 onWooAddToCartOnSingleEvent: function (product_id, qty, product_type, is_external, $form) { 7627 7628 window.pysWooProductData = window.pysWooProductData || []; 7629 if(!options.dynamicEvents.woo_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 7630 return; 7631 var event = Utils.clone(options.dynamicEvents.woo_add_to_cart_on_button_click[this.tag()]); 7632 7633 if (product_type === Utils.PRODUCT_VARIABLE && !options.google_ads.wooVariableAsSimple) { 7634 product_id = parseInt($form.find('input[name="variation_id"]').val()); 7635 } 7636 7637 if (window.pysWooProductData.hasOwnProperty(product_id)) { 7638 if (window.pysWooProductData[product_id].hasOwnProperty('google_ads')) { 7639 7640 Utils.copyProperties(window.pysWooProductData[product_id]['google_ads']["params"], event.params); 7641 event["ids"] = window.pysWooProductData[product_id]['google_ads']['ids'] 7642 event["conversion_labels"] = window.pysWooProductData[product_id]['google_ads']['conversion_labels'] 7643 7644 var groupValue = 0; 7645 if(product_type === Utils.PRODUCT_GROUPED ) { 7646 $form.find(".woocommerce-grouped-product-list .qty").each(function(index){ 7647 var childId = $(this).attr('name').replaceAll("quantity[","").replaceAll("]",""); 7648 var quantity = parseInt($(this).val()); 7649 if(isNaN(quantity)) { 7650 quantity = 0; 7651 } 7652 7653 var childItem = window.pysWooProductData[product_id]['google_ads'].grouped[childId]; 7654 7655 if(options.woo.addToCartOnButtonValueEnabled && 7656 options.woo.addToCartOnButtonValueOption !== 'global') { 7657
7658 event.params.items.forEach(function(el,index,array) { 7659 if(el.id == childItem.content_id) { 7660 if(quantity > 0){ 7661 el.quantity = quantity; 7662 el.price = childItem.price; 7663 } else { 7664 array.splice(index, 1); 7665 } 7666 } 7667 }); 7668 } 7669 groupValue += childItem.price * quantity; 7670 }); 7671 if(groupValue == 0) return; 7672 event.params.value = groupValue; 7673 } else { 7674 // update items qty param 7675 event.params.items[0].quantity = qty; 7676 } 7677 7678 7679 7680 // maybe customize value option 7681 if (options.woo.addToCartOnButtonValueEnabled && 7682 options.woo.addToCartOnButtonValueOption !== 'global' && 7683 product_type !== Utils.PRODUCT_GROUPED) { 7684 event.params.value = event.params.value * qty; 7685 } 7686 7687 7688 7689 var eventName = is_external ? options.woo.affiliateEventName : event.name; 7690 eventName = normalizeEventName(eventName); 7691 7692 this.fireEvent(eventName, event); 7693 7694 } 7695 } 7696 7697 }, 7698 7699 onWooRemoveFromCartEvent: function (event) { 7700 this.fireEvent(event.name, event); 7701 }, 7702 7703 onWooAffiliateEvent: function (product_id) { 7704 if(!options.dynamicEvents.woo_affiliate.hasOwnProperty(this.tag())) 7705 return; 7706 var event = options.dynamicEvents.woo_affiliate[this.tag()]; 7707 7708 if (window.pysWooProductData.hasOwnProperty(product_id)) { 7709 if (window.pysWooProductData[product_id].hasOwnProperty('google_ads')) { 7710 7711 event = Utils.clone(event) 7712 Utils.copyProperties(window.pysWooProductData[product_id][this.tag()], event.params) 7713 this.fireEvent(normalizeEventName(options.woo.affiliateEventName), event); 7714 7715 } 7716 } 7717 7718 }, 7719 7720 onWooPayPalEvent: function (event) { 7721 this.fireEvent(event.name, event); 7722 }, 7723 7724 onEddAddToCartOnButtonEvent: function (download_id, price_index, qty) { 7725 if(!options.dynamicEvents.edd_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 7726 return; 7727 var event = options.dynamicEvents.edd_add_to_cart_on_button_click[this.tag()]; 7728 7729 7730 if (window.pysEddProductData && window.pysEddProductData.hasOwnProperty(download_id)) { 7731 7732 var index; 7733 7734 if (price_index) { 7735 index = download_id + '_' + price_index; 7736 } else { 7737 index = download_id; 7738 } 7739 7740 if (window.pysEddProductData[download_id].hasOwnProperty(index)) { 7741 if (window.pysEddProductData[download_id][index].hasOwnProperty('google_ads')) { 7742 7743 event = Utils.clone(event) 7744 Utils.copyProperties(window.pysEddProductData[download_id][index]['google_ads']['params'], event.params); 7745 event.ids = window.pysEddProductData[download_id][index]['google_ads']['ids'] 7746 // update items qty param 7747 //params.items[0].quantity = qty; 7748 7749 this.fireEvent(event.name, event); 7750 7751 } 7752 } 7753 7754 } 7755 7756 }, 7757 7758 onEddRemoveFromCartEvent: function (event) { 7759 this.fireEvent(event.name, event); 7760 }, 7761 onPageScroll: function (event) { 7762 if (initialized && this.isEnabled()) { 7763 this.fireEvent(event.name, event); 7764 } 7765 }, 7766 onTime: function (event) { 7767 if (initialized && this.isEnabled()) { 7768 this.fireEvent(event.name, event); 7769 } 7770 }, 7771 }; 7772 7773 }(options); 7774 7775 var GTM = function (options) { 7776 7777 var initialized = false;
7778 7779 var datalayer_name = 'dataLayer'; 7780 /* 7781 * @param name 7782 * @param data 7783 */ 7784 function fireEvent(name, event) { 7785 if(typeof window.pys_event_data_filter === "function" && window.pys_disable_event_filter(name,'gtm')) { 7786 return; 7787 } 7788 7789 7790 var eventParams = event.params; 7791 var data = event.params; 7792 var valuesArray = Object.values(event.trackingIds); 7793 var ids = valuesArray.filter(function (pixelId) { 7794 return !Utils.hideMatchingPixel(pixelId, 'gtm'); 7795 }) 7796 Utils.copyProperties(Utils.getRequestParams(), eventParams); 7797 var _fireEvent = function (tracking_id,name,params, event=null) { 7798 7799 var eventData = {}; 7800 var ContainerCodeHasTag = !options.gtm.gtm_just_data_layer && tracking_id.length > 0; 7801 if(ContainerCodeHasTag) { 7802 params['send_to'] = tracking_id; 7803 } 7804 else if(!options.gtm.gtm_just_data_layer){ 7805 return 7806 } 7807 7808 if (params.hasOwnProperty('ecommerce')) { 7809 eventData.ecommerce = params.ecommerce; 7810 delete params.ecommerce; 7811 } 7812 7813 var automatedParams = { ...params }; 7814 [params['manualName'], 'manualName', 'triggerType'].forEach(key => delete automatedParams[key]); 7815 if(event && (!event.hasOwnProperty('hasAutoParam') || (event.hasOwnProperty('hasAutoParam') && event.hasAutoParam))) { 7816 eventData.automatedParameters = automatedParams; 7817 } 7818 7819// Move custom parameters to eventData 7820 7821 if (params.hasOwnProperty(params['manualName'])) { 7822 eventData[params['manualName']] = params[params['manualName']]; 7823 delete params[params['manualName']]; 7824 } 7825 7826 ['manualName','triggerType'].forEach(key => { 7827 if (params.hasOwnProperty(key)) { 7828 eventData[key] = params[key]; 7829 delete params[key]; 7830 } 7831 }); 7832 7833 eventData.manualDataLayer = Utils.resolveGtmDataLayerName(); 7834 7835 eventData.event = name; 7836 7837 if (options.debug) { 7838 if (ContainerCodeHasTag){ 7839 console.log('[Google GTM #' + tracking_id + '] ' + name, eventData); 7840 } 7841 else { 7842 console.log('[Google GTM push to "'+datalayer_name+'"] ' + name, eventData); 7843 } 7844 } 7845 window[datalayer_name].push(eventData); 7846 7847 var customEvent = new CustomEvent('gtm_event_sent', { 7848 detail: { 7849 eventName: name, 7850 params: eventData, 7851 pixel_ids: tracking_id 7852 } 7853 }); 7854 window.dispatchEvent(customEvent); 7855 7856 }; 7857 7858 var copyParams = Utils.copyProperties(eventParams, {}); // copy params because mapParamsToGTM can modify it 7859 7860 var params = mapParamsToGTM(ids,name,copyParams) 7861 7862 params.event_id = Utils.generateUniqueId(event); 7863 7864 _fireEvent(ids, name, params, event); 7865 } 7866 7867 function normalizeEventName(eventName) { 7868 7869 var matches = { 7870 ViewContent: 'view_item', 7871 AddToCart: 'add_to_cart', 7872 AddToWishList: 'add_to_wishlist', 7873 InitiateCheckout: 'begin_checkout', 7874 Purchase: 'purchase', 7875 Lead: 'generate_lead', 7876 CompleteRegistration: 'sign_up', 7877 AddPaymentInfo: 'set_checkout_option' 7878 }; 7879 7880 return matches.hasOwnProperty(eventName) ? matches[eventName] : eventName; 7881 7882 } 7883 7884 function mapParamsToGTM(tag,name,param) { 7885 var hasGTM = false; 7886 7887 // end 7888 if (Array.isArray(tag)) { 7889 hasGTM = tag.some(function (element) { 7890 return isGTM(element); 7891 }); 7892 } else if(isGTM(tag)) { 7893 hasGTM = true; 7894 } 7895 if(hasGTM) { 7896 delete param.event_category; 7897 delete param.event_label; 7898 7899 delete param.analytics_storage; 7900 delete param.ad_storage; 7901 delete param.ad_user_data; 7902 delete param.ad_personalization; 7903 7904 if(name === 'search') { 7905 param['search'] = param.search_term; 7906 delete param.search_term; 7907 delete param.dynx_itemid; 7908 delete param.dynx_pagetype; 7909 delete param.dynx_totalvalue; 7910 } 7911 } 7912 return param; 7913 } 7914 7915 function isGTM(tag) { 7916 return tag.indexOf('GTM') === 0; 7917 } 7918 /** 7919 * Public API 7920 */ 7921 return { 7922 tag: function() { 7923 return "gtm"; 7924 }, 7925 isEnabled: function () { 7926 return options.hasOwnProperty('gtm'); 7927 }, 7928 getHidePixel: function(){ 7929 if(this.isEnabled() && options.gtm.hasOwnProperty('hide_pixels')) 7930 { 7931 return options.gtm.hide_pixels; 7932 } 7933 return []; 7934 }, 7935 disable: function () { 7936 initialized = false;
7937 }, 7938 updateEnhancedConversionData : function () { 7939 if (!initialized || !this.isEnabled()) { 7940 return; 7941 } 7942 if(options.hasOwnProperty("tracking_analytics") && options.tracking_analytics.hasOwnProperty("userDataEnable") && options.tracking_analytics.userDataEnable) { 7943 var advanced = Utils.getAdvancedMergeFormData(true); 7944 if (Object.keys(advanced).length > 0) { 7945 window[datalayer_name].push({'user_data' : advanced}); 7946 } 7947 } 7948 }, 7949 loadPixel: function () { 7950 7951 if (initialized || !this.isEnabled() || !Utils.consentGiven('analytics')) { 7952 return; 7953 } 7954 datalayer_name = Utils.resolveGtmDataLayerName(); 7955 7956 for (var i = 0; i < options.gtm.trackingIds.length; i++) { 7957 var trackingId = options.gtm.trackingIds[i]; 7958 if (!Utils.hideMatchingPixel(trackingId, 'gtm') && !options.gtm.gtm_just_data_layer) { 7959 console.log('[PYS] Google Tag Manager container code loaded'); 7960 Utils.loadGTMScript(trackingId); 7961 break; 7962 } 7963 } 7964 7965 if(options.gtm.gtm_just_data_layer) { 7966 console.warn && console.warn("[PYS] Google Tag Manager container code placement set to OFF !!!"); 7967 console.warn && console.warn("[PYS] Data layer codes are active but GTM container must be loaded using custom coding !!!"); 7968 // Send the consent state even when GTM script loading is handled 7969 // externally: gtm_just_data_layer with tracking IDs configured skips 7970 // loadGTMScript() entirely. When trackingIds is empty, loadGTMScript() 7971 // below runs too and would reach this again â pushGTMConsentState() 7972 // is idempotent per page load, so the container still sees one command. 7973 Utils.pushGTMConsentState( datalayer_name ); 7974 if(options.gtm.trackingIds.length == 0){ 7975 Utils.loadGTMScript(); 7976 } 7977 } 7978 7979 if(options.hasOwnProperty("tracking_analytics") && options.tracking_analytics.hasOwnProperty("userDataEnable") && options.tracking_analytics.userDataEnable){ 7980 var advanced = Utils.getAdvancedMergeFormData(true); 7981 if(Object.keys(advanced).length > 0){ 7982 window[datalayer_name].push({'user_data' : advanced}); 7983 } 7984 } 7985 7986 7987 var config = {}; 7988 7989 if(options.user_id && options.user_id != 0) { 7990 config.user_id = options.user_id; 7991 } 7992 7993 // Cross-Domain tracking 7994 7995 var ids = options.gtm.trackingIds.filter(function (pixelId) { 7996 return !Utils.hideMatchingPixel(pixelId, 'gtm'); 7997 }); 7998 7999 8000 initialized = true; 8001 8002 Utils.fireStaticEvents('gtm'); 8003 }, 8004 8005 fireEvent: function (name, data) { 8006 8007 if (!initialized || !this.isEnabled()) { 8008 return false; 8009 } 8010 8011 data.delay = data.delay || 0; 8012 data.params = data.params || {}; 8013 8014 if (data.delay === 0) { 8015 8016 fireEvent(name, data); 8017 8018 } else { 8019 8020 setTimeout(function (name, params) { 8021 fireEvent(name, params); 8022 }, data.delay * 1000, name, data); 8023 8024 } 8025 8026 return true; 8027 8028 }, 8029 8030 onAdSenseEvent: function (event) { 8031 this.fireEvent(event.name, event); 8032 }, 8033 8034 onClickEvent: function (event) { 8035 this.fireEvent(event.name, event); 8036 }, 8037 8038 onWatchVideo: function (event) { 8039 if(!event.hasOwnProperty("youtube_disabled") 8040 || !event.youtube_disabled 8041 || event.params.video_type !== "youtube") { 8042 this.fireEvent(event.name, event); 8043 } 8044 }, 8045 8046 onCommentEvent: function (event) { 8047 8048 this.fireEvent(event.name, event); 8049 8050 }, 8051 8052 onFormEvent: function (event) { 8053 8054 this.fireEvent(event.name, event); 8055 8056 }, 8057 8058 onDownloadEvent: function (event) { 8059 8060 this.fireEvent(event.name, event); 8061 8062 }, 8063 8064 onWooAddToCartOnButtonEvent: function (product_id, prod_info = null) { 8065 if(!options.dynamicEvents.woo_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 8066 return; 8067 if (window.pysWooProductData.hasOwnProperty(product_id)) { 8068 if (window.pysWooProductData[product_id].hasOwnProperty('gtm')) { 8069 var event = Utils.clone(options.dynamicEvents.woo_add_to_cart_on_button_click[this.tag()]); 8070 Utils.copyProperties(window.pysWooProductData[product_id]['gtm'].params, event.params) 8071 event.trackingIds = window.pysWooProductData[product_id]['gtm']['trackingIds']; 8072 8073 8074 if(prod_info) 8075 { 8076 item = event.params.hasOwnProperty('ecommerce') ? event.params.ecommerce.items[0] : event.params.items[0]; 8077 8078 if(prod_info['pys_list_name_productlist_id']) 8079 { 8080 item['item_list_id'] = prod_info['pys_list_name_productlist_id'] 8081 } 8082 if(prod_info['pys_list_name_productlist_name']) 8083 { 8084 item['item_list_name'] =prod_info['pys_list_name_productlist_name'] 8085 } 8086 } 8087 this.fireEvent(event.name, event); 8088 } 8089 } 8090 8091 }, 8092 8093 onWooAddToCartOnSingleEvent: function (product_id, qty, product_type, is_external, $form, prod_info) { 8094 window.pysWooProductData = window.pysWooProductData || []; 8095 8096 if(!options.dynamicEvents.woo_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 8097 return; 8098 var event = Utils.clone(options.dynamicEvents.woo_add_to_cart_on_button_click[this.tag()]); 8099 8100 if (product_type === Utils.PRODUCT_VARIABLE && !options.gtm.wooVariableAsSimple) { 8101 product_id = parseInt($form.find('input[name="variation_id"]').val()); 8102 } 8103 8104 if (window.pysWooProductData.hasOwnProperty(product_id)) { 8105 if (window.pysWooProductData[product_id].hasOwnProperty('gtm')) { 8106 8107 Utils.copyProperties(window.pysWooProductData[product_id]['gtm'].params, event.params); 8108 8109 params = event.params.hasOwnProperty('ecommerce') ? event.params.ecommerce : event.params; 8110 8111 if(product_type === Utils.PRODUCT_GROUPED ) { 8112 var groupValue = 0; 8113 $form.find(".woocommerce-grouped-product-list .qty").each(function(index){ 8114 var childId = $(this).attr('name').replaceAll("quantity[","").replaceAll("]",""); 8115 var quantity = parseInt($(this).val()); 8116 if(isNaN(quantity)) { 8117 quantity = 0; 8118 } 8119 var childItem = window.pysWooProductData[product_id]['gtm'].grouped[childId]; 8120
8121 params.items.forEach(function(el,index,array) { 8122 if(el.id == childItem.content_id) { 8123 if(quantity > 0){ 8124 el.quantity = quantity; 8125 el.price = childItem.price; 8126 } else { 8127 array.splice(index, 1); 8128 } 8129 } 8130 }); 8131 groupValue += childItem.price * quantity; 8132 }); 8133 8134 8135 8136 if(options.woo.addToCartOnButtonValueEnabled && 8137 options.woo.addToCartOnButtonValueOption !== 'global' && 8138 params.hasOwnProperty('value')) { 8139 params.value = groupValue; 8140 } 8141 8142 if(groupValue == 0) return; // skip if no items selected 8143 } else { 8144 // update items qty param 8145 params.items[0].quantity = qty; 8146 } 8147 8148 // maybe customize value option 8149 if (options.woo.addToCartOnButtonValueEnabled && 8150 options.woo.addToCartOnButtonValueOption !== 'global' && 8151 product_type !== Utils.PRODUCT_GROUPED) 8152 { 8153 if(params.hasOwnProperty('value')) { 8154 params.value = params.items[0].price * qty; 8155 } 8156 } 8157 8158 if(prod_info) 8159 { 8160 if(prod_info['pys_list_name_productlist_id']) 8161 { 8162 params.items[0]['item_list_id'] = prod_info['pys_list_name_productlist_id'] 8163 } 8164 if(prod_info['pys_list_name_productlist_name']) 8165 { 8166 params.items[0]['item_list_name'] =prod_info['pys_list_name_productlist_name'] 8167 } 8168 } 8169 8170 var eventName = is_external ? options.woo.affiliateEventName : event.name; 8171 eventName = normalizeEventName(eventName); 8172 8173 this.fireEvent(eventName, event); 8174 8175 } 8176 } 8177 8178 }, 8179 8180 onWooCheckoutProgressStep: function (event) { 8181 this.fireEvent(event.name, event); 8182 }, 8183 8184 onWooSelectContent: function (event) { 8185 const select_prod_list = {}; 8186 8187 if (event.params.items[0].item_list_name !== undefined) { 8188 select_prod_list.list_name = event.params.items[0].item_list_name; 8189 } 8190 8191 if (event.params.items[0].item_list_id !== undefined) { 8192 select_prod_list.list_id = event.params.items[0].item_list_id; 8193 } 8194 const url = new URL(window.location.href); 8195 select_prod_list.url = url.origin + url.pathname; 8196 Cookies.set('select_prod_list', JSON.stringify(select_prod_list), { expires: 1,path: '/' }); 8197 this.fireEvent(event.name, event); 8198 }, 8199 8200 onWooRemoveFromCartEvent: function (event) { 8201 this.fireEvent(event.name, event); 8202 }, 8203 8204 onWooAffiliateEvent: function (product_id) { 8205 if(!options.dynamicEvents.woo_affiliate.hasOwnProperty(this.tag())) 8206 return; 8207 var event = options.dynamicEvents.woo_affiliate[this.tag()]; 8208 8209 if (window.pysWooProductData.hasOwnProperty(product_id)) { 8210 if (window.pysWooProductData[product_id].hasOwnProperty('gtm')) { 8211 8212 event = Utils.clone(event ); 8213 Utils.copyProperties(window.pysWooProductData[product_id][this.tag()], event.params) 8214 this.fireEvent(normalizeEventName(options.woo.affiliateEventName), event); 8215 8216 } 8217 } 8218 8219 }, 8220 8221 onWooPayPalEvent: function (event) { 8222 this.fireEvent(event.name, event); 8223 }, 8224 8225 onEddAddToCartOnButtonEvent: function (download_id, price_index, qty) { 8226 if(!options.dynamicEvents.edd_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 8227 return; 8228 var event = Utils.clone(options.dynamicEvents.edd_add_to_cart_on_button_click[this.tag()]); 8229 8230 8231 if (window.pysEddProductData && window.pysEddProductData.hasOwnProperty(download_id)) { 8232 8233 var index; 8234 8235 if (price_index) { 8236 index = download_id + '_' + price_index; 8237 } else { 8238 index = download_id; 8239 } 8240 8241 if (window.pysEddProductData[download_id].hasOwnProperty(index)) { 8242 if (window.pysEddProductData[download_id][index].hasOwnProperty('gtm')) { 8243 8244 Utils.copyProperties(window.pysEddProductData[download_id][index]['gtm'].params, event.params); 8245 item = event.params.hasOwnProperty('ecommerce') ? event.params.ecommerce.items[0] : event.params.items[0]; 8246 // update items qty param 8247 item.quantity = qty; 8248 8249 this.fireEvent(event.name,event); 8250 8251 } 8252 } 8253 8254 } 8255 8256 }, 8257 8258 onEddRemoveFromCartEvent: function (event) { 8259 this.fireEvent(event.name, event); 8260 }, 8261 8262 onPageScroll: function (event) { 8263 if (initialized && this.isEnabled()) { 8264 this.fireEvent(event.name, event); 8265 } 8266 }, 8267 onTime: function (event) { 8268 if (initialized && this.isEnabled()) { 8269 this.fireEvent(event.name, event); 8270 } 8271 }, 8272 }; 8273 8274 }(options); 8275 8276 var getPixelBySlag = function getPixelBySlag(slug) { 8277 switch (slug) { 8278 case "facebook": return window.pys.Facebook; 8279 case "ga": return window.pys.Analytics; 8280 case "gtm": return window.pys.GTM; 8281 case "google_ads": return window.pys.GAds; 8282 case "bing": return window.pys.Bing; 8283 case "pinterest": return window.pys.Pinterest; 8284 case "tiktok": return window.pys.TikTok; 8285 case "openai": return window.pys.OpenAI; 8286 case "reddit": return window.pys.Reddit; 8287 } 8288 } 8289
8290 window.pys = window.pys || {}; 8291 window.pys.Facebook = Facebook; 8292 window.pys.Analytics = Analytics; 8293 window.pys.GAds = GAds; 8294 window.pys.GTM = GTM; 8295 window.pys.Utils = Utils; 8296 window.pys.TikTok = TikTok; 8297 window.pys.OpenAI = OpenAI; 8298 window.pys.getPixelBySlag = getPixelBySlag; 8299 8300 // Allow external scripts (e.g. generatePixelCode fragments) to refresh 8301 // the uniqueId cache for a given event key, so that each new add_to_cart 8302 // action gets a fresh shared ID across all pixels. 8303 window.pys.setEventUniqueId = function(idKey, newId) { 8304 uniqueId[idKey] = newId; 8305 }; 8306 8307 window.pys.fireAjaxTriggerEvent = function(slug, eventData) { 8308 var pixel = getPixelBySlag(slug); 8309 if (!pixel) return; 8310 8311 // Extract the eventId the same way PHP does (custom_event_post_id takes priority over e_id). 8312 var eventId = (eventData.custom_event_post_id || eventData.e_id) || null; 8313 // Mirror fireTriggerEvent: respect "once per page" frequency setting. 8314 if (eventId && options.triggerFrequency && options.triggerFrequency[eventId] === 'once') { 8315 if (pysEventFiredOnce[eventId]) return; 8316 pysEventFiredOnce[eventId] = true; 8317 } 8318 8319 // Mirror fireTriggerEvent: respect the time-window setting (data already in eventData). 8320 if (!Utils.isEventInTimeWindow(eventData.name, eventData, 'ajax_' + slug)) return; 8321 8322 pixel.fireEvent(eventData.name, eventData); 8323 }; 8324 8325 // Export getPixelBySlag globally for backward compatibility 8326 window.getPixelBySlag = getPixelBySlag; 8327 8328 function pysNormalInit() { 8329 8330 if($("#pys_late_event").length > 0) { 8331 var dirAttr = $("#pys_late_event").attr("dir"); 8332 if (dirAttr) { 8333 try { 8334 var events = JSON.parse(dirAttr); 8335 } catch (e) { 8336 console.warn("Invalid JSON in pys_late_event dir attribute:", e); 8337 } 8338 } else { 8339 console.warn("pys_late_event dir attribute is undefined or empty"); 8340 } 8341 if (events) { 8342 for (var platform in events) { 8343 if (events.hasOwnProperty(platform)) { 8344 var platformEvents = events[platform]; 8345 platformEvents.forEach(function (event) { 8346 var eventData = {}; 8347 eventData[event.e_id] = [event]; 8348 if (options.staticEvents.hasOwnProperty(platform)) { 8349 Object.assign(options.staticEvents[platform], eventData); 8350 } else { 8351 options.staticEvents[platform] = eventData; 8352 } 8353 }); 8354 } 8355 } 8356 } 8357 } 8358 8359 var Pinterest = Utils.setupPinterestObject(); 8360 var Bing = Utils.setupBingObject(); 8361 var Reddit = Utils.setupRedditObject(); 8362 if(options.hasOwnProperty('cookie')) 8363 { 8364 if(options.cookie.externalID_disabled_by_api || options.cookie.disabled_all_cookie) 8365 { 8366 Cookies.remove('pbid', { path: '/',domain: cookieDomain() }) 8367 } 8368 if(options.cookie.disabled_advanced_form_data_cookie || options.cookie.disabled_all_cookie) 8369 { 8370 // Removed twice on purpose: PHP writes this cookie host-only 8371 // until pys_cd is reported (or permanently, on a site whose 8372 // front end never reaches manageCookies()), while this script 8373 // writes/deletes it at cookieDomain(). A host-only cookie and 8374 // a domain-scoped one of the same name are separate jar 8375 // entries, and a delete only matches the scope it names â so 8376 // both scopes are cleared rather than guessed at. 8377 // `domain: ''` is deliberate: js-cookie 2.1.3 treats a falsy 8378 // Domain as "omit the attribute" (host-only delete), so it's 8379 // written explicitly rather than left off. 8380 Cookies.remove('pys_advanced_form_data', { path: '/',domain: cookieDomain() }) 8381 Cookies.remove('pys_advanced_form_data', { path: '/',domain: '' }) 8382 } 8383 8384 if(options.cookie.disabled_landing_page_cookie || !options.enable_lading_page_param || options.cookie.disabled_all_cookie) 8385 { 8386 Cookies.remove('pys_landing_page', { path: '/',domain: cookieDomain() }) 8387 Cookies.remove('last_pys_landing_page', { path: '/',domain: cookieDomain() }) 8388 } 8389 if(options.cookie.disabled_trafficsource_cookie || options.cookie.disabled_all_cookie) 8390 { 8391 Cookies.remove('pysTrafficSource', { path: '/',domain: cookieDomain() }) 8392 Cookies.remove('last_pysTrafficSource', { path: '/',domain: cookieDomain() }) 8393 } 8394 if(options.cookie.disabled_first_visit_cookie || options.cookie.disabled_all_cookie) 8395 { 8396 Cookies.remove('pys_first_visit', { path: '/',domain: cookieDomain() }) 8397 8398 } 8399 if(options.cookie.disabled_utmTerms_cookie || options.cookie.disabled_all_cookie) 8400 { 8401 $.each(Utils.utmTerms, function (index, name) { 8402 Cookies.remove('pys_' + name, { path: '/',domain: cookieDomain() }) 8403 }); 8404 $.each(Utils.utmTerms, function (index, name) { 8405 Cookies.remove('last_pys_' + name, { path: '/',domain: cookieDomain() }) 8406 }); 8407 } 8408 if(options.cookie.disabled_utmId_cookie || options.cookie.disabled_all_cookie) 8409 { 8410 $.each(Utils.utmId,function(index,name) { 8411 Cookies.remove('pys_' + name, { path: '/',domain: cookieDomain() }) 8412 }) 8413 $.each(Utils.utmId,function(index,name) { 8414 Cookies.remove('last_pys_' + name, { path: '/',domain: cookieDomain() }) 8415 }); 8416 } 8417 } 8418 8419 // Exactly one manageCookies() per init, and under a consent 8420 // integration only once consent is on record. The old fall-through 8421 // ran it a second time under CookieYes, racing two pbid requests, and 8422 // ran it before any consent when no CookieYes cookie existed yet. 8423 // external_id itself does not depend on this: the visitor key is 8424 // computed server-side without a cookie. 8425 var cookiesManaged = false;
8426 8427 if (options.gdpr.cookie_law_info_integration_enabled) { 8428 var cli_cookie = Cookies.get('cookieyes-consent') ?? Cookies.get('wt_consent') ?? Cookies.get('viewed_cookie_policy'); 8429 8430 if (typeof cli_cookie !== 'undefined') { 8431 if (cli_cookie === Cookies.get('cookieyes-consent') || cli_cookie === Cookies.get('wt_consent')) { 8432 if (getCookieYes('analytics') === 'yes') { 8433 Utils.manageCookies(); 8434 cookiesManaged = true; 8435 } 8436 } else if (cli_cookie === Cookies.get('viewed_cookie_policy') && cli_cookie == 'yes') { 8437 Utils.manageCookies(); 8438 cookiesManaged = true; 8439 } 8440 } 8441 } 8442 8443 if ( ! cookiesManaged ) { 8444 if ( options.gdpr.consent_magic_integration_enabled && typeof CS_Data !== "undefined" ) { 8445 if ( CS_Data.cs_script_cat.pys == CS_Data.cs_necessary_cat_id || CS_Data.cs_script_cat.pys == 0 ) { 8446 Utils.manageCookies(); 8447 } else if ( Cookies.get( 'cs_enabled_cookie_term' + CS_Data.test_prefix + '_' + CS_Data.cs_script_cat.pys ) == 'yes' ) { 8448 Utils.manageCookies(); 8449 } 8450 } else if ( ! options.gdpr.cookie_law_info_integration_enabled ) { 8451 Utils.manageCookies(); 8452 } 8453 } 8454
8455 Utils.initializeRequestParams(); 8456 Utils.setupGdprCallbacks(); 8457 8458 if ( options.enable_auto_save_advance_matching ) { 8459 let override = options.data_persistency == 'recent_data'; 8460 // Setup Advanced Form Data 8461 if ( options.advance_matching_form.enable_advance_matching_forms ) { 8462 8463 // Handle Email fields 8464 $( document ).on( "blur", "input[type='email']", function () { 8465 // SECURITY: Ignore fields located inside the Authorize.Net payment block 8466 if ( $( this ).closest('.payment_method_anet').length > 0 ) return; 8467 8468 let email = $( this ).val().trim().toLowerCase(); 8469 if ( Utils.validateEmail( email ) ) { 8470 Utils.saveAdvancedFormData( email, null, null, null, override ); 8471 } 8472 } ) 8473 8474 // Handle Telephone fields (This is where Credit Card numbers were being captured) 8475 $( document ).on( "blur", "input[type='tel']", function () { 8476 // SECURITY: Ignore fields located inside the Authorize.Net payment block 8477 if ( $( this ).closest('.payment_method_anet').length > 0 ) return; 8478 8479 let phone = $( this ).val().trim().replace( /\D/g, "" ); 8480 if ( phone.length > 5 ) { 8481 Utils.saveAdvancedFormData( null, phone, null, null, override ); 8482 } 8483 } ) 8484 8485 // Handle Text fields 8486 $( document ).on( "blur", "input[type='text']", function () { 8487 // SECURITY: Ignore fields located inside the Authorize.Net payment block 8488 if ( $( this ).closest('.payment_method_anet').length > 0 ) return; 8489 8490 let name; 8491 if ( $( this ).attr( "name" ) && $( this ).attr( "name" ) != '' ) { 8492 name = $( this ).attr( "name" ).trim() 8493 } 8494 if ( name && options.advance_matching_form.advance_matching_fn_names.includes( name ) ) { 8495 let value = $( this ).val().trim(); 8496 if ( value.length > 0 ) { 8497 Utils.saveAdvancedFormData( null, null, value, null, override ); 8498 } 8499 } 8500 if ( name && options.advance_matching_form.advance_matching_ln_names.includes( name ) ) { 8501 let value = $( this ).val().trim(); 8502 if ( value.length > 0 ) { 8503 Utils.saveAdvancedFormData( null, null, null, value, override ); 8504 } 8505 } 8506 if ( name && options.advance_matching_form.advance_matching_tel_names.includes( name ) ) { 8507 let value = $( this ).val().trim(); 8508 if ( value.length > 0 ) { 8509 Utils.saveAdvancedFormData( null, value, null, null, override ); 8510 } 8511 } 8512 if ( name && options.advance_matching_form.advance_matching_em_names.includes( name ) ) { 8513 let email = $( this ).val().trim().toLowerCase(); 8514 8515 if ( Utils.validateEmail( email ) ) { 8516 Utils.saveAdvancedFormData( email, null, null, null, false ); 8517 } 8518 } 8519 } ) 8520 } 8521 8522 //Setup Advanced Url Data 8523 if ( Object.keys( options.advance_matching_url ).length > 0 && options.advance_matching_url.enable_advance_matching_url ) { 8524 const url_params = new URLSearchParams( window.location.search ); 8525 url_params.forEach( ( value, key ) => { 8526 if ( options.advance_matching_url.advance_matching_fn_names.includes( key ) ) { 8527 Utils.saveAdvancedFormData( null, null, value.trim(), null, override ); 8528 } 8529 8530 if ( options.advance_matching_url.advance_matching_ln_names.includes( key ) ) { 8531 Utils.saveAdvancedFormData( null, null, null, value.trim(), override ); 8532 } 8533 8534 if ( options.advance_matching_url.advance_matching_tel_names.includes( key ) ) { 8535 Utils.saveAdvancedFormData( null, value.trim(), null, null, override ); 8536 } 8537 8538 if ( options.advance_matching_url.advance_matching_em_names.includes( key ) ) { 8539 let email = value.trim().toLowerCase() 8540 if ( Utils.validateEmail( email ) ) { 8541 Utils.saveAdvancedFormData( email, null, null, null, override ); 8542 } 8543 } 8544 } ); 8545 } 8546 } 8547 8548 // setup Click Event 8549 if ( 8550 options.dynamicEvents.hasOwnProperty("automatic_event_internal_link") || 8551 options.dynamicEvents.hasOwnProperty("automatic_event_outbound_link") || 8552 options.dynamicEvents.hasOwnProperty("automatic_event_tel_link") || 8553 options.dynamicEvents.hasOwnProperty("automatic_event_email_link") || 8554 options.dynamicEvents.hasOwnProperty("automatic_event_download") 8555 ) { 8556 8557 $(document).onFirst('click', 'a, button, input[type="button"], input[type="submit"]', function (e) { 8558 8559 var $elem = $(this); 8560 8561 8562 // Download 8563 if(options.dynamicEvents.hasOwnProperty("automatic_event_download")) { 8564 var isFired = false;
8565 if ($elem.is('a')) { 8566 var href = $elem.attr('href'); 8567 if (typeof href !== "string") { 8568 return; 8569 } 8570 href = href.trim(); 8571 var extension = Utils.getLinkExtension(href); 8572 var track_download = false; 8573 8574 if (extension.length > 0) { 8575 8576 if(options.dynamicEvents.hasOwnProperty("automatic_event_download") ) { 8577 var pixels = Object.keys(options.dynamicEvents.automatic_event_download); 8578 for (var i = 0; i < pixels.length; i++) { 8579 var event = Utils.clone(options.dynamicEvents.automatic_event_download[pixels[i]]); 8580 var extensions = event.extensions; 8581 if (extensions.includes(extension)) { 8582 8583 if(pixels[i] == "tiktok" || pixels[i] == "reddit") { 8584 getPixelBySlag(pixels[i]).fireEvent(event.name, event); 8585 } else { 8586 if (options.enable_remove_download_url_param) { 8587 href = stripUrlQuery(href); 8588 } 8589 event.params.download_url = href; 8590 event.params.download_type = extension; 8591 event.params.download_name = Utils.getLinkFilename(href); 8592 getPixelBySlag(pixels[i]).onDownloadEvent(event); 8593 } 8594 8595 isFired = true; 8596 } 8597 } 8598 } 8599 } 8600 } 8601 if(isFired) { // prevent duplicate events on the same element 8602 return; 8603 } 8604 } 8605 8606 8607 if (!e.hasOwnProperty('originalEvent')) { 8608 return; 8609 } 8610 if($elem.hasClass("add_to_cart_button") || 8611 $elem.hasClass("single_add_to_cart_button") ) { // add_to_cart_button fire in woo 8612 return; 8613 } 8614 8615 if(options.dynamicEvents.hasOwnProperty("wcf_add_to_cart_on_next_step_click") 8616 && $elem.hasClass("wcf-next-step-link")) { 8617 return;// add_to_cart_button fire in woo cf 8618 } 8619 if(options.dynamicEvents.hasOwnProperty("wcf_add_to_cart_on_bump_click") 8620 && $elem.hasClass("wcf-bump-order-cb")) { 8621 return;// add_to_cart_button fire in woo cf 8622 } 8623 8624 if($elem.hasClass("remove_from_cart_button") ) { // cancel remove from cart 8625 return; 8626 } 8627 if($elem.hasClass("remove") ) { // cancel remove from cart 8628 if($elem.parents('.cart_item').length || $elem.parents('.mini_cart_item').length) 8629 return; 8630 } 8631 if($elem.attr("name") == "update_cart" || $elem.attr("name") == "apply_coupon") { // cancel update cart or coupon button 8632 return; 8633 } 8634 8635 8636 if ($elem.hasClass('pys_block')) { 8637 return; // avoiding fake double clicks from Affiliate event 8638 } 8639 var text = ""; 8640 var target_url = ""; 8641 var linkType = "Internal Click"; 8642 8643 if ($elem.is('a')) { 8644 var href = $elem.attr('href'); 8645 8646 // fixes #112 8647 if (typeof href !== "string") { 8648 return; 8649 } 8650 href = href.trim(); 8651 8652 text = $elem.contents().filter(function() { 8653 return this.nodeType === 3; // Filter text nodes only 8654 }).text().trim(); 8655 if(options.enable_remove_target_url_param) { 8656 target_url = stripUrlQuery(href); 8657 } else { 8658 target_url = href 8659 } 8660 8661 8662 //Email Event 8663 if ( href.startsWith( 'mailto:' ) ) { 8664 if ( options.triggerEventTypes.hasOwnProperty( "email_link" ) ) { 8665 return; 8666 } else { 8667 setupEmailLinks(); 8668 } 8669 8670 return; // not fire next 8671 } 8672 8673 // Phone 8674 if (href.startsWith('tel:')) { 8675 if(options.dynamicEvents.hasOwnProperty("automatic_event_tel_link")) { 8676 var pixels = Object.keys(options.dynamicEvents.automatic_event_tel_link); 8677 for(var i = 0;i<pixels.length;i++) { 8678 var event = Utils.clone(options.dynamicEvents.automatic_event_tel_link[pixels[i]]); 8679 if ( pixels[i] !== 'tiktok' && pixels[i] !== 'reddit') { 8680 Utils.copyProperties(Utils.getRequestParam
8680s(), event.params); 8681 } 8682 getPixelBySlag(pixels[i]).fireEvent(event.name, event); 8683 } 8684 } 8685 return; // not fire next 8686 } 8687 8688 if (href.startsWith('http')) { 8689 // link type 8690 var host = $elem.context != undefined ? $elem.context.host : $elem[0].host; 8691 if (document.location.host != host) { 8692 linkType = 'External Click'; 8693 } 8694 } 8695 } else if ($elem.is('button')) { 8696 if( $elem.hasClass("forminator-button-submit")) { 8697 //disable duplicate events 8698 return; 8699 } 8700 text = $elem.contents().filter(function() { 8701 return this.nodeType === 3; // Filter text nodes only 8702 }).text().trim(); 8703 } else if ($elem.is('input[type="button"]')) { 8704 text = $elem.val(); 8705 } else if ($elem.is('input[type="submit"]')) { 8706 if ( $elem.parents("form.comment-form")) { 8707 //disable duplicate events 8708 return; 8709 } 8710 if ($elem.parents("form")) { 8711 //disable duplicate events 8712 return; 8713 } 8714 text = $elem.val(); 8715 } else { 8716 return; 8717 } 8718 8719 8720 8721 text = Utils.filterEmails(text); 8722 8723 8724 if( linkType === "Internal Click" 8725 && options.dynamicEvents.hasOwnProperty("automatic_event_internal_link") 8726 ) { 8727 var pixels = Object.keys(options.dynamicEvents.automatic_event_internal_link); 8728 8729 for(var i = 0;i<pixels.length;i++) { 8730 var event = Utils.clone(options.dynamicEvents.automatic_event_internal_link[pixels[i]]); 8731 8732 if(pixels[i] !== "tiktok" && pixels[i] !== "reddit") { // TT and reddit don't support custom parameters 8733 event.params["text"] = text; 8734 if(target_url){ 8735 event.params["target_url"] = target_url; 8736 } 8737 Utils.copyProperties(Utils.getRequestParam
8737s(), event.params); 8738 } 8739 8740 8741 getPixelBySlag(pixels[i]).fireEvent(event.name, event); 8742 } 8743 } 8744 8745 if( linkType === "External Click" 8746 && options.dynamicEvents.hasOwnProperty("automatic_event_outbound_link") 8747 ) { 8748 var pixels = Object.keys(options.dynamicEvents.automatic_event_outbound_link); 8749 8750 for(var i = 0;i<pixels.length;i++) { 8751 var event = Utils.clone(options.dynamicEvents.automatic_event_outbound_link[pixels[i]]); 8752 8753 if(pixels[i] !== "tiktok" && pixels[i] !== "reddit") { // TT and reddit don't support custom parameters 8754 event.params["text"] = text; 8755 if(target_url){ 8756 event.params["target_url"] = target_url; 8757 } 8758 Utils.copyProperties(Utils.getRequestParams(), event.params); 8759 } 8760 8761 8762 getPixelBySlag(pixels[i]).fireEvent(event.name, event); 8763 } 8764 } 8765 8766 8767 8768 8769 }); 8770 8771 } 8772 8773 // setup AdSense Event 8774 if (options.dynamicEvents.hasOwnProperty("automatic_event_adsense")) { 8775 8776 var isOverGoogleAd = false; 8777 8778 $(document) 8779 .on('mouseover', 'ins iframe', function () { //class adsbygoogle adsbygoogle-noablate ?? 8780 isOverGoogleAd = true; 8781 }) 8782 .on('mouseout', 'iframe', function () { 8783 isOverGoogleAd = false; 8784 }); 8785 8786 $(window) 8787 .on( "blur",function () { 8788 if (isOverGoogleAd) { 8789 if(options.dynamicEvents.hasOwnProperty("automatic_event_adsense")) { 8790 var pixels = Object.keys(options.dynamicEvents.automatic_event_adsense); 8791 for (var i = 0; i < pixels.length; i++) { 8792 var event = Utils.clone(options.dynamicEvents.automatic_event_adsense[pixels[i]]); 8793 if ( pixels[i] !== 'tiktok' && pixels[i] !== 'reddit') { 8794 Utils.copyProperties(Utils.getRequestParam
8794s(), event.params); 8795 } 8796 getPixelBySlag(pixels[i]).onAdSenseEvent(event); 8797 } 8798 } 8799 8800 $.each(options.triggerEventTypes, function (triggerType, events) { 8801 $.each(events, function (eventId, triggers) { 8802 switch (triggerType) { 8803 case 'ad_sense_click': 8804 Utils.handleTriggerFired(eventId, triggerType); 8805 break; 8806 } 8807 }); 8808 }); 8809 } 8810 }) 8811 .trigger("focus"); 8812 8813 } 8814 8815 //setup adsense for custom events 8816 var dynamicAdsenseEventsTriggers = 0 8817 $.each(options.triggerEventTypes, function (triggerType, events) { 8818 if(triggerType == "ad_sense_click") { 8819 dynamicAdsenseEventsTriggers++; 8820 } 8821 }); 8822 if (dynamicAdsenseEventsTriggers > 0) { 8823 8824 var isOverGoogleAd = false; 8825 8826 $(document) 8827 .on('mouseover', 'ins > ins > iframe', function () { 8828 isOverGoogleAd = true; 8829 }) 8830 .on('mouseout', 'iframe', function () { 8831 isOverGoogleAd = false; 8832 }); 8833 8834 $(window) 8835 .on( "blur",function () { 8836 if (isOverGoogleAd) { 8837 8838 $.each(options.triggerEventTypes, function (triggerType, events) { 8839 $.each(events, function (eventId, triggers) { 8840 switch (triggerType) { 8841 case 'ad_sense_click': 8842 Utils.handleTriggerFired(eventId, triggerType); 8843 break; 8844 } 8845 }); 8846 }); 8847 } 8848 }) 8849 .trigger("focus"); 8850 8851 } 8852 8853 8854 // page scroll event 8855 if (options.dynamicEvents.hasOwnProperty("automatic_event_scroll") 8856 ) { 8857 8858 var singlePageScroll = function () { 8859 8860 8861 var docHeight = $(document).height() - $(window).height(); 8862 var isFired = false; 8863 8864 if (options.dynamicEvents.hasOwnProperty("automatic_event_scroll")) { 8865 var pixels = Object.keys(options.dynamicEvents.automatic_event_scroll); 8866 for(var i = 0;i<pixels.length;i++) { 8867 var event = Utils.clone(options.dynamicEvents.automatic_event_scroll[pixels[i]]); 8868 var scroll = Math.round(docHeight * event.scroll_percent / 100)// convert % to absolute positions 8869 8870 if(scroll < $(window).scrollTop()) { 8871 if ( pixels[i] !== 'tiktok' && pixels[i] !== 'reddit') { 8872 Utils.copyProperties(Utils.getRequestParam
8872s(), event.params); 8873 } 8874 getPixelBySlag(pixels[i]).onPageScroll(event); 8875 isFired = true 8876 } 8877 } 8878 } 8879 if(isFired) { 8880 $(document).off("scroll",singlePageScroll); 8881 } 8882 } 8883 $(document).on("scroll",singlePageScroll); 8884 } 8885 8886 8887 if (options.dynamicEvents.hasOwnProperty("automatic_event_time_on_page")) { 8888 var pixels = Object.keys(options.dynamicEvents.automatic_event_time_on_page); 8889 var time = options.dynamicEvents.automatic_event_time_on_page[pixels[0]].time_on_page; // the same for all pixel 8890 setTimeout(function(){ 8891 for(var i = 0;i<pixels.length;i++) { 8892 var event = Utils.clone(options.dynamicEvents.automatic_event_time_on_page[pixels[i]]); 8893 if ( pixels[i] !== 'tiktok' && pixels[i] !== 'reddit') { 8894 Utils.copyProperties(Utils.getRequestParam
8894s(), event.params); 8895 } 8896 getPixelBySlag(pixels[i]).onTime(event); 8897 } 8898 },time*1000); 8899 } 8900 8901 // setup Dynamic events 8902 8903 $.each(options.triggerEventTypes, function (triggerType, events) { 8904 8905 $.each(events, function (eventId, triggers) { 8906 switch (triggerType) { 8907 case 'url_click': 8908 //@see: Utils.setupURLClickEvents() 8909 break; 8910 8911 case 'css_click': 8912 var cssTriggerData = Array.isArray(triggers) ? { selectors: triggers, click_count: 1, click_time_limit: 0 } : triggers; 8913 var cssSelectors = cssTriggerData.selectors || triggers; 8914 var cssClickCount = cssTriggerData.click_count || 1; 8915 var cssTimeLimit = parseInt(cssTriggerData.click_time_limit, 10) || 0; 8916 Utils.setupCSSClickEvents(eventId, cssSelectors, triggerType, cssClickCount, cssTimeLimit); 8917 break; 8918 8919 case 'css_mouseover': 8920 Utils.setupMouseOverClickEvents(eventId, triggers, triggerType); 8921 break; 8922 8923 case 'scroll_pos': 8924 Utils.setupScrollPosEvents(eventId, triggers, triggerType); 8925 break; 8926 case 'comment': 8927 Utils.setupCommentEvents(eventId, triggers); 8928 break; 8929 case 'video_view': 8930 //@see: Utils.checkYouTubeCompletion() and Utils.addYouTubeEvents() 8931 break; 8932 8933 case 'form_field': 8934 Utils.setupFormFieldEvents(eventId, triggers, triggerType); 8935 break; 8936 8937 case 'copy_element': 8938 Utils.setupCopyElementEvents(eventId, triggers); 8939 break; 8940 8941 case 'video_speed': 8942 Utils.setupVideoSpeedEvents(eventId, triggers); 8943 break; 8944 } 8945 8946 }); 8947 8948 if ( triggerType == "email_link" ) { 8949 Utils.setupEmailLinkEvents( events ) 8950 } 8951 8952 }); 8953 8954 // Global RageClick tracking 8955 if (options.dynamicEvents.hasOwnProperty('automatic_event_rage_click')) { 8956 (function() { 8957 var RAGE_THRESHOLD = Math.max(2, parseInt(options.rage_click_threshold, 10) || 3); 8958 // rage_click_time_limit from admin in seconds; allow floats (e.g. 1.5); minimum 0.5s. 8959 var RAGE_TIMEOUT = Math.max(0.5, parseFloat(options.rage_click_time_limit) || 2) * 1000; 8960 var rageMap = typeof WeakMap !== 'undefined' ? new WeakMap() : null; 8961 8962 if (!rageMap) return; 8963 8964 // Expanded interactive selector â includes developer-marked elements. 8965 var INTERACTIVE_SELECTOR = 8966 'button, a, input, select, textarea, label, ' + 8967 '[role="button"], [role="link"], [onclick], [tabindex], [data-track], [data-testid]'; 8968 8969 // Walk up to the nearest meaningful interactive ancestor. 8970 // Returns null when none found â non-interactive clicks are silently ignored. 8971 // Note: having an id alone does NOT make an element interactive 8972 // (e.g. div#comments, div#reviews are structural, not clickable). 8973 function findClickTarget(el) { 8974 while (el && el !== document.body) { 8975 if (el.matches && el.matches(INTERACTIVE_SELECTOR)) { 8976 return el; 8977 } 8978 el = el.parentElement; 8979 } 8980 return null; 8981 } 8982 8983 // Build a breadcrumb path: up to 3 DOM nodes ending at el. 8984 // Example: "nav#menu > ul.links > button.cta" 8985 function buildElementPath(el, depth) { 8986 depth = depth || 3; 8987 var parts = []; 8988 var cur = el; 8989 while (cur && cur !== document.body && parts.length < depth) { 8990 var tag = cur.tagName ? cur.tagName.toLowerCase() : ''; 8991 var id = cur.id ? '#' + cur.id : ''; 8992 var rawCls = cur.className && typeof cur.className === 'string' 8993 ? cur.className 8994 : (cur.className && cur.className.baseVal ? cur.className.baseVal : ''); 8995 var cls = rawCls ? '.' + rawCls.trim().split(/\s+/).slice(0, 2).join('.') : ''; 8996 parts.unshift(tag + id + cls); 8997 cur = cur.parentElement; 8998 } 8999 return parts.join(' > '); 9000 } 9001 9002 function describeElement(el) { 9003 var raw = el.className && (typeof el.className === 'string' 9004 ? el.className 9005 : (el.className.baseVal || '')); 9006 var classes = raw ? raw.trim().split(/\s+/).slice(0, 3).join(' ') : ''; 9007 9008 var text = (el.innerText || el.value || el.getAttribute('aria-label') || '') 9009 .trim().replace(/\s+/g, ' ').slice(0, 80); 9010 9011 var href = el.tagName === 'A' ? (el.getAttribute('href') || '') : ''; 9012 var tag = el.tagName ? el.tagName.toLowerCase() : ''; 9013 var path = buildElementPath(el); 9014
9015 // Only include fields with non-empty values â don't pollute the event 9016 // payload with empty strings that add noise to dashboards. 9017 var result = {}; 9018 if (tag) { result.element_tag = tag; result.element = tag; } // element = backward-compat alias 9019 if (el.id) result.element_id = el.id; 9020 if (classes) result.element_classes = classes; 9021 if (text) result.element_text = text; 9022 if (href) result.element_href = href; 9023 if (path) result.element_path = path; 9024 return result; 9025 } 9026 9027 document.addEventListener('click', function(event) { 9028 // Resolve to the nearest meaningful interactive ancestor. 9029 // Non-interactive elements (bare <div>, <span>, <h2> etc.) are silently 9030 // ignored â no fallback to event.target. 9031 var target = findClickTarget(event.target); 9032 if (!target) return; 9033 9034 var now = Date.now(); 9035 var st = rageMap.get(target) || { count: 0, last: 0, timer: null }; 9036 9037 if (now - st.last > RAGE_TIMEOUT) st.count = 0; 9038 st.count++; 9039 st.last = now; 9040 9041 if (st.timer) clearTimeout(st.timer); 9042 st.timer = setTimeout(function() { rageMap.delete(target); }, RAGE_TIMEOUT); 9043 rageMap.set(target, st); 9044 9045 if (st.count >= RAGE_THRESHOLD) { 9046 rageMap.delete(target); 9047 var params = describeElement(target); // contains only non-empty fields 9048 9049 Object.keys(options.dynamicEvents.automatic_event_rage_click).forEach(function(slug) { 9050 var pysEvent = Utils.clone(options.dynamicEvents.automatic_event_rage_click[slug]); 9051 Utils.copyProperties(params, pysEvent.params); 9052 var px = getPixelBySlag(slug); 9053 if (px) px.fireEvent(pysEvent.name, pysEvent); 9054 }); 9055 } 9056 }, true); 9057 })(); 9058 } 9059 9060 // Global Video Speed tracking 9061 if (options.dynamicEvents.hasOwnProperty('automatic_event_video_speed')) { 9062 (function() { 9063 var pysFireVideoSpeed = function(videoEl, currRate) { 9064 var html5Url = videoEl ? (videoEl.src || videoEl.currentSrc || '') : ''; 9065 var params = { 9066 video_url: html5Url, 9067 video_type: Utils.getVideoTypeFromUrl(html5Url), 9068 video_title: Utils.getHtml5VideoTitle(html5Url), 9069 playback_speed: currRate, 9070 element: videoEl ? (videoEl.tagName ? videoEl.tagName.toLowerCase() : 'video') : 'video' 9071 }; 9072 Object.keys(options.dynamicEvents.automatic_event_video_speed).forEach(function(slug) { 9073 var pysEvent = Utils.clone(options.dynamicEvents.automatic_event_video_speed[slug]); 9074 Utils.copyProperties(params, pysEvent.params); 9075 var px = getPixelBySlag(slug); 9076 if (px) px.fireEvent(pysEvent.name, pysEvent); 9077 }); 9078 }; 9079 9080 // HTML5 <video> 9081 document.addEventListener('ratechange', function(event) { 9082 var video = event.target; 9083 if (!video || video.tagName !== 'VIDEO') return; 9084 var prev = video._pysGlobalSpeedRate !== undefined ? video._pysGlobalSpeedRate : 1; 9085 var curr = video.playbackRate; 9086 if (prev === curr) return; 9087 video._pysGlobalSpeedRate = curr; 9088 pysFireVideoSpeed(video, curr); 9089 }, true); 9090 })(); 9091 } 9092 9093 // setup WooCommerce events 9094 if (options.woo.enabled) { 9095 9096 // Woo CartFlow AddToCart 9097 if (options.dynamicEvents.hasOwnProperty("wcf_add_to_cart_on_next_step_click")) { 9098 $("body").on("click",'.wcf-next-step-link',function () { 9099 var pixels = Object.keys(options.dynamicEvents.wcf_add_to_cart_on_next_step_click); 9100 for(var i = 0;i<pixels.length;i++) { 9101 var event = Utils.clone(options.dynamicEvents.wcf_add_to_cart_on_next_step_click[pixels[i]]) 9102 getPixelBySlag(pixels[i]).fireEvent(event.name, event); 9103 } 9104 }); 9105 } 9106 // Woo CartFlow Bump AddToCart 9107 if(options.dynamicEvents.hasOwnProperty("wcf_add_to_cart_on_bump_click") || 9108 options.dynamicEvents.hasOwnProperty("wcf_bump")) { 9109 $("body").on('change','.wcf-bump-order-cb',function () { 9110 9111 if(this.checked) { 9112 if(options.dynamicEvents.hasOwnProperty("wcf_add_to_cart_on_bump_click")) { 9113 var pixels = Object.keys(options.dynamicEvents.wcf_add_to_cart_on_bump_click); 9114 for(var i = 0;i<pixels.length;i++) { 9115 var event = Utils.clone(options.dynamicEvents.wcf_add_to_cart_on_bump_click[pixels[i]]); 9116 getPixelBySlag(pixels[i]).fireEvent(event.name, event); 9117 } 9118 } 9119 9120 9121 if(options.dynamicEvents.hasOwnProperty("wcf_bump")) { 9122 var pixels = Object.keys(options.dynamicEvents.wcf_bump); 9123 for(var i = 0;i<pixels.length;i++) { 9124 var event = Utils.clone(options.dynamicEvents.wcf_bump[pixels[i]]); 9125 getPixelBySlag(pixels[i]).fireEvent(event.name, event); 9126 } 9127 } 9128 9129 } else { 9130 if(options.dynamicEvents.hasOwnProperty("wcf_remove_from_cart_on_bump_click")) { 9131 var pixels = Object.keys(options.dynamicEvents.wcf_remove_from_cart_on_bump_click); 9132 for(var i = 0;i<pixels.length;i++) { 9133 var event = Utils.clone(options.dynamicEvents.wcf_remove_from_cart_on_bump_click[pixels[i]]); 9134 getPixelBySlag(pixels[i]).fireEvent(event.name, event); 9135 } 9136 } 9137 9138 } 9139 9140 9141 }); 9142 } 9143 9144 9145 // WooCommerce AddToCart 9146 if (options.dynamicEvents.hasOwnProperty("woo_add_to_cart_on_button_click") 9147 && options.woo.hasOwnProperty("addToCartCatchMethod") 9148 && options.woo.addToCartCatchMethod === "add_cart_js" 9149 ) { 9150 // Loop, any kind of "simple" product, except external 9151 $('.add_to_cart_button:not(.product_type_variable,.product_type_bundle,.single_add_to_cart_button)').on("click",function (e) { 9152 9153 var $button = $(this); 9154 var $prod_info = $button.closest('.product').find('.pys_list_name_productdata').data(); 9155 var product_id = $(this).data('product_id'); 9156 Cookies.set('productlist', JSON.stringify($prod_info), { expires: 1,path: '/' }) 9157 var product_id = $(this).data('product_id'); 9158 if (typeof product_id !== 'undefined') { 9159 9160 if(options.dynamicEvents.hasOwnProperty("woo_add_to_cart_on_button_click")){ 9161 var tmpEventID = pys_generate_token(); 9162 $.each(options.dynamicEvents.woo_add_to_cart_on_button_click, function (i, tag) { 9163 tag.eventID = tmpEventID; 9164 // Also refresh the uniqueId cache so server-API path 9165 // (ajaxForServerStaticEvent) gets the same fresh ID. 9166 var idKey = tag.hasOwnProperty('custom_event_post_id') ? tag.custom_event_post_id : tag.e_id; 9167 uniqueId[idKey] = tmpEventID; 9168 }); 9169 } 9170 9171 Facebook.onWooAddToCartOnButtonEvent(product_id,$(this)); 9172 Analytics.onWooAddToCartOnButtonEvent(product_id, $prod_info); 9173 GTM.onWooAddToCartOnButtonEvent(product_id, $prod_info); 9174 GAds.onWooAddToCartOnButtonEvent(product_id); 9175 Pinterest.onWooAddToCartOnButtonEvent(product_id); 9176 Bing.onWooAddToCartOnButtonEvent(product_id); 9177 TikTok.onWooAddToCartOnButtonEvent(product_id); 9178 Reddit.onWooAddToCartOnButtonEvent(product_id); 9179 OpenAI.onWooAddToCartOnButtonEvent(product_id); 9180 } 9181 9182 }); 9183 9184 // Single Product 9185 // tap try to https://stackoverflow.com/questions/30990967/on-tap-click-event-firing-twice-how-to-avoid-it 9186 // $(document) not work 9187 $('body').onFirst('click','button.single_add_to_cart_button,.single_add_to_cart_button',function (e) { 9188 9189 var $button = $(this); 9190 var $prod_info = $button.siblings('.pys_list_name_productdata').data(); 9191 var product_id = $(this).data('product_id'); 9192 Cookies.set('productlist', JSON.stringify($prod_info), { expires: 1,path: '/' }) 9193 if ($button.hasClass('disabled')) { 9194 return; 9195 } 9196 9197 var $form = $button.closest('form'); 9198 9199 var product_type = Utils.PRODUCT_SIMPLE; 9200 var is_external = false;
9201 9202 if ($form.length === 0) { 9203 is_external = true; 9204 } else if ($form.hasClass('variations_form')) { 9205 product_type = Utils.PRODUCT_VARIABLE; 9206 } else if($form.hasClass('bundle_form')) { 9207 product_type = Utils.PRODUCT_BUNDLE; 9208 } else if($form.hasClass('grouped_form')) { 9209 product_type = Utils.PRODUCT_GROUPED; 9210 } 9211 9212 9213 9214 var product_id; 9215 var qty = 1; 9216 if (product_type === Utils.PRODUCT_GROUPED) { 9217 qty = 1; 9218 product_id = parseInt($form.find('*[name="add-to-cart"]').val()); 9219 } else if (product_type === Utils.PRODUCT_VARIABLE) { 9220 product_id = parseInt($form.find('*[name="add-to-cart"]').val()); 9221 var qtyTag = $form.find('input[name="quantity"]'); 9222 if(qtyTag.length <= 0) { 9223 qtyTag = $form.find('select[name="quantity"]'); 9224 } 9225 qty = parseInt(qtyTag.val()); 9226 } else if (is_external) { 9227 product_id = options.woo.singleProductId; 9228 qty = 1; 9229 } else { 9230 product_id = parseInt($form.find('*[name="add-to-cart"]').val()); 9231 var qtyTag = $form.find('input[name="quantity"]'); 9232 if(qtyTag.length <= 0) { 9233 qtyTag = $form.find('select[name="quantity"]'); 9234 } 9235 qty = parseInt(qtyTag.val()); 9236 } 9237 9238 // 9239 if(options.dynamicEvents.hasOwnProperty("woo_add_to_cart_on_button_click")){ 9240 var tmpEventID = pys_generate_token(); 9241 $.each(options.dynamicEvents.woo_add_to_cart_on_button_click, function (i, tag) { 9242 tag.eventID = tmpEventID; 9243 // Also refresh the uniqueId cache so server-API path 9244 // (ajaxForServerStaticEvent) gets the same fresh ID. 9245 var idKey = tag.hasOwnProperty('custom_event_post_id') ? tag.custom_event_post_id : tag.e_id; 9246 uniqueId[idKey] = tmpEventID; 9247 }); 9248 } 9249 9250 Facebook.onWooAddToCartOnSingleEvent(product_id, qty, product_type, is_external, $form); 9251 Analytics.onWooAddToCartOnSingleEvent(product_id, qty, product_type, is_external, $form, $prod_info); 9252 GTM.onWooAddToCartOnSingleEvent(product_id, qty, product_type, is_external, $form, $prod_info); 9253 GAds.onWooAddToCartOnSingleEvent(product_id, qty, product_type, is_external, $form); 9254 Pinterest.onWooAddToCartOnSingleEvent(product_id, qty, product_type, is_external, $form); 9255 Bing.onWooAddToCartOnSingleEvent(product_id, qty, product_type, is_external, $form); 9256 TikTok.onWooAddToCartOnSingleEvent(product_id, qty, product_type, is_external, $form); 9257 Reddit.onWooAddToCartOnSingleEvent(product_id, qty, product_type, is_external, $form); 9258 OpenAI.onWooAddToCartOnSingleEvent(product_id, qty, product_type, is_external, $form); 9259 }); 9260 9261 } else { 9262 $('.add_to_cart_button:not(.product_type_variable,.product_type_bundle,.single_add_to_cart_button)').on("click",function (e) { 9263 var $button = $(this); 9264 var $prod_info = $button.siblings('.pys_list_name_productdata').data(); 9265 var product_id = $(this).data('product_id'); 9266 Cookies.set('productlist', JSON.stringify($prod_info), { expires: 1,path: '/' }) 9267 }); 9268 9269 // Single Product 9270 // tap try to https://stackoverflow.com/questions/30990967/on-tap-click-event-firing-twice-how-to-avoid-it 9271 // $(document) not work 9272 $('body').onFirst('click','button.single_add_to_cart_button,.single_add_to_cart_button',function (e) { 9273 9274 var $button = $(this); 9275 var $prod_info = $button.siblings('.pys_list_name_productdata').data(); 9276 Cookies.set('productlist', $prod_info, { expires: 1,path: '/' }) 9277 }); 9278 } 9279 9280 // WooCommerce Affiliate 9281 if (options.dynamicEvents.hasOwnProperty("woo_affiliate")) { 9282 9283 // Loop, external 9284 $('.product_type_external').on("click",function (e) { 9285 9286 var product_id = $(this).data('product_id'); 9287 9288 if (typeof product_id !== 'undefined') { 9289 Facebook.onWooAffiliateEvent(product_id); 9290 Analytics.onWooAffiliateEvent(product_id); 9291 GTM.onWooAffiliateEvent(product_id); 9292 GAds.onWooAffiliateEvent(product_id); 9293 Pinterest.onWooAffiliateEvent(product_id); 9294 Bing.onWooAffiliateEvent(product_id); 9295 } 9296 9297 }); 9298 9299 } 9300 9301 // WooCommerce RemoveFromCart 9302 if (options.dynamicEvents.hasOwnProperty("woo_remove_from_cart")) { 9303 9304 $('body').on('click', options.woo.removeFromCartSelector, function (e) { 9305 9306 var $a = $(e.currentTarget), 9307 href = $a.attr('href'); 9308 9309 // extract cart item hash from remove button URL 9310 var regex = new RegExp("[\\?&]remove_item=([^&#]*)"), 9311 results = regex.exec(href); 9312 9313 if (results !== null) { 9314 9315 var item_hash = results[1]; 9316 9317 if (options.dynamicEvents["woo_remove_from_cart"].hasOwnProperty(item_hash)) { 9318 var events = options.dynamicEvents["woo_remove_from_cart"][item_hash]; 9319 Utils.fireEventForAllPixel("onWooRemoveFromCartEvent",events) 9320 } 9321 9322 } 9323 9324 }); 9325 9326 } 9327 9328 // WooCommerce PayPal 9329 if (options.dynamicEvents.hasOwnProperty("woo_paypal")) { 9330 9331 // Non-default binding used to avoid situations when some code in external js 9332 // stopping events propagation, eg. returns false, and our handler will never called 9333 $(document).onFirst('submit click', '#place_order', function (e) { 9334 9335 var method = $('form[name="checkout"] input[name="payment_method"]:checked').val(); 9336 9337 if (method !== 'paypal') { 9338 return; 9339 } 9340 var events = options.dynamicEvents.woo_paypal; 9341 Utils.fireEventForAllPixel("onWooPayPalEvent",events) 9342 9343 }); 9344 9345 } 9346 9347 // WooCommerce checkout progress 9348 if(options.dynamicEvents.hasOwnProperty("woo_initiate_checkout_progress_f") ) { 9349 9350 $(document).on("change",".woocommerce-validated #billing_first_name",function () { 9351 var pixels = Object.keys(options.dynamicEvents.woo_initiate_checkout_progress_f); 9352 for (var i = 0; i < pixels.length; i++) { 9353 var event = Utils.clone(options.dynamicEvents.woo_initiate_checkout_progress_f[pixels[i]]); 9354 getPixelBySlag(pixels[i]).onWooCheckoutProgressStep(event); 9355 } 9356 }); 9357 } 9358 if(options.dynamicEvents.hasOwnProperty("woo_initiate_checkout_progress_l")) { 9359 9360 $(document).on("change",".woocommerce-validated #billing_last_name",function () { 9361 var pixels = Object.keys(options.dynamicEvents.woo_initiate_checkout_progress_l); 9362 for (var i = 0; i < pixels.length; i++) { 9363 var event = Utils.clone(options.dynamicEvents.woo_initiate_checkout_progress_l[pixels[i]]); 9364 getPixelBySlag(pixels[i]).onWooCheckoutProgressStep(event); 9365 } 9366 }); 9367 } 9368 9369 if(options.dynamicEvents.hasOwnProperty("woo_initiate_checkout_progress_e")) { 9370 9371 $(document).on("change",".woocommerce-validated #billing_email",function () { 9372 var pixels = Object.keys(options.dynamicEvents.woo_initiate_checkout_progress_e); 9373 for (var i = 0; i < pixels.length; i++) { 9374 var event = Utils.clone(options.dynamicEvents.woo_initiate_checkout_progress_e[pixels[i]]); 9375 getPixelBySlag(pixels[i]).onWooCheckoutProgressStep(event); 9376 } 9377 }); 9378 } 9379 if(options.dynamicEvents.hasOwnProperty("woo_initiate_checkout_progress_o")) { 9380 $(document).onFirst('submit click', '#place_order', function () { 9381 var shippingMethodFull = $('[name="shipping_method[0]"]:checked').val(); 9382 var shippingMethod = shippingMethodFull ? shippingMethodFull.split(':')[0] : ''; 9383 var pixels = Object.keys(options.dynamicEvents.woo_initiate_checkout_progress_o); 9384 for (var i = 0; i < pixels.length; i++) { 9385 var event = Utils.clone(options.dynamicEvents.woo_initiate_checkout_progress_o[pixels[i]]); 9386 // Add shipping_method to event params 9387 event.params = event.params || {}; 9388 event.params.checkout_option = shippingMethod; 9389 getPixelBySlag(pixels[i]).onWooCheckoutProgressStep(event); 9390 } 9391 }); 9392 } 9393 9394 9395 // WooCommerce 9396 9397 $('.product.type-product a.woocommerce-loop-product__link').onFirst('click', function (evt) { 9398 var productId = $(this).closest('.product').find(".add_to_cart_button").attr("data-product_id"); 9399 var eventTypes = ["woo_select_content_search", "woo_select_content_shop", "woo_select_content_tag", "woo_select_content_single", "woo_select_content_category"]; 9400 var isEventFired = false;
9401 9402 for (var i = 0; i < eventTypes.length; i++) { 9403 var eventType = eventTypes[i]; 9404 if (options.dynamicEvents.hasOwnProperty(eventType) && options.dynamicEvents[eventType].hasOwnProperty(productId)) { 9405 if(Analytics.isEnabled()){ 9406 Analytics.onWooSelectContent(options.dynamicEvents[eventType][productId][Analytics.tag()]); 9407 } 9408 if(GTM.isEnabled()){ 9409 GTM.onWooSelectContent(options.dynamicEvents[eventType][productId][GTM.tag()]); 9410 } 9411 isEventFired = true; 9412 break; 9413 } 9414 } 9415 9416 if (!isEventFired) { 9417 let prod_info = $(this).parent().find('.pys_list_name_productdata').data(); 9418 if (prod_info) { 9419 const select_prod_list = {}; 9420 if (prod_info['pys_list_name_productlist_name']) { 9421 select_prod_list.list_name = prod_info['pys_list_name_productlist_name'] 9422 } 9423 if (prod_info['pys_list_name_productlist_id']) { 9424 select_prod_list.list_id = prod_info['pys_list_name_productlist_id'] 9425 } 9426 const url = new URL(window.location.href); 9427 select_prod_list.url = url.origin + url.pathname; 9428 Cookies.set('select_prod_list', JSON.stringify(select_prod_list), { expires: 1,path: '/' }); 9429 } 9430 } 9431 }); 9432 9433 } 9434 9435 // setup EDD events 9436 if (options.edd.enabled) { 9437 9438 // EDD AddToCart 9439 if (options.dynamicEvents.hasOwnProperty("edd_add_to_cart_on_button_click")) { 9440 9441 $('form.edd_download_purchase_form .edd-add-to-cart').on("click",function (e) { 9442 9443 var $button = $(this); 9444 var $form = $button.closest('form'); 9445 var variable_price = $button.data('variablePrice'); // yes/no 9446 var price_mode = $button.data('priceMode'); // single/multi 9447 var ids = []; 9448 var quantities = []; 9449 var qty; 9450 var id; 9451 9452 if (variable_price === 'yes' && price_mode === 'multi') { 9453 9454 id = $form.find('input[name="download_id"]').val(); 9455 9456 // get selected variants 9457 $.each($form.find('input[name="edd_options[price_id][]"]:checked'), function (i, el) { 9458 ids.push(id + '_' + $(el).val()); 9459 }); 9460 9461 // get qty for selected variants 9462 $.each(ids, function (i, variant_id) { 9463 9464 var variant_index = variant_id.split('_', 2); 9465 qty = $form.find('input[name="edd_download_quantity_' + variant_index[1] + '"]').val(); 9466 9467 if (typeof qty !== 'undefined') { 9468 quantities.push(qty); 9469 } else { 9470 quantities.push(1); 9471 } 9472 9473 }); 9474 9475 } else if (variable_price === 'yes' && price_mode === 'single') { 9476 9477 id = $form.find('input[name="download_id"]').val(); 9478 ids.push(id + '_' + $form.find('input[name="edd_options[price_id][]"]:checked').val()); 9479 9480 qty = $form.find('input[name="edd_download_quantity"]').val(); 9481 9482 if (typeof qty !== 'undefined') { 9483 quantities.push(qty); 9484 } else { 9485 quantities.push(1); 9486 } 9487 9488 } else { 9489 9490 ids.push($button.data('downloadId')); 9491 9492 qty = $form.find('input[name="edd_download_quantity"]').val(); 9493 9494 if (typeof qty !== 'undefined') { 9495 quantities.push(qty); 9496 } else { 9497 quantities.push(1); 9498 } 9499 } 9500 9501 // fire event for each download/variant 9502 $.each(ids, function (i, download_id) { 9503 9504 var q = parseInt(quantities[i]); 9505 var variant_index = download_id.toString().split('_', 2); 9506 var price_index; 9507 9508 if (variant_index.length === 2) { 9509 download_id = variant_index[0]; 9510 price_index = variant_index[1]; 9511 } 9512 9513 Facebook.onEddAddToCartOnButtonEvent(download_id, price_index, q); 9514 Analytics.onEddAddToCartOnButtonEvent(download_id, price_index, q); 9515 GTM.onEddAddToCartOnButtonEvent(download_id, price_index, q); 9516 GAds.onEddAddToCartOnButtonEvent(download_id, price_index, q); 9517 Pinterest.onEddAddToCartOnButtonEvent(download_id, price_index, q); 9518 Bing.onEddAddToCartOnButtonEvent(download_id, price_index, q); 9519 TikTok.onEddAddToCartOnButtonEvent(download_id, price_index, q); 9520 Reddit.onEddAddToCartOnButtonEvent(download_id, price_index, q); 9521 OpenAI.onEddAddToCartOnButtonEvent(download_id, price_index, q); 9522 9523 }); 9524 9525 }); 9526 9527 } 9528 9529 // EDD RemoveFromCart 9530 if (options.dynamicEvents.hasOwnProperty("edd_remove_from_cart") ) { 9531 9532 $('form#edd_checkout_cart_form .edd_cart_remove_item_btn').on("click",function (e) { 9533 9534 var href = $(this).attr('href'); 9535 var key = href.substring(href.indexOf('=') + 1).charAt(0); 9536 9537 if (options.dynamicEvents.edd_remove_from_cart.hasOwnProperty(key)) { 9538 var events = options.dynamicEvents.edd_remove_from_cart[key]; 9539 Utils.fireEventForAllPixel("onEddRemoveFromCartEvent",events) 9540 } 9541 9542 }); 9543 9544 } 9545 9546 } 9547 9548 Utils.setupURLClickEvents(); 9549 9550 // setup Comment Event 9551 if (options.dynamicEvents.hasOwnProperty("automatic_event_c
9551omment") 9552 ) { 9553 9554 $('form.comment-form').on("submit",function () { 9555 if (options.dynamicEvents.hasOwnProperty("automatic_event_comment")) { 9556 var pixels = Object.keys(options.dynamicEvents.automatic_event_comment); 9557 for (var i = 0; i < pixels.length; i++) { 9558 var event = Utils.clone(options.dynamicEvents.automatic_event_comment[pixels[i]]); 9559 if ( pixels[i] !== 'tiktok' && pixels[i] !== 'reddit') { 9560 Utils.copyProperties(Utils.getRequestParams(), event.params); 9561 } 9562 getPixelBySlag(pixels[i]).onCommentEvent(event); 9563 } 9564 } 9565 }); 9566 9567 } 9568 9569 // setup Form Event 9570 if ( options.dynamicEvents.hasOwnProperty("automatic_event_form")) { 9571 9572 $(document).onFirst('submit', 'form', function (e) { 9573 9574 var $form = $(this); 9575 9576 // exclude WP forms 9577 const formClass = $form.attr('class') || ''; 9578 const formRole = ($form.attr('role') || '').toLowerCase(); 9579 const formId = $form.attr('id'); 9580 9581 if ( 9582 $form.hasClass('comment-form') || 9583 $form.hasClass('search-form') || 9584 $form.hasClass('woocommerce-ordering') || 9585 formClass.toLowerCase().includes('search') || formClass.toLowerCase().includes('add-to-cart') || 9586 formRole === 'search' || 9587 formId === 'adminbarsearch' 9588 ) { 9589 return; 9590 } 9591 9592 // exclude Woo forms 9593 if ($form.hasClass('woocommerce-product-search') || $form.hasClass('cart') || $form.hasClass('woocommerce-cart-form') || 9594 $form.hasClass('woocommerce-shipping-calculator') || $form.hasClass('checkout') || $form.hasClass('checkout_coupon')) { 9595 return; 9596 } 9597 9598 // exclude EDD forms 9599 if ($form.hasClass('edd_form') || $form.hasClass('edd_download_purchase_form')) { 9600 return; 9601 } 9602 // exclude CF7 forms 9603 if ($form.hasClass('wpcf7-form')) { 9604 return; 9605 } 9606 // exclude Gravity forms 9607 if ($form.attr('id') && $form.attr('id').includes('gform')) { 9608 var target = $form.attr('target'); 9609 if (target && target.indexOf('gform_ajax_frame') !== -1) { 9610 return; 9611 } 9612 } 9613 // exclude Forminator forms 9614 if ($form.hasClass('forminator-custom-form') || $form.hasClass('forminator_ajax')) { 9615 return; 9616 } 9617 // exclude WPforms forms 9618 if ($form.hasClass('wpforms-form') || $form.hasClass('wpforms-ajax-form')) { 9619 return; 9620 } 9621 // exclude Formidable forms 9622 /*if ($form.hasClass('frm-show-form')) { 9623 return; 9624 }*/ 9625 // exclude Ninja Forms forms 9626 if ($form.parent().hasClass('nf-form-layout')) { 9627 return; 9628 } 9629 // exclude Fluent forms 9630 if ($form.hasClass('frm-fluent-form')) { 9631 return; 9632 } 9633 9634 // exclude WS form forms 9635 if ($form.hasClass('wsf-form') ) { 9636 return; 9637 } 9638 9639 // exclude WS form forms 9640 if ( $form.hasClass( 'elementor-form' ) ) { 9641 return; 9642 } 9643 9644 if(!options.enable_success_send_form) { 9645 var params = { 9646 form_id: $form.attr('id'), 9647 form_class: $form.attr('class') ? $form.attr('class') : $form.attr('id'), 9648 text: $form.find('[type="submit"]').is('input') ? 9649 $form.find('[type="submit"]').val() : $form.find('[type="submit"]').text() 9650 }; 9651 9652 if (options.dynamicEvents.hasOwnProperty("automatic_event_form")) { 9653 var pixels = Object.keys(options.dynamicEvents.automatic_event_form); 9654 for (var i = 0; i < pixels.length; i++) { 9655 var event = Utils.clone(options.dynamicEvents.automatic_event_form[pixels[i]]); 9656 9657 if (pixels[i] === "tiktok" || pixels[i] === "reddit") { 9658 getPixelBySlag(pixels[i]).fireEvent(event.name, event); 9659 } else { 9660 Utils.copyProperties(params, event.params,) 9661 Utils.copyProperties(Utils.getRequestParam
9661s(), event.params); 9662 getPixelBySlag(pixels[i]).onFormEvent(event); 9663 } 9664 } 9665 } 9666 } 9667 }); 9668 9669 9670 } 9671 9672 jQuery(document).on('elementor/popup/show', function(event) { 9673 for (var i = 0; i < jQuery(".wpcf7-form").length; i++) { 9674 wpcf7.init(jQuery(".wpcf7-form")[i]); 9675 } 9676 }); 9677 9678 document.addEventListener( 'wpcf7mailsent', function ( event ) { 9679 let form_id = event.detail.contactFormId, 9680 sendEventId = null, 9681 disabled_form_action = false, 9682 form_submit_mode = 'both'; 9683 if ( options.triggerEventTypes.hasOwnProperty( 'CF7' ) ) { 9684 $.each( options.triggerEventTypes.CF7, function ( eventId, triggers ) { 9685 $.each( triggers, function ( index, trigger ) { 9686 $.each( trigger.forms, function ( i, value ) { 9687 if ( value == form_id ) { 9688 sendEventId = eventId; 9689 disabled_form_action = trigger.disabled_form_action; 9690 form_submit_mode = trigger.form_submit_mode || 'both'; 9691 } 9692 } ) 9693 } ); 9694 } ); 9695 } 9696 if ( sendEventId != null && ( form_submit_mode === 'both' || form_submit_mode === 'ajax_only' ) ) { 9697 Utils.handleTriggerFired( sendEventId, 'CF7' ); 9698 } 9699 sendFormAction( $( event.target ), form_id, sendEventId, disabled_form_action, form_submit_mode ); 9700 9701 }, false ); 9702 9703 //GravityForm 9704 jQuery( document ).on( 'gform_confirmation_loaded', function ( event, formId ) { 9705 let form_id = formId, 9706 sendEventId = null, 9707 disabled_form_action = false, 9708 form_submit_mode = 'both'; 9709 if ( options.triggerEventTypes.hasOwnProperty( 'gravity' ) ) { 9710 $.each( options.triggerEventTypes.gravity, function ( eventId, triggers ) { 9711 $.each( triggers, function ( index, trigger ) { 9712 $.each( trigger.forms, function ( i, value ) { 9713 if ( value == form_id ) { 9714 disabled_form_action = trigger.disabled_form_action; 9715 sendEventId = eventId; 9716 form_submit_mode = trigger.form_submit_mode || 'both'; 9717 } 9718 } ) 9719 } ); 9720 } ); 9721 } 9722 9723 if ( sendEventId != null && ( form_submit_mode === 'both' || form_submit_mode === 'ajax_only' ) ) { 9724 Utils.handleTriggerFired( sendEventId, 'gravity' ); 9725 } 9726 sendFormAction( $( event.target ), form_id, sendEventId, disabled_form_action, form_submit_mode ); 9727 jQuery( document ).off( 'gform_confirmation_loaded' ); 9728 } ); 9729 //Forminator 9730 $( document ).on( 'forminator:form:submit:success', function ( event ) { 9731 9732 let form_id = $( event.target ).find( 'input[name="form_id"]' ).val(), 9733 sendEventId = null, 9734 disabled_form_action = false, 9735 form_submit_mode = 'both'; 9736 if ( options.triggerEventTypes.hasOwnProperty( 'forminator' ) ) { 9737 $.each( options.triggerEventTypes.forminator, function ( eventId, triggers ) { 9738 $.each( triggers, function ( index, trigger ) { 9739 $.each( trigger.forms, function ( i, value ) { 9740 if ( value == form_id ) { 9741 disabled_form_action = trigger.disabled_form_action; 9742 sendEventId = eventId; 9743 form_submit_mode = trigger.form_submit_mode || 'both'; 9744 } 9745 } ) 9746 } ); 9747 } ); 9748 } 9749 if ( sendEventId != null && ( form_submit_mode === 'both' || form_submit_mode === 'ajax_only' ) ) { 9750 Utils.handleTriggerFired( sendEventId, 'forminator' ); 9751 } 9752 sendFormAction( $( event.target ), form_id, sendEventId, disabled_form_action, form_submit_mode ); 9753 } ); 9754 9755 //WPForm 9756 $( 'form.wpforms-form' ).on( 'wpformsAjaxSubmitSuccess', ( event ) => { 9757 9758 let form_id = $( event.target ).attr( 'data-formid' ), 9759 sendEventId = null, 9760 disabled_form_action = false, 9761 form_submit_mode = 'both'; 9762 9763 if ( options.triggerEventTypes.hasOwnProperty( 'wpforms' ) ) { 9764 $.each( options.triggerEventTypes.wpforms, function ( eventId, triggers ) { 9765 $.each( triggers, function ( index, trigger ) { 9766 $.each( trigger.forms, function ( i, value ) { 9767 if ( value == form_id ) { 9768 sendEventId = eventId; 9769 disabled_form_action = trigger.disabled_form_action; 9770 form_submit_mode = trigger.form_submit_mode || 'both'; 9771 } 9772 } ) 9773 } ); 9774 } ); 9775 } 9776 if ( sendEventId != null && ( form_submit_mode === 'both' || form_submit_mode === 'ajax_only' ) ) { 9777 Utils.handleTriggerFired( sendEventId, 'wpforms' ); 9778 } 9779 sendFormAction( $( event.target ), form_id, sendEventId, disabled_form_action, form_submit_mode ); 9780 } ) 9781 9782 $( document ).on( 'frmFormComplete', function ( event, form, response ) { 9783 const form_id = $( form ).find( 'input[name="form_id"]' ).val(); 9784 let sendEventId = null, 9785 disabled_form_action = false, 9786 form_submit_mode = 'both'; 9787 if ( options.triggerEventTypes.hasOwnProperty( 'formidable' ) ) { 9788 $.each( options.triggerEventTypes.formidable, function ( eventId, triggers ) { 9789 $.each( triggers, function ( index, trigger ) { 9790 $.each( trigger.forms, function ( i, value ) { 9791 if ( value == form_id ) { 9792 disabled_form_action = trigger.disabled_form_action; 9793 sendEventId = eventId; 9794 form_submit_mode = trigger.form_submit_mode || 'both'; 9795 } 9796 } ) 9797 } ); 9798 } ); 9799 } 9800 if ( sendEventId != null && ( form_submit_mode === 'both' || form_submit_mode === 'ajax_only' ) ) { 9801 Utils.handleTriggerFired( sendEventId, 'formidable' ); 9802 } 9803 sendFormAction( $( event.target ), form_id, sendEventId, disabled_form_action, form_submit_mode ); 9804 9805 } ); 9806 9807 // Ninja Forms 9808 $( document ).onFirst( 'nfFormSubmitResponse', function ( event, data ) { 9809 const form_id = data.response.data.form_id; 9810 let sendEventId = null, 9811 disabled_form_action = false, 9812 form_submit_mode = 'both'; 9813 if ( options.triggerEventTypes.hasOwnProperty( 'ninjaform' ) ) { 9814 $.each( options.triggerEventTypes.ninjaform, function ( eventId, triggers ) { 9815 $.each( triggers, function ( index, trigger ) { 9816 $.each( trigger.forms, function ( i, value ) { 9817 if ( value == form_id ) { 9818 disabled_form_action = trigger.disabled_form_action; 9819 sendEventId = eventId; 9820 form_submit_mode = trigger.form_submit_mode || 'both'; 9821 } 9822 } ) 9823 } ); 9824 } ); 9825 } 9826 if ( sendEventId != null && ( form_submit_mode === 'both' || form_submit_mode === 'ajax_only' ) ) { 9827 Utils.handleTriggerFired( sendEventId, 'ninjaform' ); 9828 } 9829 sendFormAction( $( event.target ), form_id, sendEventId, disabled_form_action, form_submit_mode ); 9830 } ); 9831 9832 9833 $(document).on('fluentform_submission_success', function ( event, data ) { 9834 if(!data || !data.form){ 9835 return; 9836 } 9837 let $form = data.form; // Submitted form 9838 let config = data.config; // Form configuration 9839 let response = data.response; // Server response 9840 9841 9842 let $formItem = $form; 9843 let form_id = config.id; 9844 let sendEventId = null, 9845 disabled_form_action = false, 9846 form_submit_mode = 'both'; 9847 if ( options.triggerEventTypes.hasOwnProperty( 'fluentform' ) ) { 9848 $.each( options.triggerEventTypes.fluentform, function ( eventId, triggers ) { 9849 $.each( triggers, function ( index, trigger ) { 9850 $.each( trigger.forms, function ( i, value ) { 9851 if ( value == form_id ) { 9852 disabled_form_action = trigger.disabled_form_action; 9853 sendEventId = eventId; 9854 form_submit_mode = trigger.form_submit_mode || 'both'; 9855 } 9856 } ) 9857 } ); 9858 } ); 9859 } 9860 9861 if ( sendEventId != null && ( form_submit_mode === 'both' || form_submit_mode === 'ajax_only' ) ) { 9862 Utils.handleTriggerFired( sendEventId, 'fluentform' ); 9863 } 9864 sendFormAction( $( event.target ), form_id, sendEventId, disabled_form_action, form_submit_mode ); 9865 } ); 9866 9867 //WSForm 9868 $( document ).on( 'wsf-submit-complete', ( event, form_object, form_id ) => { 9869 let sendEventId = null, 9870 disabled_form_action = false, 9871 form_submit_mode = 'both'; 9872 if ( options.triggerEventTypes.hasOwnProperty( 'wsform' ) ) { 9873 $.each( options.triggerEventTypes.wsform, function ( eventId, triggers ) { 9874 $.each( triggers, function ( index, trigger ) { 9875 $.each( trigger.forms, function ( i, value ) { 9876 if ( value == form_id ) { 9877 disabled_form_action = trigger.disabled_form_action; 9878 sendEventId = eventId; 9879 form_submit_mode = trigger.form_submit_mode || 'both'; 9880 } 9881 } ) 9882 } ); 9883 } ); 9884 } 9885 9886 if ( sendEventId != null && ( form_submit_mode === 'both' || form_submit_mode === 'ajax_only' ) ) { 9887 Utils.handleTriggerFired( sendEventId, 'wsform' ); 9888 } 9889 sendFormAction( $( event.target ), form_id, sendEventId, disabled_form_action, form_submit_mode ); 9890 } ) 9891 9892 // Elementor Forms 9893 let forms = document.querySelectorAll( '.elementor-form' ); 9894 // Apply the observer to each form
9895 forms.forEach( function ( form ) { 9896 observeElementorForm( form ); 9897 } ); 9898 9899 // watching Elementor Popup forms 9900 let observer = new MutationObserver( function ( mutationsList, observer ) { 9901 for ( let i = 0; i < mutationsList.length; i++ ) { 9902 let popupForm = document.querySelector( '[class*="elementor-popup"] form' ); 9903 if ( popupForm ) { 9904 observeElementorForm( popupForm ); 9905 observer.disconnect(); 9906 break; 9907 } 9908 } 9909 } ); 9910 observer.observe( document.body, { childList: true, subtree: true } ); 9911 9912 // load pixel APIs 9913 Utils.loadPixels(); 9914 9915 // setup Enrich content 9916 if(Utils.isCheckoutPage()) { 9917 Utils.addCheckoutFields(); 9918 } 9919 if(options.woo.hasOwnProperty('woo_view_content_variation_is_selected') && options.woo.woo_view_content_variation_is_selected) { 9920 handleWooViewContentVariation() 9921 } 9922 } 9923 9924 $(document).ready(function () { 9925 if ( options.dynamicDataUrl ) { 9926 fetch( options.dynamicDataUrl, { 9927 method: 'GET', 9928 credentials: 'same-origin', 9929 cache: 'no-store', 9930 headers: { 'Accept': 'application/json' } 9931 } ) 9932 .then( function(r) { return r.ok ? r.json() : Promise.reject(r.status); } ) 9933 .then( function(dynamic) { 9934 options.ajax_event = dynamic.ajax_event; 9935 options.user_id = dynamic.user_id; 9936 options.cache_bypass = dynamic.cache_bypass; 9937 options.tracking_analytics = dynamic.tracking_analytics; 9938 if ( dynamic.gdpr_dynamic && options.gdpr ) { 9939 Object.assign( options.gdpr, dynamic.gdpr_dynamic ); 9940 if ( dynamic.gdpr_dynamic.analytics_storage_value !== null ) 9941 options.gdpr.analytics_storage.value = dynamic.gdpr_dynamic.analytics_storage_value; 9942 if ( dynamic.gdpr_dynamic.ad_storage_value !== null ) 9943 options.gdpr.ad_storage.value = dynamic.gdpr_dynamic.ad_storage_value; 9944 if ( dynamic.gdpr_dynamic.ad_user_data_value !== null ) 9945 options.gdpr.ad_user_data.value = dynamic.gdpr_dynamic.ad_user_data_value; 9946 if ( dynamic.gdpr_dynamic.ad_personalization_value !== null ) 9947 options.gdpr.ad_personalization.value = dynamic.gdpr_dynamic.ad_personalization_value; 9948 } 9949 options.cookie = dynamic.cookie; 9950 // Per-pixel user data (advanced matching) that was emptied out of 9951 // the cached HTML. An inline value that still holds something wins: 9952 // on pages we never strip (order received / EDD success) the HTML 9953 // carries the order's own data, which beats the account-level data 9954 // this endpoint can see. 9955 if ( dynamic.pixels ) { 9956 for ( var slug in dynamic.pixels ) { 9957 if ( ! dynamic.pixels.hasOwnProperty( slug ) || ! options[ slug ] ) continue; 9958 for ( var key in dynamic.pixels[ slug ] ) { 9959 if ( ! dynamic.pixels[ slug ].hasOwnProperty( key ) ) continue; 9960 var inline = options[ slug ][ key ]; 9961 if ( ! inline || Object.keys( inline ).length === 0 ) { 9962 options[ slug ][ key ] = dynamic.pixels[ slug ][ key ]; 9963 } 9964 } 9965 } 9966 } 9967 9968 options.dynamicDataApplied = true; 9969 9970 if ( window.pys && window.pys.OpenAI 9971 && typeof window.pys.OpenAI.refreshAdvancedMatching === 'function' ) { 9972 window.pys.OpenAI.refreshAdvancedMatching(); 9973 } 9974 9975 pysNormalInit(); 9976 } ) 9977 .catch( function(err) { 9978 if ( options.debug ) console.warn( '[PYS] dynamic-options fetch failed:', err ); 9979 9980 options.dynamicDataApplied = true; 9981 pysNormalInit(); 9982 } ); 9983 } else { 9984 pysNormalInit(); 9985 } 9986 }); 9987 9988 9989 function handleWooViewContentVariation() { 9990 var form = $('form.variations_form'); 9991 9992 form.on('change', 'select', function() { 9993 var selectedAttributes = {}; 9994 form.find('select').each(function() { 9995 var attributeName = $(this).data('attribute_name') || $(this).attr('name'); 9996 var attributeValue = $(this).val(); 9997 if (attributeValue) { 9998 selectedAttributes[attributeName] = attributeValue; 9999 } 10000 }); 10001 10002 $.ajax({ 10003 url: options.ajaxUrl, 10004 type: 'POST', 10005 data: { 10006 action: 'get_variation_info', 10007 product_id: form.data('product_id'), // Pass the main product ID 10008 attributes: selectedAttributes 10009 }, 10010 success: function(response) { 10011 if (response.success) { 10012 $.each(response.data, function(slug) { 10013 $.each(response.data[slug], function(index, event) { 10014 event.isVariationTrigger = true; 10015 getPixelBySlag(slug).fireEvent(event.name, event); 10016 }); 10017 }); 10018 } else { 10019 console.log('Error retrieving data:', response.data); 10020 } 10021 } 10022 }); 10023 }); 10024 } 10025 var sendFormAction = function (form_target, formId, customEventId = null, disabled = false, form_submit_mode = 'both'){
10026 // Check submit mode - skip automatic event if reload_only 10027 if (disabled == true || form_submit_mode === 'reload_only' ) { 10028 return; 10029 } 10030 10031 var params = { 10032 form_id: formId, 10033 text: form_target.find('[type="submit"]').is('input') ? form_target.find('[type="submit"]').val() : 10034 form_target.find('.forminator-button-submit').text() != '' ? form_target.find('.forminator-button-submit').text() : 10035 form_target.find('[type="submit"]').text() 10036 }; 10037 10038 if (options.dynamicEvents.hasOwnProperty("automatic_event_form")) { 10039 var pixels = Object.keys(options.dynamicEvents.automatic_event_form); 10040 for (var i = 0; i < pixels.length; i++) { 10041 10042 if ( disabled && customEventId && typeof options.triggerEvents[customEventId][pixels[i]] !== 'undefined') { 10043 continue; 10044 } 10045 var event = options.dynamicEvents.automatic_event_form[pixels[i]]; 10046 if (pixels[i] === "tiktok" || pixels[i] === "reddit") { 10047 getPixelBySlag(pixels[i]).fireEvent(event.name, event); 10048 } else { 10049 Utils.copyProperties(params, event.params)
10050 Utils.copyProperties(Utils.getRequestParams(), event.params); 10051 getPixelBySlag(pixels[i]).onFormEvent(event); 10052 } 10053 } 10054 } 10055 } 10056 10057 var setupEmailLinks = function ( disabled_for_pixels = [] ) { 10058 if ( options.dynamicEvents.hasOwnProperty( "automatic_event_email_link" ) ) { 10059 var pixels = Object.keys( options.dynamicEvents.automatic_event_email_link ); 10060 for ( var i = 0; i < pixels.length; i++ ) { 10061 if ( disabled_for_pixels.indexOf(pixels[ i ]) === -1 ) { 10062 var event = Utils.clone( options.dynamicEvents.automatic_event_email_link[ pixels[ i ] ] ); 10063 if ( pixels[ i ] !== 'tiktok' && pixels[ i ] !== 'reddit' ) { 10064 Utils.copyProperties( Utils.getRequestParams(), event.params ); 10065 } 10066 getPixelBySlag( pixels[ i ] ).fireEvent( event.name, event ); 10067 } 10068 } 10069 } 10070 } 10071 10072 //Elementor Forms Observer 10073 var observeElementorForm = function ( form ) { 10074 let observer = new MutationObserver( function ( mutations ) { 10075 10076 for ( let i = 0; i < mutations.length; i++ ) { 10077 let mutation = mutations[ i ]; 10078 10079 if ( mutation.type === 'childList' || mutation.type === 'attributes' ) { 10080 if ( $( form ).find( '.elementor-message-success' ).length > 0 ) { 10081 10082 let form_id = $( form ).attr( 'id' ); 10083 if ( !form_id ) { 10084 form_id = $( form ).find( 'input[name="form_id"]' ).val(); 10085 } 10086 10087 let sendEventId = null, 10088 disabled_form_action = false, 10089 form_submit_mode = 'both'; 10090 if ( options.triggerEventTypes.hasOwnProperty( 'elementor_form' ) ) { 10091 $.each( options.triggerEventTypes.elementor_form, function ( eventId, triggers ) { 10092 $.each( triggers, function ( index, trigger ) { 10093 $.each( trigger.forms, function ( i, value ) { 10094 if ( value == form_id ) { 10095 disabled_form_action = trigger.disabled_form_action; 10096 sendEventId = eventId; 10097 form_submit_mode = trigger.form_submit_mode || 'both'; 10098 } 10099 }) 10100 } ); 10101 } ); 10102 } 10103 if ( sendEventId != null && ( form_submit_mode === 'both' || form_submit_mode === 'ajax_only' ) ) { 10104 Utils.handleTriggerFired( sendEventId, 'elementor_form' ); 10105 } 10106 sendFormAction( $( event.target ), form_id, sendEventId, disabled_form_action, form_submit_mode ); 10107 10108 observer.disconnect(); 10109 break; 10110 } 10111 } 10112 } 10113 } ); 10114 10115 // Start observing the form for changes 10116 observer.observe( form, { 10117 attributes: true, 10118 childList: true, 10119 subtree: true 10120 } ); 10121 } 10122 10123}(jQuery, pysOptions); 10124 10125 10126function pys_generate_token() { 10127 return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => 10128 (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) 10129 ); 10130} 10131function getBundlePriceOnSingleProduct(data) { 10132 var items_sum = 0; 10133 jQuery(".bundle_form .bundled_product").each(function(index){ 10134 var id = jQuery(this).find(".cart").data("bundled_item_id"); 10135 var item_price = data.prices[id]; 10136 var item_quantity = jQuery(this).find(".bundled_qty").val(); 10137 if(!jQuery(this).hasClass("bundled_item_optional") || 10138 jQuery(this).find(".bundled_product_optional_checkbox input").prop('checked')) { 10139 items_sum += item_price*item_quantity; 10140 } 10141 }); 10142 return items_sum; 10143} 10144function getUrlParameter(sParam) { 10145 var sPageURL = window.location.search.substring(1), 10146 sURLVariables = sPageURL.split('&'), 10147 sParameterName, 10148 i; 10149 10150 for (i = 0; i < sURLVariables.length; i++) { 10151 sParameterName = sURLVariables[i].split('='); 10152 10153 if (sParameterName[0] === sParam) { 10154 // Twin of ServerEventHelper::getUrlParameter(). A parameter with 10155 // no '=' has no value;
10155 skip it rather than inventing one, so 10156 // ?fbclid&fbclid=REAL still finds the real one. 10157 if (sParameterName[1] === undefined) { 10158 continue; 10159 } 10160 try { 10161 return decodeURIComponent(sParameterName[1]); 10162 } catch (e) { 10163 // decodeURIComponent throws on a malformed escape ("%", 10164 // "%zz", "a%2"). Return the raw value instead of throwing, 10165 // matching PHP's urldecode(), which is lenient about it. 10166 return sParameterName[1]; 10167 } 10168 } 10169 } 10170 return false; 10171}; 10172function inArray(needle, haystack) { 10173 var length = haystack.length; 10174 for(var i = 0; i < length; i++) { 10175 if(haystack[i] == needle) return true; 10176 } 10177 return false; 10178} 10179 10180function getCookieYes(key) { 10181 const cookiesObj = document.cookie 10182 .split(";") 10183 .reduce((ac, cv) => { 10184 const [k, v] = cv.split("="); 10185 if (k && v) ac[k.trim()] = v; 10186 return ac; 10187 }, {}); 10188 const consentCookie = cookiesObj["cookieyes-consent"] || cookiesObj["wt_consent"]; 10189 if (!consentCookie) return undefined; 10190 const { [key]: value } = consentCookie 10191 .split(",") 10192 .reduce((obj, pair) => { 10193 const [k, v] = pair.split(":"); 10194 if (k && v) obj[k] = v; 10195 return obj; 10196 }, {}); 10197 return value; 10198} 10199 10200function getRootDomain(useSubdomain = false) { 10201 const hostname = window.location.hostname; // Get the current hostname 10202 // Check if tldjs is defined before using it 10203 if (typeof tldjs === "undefined") { 10204 console.warn("tldjs is not defined"); 10205 return hostname; // Return hostname as a fallback 10206 } 10207 10208 const rootDomain = tldjs.getDomain(hostname); // Use tldjs to extract the root domain 10209 10210 // Return the root domain with or without a leading dot based on useSubdomain 10211 return rootDomain ? (useSubdomain ? '.' + rootDomain : rootDomain) : hostname; 10212}
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.