PageSourceSearch

https://bloodyelbow.com/wp-content/plugins/prop-audience/js/prop-audience.js

js bloodyelbow.com collected 2026-10-01 09:14:09 UTC 12,943 bytes, 408 lines download raw bytes

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.