1// enable exit intent by calling: prop_audience_obj.enableExitIntent(); 2// disable exit intent by calling: prop_audience_obj.disableExitIntent(); 3 4let prop_audience_obj = { 5 hasNewsletter: false, // user has subscribed to newsletter 6 hasPushNotifications: false, // user has subscibed to notifications 7 customContent: null, 8 templateUrl: prop_audience.template_url || '', 9 exitConfig: null, // store config so we can access later 10 exitEnabled: false, // exit intent is disabled by default 11 exitListeners: {}, 12 13 load() { 14 try { 15 const saved = localStorage.getItem('prop_audience'); 16 if (saved) Object.assign(this, JSON.parse(saved)); 17 } catch (e) { 18 console.warn('Could not parse stored audience data', e); 19 } 20 }, 21 save() { 22 localStorage.setItem('prop_audience', JSON.stringify({ 23 hasNewsletter: this.hasNewsletter, 24 hasPushNotifications: this.hasPushNotifications 25 })); 26 }, 27 markNewsletterSignedUp() { 28 this.hasNewsletter = true; 29 this.save(); 30 }, 31 markPushSignedUp() { 32 this.hasPushNotifications = true; 33 this.save(); 34 }, 35 36 // CTA Logic 37 // Function to replace an element with new element 38 propReplaceElement: async function (element) { 39 40 const el = document.querySelector(element); 41 42 // Find the element 43 if (el) { 44 45 if (prop_audience.custom_content) { 46 47 // If custom content exists, use it instead of the templates 48 content = prop_audience.custom_content; 49 } 50 else { 51 52 // Otherwise, fetch the template 53 content = await this.fetchTemplate(); 54 } 55 56 el.outerHTML = content; 57 58 this.attachNewsletterFormListener(); 59 60 } 61 else 62 { 63 console.warn('Element not found:', element); 64 } 65 }, 66 67 fetchTemplate: async function () { 68 69 try { 70 71 /*this.templateUrl = this.templateUrl.replace( 72 'https://local.arsenalinsider.com', 73 'https://d6882e55518d.ngrok-free.app' 74 );*/ 75 76 console.log("fetching", this.templateUrl); 77 //alert("fetching - " + this.templateUrl); 78 79 const response = await fetch(this.templateUrl); 80 81 // Use the localized URL 82 if (!response.ok) 83 { 84 throw new Error('Network response was not ok'); 85 } 86 87 const data = await response.json(); 88 89 // Return the fetched HTML 90 return data.html; 91 92 } 93 catch (error) 94 { 95 96 console.error('Error fetching the signup form:', error); 97 98 // Return empty string in case of an error 99 return ''; 100 101 } 102 }, 103 104 injectExitPopup: async function () { 105 106 let form = ''; 107 108 if (prop_audience.custom_content) { 109 form = prop_audience.custom_content; 110 } else { 111 112 form = await this.fetchTemplate(); 113 } 114 115 const { element_id, background_element_id } = this.exitConfig; 116 117 // Build the popup markup 118 const popupHtml = ` 119 <div id="${element_id}" role="dialog" aria-modal="true" style="display:none"> 120 ${form} 121 <button id="${element_id}-close" aria-label="Close popup">Ã</button> 122 </div> 123 <div id="${background_element_id}" style="display:none"></div> 124 `; 125 126 // Insert into the DOM 127 if (!document.getElementById(element_id)) { 128 document.body.insertAdjacentHTML('beforeend', popupHtml); 129 } 130 131 // Attach form listener + close listener 132 this.attachNewsletterFormListener(); 133 134 const closeBtn = document.getElementById(`${element_id}-close`); 135 if (closeBtn) { 136 closeBtn.addEventListener("click", () => this.hidePopup()); 137 } 138 }, 139 140 attachNewsletterFormListener() { 141 142 const form = document.getElementById('prop-audience-form'); 143 if (!form) return; 144 145 // Remove any existing listener to prevent duplicates 146 form.removeEventListener('submit', form._propAudienceHandler || (() => {})); 147 148 const handler = async (e) => { 149 e.preventDefault(); 150 151 const email = form.querySelector('[name="email"]')?.value?.trim(); 152 if (!email) { 153 console.error('No email entered'); 154 return; 155 } 156 157 try { 158 const response = await fetch(prop_audience.submit_url, { 159 method: 'POST', 160 headers: { 161 'Content-Type': 'application/json' 162 }, 163 body: JSON.stringify({ email, nonce: prop_audience.nonce }) 164 }); 165 166 let data; 167 try { 168 data = await response.json(); 169 } catch (err) { 170 throw new Error("Invalid JSON response"); 171 } 172 173 if (data.success) { 174 this.markNewsletterSignedUp(); 175 form.innerHTML = `<div class="prop-audience-success-message"><p>Thank you! You have successfully subscribed.</p></div>`; 176 177 // fire GA4 event to track sign ups 178 if (typeof gtag === "function") { 179 gtag("event", "sign_up", { 180 method: "exit_intent_popup", 181 service: "newsletter" 182 }); 183 } 184 185 } else { 186 console.error('Form failed:', data.message || 'Unknown error'); 187 // Donât wipe the form â just show error 188 const errorBox = form.querySelector('.prop-audience-error-message') || document.createElement('div'); 189 errorBox.className = 'prop-audience-error-message';
190 errorBox.innerHTML = `<p>${data.message || 'Subscription failed.'}</p>`; 191 form.appendChild(errorBox); 192 } 193 194 } catch (err) { 195 console.error('Form submit error:', err); 196 } 197 }; 198 199 // Attach handler and store reference to prevent duplication 200 form.addEventListener('submit', handler); 201 form._propAudienceHandler = handler; 202 }, 203 hidePopup() { 204 205 if (!this.exitConfig) return; 206 207 // mark the time when user closes the popup 208 localStorage.setItem('prop_audience.exit.last_shown', Date.now().toString()); 209 210 this.togglePopup(this.exitConfig.element_id, this.exitConfig.background_element_id, false); 211 }, 212 213 showPopup: async function () { 214 215 if (!this.exitConfig) return; 216 217 const config = this.exitConfig; 218 if (!config.delay_expired) return; 219 if (this.hasNewsletter) return; // never show if already subscribed 220 if (typeof config.will_show === 'function' && config.will_show() === false) return; // let's us conditional stop exit intent inside our will_show fucntion 221 222 const lastShown = Number(localStorage.getItem('prop_audience.exit.last_shown')) || 0; 223 const now = Date.now(); 224 225 // Only show exit intent again if 24 hours have passed 226 if (now - lastShown < 24 * 60 * 60 * 1000) return; 227 228 // testing 10 seconds 229 //if (now - lastShown < 10 * 1000) return; 230 231 //if (!document.getElementById(config.element_id)) { 232 // await this.injectExitPopup(); 233 //} 234 235 this.togglePopup(config.element_id, config.background_element_id, true); 236 237 // fire GA4 event for newsletter modal shown 238 if (typeof gtag === "function") { 239 gtag("event", "exit_intent_popup_shown", { 240 popup_type: "newsletter", 241 non_interaction: true 242 }); 243 } 244 }, 245 246 // Exit Intent 247 setupExitIntent(opts = {}) { 248 this.exitConfig = { 249 element_id: '', 250 background_element_id: '', 251 top_only: false, 252 click_outside_to_close: true, 253 delay: 3000, 254 will_show: null, 255 delay_expired: false, 256 ...opts 257 }; 258 // Do not enable immediately - will be enabled by loader 259 }, 260 261 enableExitIntent: async function(){ 262 263 if (this.exitEnabled || !this.exitConfig) return; 264 this.exitEnabled = true; 265 266 const config = this.exitConfig; 267 268 // desktop 269 const onMouseOut = (e) => { 270 if (!this.exitEnabled) return; 271 if (e.clientY <= 1 || !config.top_only) this.showPopup(); 272 }; 273 274 const onClick = (e) => { 275 if (!this.exitEnabled) return; 276 const el = document.getElementById(config.element_id); 277 if (!el || window.getComputedStyle(el).display === 'none') return; 278 const rect = el.getBoundingClientRect(); 279 if (e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom) { 280 if (config.click_outside_to_close) this.hidePopup(); 281 } 282 }; 283 284 document.documentElement.addEventListener('mouseleave', onMouseOut); 285 document.documentElement.addEventListener('click', onClick); 286 287 // store the intent listeners so we can remove them later if need be 288 this.exitListeners = { 289 onMouseOut, 290 onClick 291 }; 292 293 // mobile 294 const isMobile = ( 295 /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent) || 296 window.matchMedia("(max-width: 768px)").matches 297 ); 298 299 if (isMobile) { 300 301 let inactivityTimer; 302 303 const resetInactivity = () => { 304 clearTimeout(inactivityTimer); 305 inactivityTimer = setTimeout(() => { 306 if (this.exitEnabled) this.showPopup(); 307 }, 15000); // 15 seconds 308 }; 309 310 // Events that count as "activity" on mobile 311 const activityEvents = ['touchstart', 'click', 'keydown']; 312
313 activityEvents.forEach(evt => 314 document.addEventListener(evt, resetInactivity, { passive: true }) 315 ); 316 317 // Kick off inactivity timer immediately 318 resetInactivity(); 319 320 // add the mobile exit listeners 321 this.exitListeners = { 322 ...this.exitListeners, 323 activityEvents, 324 resetInactivity, 325 inactivityTimer, 326 }; 327 } 328 329 config.delay_expired = false; 330 331 const delayTime = config.delay > 0 ? config.delay : 0; 332 333 setTimeout(() => { 334 config.delay_expired = true; // unlock exit intent after delay 335 }, delayTime); 336 337 if (!document.getElementById(config.element_id)) { 338 await this.injectExitPopup(); 339 } 340 341 console.log("Exit intent enabled"); 342 }, 343 344 disableExitIntent() { 345 346 this.exitEnabled = false; 347 348 // Desktop listeners 349 if (this.exitListeners.onMouseOut) { 350 document.documentElement.removeEventListener('mouseleave', this.exitListeners.onMouseOut); 351 } 352 if (this.exitListeners.onClick) { 353 document.documentElement.removeEventListener('click', this.exitListeners.onClick); 354 } 355 356 // Mobile listeners 357 if (this.exitListeners.activityEvents) { 358 this.exitListeners.activityEvents.forEach((evt) => 359 document.documentElement.removeEventListener(evt, this.exitListeners.resetInactivity) 360 ); 361 } 362 if (this.exitListeners.inactivityTimer) { 363 clearTimeout(this.exitListeners.inactivityTimer); 364 } 365 366 this.exitListeners = {}; 367 368 console.log("Exit intent disabled"); 369 }, 370 371 togglePopup(contentId, bgId, show) { 372 [contentId, bgId].forEach(id => { 373 const el = document.getElementById(id); 374 if (el) el.style.display = show ? 'block' : 'none'; 375 }); 376 } 377}; 378 379// Init on load 380(function() { 381 382 prop_audience_obj.load(); // load settings from loal storage 383 prop_audience_obj.attachNewsletterFormListener(); 384 prop_audience_obj.setupExitIntent({ 385 element_id: 'prop-audience-exit', 386 background_element_id: 'prop-audience-exit-bg', 387 388 // callback we fire if the exit intent modal is gonna trigger 389 will_show: () => { 390 391 console.log("Exit intent triggered"); 392 393 // Return whether we should actually show the modal 394 return !prop_audience_obj.hasNewsletter; 395 } 396 }); 397 398 // mobile testing 399 //prop_audience_obj.enableExitIntent(); 400 401})(); 402 403if(!window.prop_audience_obj){ 404 window.prop_audience_obj = prop_audience_obj; 405} 406 407 408
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.