1(function () { 2 "use strict"; 3 4 document.addEventListener("DOMContentLoaded", function () { 5 const dialog = document.getElementById("popupOverlay"); 6 if (!dialog) return; 7 8 const popupId = dialog.dataset.popupId || "unknown"; 9 const popupVersion = dialog.dataset.popupVersion || "0"; 10 const frequency = dialog.dataset.popupFrequency || "once"; 11 const presentation = dialog.dataset.popupPresentation || "modal"; 12 const repeatDays = Number(dialog.dataset.popupRepeatDays || 7); 13 const trigger = dialog.dataset.popupTrigger || "delay"; 14 const delayMs = Math.max(0, Number(dialog.dataset.popupDelay || 0) * 1000); 15 const scrollDepth = Math.min(100, Math.max(10, Number(dialog.dataset.popupScroll || 50))); 16 const storageKey = `neowave_popup:${popupId}:${popupVersion}`; 17 const legacyStorageKey = `popupDismissed_${popupId}_${popupVersion}`; 18 const closeButton = dialog.querySelector("[data-popup-close]"); 19 const popupImage = dialog.querySelector(".popup-image"); 20 const analyticsForm = dialog.querySelector("[data-popup-analytics]"); 21 const csrfToken = analyticsForm?.querySelector("[name=csrfmiddlewaretoken]")?.value || ""; 22 const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; 23 let showTimer = null; 24 let closingTimer = null; 25 let isClosing = false; 26 let dismissalTracked = false; 27 28 function hideBrokenImage() { 29 const media = popupImage?.closest(".popup-media"); 30 if (media) media.hidden = true; 31 } 32 33 popupImage?.addEventListener("error", hideBrokenImage); 34 if (popupImage?.complete && popupImage.naturalWidth === 0) hideBrokenImage(); 35 36 function storageGet(storage, key) { 37 try { 38 return storage.getItem(key); 39 } catch (error) { 40 return null; 41 } 42 } 43 44 function storageSet(storage, key, value) { 45 try { 46 storage.setItem(key, value); 47 return true; 48 } catch (error) { 49 dialog.dataset.dismissed = value; 50 return false; 51 } 52 } 53 54 function isDismissed() { 55 if (dialog.dataset.dismissed) return true; 56 if (frequency === "always") return false; 57 if (frequency === "session") { 58 return Boolean(storageGet(window.sessionStorage, storageKey)); 59 } 60 61 const savedValue = storageGet(window.localStorage, storageKey); 62 if (!savedValue && frequency === "once") { 63 return Boolean(storageGet(window.localStorage, legacyStorageKey)); 64 } 65 if (!savedValue) return false; 66 if (frequency === "days") { 67 const dismissedAt = Number(savedValue); 68 const duration = repeatDays * 24 * 60 * 60 * 1000; 69 return Number.isFinite(dismissedAt) && Date.now() - dismissedAt < duration; 70 } 71 return true; 72 } 73 74 function markDismissed() { 75 const value = frequency === "days" ? String(Date.now()) : "1"; 76 if (frequency === "always") { 77 dialog.dataset.dismissed = value; 78 } else if (frequency === "session") { 79 storageSet(window.sessionStorage, storageKey, value); 80 } else { 81 storageSet(window.localStorage, storageKey, value); 82 } 83 } 84 85 function sendEvent(eventName) { 86 if (!analyticsForm || !csrfToken) return; 87 const body = new URLSearchParams({event: eventName}); 88 window.fetch(analyticsForm.action, { 89 method: "POST", 90 body, 91 credentials: "same-origin", 92 keepalive: true, 93 headers: { 94 "X-CSRFToken": csrfToken, 95 "X-Requested-With": "XMLHttpRequest", 96 }, 97 }).catch(function () { 98 // Campaign metrics are best-effort and never block the visitor. 99 }); 100 } 101 102 function clearTriggers() { 103 if (showTimer) { 104 window.clearTimeout(showTimer); 105 showTimer = null; 106 } 107 window.removeEventListener("scroll", checkScroll); 108 document.removeEventListener("mouseout", checkExitIntent); 109 } 110 111 function showPopup() { 112 if (isDismissed() || dialog.open) return; 113 clearTriggers(); 114 115 if (presentation === "card" && typeof dialog.show === "function") { 116 dialog.show(); 117 } else if (typeof dialog.showModal === "function") { 118 dialog.showModal(); 119 } else { 120 dialog.setAttribute("open", ""); 121 if (presentation !== "card") dialog.setAttribute("aria-modal", "true"); 122 } 123 124 if (presentation !== "card") document.body.classList.add("popup-open"); 125 window.requestAnimationFrame(function () { 126 dialog.classList.add("is-open"); 127 if (presentation !== "card") closeButton?.focus({preventScroll: true}); 128 }); 129 sendEvent("impression"); 130 } 131 132 function finishClose() { 133 if (dialog.open && typeof dialog.close === "function") { 134 dialog.close(); 135 } else { 136 dialog.removeAttribute("open"); 137 document.body.classList.remove("popup-open"); 138 } 139 } 140 141 function dismissPopup() { 142 if (!dialog.open || isClosing) return; 143 isClosing = true; 144 markDismissed(); 145 if (!dismissalTracked) { 146 sendEvent("dismiss");
147 dismissalTracked = true; 148 } 149 dialog.classList.remove("is-open"); 150 dialog.classList.add("is-closing"); 151 closingTimer = window.setTimeout(finishClose, reduceMotion ? 0 : 180); 152 } 153 154 function checkScroll() { 155 const documentHeight = Math.max( 156 document.documentElement.scrollHeight, 157 document.body.scrollHeight 158 ); 159 const scrollable = documentHeight - window.innerHeight; 160 if (scrollable <= 0) { 161 clearTriggers(); 162 showTimer = window.setTimeout(showPopup, delayMs); 163 return; 164 } 165 const progress = (window.scrollY / scrollable) * 100; 166 if (progress >= scrollDepth) showPopup(); 167 } 168 169 function checkExitIntent(event) { 170 if (!event.relatedTarget && event.clientY <= 8) showPopup(); 171 } 172 173 closeButton?.addEventListener("click", dismissPopup); 174 175 dialog.addEventListener("cancel", function (event) { 176 event.preventDefault(); 177 dismissPopup(); 178 }); 179 180 document.addEventListener("keydown", function (event) { 181 if (presentation === "card" && event.key === "Escape" && dialog.open) { 182 dismissPopup(); 183 } 184 }); 185 186 dialog.addEventListener("click", function (event) { 187 if (event.target !== dialog) return; 188 const rect = dialog.getBoundingClientRect(); 189 const inside = ( 190 event.clientX >= rect.left 191 && event.clientX <= rect.right 192 && event.clientY >= rect.top 193 && event.clientY <= rect.bottom 194 ); 195 if (!inside) dismissPopup(); 196 }); 197 198 dialog.addEventListener("close", function () { 199 if (closingTimer) window.clearTimeout(closingTimer); 200 closingTimer = null; 201 isClosing = false; 202 dialog.classList.remove("is-open", "is-closing"); 203 document.body.classList.remove("popup-open"); 204 }); 205 206 dialog.querySelectorAll("[data-popup-action]").forEach(function (action) { 207 action.addEventListener("click", function () { 208 markDismissed(); 209 sendEvent(action.dataset.popupAction); 210 }); 211 }); 212 213 if (isDismissed()) return; 214 if (trigger === "scroll") { 215 window.addEventListener("scroll", checkScroll, {passive: true}); 216 checkScroll(); 217 } else if (trigger === "exit" && window.matchMedia("(pointer: fine)").matches) { 218 document.addEventListener("mouseout", checkExitIntent); 219 } else { 220 showTimer = window.setTimeout(showPopup, delayMs); 221 } 222 }); 223})();
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.