1/* ========================================================== 2 Fair Future Foundation - Flyouts Manager (WA + NL + TWINT) 3 File: /assets/js/fff-flyouts.js 4 5 Notes: 6 - Divi abort logic (builder/preview/iframes) 7 - Global lock counter (prevents stacking with other overlays) 8 - Cooldowns via localStorage 9 - Retry open logic 10 - Auto-hide for harmony (WA + NL + TWINT) 11 - CH targeting (TWINT) 12 - FIX: close immediately on link click (important for target=_blank) 13 - NEW: once per visit (per tab) via sessionStorage 14 - NEW: per-flyout page exclusions 15 ========================================================== */ 16 17(function () { 18 "use strict"; 19 20 // ---------------------------------------------------------- 21 // Abort in Divi Builder / previews / iframes 22 // ---------------------------------------------------------- 23 function shouldAbortForDivi() { 24 try { 25 const qs = window.location.search || ""; 26 27 const hasDiviParam = 28 qs.indexOf("et_fb=") !== -1 || 29 qs.indexOf("et_bfb=") !== -1 || 30 qs.indexOf("et_tb=") !== -1 || 31 qs.indexOf("et_pb_preview=") !== -1; 32 33 const html = document.documentElement; 34 const body = document.body; 35 36 const hasDiviClass = 37 (html && (html.classList.contains("et-fb") || html.classList.contains("et-tb"))) || 38 (body && (body.classList.contains("et-fb") || body.classList.contains("et-tb"))); 39 40 const inIframe = (function () { 41 try { 42 return window.self !== window.top; 43 } catch (e) { 44 return true; 45 } 46 })(); 47 48 if (hasDiviParam || hasDiviClass || inIframe) return true; 49 if (document.getElementById("et-fb-app") || document.querySelector(".et-fb-root")) return true; 50 51 return false; 52 } catch (e) { 53 return false; 54 } 55 } 56 57 if (shouldAbortForDivi()) return; 58 59 // ========================================================== 60 // OPT 09I - Flyout Manager V2 61 // One campaign per browser-tab visit. 62 // ========================================================== 63 64 const DAY_MS = 86400000; 65 66 const SESSION_KEY = 67 "fff_flyouts_seen_v2"; 68 69 const ROTATION_KEY = 70 "fff_flyout_last_owner_v2"; 71 72 const OPEN_DELAY_MS = 6000; 73 const RETRY_MS = 1500; 74 const MAX_RETRIES = 10; 75 const AUTO_HIDE_MS = 9000; 76 77 78 function nowMs() { 79 return Date.now(); 80 } 81 82 83 function getMs(key) { 84 85 try { 86 87 const n = Number( 88 localStorage.getItem(key) 89 ); 90 91 return Number.isFinite(n) 92 ? n 93 : 0; 94 95 } catch (e) { 96 97 return 0; 98 } 99 } 100 101 102 function setMs(key, value) { 103 104 try { 105 106 localStorage.setItem( 107 key, 108 String(value) 109 ); 110 111 } catch (e) {} 112 } 113 114 115 function getText(key) { 116 117 try { 118 119 return ( 120 localStorage.getItem(key) 121 || "" 122 ); 123 124 } catch (e) { 125 126 return ""; 127 } 128 } 129 130 131 function setText(key, value) { 132 133 try { 134 135 localStorage.setItem( 136 key, 137 String(value) 138 ); 139 140 } catch (e) {} 141 } 142 143 144 function hasSeenThisVisit() { 145 146 try { 147 148 return ( 149 sessionStorage.getItem( 150 SESSION_KEY 151 ) === "1" 152 153 || 154 155 sessionStorage.getItem( 156 "fff_flyouts_seen"
157 ) === "1" 158 ); 159 160 } catch (e) { 161 162 return false; 163 } 164 } 165 166 167 function markSeenThisVisit() { 168 169 try { 170 171 sessionStorage.setItem( 172 SESSION_KEY, 173 "1" 174 ); 175 176 } catch (e) {} 177 } 178 179 180 // ---------------------------------------------------------- 181 // Overlay lock 182 // ---------------------------------------------------------- 183 184 function getLockCount() { 185 186 const n = Number( 187 document.documentElement 188 .dataset 189 .fffLockCount 190 || "0" 191 ); 192 193 return Number.isFinite(n) 194 ? n 195 : 0; 196 } 197 198 199 function setLockCount(n) { 200 201 const safe = Math.max(0, n); 202 const root = document.documentElement; 203 204 root.dataset.fffLockCount = 205 String(safe); 206 207 root.classList.toggle( 208 "fff-modal-open", 209 safe > 0 210 ); 211 } 212 213 214 function lockAcquire(owner) { 215 216 const root = document.documentElement; 217 const key = "fffLock_" + owner; 218 219 if (root.dataset[key] === "1") { 220 return; 221 } 222 223 root.dataset[key] = "1"; 224 225 setLockCount( 226 getLockCount() + 1 227 ); 228 } 229 230 231 function lockRelease(owner) { 232 233 const root = document.documentElement; 234 const key = "fffLock_" + owner; 235 236 if (root.dataset[key] !== "1") { 237 return; 238 } 239 240 root.dataset[key] = "0"; 241 242 setLockCount( 243 getLockCount() - 1 244 ); 245 } 246 247 248 function anyFlyoutVisible() { 249 250 return !!document.querySelector( 251 ".fff-wa-flyout-box.is-visible," 252 + ".fff-nl-flyout-box.is-visible," 253 + ".fff-twint-flyout-box.is-visible" 254 ); 255 } 256 257 258 function overlayActive() { 259 260 if (getLockCount() > 0) { 261 return true; 262 } 263 264 if ( 265 document.body 266 && document.body.classList.contains( 267 "cmplz-banner-active" 268 ) 269 ) { 270 return true; 271 } 272 273 if ( 274 document.querySelector( 275 ".mfp-wrap" 276 ) 277 ) { 278 return true; 279 } 280 281 return false; 282 } 283 284 285 // ---------------------------------------------------------- 286 // TWINT targeting 287 // ---------------------------------------------------------- 288 289 function isLikelySwissVisitor() { 290 291 try { 292 293 const allLanguages = [ 294 navigator.language || "" 295 ].concat( 296 navigator.languages || [] 297 ); 298 299 const swissLanguage = 300 allLanguages.some( 301 function (language) { 302 303 return String(language) 304 .toLowerCase() 305 .indexOf("-ch") 306 !== -1; 307 } 308 ); 309 310 let timezone = ""; 311 312 try { 313 314 timezone = 315 Intl.DateTimeFormat() 316 .resolvedOptions() 317 .timeZone 318 || ""; 319 320 } catch (e) {} 321 322 return ( 323 swissLanguage 324 || timezone.toLowerCase() 325 === "europe/zurich" 326 ); 327 328 } catch (e) { 329 330 return false; 331 } 332 } 333 334 335 const campaigns = [ 336 337 { 338 owner: "wa", 339 selector: ".fff-wa-flyout-box", 340 closeSelector: ".fff-wa-close", 341 storageKey: "fff_wa_flyout_closed_until", 342 closeDays: 7, 343 clickDays: 30, 344 eligible: function () { 345 return true; 346 } 347 }, 348 349 { 350 owner: "nl", 351 selector: ".fff-nl-flyout-box", 352 closeSelector: ".fff-nl-close", 353 storageKey: "fff_nl_flyout_closed_until", 354 closeDays: 7, 355 clickDays: 45, 356 eligible: function () { 357 return true; 358 } 359 }, 360 361 { 362 owner: "twint", 363 selector: ".fff-twint-flyout-box", 364 closeSelector: ".fff-twint-close", 365 storageKey: "fff_twint_flyout_closed_until", 366 closeDays: 14, 367 clickDays: 30, 368 eligible: isLikelySwissVisitor 369 } 370 371 ]; 372 373 374 function inCooldown(campaign) { 375 376 const until = 377 getMs(campaign.storageKey); 378 379 return ( 380 until > 0 381 && nowMs() < until 382 ); 383 } 384 385 386 function getEligibleCampaigns() { 387 388 return campaigns.filter( 389 function (campaign) { 390 391 const box = 392 document.querySelector( 393 campaign.selector 394 ); 395 396 if ( 397 !box 398 || !campaign.eligible() 399 || inCooldown(campaign) 400 ) { 401 return false; 402 } 403 404 campaign.box = box; 405 406 return true; 407 } 408 ); 409 } 410 411 412 function chooseCampaign(list) { 413 414 if (!list.length) { 415 return null; 416 } 417 418 const last = 419 getText(ROTATION_KEY); 420 421 const index = 422 list.findIndex( 423 function (campaign) { 424 425 return ( 426 campaign.owner 427 === last 428 ); 429 } 430 ); 431 432 return ( 433 index < 0 434 ? list[0] 435 : list[ 436 (index + 1) % list.length 437 ] 438 ); 439 } 440 441 442 function runCampaign(campaign) { 443 444 const box = campaign.box; 445 446 if (!box) { 447 return; 448 } 449 450 if ( 451 box.parentElement 452 !== document.body 453 ) { 454 document.body.appendChild(box); 455 } 456 457 // fff-flyout-keyboard-safety-v1 458 459 460 box.setAttribute("inert", ""); 461 462 463 let stopped = false;
464 let openTimer = null; 465 let autoTimer = null; 466 let retries = 0; 467 468 469 function clearTimers() { 470 471 if (openTimer) { 472 clearTimeout(openTimer); 473 } 474 475 if (autoTimer) { 476 clearTimeout(autoTimer); 477 } 478 479 openTimer = null; 480 autoTimer = null; 481 } 482 483 484 function cooldown(days) { 485 486 setMs( 487 campaign.storageKey, 488 nowMs() + days * DAY_MS 489 ); 490 } 491 492 493 function release() { 494 495 box.classList.remove( 496 "is-visible" 497 ); 498 499 box.setAttribute("inert", ""); 500 501 box.setAttribute( 502 "aria-hidden", 503 "true" 504 ); 505 506 lockRelease( 507 campaign.owner 508 ); 509 } 510 511 512 function stop() { 513 514 stopped = true; 515 clearTimers(); 516 } 517 518 519 function autoHide() { 520 521 if ( 522 stopped 523 || !box.classList.contains( 524 "is-visible" 525 ) 526 ) { 527 return; 528 } 529 530 release(); 531 stop(); 532 } 533 534 535 function startAutoHide() { 536 537 if (stopped) { 538 return; 539 } 540 541 if (autoTimer) { 542 clearTimeout(autoTimer); 543 } 544 545 autoTimer = setTimeout( 546 autoHide, 547 AUTO_HIDE_MS 548 ); 549 } 550 551 552 function cancelAutoHide() { 553 554 if (autoTimer) { 555 clearTimeout(autoTimer); 556 } 557 558 autoTimer = null; 559 } 560 561 562 function open() { 563 564 if ( 565 stopped 566 || hasSeenThisVisit() 567 || inCooldown(campaign) 568 || anyFlyoutVisible() 569 || overlayActive() 570 ) { 571 return false; 572 } 573 574 /* 575 * Passive display: 576 * suppress same campaign for 24 h. 577 */ 578 cooldown(1); 579 580 markSeenThisVisit(); 581 582 setText( 583 ROTATION_KEY, 584 campaign.owner 585 ); 586 587 box.removeAttribute("inert"); 588 589 box.setAttribute( 590 "aria-hidden", 591 "false" 592 ); 593 594 box.classList.add( 595 "is-visible" 596 ); 597 598 lockAcquire( 599 campaign.owner 600 ); 601 602 startAutoHide(); 603 604 return true; 605 } 606 607 608 function attemptOpen() { 609 610 if (stopped) { 611 return; 612 } 613 614 if ( 615 document.visibilityState === "visible" 616 && !overlayActive() 617 && !anyFlyoutVisible() 618 && open() 619 ) { 620 return; 621 } 622 623 retries += 1; 624 625 if (retries >= MAX_RETRIES) { 626 stop(); 627 return; 628 } 629 630 openTimer = setTimeout( 631 attemptOpen, 632 RETRY_MS 633 ); 634 } 635 636 637 box.addEventListener( 638 639 640 641 "focusin", 642 643 644 645 cancelAutoHide 646 647 648 649 ); 650 651 652 653 654 box.addEventListener( 655 656 657 658 "keydown", 659 660 661 662 function (event) { 663 664 665 666 667 if (event.key !== "Escape") { 668 669 670 671 return; 672 673 674 675 } 676 677 678 679 680 event.preventDefault(); 681 682 683 684 685 release(); 686 687 688 689 cooldown(campaign.closeDays); 690 691 692 693 stop(); 694 695 696 697 } 698 699 700 701 ); 702 703 704 705 box.addEventListener( 706 "mouseenter", 707 cancelAutoHide, 708 { passive: true } 709 ); 710 711 712 box.addEventListener( 713 "mouseleave", 714 startAutoHide, 715 { passive: true } 716 ); 717 718 719 box.addEventListener( 720 "focusin", 721 cancelAutoHide 722 ); 723 724 725 box.addEventListener( 726 "focusout", 727 function (event) { 728 729 if ( 730 !event.relatedTarget 731 || !box.contains( 732 event.relatedTarget 733 ) 734 ) { 735 startAutoHide(); 736 } 737 } 738 ); 739 740 741 box.addEventListener( 742 "click", 743 function (event) { 744 745 const target = 746 event.target instanceof Element 747 ? event.target 748 : null; 749 750 if (!target) { 751 return; 752 } 753 754 const close = 755 target.closest( 756 campaign.closeSelector 757 ); 758 759 if (close) { 760 761 event.preventDefault(); 762 event.stopPropagation(); 763 764 release(); 765 766 cooldown( 767 campaign.closeDays 768 ); 769 770 stop(); 771 772 return; 773 } 774 775 const link = 776 target.closest("a"); 777 778 if (link) { 779 780 release(); 781 782 cooldown( 783 campaign.clickDays 784 ); 785 786 stop(); 787 } 788 789 }, 790 true 791 ); 792 793 794 openTimer = setTimeout( 795 attemptOpen, 796 OPEN_DELAY_MS 797 ); 798 } 799 800 801 function boot() { 802 803 if (hasSeenThisVisit()) { 804 return; 805 } 806 807 const campaign = 808 chooseCampaign( 809 getEligibleCampaigns() 810 ); 811 812 if (campaign) { 813 runCampaign(campaign); 814 } 815 } 816 817 818 if ( 819 document.readyState === "loading" 820 ) { 821 822 document.addEventListener( 823 "DOMContentLoaded", 824 boot, 825 { once: true } 826 ); 827 828 } else { 829
830 boot(); 831 } 832 833 834})();
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.