PageSourceSearch

https://fairfuturefoundation.org/wp-content/themes/fff-divi-child/assets/js/fff-flyouts.js?ver=1789004355

js fairfuturefoundation.org collected 2026-10-02 05:57:44 UTC 12,657 bytes, 834 lines download raw bytes

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.