PageSourceSearch

https://donorbox.org/storyblok_pages_assets/application-f89c99745e…a4794217cb78f8ad442cbb0fbb77989c188edf18.js

js donorbox.org collected 2026-09-24 06:03:23 UTC 39,978 bytes, 1,310 lines download raw bytes

1function DonorboxCookie (name, value, days){
2  if (value) {
3    this.set(name, value, days);
4  }
5}
6
7if (!DonorboxCookie.prototype.set) {
8 DonorboxCookie.prototype.set = function (name, value, days) {
9   var expires = "";
10
11   if (days) {
12     var date = new Date();
13     date.setTime(date.getTime() + (days*24*60*60*1000));
14     expires = "; expires=" + date.toUTCString();
15   }
16
17   document.cookie = name + "=" + (value || "")  + expires + "; path=/";
18 }
19}
20
21if (!DonorboxCookie.prototype.get) {
22 DonorboxCookie.prototype.get = function (name) {
23   var cookieArr = document.cookie.split(";");
24
25   for(var i = 0; i < cookieArr.length; i++) {
26     var cookiePair = cookieArr[i].split("=");
27     if(name == cookiePair[0].trim()) {
28       return decodeURIComponent(cookiePair[1]);
29     }
30   }
31   return null;
32 }
33};
34const accordion_selectors = [ ".accordion_container details.accordion_item" ];
35const ANIMATION_DURATION = 300;
36const EASING = "cubic-bezier(0.4, 0, 0.2, 1)";
37
38function getContent(item) {
39  return item.querySelector('.accordion_item_content');
40}
41
42function animateHeight(content, from, to, onDone) {
43  content.style.overflow = 'hidden';
44  content.style.height = from + 'px';
45  content.style.transition = `height ${ANIMATION_DURATION}ms ${EASING}, opacity ${ANIMATION_DURATION}ms ${EASING}`;
46  content.style.opacity = from === 0 ? '0' : '1';
47
48  requestAnimationFrame(() => {
49    requestAnimationFrame(() => {
50      content.style.height = to + 'px';
51      content.style.opacity = to === 0 ? '0' : '1';
52    });
53  });
54
55  const cleanup = (event) => {
56    if (event.propertyName !== 'height') return;
57    content.removeEventListener('transitionend', cleanup);
58    content.style.transition = '';
59    content.style.overflow = '';
60    content.style.height = '';
61    content.style.opacity = '';
62    if (typeof onDone === 'function') onDone();
63  };
64  content.addEventListener('transitionend', cleanup);
65}
66
67function expand(item, onDone) {
68  const content = getContent(item);
69  if (!content) {
70    item.setAttribute('open', '');
71    if (typeof onDone === 'function') onDone();
72    return;
73  }
74  item.setAttribute('open', '');
75  const target = content.scrollHeight;
76  animateHeight(content, 0, target, onDone);
77}
78
79function collapse(item, onDone) {
80  const content = getContent(item);
81  if (!content) {
82    item.removeAttribute('open');
83    if (typeof onDone === 'function') onDone();
84    return;
85  }
86  const start = content.scrollHeight;
87  animateHeight(content, start, 0, () => {
88    item.removeAttribute('open');
89    if (typeof onDone === 'function') onDone();
90  });
91}
92
93function bindSmoothToggle(items, singleOpen) {
94  items.forEach(item => {
95    const summary = item.querySelector('summary');
96    if (!summary || summary.dataset.smoothBound === '1') return;
97    summary.dataset.smoothBound = '1';
98
99    summary.addEventListener('click', (event) => {
100      event.preventDefault();
101      if (item.dataset.animating === '1') return;
102      item.dataset.animating = '1';
103
104      const finish = () => { item.dataset.animating = ''; };
105
106      if (item.hasAttribute('open')) {
107        collapse(item, finish);
108      } else {
109        if (singleOpen) {
110          items.forEach(other => {
111            if (other !== item && other.hasAttribute('open')) collapse(other);
112          });
113        }
114        expand(item, finish);
115      }
116    });
117  });
118}
119
120function faqDefaultOpen(items, faqDefaultOpenIndex) {
121  const index = parseInt(faqDefaultOpenIndex, 10);
122  if (Number.isNaN(index)) return;
123  if (index > 0 && index <= items.length) {
124    items[index - 1].setAttribute('open', '');
125  }
126}
127
128document.addEventListener('DOMContentLoaded', () => {
129  const section_selectors = [ ".accordion_container" ];
130  const faqSections = document.querySelectorAll(section_selectors.join(','));
131  if (faqSections.length === 0) return;
132
133  const items = document.querySelectorAll(accordion_selectors.join(','));
134  if (items.length === 0) return;
135
136  let singleOpen = false;
137  faqSections.forEach(faqSection => {
138    if (faqSection.getAttribute('data-accordion-behavior') === 'single_open') {
139      singleOpen = true;
140    }
141    const faqDefaultOpenIndex = faqSection.getAttribute('data-accordion-open-index');
142    if (faqDefaultOpenIndex) {
143      faqDefaultOpen(items, faqDefaultOpenIndex);
144    }
145  });
146
147  bindSmoothToggle(items, singleOpen);
148});
149/**
150 * Reusable carousel: configurable autoplay and slide interval via data attributes.
151 * Use on any container with [data-carousel]. Works with testimonials, case studies, pricing, etc.
152 *
153 * Data attributes:
154 *   data-carousel                    - enables carousel behavior
155 *   data-carousel-autoplay="true"    - enable automatic sliding (default: false)
156 *   data-carousel-interval="5000"     - autoplay interval in ms (default: 5000)
157 *
158 * Expected DOM structure:
159 *   [data-carousel]  (use overflow: hidden on this or on .carousel__viewport)
160 *     .carousel__track
161 *       .carousel__slide (repeated)
162 *   Optional: .carousel__prev, .carousel__next, .carousel-dot (per slide)
163 */
164
165const DEFAULT_INTERVAL_MS = 5000;
166
167function parseBool(value) {
168  if (value == null || value === "") return false;
169  return String(value).toLowerCase() === "true" || String(value) === "1";
170}
171
172function parsePositiveInt(value, fallback) {
173  const n = parseInt(value, 10);
174  return Number.isFinite(n) && n > 0 ? n : fallback;
175}
176
177function initCarousel(root) {
178  const track = root.querySelector(".carousel__track");
179  const slides = root.querySelectorAll(".carousel__slide");
180  const prevBtn = root.querySelector(".carousel__prev");
181  const nextBtn = root.querySelector(".carousel__next");
182  const dots = root.querySelectorAll(".carousel-dot");
183
184  if (!track || !slides.length) return null;
185
186  const total = slides.length;
187  let currentIndex = 0;
188  let autoplayTimer = null;
189
190  const autoplay = parseBool(root.dataset.carouselAutoplay);
191  const intervalMs = parsePositiveInt(
192    root.dataset.carouselInterval,
193    DEFAULT_INTERVAL_MS
194  );
195
196  function goTo(index) {
197    currentIndex = ((index % total) + total) % total;
198    const offset = -currentIndex * 100;
199    track.style.transform = `translate3d(${offset}%, 0, 0)`;
200    dots.forEach((d, i) => d.classList.toggle("is-active", i === currentIndex));
201  }
202
203  function next() {
204    goTo(currentIndex + 1);
205  }
206
207  function prev() {
208    goTo(currentIndex - 1);
209  }
210
211  function startAutoplay() {
212    stopAutoplay();
213    autoplayTimer = setInterval(next, intervalMs);
214  }
215
216  function stopAutoplay() {
217    if (autoplayTimer) {
218      clearInterval(autoplayTimer);
219      autoplayTimer = null;
220    }
221  }
222
223  if (prevBtn) prevBtn.addEventListener("click", () => { stopAutoplay(); prev(); });
224  if (nextBtn) nextBtn.addEventListener("click", () => { stopAutoplay(); next(); });
225
226  dots.forEach((d, i) => d.addEventListener("click", () => { stopAutoplay(); goTo(i); }));
227
228  if (autoplay) startAutoplay();
229
230  root.addEventListener("mouseenter", stopAutoplay);
231  root.addEventListener("mouseleave", () => { if (autoplay) startAutoplay(); });
232
233  goTo(0);
234  return { goTo, next, prev, startAutoplay, stopAutoplay };
235}
236
237function initAllCarousels() {
238  document.querySelectorAll("[data-carousel]").forEach((el) => {
239    if (!parseBool(el.dataset.carousel)) return;
240    initCarousel(el);
241  });
242}
243
244document.addEventListener("DOMContentLoaded", initAllCarousels);
245function download_file(fileURL, fileName) {
246  // for non-IE
247  if (!window.ActiveXObject) {
248      var save = document.createElement('a');
249      save.href = fileURL;
250      save.target = '_blank';
251      var filename = fileURL.substring(fileURL.lastIndexOf('/')+1);
252      save.download = fileName || filename;
253      if ( navigator.userAgent.toLowerCase().match(/(ipad|iphone|safari)/) &&
254          navigator.userAgent.search("Chrome") < 0) {
255          document.location = save.href;
256          // window event not working here
257      }else{
258          var evt = new MouseEvent('click', {
259              'view': window,
260              'bubbles': true,
261              'cancelable': false
262          });
263          save.dispatchEvent(evt);
264          (window.URL || window.webkitURL).revokeObjectURL(save.href);
265      }
266  }
267
268  // for IE < 11
269  else if ( !! window.ActiveXObject && document.execCommand)     {
270      var _window = window.open(fileURL, '_blank');
271      _window.document.close();
272      _window.document.execCommand('SaveAs', true, fileName || fileURL)
273      _window.close();
274  }
275};
276function copyToClipboard(text) {
277  if (navigator.clipboard && window.isSecureContext) {
278    return navigator.clipboard.writeText(text);
279  }
280}
281
282function showCopiedTooltip(button) {
283  var tooltipSpan = button.querySelector(".tooltip-text");
284  if (!tooltipSpan) return;
285
286  var originalText = tooltipSpan.textContent;
287  tooltipSpan.textContent = "Copied!";
288
289  setTimeout(function () {
290    tooltipSpan.textContent = originalText;
291  }, 2500);
292}
293
294document.addEventListener("DOMContentLoaded", function () {
295  // Select only copy buttons (must have data-url)
296  var copyButtons = document.querySelectorAll('.copy_link[data-url]');
297
298  if (!copyButtons.length) return;
299
300  copyButtons.forEach(function (button) {
301
302    button.addEventListener("click", function (e) {
303      e.preventDefault();
304
305      var url = button.getAttribute("data-url");
306      if (!url) return;
307
308      copyToClipboard(url)
309        .then(function () {
310          showCopiedTooltip(button);
311        })
312        .catch(function () {
313          console.warn("Copy failed");
314        });
315    });
316
317  });
318});
319function validateEmail(value, form) {
320  const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
321  const trimmed = value ? value.trim() : null;
322  if (!trimmed) return form?.dataset?.required ?? null;
323  if (!EMAIL_REGEX.test(trimmed)) return form?.dataset?.validEmail ?? null;
324  return null;
325}
326
327function setMessage(el, text, isSuccess) {
328  if (!el) return;
329  el.textContent = text || "";
330  el.classList.remove("success", "error");
331  if (text) el.classList.add(isSuccess ? "success" : "error");
332}
333
334function initSubscribeForm(form) {
335  const email = form.querySelector('input[name="email"]');
336  const submitBtn = form.querySelector('input[type="submit"]');
337  const messageEl = document.querySelector(".subscribe-form-message");
338  const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute("content");
339  const successMsg = form.dataset.subscribeSuccess || "";
340  const errorMsg = form.dataset.subscribeError || "";
341
342  function setSubmitting(submitting) {
343    if (!submitBtn) return;
344    submitBtn.disabled = submitting;
345    submitBtn.value = submitting ? submitBtn.dataset.waiting : submitBtn.dataset.buttonLabel;
346  }
347
348  function clearMessage() {
349    setMessage(messageEl, "", false);
350  }
351
352  form.addEventListener("submit", (e) => {
353    e.preventDefault();
354    clearMessage();
355    const emailValue = email && email.value ? email.value.trim() : "";
356    const error = validateEmail(emailValue, form);
357    if (error) {
358      setMessage(messageEl, error, false);
359      return;
360    }
361    setSubmitting(true);
362    const formType = form.querySelector('input[name="form_type"]').value;
363
364    fetch(form.action, {
365      method: "POST",
366      headers: { "X-CSRF-Token": csrfToken, "Content-Type": "application/json", "Accept": "application/json" },
367      body: JSON.stringify({ email: emailValue, form_type: formType })
368    })
369    .then((res) => {
370      const isSuccess = res.status === 200;
371      setMessage(messageEl, isSuccess ? successMsg : errorMsg, isSuccess);
372
373      if (isSuccess) {
374        form.reset();
375        form.classList.add("hidden");
376      }
377    })
378    .catch(() => setMessage(messageEl, errorMsg, false))
379    .finally(() => setSubmitting(false));
380  });
381
382  if (email) {
383    email.addEventListener("input", clearMessage);
384    email.addEventListener("blur", clearMessage);
385  }
386}
387
388document.addEventListener("DOMContentLoaded", function() {
389  document.querySelectorAll(".subscribe-form").forEach(initSubscribeForm);
390});
391var scrollEventDebounce = null
392
393function headerScrollEvent () {
394    clearTimeout(scrollEventDebounce);
395
396    debounce = setTimeout(function() {
397        var docBody = document.body; //IE 'quirks'
398        var docElem = document.documentElement; //IE with doctype
399        docElem = (docElem.clientHeight) ? docElem : docBody;
400
401        if (typeof app_header !== 'undefined') {
402            if (docElem.scrollTop === 0) {
403                app_header.classList.remove('header_active');
404            } else {
405                app_header.classList.add('header_active');
406            }
407        }
408
409    },10);
410}
411
412window.addEventListener("scroll", headerScrollEvent, {passive: true});
413const VideoPlayer = (function() {
414    const selectors = {
415        playButton: '.video_player_play_btn',
416        popup: '#video_popup',
417        iframes: '#video_popup iframe, .video_player_iframe'
418    };
419
420    const config = {
421        maxWidth: 1024,
422        viewportRatio: 0.88,
423        aspectRatio: 9 / 16
424    };
425
426    function createElement(tag, attributes) {
427        var element = document.createElement(tag);
428        Object.keys(attributes || {}).forEach(function(key) {
429            if (key === 'className') {
430                element.className = attributes[key];
431            } else {
432                element.setAttribute(key, attributes[key]);
433            }
434        });
435        return element;
436    }
437
438    function calculateDimensions() {
439        var viewportWidth = window.innerWidth * config.viewportRatio;
440        var width = Math.min(viewportWidth, config.maxWidth);
441        return {
442            width: width,
443            height: width * config.aspectRatio
444        };
445    }
446
447    function getVideoPadding(iframe) {
448        var container = iframe.closest('.video_player_container');
449        if (!container) return 0;
450
451        var padding = window.getComputedStyle(container).paddingLeft;
452        var value = parseFloat(padding);
453
454        return isNaN(value) ? 0 : value;
455    }
456
457    function setIframeSize() {
458        var iframes = document.querySelectorAll(selectors.iframes);
459        if (!iframes.length) return;
460
461        var dimensions = calculateDimensions();
462
463        iframes.forEach(function(iframe) {
464            var padding = getVideoPadding(iframe);
465            var width = Math.max(dimensions.width - (padding * 2), 0);
466            var height = width * config.aspectRatio;
467
468            iframe.style.width = width + 'px';
469            iframe.style.height = height + 'px';
470            if (iframe.parentElement) {
471                iframe.parentElement.style.width = dimensions.width + 'px';
472            }
473        });
474    }
475
476    function createPopup(videoSrc) {
477        var wrapper = createElement('section');
478        var popup = createElement('div', { id: 'video_popup' });
479        var videoWrapper = createElement('div', { className: 'video-wrapper' });
480        var iframe = createElement('iframe', {
481            id: 'popup_video_iframe',
482            src: videoSrc,
483            frameborder: '0',
484            allow: 'autoplay; fullscreen',
485            loading: 'lazy'
486        });
487        var closeBtn = createElement('button', { id: 'popup_video_close' });
488
489        videoWrapper.append(iframe, closeBtn);
490        popup.append(videoWrapper);
491        wrapper.append(popup);
492        document.body.append(wrapper);
493
494        popup.style.display = 'block';
495        setIframeSize();
496
497        closeBtn.addEventListener('click', function() {
498            popup?.parentElement?.remove();
499        });
500    }
501
502    function closePopupOnOutsideClick(event) {
503        if (event.target.id === 'video_popup') {
504            event.target.remove();
505        }
506    }
507
508    function initPlayButton(button) {
509        button.addEventListener('click', function() {
510            var videoSrc = button.getAttribute('data-src');
511            if (videoSrc) {
512                removeExistingPopups();
513                createPopup(videoSrc);
514            }
515        });
516    }
517
518    function removeExistingPopups() {
519        var popup = document.querySelectorAll(selectors.popup);
520        popup.forEach(function(popup) {
521            popup?.parentElement?.remove();
522        });
523    }
524
525    function init() {
526        var playButtons = document.querySelectorAll(selectors.playButton);
527        playButtons.forEach(initPlayButton);
528
529        window.addEventListener('click', closePopupOnOutsideClick);
530        window.addEventListener('load', setIframeSize);
531        window.addEventListener('resize', setIframeSize);
532    }
533
534    init();
535
536    return {
537        setIframeSize: setIframeSize,
538        init: init
539    };
540})();
541
542document.addEventListener('turbo:load', VideoPlayer.init);
543document.addEventListener('turbo:frame-load', VideoPlayer.init);
544(function(window, document) {
545  const tabNavSelector = ".tabs__nav";
546  const tabRadioSelector = 'input[type="radio"][name^="tab_"]';
547
548  function getTabElements(radioButton) {
549    const tabId = radioButton.getAttribute("aria-controls");
550    return {
551      tabContent: tabId ? document.getElementById(tabId) : null,
552      tabLabel: tabId ? document.getElementById(`${tabId}_label`) : null
553    };
554  }
555
556  function hideTab(radioButton) {
557    const { tabContent, tabLabel } = getTabElements(radioButton);
558
559    if (tabContent) tabContent.classList.add("hidden");
560    if (tabLabel) {
561      tabLabel.classList.remove("active");
562      tabLabel.setAttribute("aria-selected", "false");
563    }
564  }
565
566  function showTab(radioButton) {
567    const { tabContent, tabLabel } = getTabElements(radioButton);
568
569    if (tabContent) tabContent.classList.remove("hidden");
570    if (tabLabel) {
571      tabLabel.classList.add("active");
572      tabLabel.setAttribute("aria-selected", "true");
573    }
574  }
575
576  function hideSiblingTabs(selectedRadioButton) {
577    const tabNav = selectedRadioButton.closest(tabNavSelector);
578    if (!tabNav) return;
579
580    tabNav.querySelectorAll(tabRadioSelector).forEach((radioButton) => {
581      if (radioButton !== selectedRadioButton) hideTab(radioButton);
582    });
583  }
584
585  function handleTabChange(event) {
586    const radioButton = event.currentTarget;
587    if (!radioButton) return;
588
589    if (radioButton.checked) {
590      hideSiblingTabs(radioButton);
591      showTab(radioButton);
592    } else {
593      hideTab(radioButton);
594    }
595  }
596
597  function initTabs() {
598    document.querySelectorAll(tabNavSelector).forEach((tabNav) => {
599      tabNav.querySelectorAll(tabRadioSelector).forEach((radioButton) => {
600        radioButton.addEventListener("change", handleTabChange);
601
602        if (radioButton.checked) {
603          hideSiblingTabs(radioButton);
604          showTab(radioButton);
605        } else {
606          hideTab(radioButton);
607        }
608      });
609    });
610  }
611
612  document.addEventListener("DOMContentLoaded", initTabs);
613})(window, document);
614document.addEventListener("DOMContentLoaded", () => {
615  const carouselContainers = document.querySelectorAll("[data-card-carousel=true]")
616  carouselContainers.forEach(carouselContainer => {
617
618    const carousel = carouselContainer.querySelector(".card_wrapper")
619    const items = carousel.querySelectorAll(".resource_card")
620    const carouselStyle = getComputedStyle(carousel)
621    const gap = parseInt((carouselStyle?.getPropertyValue("gap") ?? 16).replace("px", ""))
622
623    let index = 0
624
625    function itemWidth() {
626      return items[0].offsetWidth + gap
627    }
628
629    function itemsPerView() {
630      return Math.round(carousel.clientWidth / items[0].offsetWidth)
631    }
632
633    function autoSlide(carouselContainer) {
634      let timer = setTimeout(() => {
635        carouselContainer.querySelector(".next_nav").click();
636        timer = autoSlide(carouselContainer);
637      }, 5000);
638
639      return {
640        timer,
641        clear: () => clearTimeout(timer)
642      };
643    }
644
645    carouselContainer.querySelector(".next_nav").onclick = () => {
646      const maxIndex = items.length - itemsPerView()
647
648      index = index + 1
649
650      if (index > maxIndex) index = 0
651
652      carousel.scrollTo({
653        left: index * itemWidth(),
654        behavior: "smooth"
655      })
656    }
657
658    carouselContainer.querySelector(".prev_nav").onclick = () => {
659      const maxIndex = items.length - itemsPerView()
660
661      index = index - 1
662
663      if (index < 0) index = maxIndex
664
665      carousel.scrollTo({
666        left: index * itemWidth(),
667        behavior: "smooth"
668      })
669    }
670
671    let autoSlideObj;
672    if (parseBool(carouselContainer.dataset.cardCarouselAutoplay)) {
673      autoSlideObj = autoSlide(carouselContainer);
674    } else {
675      autoSlideObj?.clear();
676    }
677
678    window.addEventListener("resize", () => {
679      index = 0
680      carousel.scrollTo({ left: 0, behavior: "smooth" })
681    })
682  });
683});
684document.addEventListener('scroll', (event) => {
685  const floatingCta = document.querySelector('.floating_cta');
686  if (floatingCta) {
687    const scrollOffset = floatingCta.dataset.scrollOffset;
688    const scrollPosition = event.target.documentElement.scrollTop || event.target.body.scrollTop;
689
690    if (scrollPosition > scrollOffset) {
691      floatingCta.classList.add('visible');
692    } else {
693      floatingCta.classList.remove('visible');
694    }
695  }
696});
697(function () {
698  const UTM_PARAMS = ['utm_source','utm_medium','utm_campaign','utm_term','utm_content','utm_org', 'keyword', 'original_url'];
699  const ADS_PARAMS = ['gclid','li_fat_id'];
700  const ALL_PARAMS = [...UTM_PARAMS, ...ADS_PARAMS];
701  const INTERNAL_HOSTS = ['donorbox.org', 'donatetools.com'];
702  const IGNORED_PREFIXES = ['#', 'mailto:', 'tel:', 'javascript:'];
703  const fromUrl = new URLSearchParams(location.search);
704  const fromCookie = Object.fromEntries(
705    document.cookie.split('; ').map(cookie => {
706      const [key, ...rest] = cookie.split('=');
707
708      const value = decodeURIComponent(
709        rest.join('=').replace(/\+/g, ' ')
710      );
711
712      return [key, value];
713    })
714  );
715  const forward = {};
716  const hasUtmSourceCookie = Boolean(fromCookie['utm_source']);
717  const pageSlug = document.body.dataset.landingPageSlug;
718  const attributionParamsPresent = document.body.dataset.attributionParamsPresent === "true";
719  const allowedParams = hasUtmSourceCookie ? ALL_PARAMS : ADS_PARAMS;
720  allowedParams.forEach(k => {
721    const v = fromUrl.get(k) || fromCookie[k];
722    if (v) forward[k] = v;
723  });
724
725  function forwardAttributionParams(url, linkEl) {
726    const u = new URL(url, location.origin);
727    if (u.host && !INTERNAL_HOSTS.includes(u.host) && u.origin !== location.origin) return url;
728    if (IGNORED_PREFIXES.some(prefix => url.startsWith(prefix))) return url;
729
730    if (attributionParamsPresent) {
731      // existing forward map (paid UTMs + ads)
732      Object.entries(forward).forEach(([k, v]) => {
733        if (!u.searchParams.has(k)) u.searchParams.set(k, v);
734      });
735    } else {
736      // ads only (existing)
737      ADS_PARAMS.forEach(k => {
738        const v = fromUrl.get(k) || fromCookie[k];
739        if (v && !u.searchParams.has(k)) u.searchParams.set(k, v);
740      });
741      // organic landing-page defaults
742      if (pageSlug) {
743        if (!u.searchParams.has("utm_source")) u.searchParams.set("utm_source", "donorbox-website");
744        if (!u.searchParams.has("utm_medium")) u.searchParams.set("utm_medium", "landing-page");
745        if (!u.searchParams.has("utm_campaign")) u.searchParams.set("utm_campaign", pageSlug);
746      }
747      const ctaPosition = linkEl?.dataset?.ctaPosition;
748      if (ctaPosition && !u.searchParams.has("utm_content")) u.searchParams.set("utm_content", ctaPosition);
749    }
750
751    return u.toString();
752  }
753
754  document.addEventListener("DOMContentLoaded", () => {
755    document.querySelectorAll('a[href]').forEach(a => {
756      a.href = forwardAttributionParams(a.href, a);
757    });
758  });
759
760  document.addEventListener("turbo:frame-load", (event) => {
761    const frame = event.target;
762    frame.querySelectorAll('a[href]').forEach(a => {
763      a.href = forwardAttributionParams(a.href, a);
764    });
765  });
766
767  function decorateLinksIn(root) {
768    if (!root || typeof root.querySelectorAll !== 'function') return;
769    root.querySelectorAll('a[href]').forEach(a => {
770      a.href = forwardAttributionParams(a.href, a);
771    });
772  }
773
774  document.addEventListener("turbo:before-stream-render", (event) => {
775    const originalRender = event.detail.render;
776
777    event.detail.render = async function (streamElement) {
778      await originalRender(streamElement);
779
780      const targets = typeof streamElement.targetElements === 'function'
781        ? streamElement.targetElements()
782        : (streamElement.targetElements || []);
783
784      targets.forEach(decorateLinksIn);
785    };
786  });
787})();
788(function () {
789    'use strict';
790
791    const MODAL_OPEN_CLASS = 'modal-open';
792    const TRIGGER_SELECTOR = '[data-modal-open]';
793    const MODAL_SELECTOR   = '.popup_modal';
794    const CLOSE_SELECTOR   = '.close';
795
796    function openModal(modal) {
797        if (!modal) return;
798
799        modal.style.display = 'flex';
800        modal.style.display = '-ms-flexbox';
801        document.body.classList.add(MODAL_OPEN_CLASS);
802
803        document.addEventListener('keydown', handleEscape);
804    }
805
806    function closeModal(modal) {
807        if (!modal) return;
808
809        modal.style.display = 'none';
810        document.body.classList.remove(MODAL_OPEN_CLASS);
811
812        document.removeEventListener('keydown', handleEscape);
813    }
814
815    function handleEscape(event) {
816        if (event.key !== 'Escape') return;
817
818        document.querySelectorAll(MODAL_SELECTOR).forEach(function (modal) {
819            if (modal.style.display !== 'none' && modal.style.display !== '') {
820                closeModal(modal);
821            }
822        });
823    }
824
825    function bindTriggers() {
826        document.querySelectorAll(TRIGGER_SELECTOR).forEach(function (trigger) {
827            trigger.addEventListener('click', function (event) {
828                event.preventDefault();
829                var modal = document.getElementById(trigger.dataset.modalId);
830                openModal(modal);
831            });
832        });
833    }
834
835    function bindCloseButtons() {
836        document.querySelectorAll(MODAL_SELECTOR + ' ' + CLOSE_SELECTOR).forEach(function (closeBtn) {
837            closeBtn.addEventListener('click', function () {
838                var modal = closeBtn.closest(MODAL_SELECTOR);
839                closeModal(modal);
840            });
841        });
842    }
843
844    function bindOverlayClose() {
845        window.addEventListener('click', function (event) {
846            if (event.target.classList.contains('popup_modal')) {
847                closeModal(event.target);
848            }
849        });
850    }
851
852    function init() {
853        bindTriggers();
854        bindCloseButtons();
855        bindOverlayClose();
856    }
857
858    if (document.readyState === 'loading') {
859        document.addEventListener('DOMContentLoaded', init);
860    } else {
861        init();
862    }
863})();
864document.addEventListener("DOMContentLoaded", () => {
865  const marqueeCarousels = document.querySelectorAll("[data-marquee-carousel]");
866
867  marqueeCarousels.forEach(marqueeCarousel => {
868    const marqueeTrack = marqueeCarousel.querySelector(".marquee_track");
869    const marqueeItems = marqueeTrack.querySelector("div");
870    marqueeItems.innerHTML += marqueeItems.innerHTML;
871  });
872});
873(function () {
874  'use strict';
875
876  const SCRIPT_ID = 'demio-js';
877  const SCRIPT_SRC = 'https://cdn.demio.com/production/dashboard/embed.bundle.js';
878  const CONTAINER_SELECTOR = '.demio_form_container';
879  const STORYBLOK_BODY_ID = 'storyblok-body';
880
881  // Demio's embed bundle scans the DOM and mounts forms on first execution
882  // only. In Storyblok preview mode the bridge replaces #storyblok-body via
883  // innerHTML on every edit, so the already-executed bundle does not see the
884  // new placeholder. Re-inject the script tag — a fresh load causes the
885  // bundle to re-scan and mount the updated form.
886  function reloadDemioBundle() {
887      var existing = document.getElementById(SCRIPT_ID);
888      if (existing) existing.remove();
889
890      var script = document.createElement('script');
891      script.async = true;
892      script.id = SCRIPT_ID;
893      script.type = 'text/javascript';
894      script.src = SCRIPT_SRC;
895      document.body.appendChild(script);
896  }
897
898  function containerWasAdded(mutations) {
899      for (var i = 0; i < mutations.length; i++) {
900          var added = mutations[i].addedNodes;
901          for (var j = 0; j < added.length; j++) {
902              var node = added[j];
903              if (node.nodeType !== 1) continue;
904              if (node.matches && node.matches(CONTAINER_SELECTOR)) return true;
905              if (node.querySelector && node.querySelector(CONTAINER_SELECTOR)) return true;
906          }
907      }
908      return false;
909  }
910
911  function watchForPreviewUpdates() {
912      var body = document.getElementById(STORYBLOK_BODY_ID);
913      if (!body || typeof MutationObserver === 'undefined') return;
914
915      var observer = new MutationObserver(function (mutations) {
916          if (containerWasAdded(mutations)) reloadDemioBundle();
917      });
918
919      observer.observe(body, { childList: true, subtree: true });
920  }
921
922  if (document.readyState === 'loading') {
923      document.addEventListener('DOMContentLoaded', watchForPreviewUpdates);
924  } else {
925      watchForPreviewUpdates();
926  }
927})();
928document.addEventListener('DOMContentLoaded', () => {
929  document.querySelectorAll('.image_grid_expand_btn').forEach(btn => {
930    btn.addEventListener('click', () => {
931      const container = btn.closest('.image_grid').querySelector('.image_grid_container');
932      container.classList.remove('is_collapsed');
933      container.classList.add('is_expanded');
934      btn.remove()
935    });
936  });
937});
938const TIMEZONE_MAP = {
939  ET:   "America/New_York",
940  CT:   "America/Chicago",
941  PT:   "America/Los_Angeles",
942  CET:  "Europe/Berlin",
943  AEST: "Australia/Sydney",
944};
945
946const formatterFor = (timeZone) =>
947  new Intl.DateTimeFormat("en-US", {
948    timeZone,
949    month: "long",
950    day: "numeric",
951    year: "numeric",
952    hour: "2-digit",
953    minute: "2-digit",
954    hour12: false,
955  });
956
957const formatWebinarDate = (formatter, date) =>
958  formatter.format(date).replace(" at ", " ");
959
960document.addEventListener("change", (e) => {
961  if (!e.target.matches("select[name='timezone']")) return;
962
963  const label = e.target.value || "ET";
964  const zone = TIMEZONE_MAP[label] || TIMEZONE_MAP["ET"];
965  const formatter = formatterFor(zone);
966
967  document.querySelectorAll(".webinar_details .webinar_date").forEach((el) => {
968    const iso = el.dataset.datetime;
969    if (!iso) return;
970
971    const date = new Date(iso);
972    if (Number.isNaN(date.getTime())) return;
973
974    el.innerHTML =
975      `${formatWebinarDate(formatter, date)} ` +
976      `<span class="webinar_timezone">${label}</span>`;
977  });
978});
979const caseStudyContainer = document.querySelector(".case_study_listing");
980
981const observer = new MutationObserver((mutations) => {
982  mutations.forEach((mutation) => {
983    mutation.addedNodes.forEach((node) => {
984      if (node.nodeType === Node.ELEMENT_NODE && node.matches(".case_study_card")) {
985        VideoPlayer.init();
986      }
987    });
988  });
989});
990
991if (caseStudyContainer) {
992  observer.observe(caseStudyContainer, {
993    childList: true,
994    subtree: true
995  });
996};
997document.addEventListener("DOMContentLoaded", function() {
998  const languageSelector = document.querySelectorAll("select[name='language_selector']");
999  if (languageSelector.length === 0) return;
1000
1001  languageSelector.forEach(function(select) {
1002    select.addEventListener("change", function() {
1003      window.location.href = select.value;
1004    });
1005  });
1006});
1007(function(window, document) {
1008  const TOC_ROOT_SELECTOR = "[data-toc-enabled='true']";
1009  const TOC_PANEL_SELECTOR = "[data-toc-panel='true']";
1010  const TOC_SOURCE_SELECTOR = "[data-toc-source='true']";
1011  const TOC_ITEMS_SELECTOR = "[data-toc-items='true']";
1012  const TOC_ENTRY_SELECTOR = "[data-toc-title][data-toc-id]";
1013  const TOC_LINK_SELECTOR = "[data-toc-link='true']";
1014  const TOC_DETAILS_SELECTOR = ".case_study_content__toc-details";
1015  const TABLET_MEDIA_QUERY = "(max-width: 768px)";
1016  const SCROLL_OFFSET = 140;
1017
1018  function syncTocDetailsState(root) {
1019    const details = root.querySelector(TOC_DETAILS_SELECTOR);
1020    if (!details) return;
1021
1022    const setOpenState = () => {
1023      if (window.matchMedia(TABLET_MEDIA_QUERY).matches) {
1024        details.removeAttribute("open");
1025      } else {
1026        details.setAttribute("open", "");
1027      }
1028    };
1029
1030    setOpenState();
1031    window.matchMedia(TABLET_MEDIA_QUERY).addEventListener("change", setOpenState);
1032  }
1033
1034  function setActiveLink(itemsList, id) {
1035    if (!id) return;
1036
1037    itemsList.querySelectorAll(TOC_LINK_SELECTOR).forEach(link => {
1038      link.classList.toggle("active", link.getAttribute("href") === `#${id}`);
1039    });
1040  }
1041
1042  function createTocItem(title, id) {
1043    const listItem = document.createElement("li");
1044    listItem.setAttribute("data-toc-item", "true");
1045
1046    const link = document.createElement("a");
1047    link.setAttribute("data-toc-link", "true");
1048    link.href = `#${id}`;
1049    link.textContent = title;
1050
1051    listItem.appendChild(link);
1052    return listItem;
1053  }
1054
1055  function bindTocItemClicks(itemsList) {
1056    itemsList.querySelectorAll(TOC_LINK_SELECTOR).forEach(link => {
1057      link.addEventListener("click", (event) => {
1058        event.preventDefault();
1059
1060        const targetId = link.getAttribute("href")?.slice(1);
1061        if (!targetId) return;
1062
1063        setActiveLink(itemsList, targetId);
1064
1065        const target = document.getElementById(targetId);
1066        if (target) {
1067          const targetRect = target.getBoundingClientRect();
1068          const absoluteElementTop = window.pageYOffset + targetRect.top;
1069          const offset = 120;
1070          window.scrollTo({
1071            top: absoluteElementTop - offset,
1072            behavior: "smooth"
1073          });
1074        }
1075      });
1076    });
1077  }
1078
1079  function bindScrollSpy(itemsList, entries) {
1080    const sections = Array.from(entries).filter(entry => entry.id);
1081    if (sections.length === 0) return;
1082
1083    let ticking = false;
1084
1085    const updateActiveLink = () => {
1086      let activeId = sections[0].id;
1087
1088      sections.forEach(section => {
1089        if (section.getBoundingClientRect().top <= SCROLL_OFFSET) {
1090          activeId = section.id;
1091        }
1092      });
1093
1094      setActiveLink(itemsList, activeId);
1095    };
1096
1097    window.addEventListener("scroll", () => {
1098      if (ticking) return;
1099
1100      ticking = true;
1101      window.requestAnimationFrame(() => {
1102        updateActiveLink();
1103        ticking = false;
1104      });
1105    }, { passive: true });
1106
1107    updateActiveLink();
1108  }
1109
1110  function buildTocForRoot(root) {
1111    const panel = root.querySelector(TOC_PANEL_SELECTOR);
1112    const source = root.querySelector(TOC_SOURCE_SELECTOR);
1113    if (!panel || !source) return;
1114
1115    const entries = source.querySelectorAll(TOC_ENTRY_SELECTOR);
1116    if (entries.length === 0) return;
1117
1118    let itemsList = panel.querySelector(TOC_ITEMS_SELECTOR);
1119    if (!itemsList) {
1120      itemsList = document.createElement("ul");
1121      itemsList.setAttribute("data-toc-items", "true");
1122      panel.appendChild(itemsList);
1123    }
1124
1125    itemsList.innerHTML = "";
1126
1127    entries.forEach(entry => {
1128      const title = entry.getAttribute("data-toc-title");
1129      const id = entry.getAttribute("data-toc-id");
1130      if (!title || !id) return;
1131
1132      if (!entry.id) entry.id = id;
1133
1134      itemsList.appendChild(createTocItem(title, id));
1135    });
1136
1137    bindTocItemClicks(itemsList);
1138    bindScrollSpy(itemsList, entries);
1139    syncTocDetailsState(root);
1140  }
1141
1142  function initTableOfContent() {
1143    document.querySelectorAll(TOC_ROOT_SELECTOR).forEach(buildTocForRoot);
1144  }
1145
1146  window.TableOfContent = { init: initTableOfContent };
1147  document.addEventListener("DOMContentLoaded", initTableOfContent);
1148})(window, document);
1149function initNavBarIntent() {
1150  var sticky_nav_bar = document.getElementById("sticky_nav_bar");
1151
1152  if (sticky_nav_bar) {
1153    var sticky_nav_bar_close = document.getElementById("sticky_nav_bar_close_icon");
1154    var currentVersion = sticky_nav_bar.dataset.version;
1155    var dismissedVersion = new DonorboxCookie().get('sticky_nav_bar_version');
1156    var isDismissed = new DonorboxCookie().get('sticky_nav_bar');
1157
1158    function hideStickyNavBar() {
1159      new DonorboxCookie('sticky_nav_bar', true, 3);
1160      new DonorboxCookie('sticky_nav_bar_version', currentVersion, 3);
1161      sticky_nav_bar.classList.add("d-none");
1162    }
1163
1164    function showStickyNavBar() {
1165      sticky_nav_bar.classList.remove("d-none");
1166    }
1167
1168    if (sticky_nav_bar_close) {
1169      sticky_nav_bar_close.onclick = function () {
1170        hideStickyNavBar();
1171      };
1172    }
1173
1174    if (dismissedVersion === currentVersion && isDismissed) {
1175      sticky_nav_bar.classList.add("d-none");
1176    } else {
1177      showStickyNavBar();
1178    }
1179  }
1180}
1181
1182document.addEventListener("turbo:load", initNavBarIntent);
1183document.addEventListener("DOMContentLoaded", initNavBarIntent);
1184(function (window, document) {
1185  const ROOT_SELECTOR = "[data-crm-contact-tier-pricing]";
1186
1187  function parseTiers(root) {
1188    try {
1189      const tiers = JSON.parse(root.dataset.tiers || "[]");
1190      return Array.isArray(tiers) ? tiers : [];
1191    } catch (_error) {
1192      return [];
1193    }
1194  }
1195
1196  function updateSliderFill(slider) {
1197    const min = Number(slider.min) || 0;
1198    const max = Number(slider.max) || 100;
1199    const value = ((Number(slider.value) - min) / (max - min || 1)) * 100;
1200    slider.style.background =
1201      "linear-gradient(to right, #128AED " + value + "%, #ABC1E3 " + value + "%)";
1202  }
1203
1204  function setVisible(element, visible) {
1205    if (!element) return;
1206    if (visible) {
1207      element.removeAttribute("hidden");
1208      element.style.display = "";
1209    } else {
1210      element.setAttribute("hidden", "hidden");
1211      element.style.display = "none";
1212    }
1213  }
1214
1215  function updateBilling(root) {
1216    const slider = root.querySelector(".crm_contact_tier_pricing__range");
1217    const tiers = parseTiers(root);
1218    if (!slider || tiers.length === 0) return;
1219
1220    const step = Number(slider.step) || 100;
1221    const index = Math.min(Math.round(Number(slider.value) / step), tiers.length - 1);
1222    const tier = tiers[index] || {};
1223    const period = root.dataset.period || "monthly";
1224    const monthlyInfo = root.querySelector('[data-billing-info="monthly"]');
1225    const annualInfo = root.querySelector('[data-billing-info="annually"]');
1226    const contactSales = root.querySelector("[data-contact-sales]");
1227
1228    updateSliderFill(slider);
1229    slider.setAttribute("aria-valuenow", String(index));
1230
1231    if (tier.show_contact_sales) {
1232      setVisible(monthlyInfo, false);
1233      setVisible(annualInfo, false);
1234      setVisible(contactSales, true);
1235      return;
1236    }
1237
1238    setVisible(contactSales, false);
1239
1240    if (period === "monthly") {
1241      const amount = monthlyInfo && monthlyInfo.querySelector("[data-price]");
1242      if (amount) amount.textContent = tier.monthly_price || "";
1243      setVisible(monthlyInfo, true);
1244      setVisible(annualInfo, false);
1245    } else {
1246      const amount = annualInfo && annualInfo.querySelector("[data-price]");
1247      if (amount) amount.textContent = tier.annual_price || "";
1248      setVisible(monthlyInfo, false);
1249      setVisible(annualInfo, true);
1250    }
1251  }
1252
1253  function switchPeriod(root, period) {
1254    root.dataset.period = period;
1255    root.querySelectorAll(".tab_button").forEach((button) => {
1256      const active = button.dataset.period === period;
1257      button.classList.toggle("active", active);
1258      button.setAttribute("aria-selected", active ? "true" : "false");
1259    });
1260    updateBilling(root);
1261  }
1262
1263  function initRoot(root) {
1264    if (root.dataset.initialized === "true") return;
1265    root.dataset.initialized = "true";
1266    root.dataset.period = root.dataset.period || "monthly";
1267
1268    const slider = root.querySelector(".crm_contact_tier_pricing__range");
1269    if (slider) {
1270      slider.addEventListener("input", () => updateBilling(root));
1271    }
1272
1273    root.querySelectorAll(".tab_button").forEach((button) => {
1274      button.addEventListener("click", () => {
1275        switchPeriod(root, button.dataset.period || "monthly");
1276      });
1277    });
1278
1279    updateBilling(root);
1280  }
1281
1282  function initCrmContactTierPricing() {
1283    document.querySelectorAll(ROOT_SELECTOR).forEach(initRoot);
1284  }
1285
1286  document.addEventListener("DOMContentLoaded", initCrmContactTierPricing);
1287  document.addEventListener("turbo:load", initCrmContactTierPricing);
1288})(window, document);
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310

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.