PageSourceSearch

https://www.ultimatebeaver.com/wp-content/plugins/spectra-pro/spec…/assets/js/modal-script.js?ver=3.0.0-beta.2

js ultimatebeaver.com collected 2026-10-03 21:38:15 UTC 7,757 bytes, 251 lines download raw bytes

1// Constants
2const EXIT_INTENT_THRESHOLD = 20; // Pixels from top to trigger exit intent
3const MS_PER_DAY = 24 * 60 * 60 * 1000; // Milliseconds in a day
4const DEFAULT_HIDE_DAYS = 7; // Default days to hide modal with cookie
5
6/**
7 * Get a cookie value by name.
8 *
9 * @param {string} name - The name of the cookie.
10 * @return {string|undefined} - The cookie value if found, otherwise undefined.
11 */
12const getCookie = ( name ) => {
13	const value = `; ${document.cookie}`;
14	const parts = value.split( `; ${name}=` );
15	return parts.length === 2 ? parts.pop().split( ';' ).shift() : undefined;
16};
17
18/**
19 * Set a cookie to hide a modal popup for a number of days.
20 *
21 * @param {string} blockId - The modal block ID.
22 * @param {number} days    - Days to persist the cookie.
23 */
24const setPopupCookie = ( blockId, days ) => {
25	const cookieKey = `block-${blockId}`;
26	const expires = new Date( Date.now() + days * MS_PER_DAY ).toUTCString();
27	document.cookie = `${cookieKey}=true; expires=${expires}; path=/`;
28};
29
30/**
31 * Determine whether a modal should be shown.
32 *
33 * @param {Object} args - Configuration arguments.
34 * @return {boolean} - Whether the modal can be shown.
35 */
36const canShow = ( args = {} ) => {
37	const cookieName = `block-${args.blockId}`;
38	const cookie = getCookie( cookieName );
39
40	if ( args.enableCookies && cookie && args.modalTrigger === 'automatic' ) {
41		return false;
42	} else if ( !args.enableCookies ) {
43		document.cookie = `${cookieName}=; max-age=-1; path=/`;
44	}
45
46	const modal = document.getElementById( args.blockId );
47	return !( modal?.classList?.contains( 'active' ) );
48};
49
50/**
51 * Show the modal by adding active class and dispatching events.
52 *
53 * @param {Object} args - Arguments containing blockId and trigger info.
54 */
55const openModal = ( args ) => {
56	const modal = document.getElementById( args.blockId );
57	if ( !modal ) return;
58
59	modal.classList.add( 'active' );
60	modal.setAttribute( 'data-modal-trigger', args.modalTrigger || 'manual' );
61
62	document.dispatchEvent( new CustomEvent( 'spectra:modal:opened', {
63		detail: {
64			blockId: args.blockId,
65			modalTrigger: args.modalTrigger || 'manual',
66			triggerElement: args.triggerElement || null
67		},
68		bubbles: true
69	} ) );
70};
71
72let exitIntentTriggered = false;
73const exitIntentModals = new Map(); // Store all exit intent modals
74
75/**
76 * Register a modal for exit intent triggering.
77 *
78 * @param {Object} args - Modal configuration.
79 */
80const attachExitIntent = ( args = {} ) => exitIntentModals.set( args.blockId, args );
81
82/**
83 * Automatically show modal after a delay.
84 *
85 * @param {Object} args - Modal configuration.
86 */
87const attachAutomaticOpen = ( args = {} ) => {
88	const delay = args.noOfSecondsToShow === 0 ? 0 : args.noOfSecondsToShow * 1000;
89	
90	setTimeout( () => {
91		if ( canShow( args ) ) {
92			openModal( args );
93			if ( args.enableCookies && args.setCookiesOn === 'page-refresh' ) {
94				setPopupCookie( args.blockId, args.hideForDays || DEFAULT_HIDE_DAYS );
95			}
96		}
97	}, delay );
98};
99
100/**
101 * Attach click and keyboard handlers to custom trigger elements.
102 *
103 * @param {Object} args - Modal configuration with cssClass or cssId.
104 */
105const attachCustomTriggers = ( args = {} ) => {
106	const setupTrigger = ( trigger ) => {
107		const handleOpen = ( e ) => {
108			e.preventDefault();
109			openModal( { ...args, triggerElement: trigger } );
110		};
111
112		trigger.addEventListener( 'click', handleOpen );
113
114		if ( trigger.tagName !== 'BUTTON' && trigger.tagName !== 'A' ) {
115			trigger.setAttribute( 'tabindex', trigger.getAttribute( 'tabindex' ) || '0' );
116			trigger.setAttribute( 'role', trigger.getAttribute( 'role' ) || 'button' );
117			trigger.addEventListener( 'keydown', ( e ) => {
118				if ( e.key === ' ' || e.key === 'Enter' ) handleOpen( e );
119			} );
120		} else if ( trigger.tagName === 'A' ) {
121			trigger.addEventListener( 'keydown', ( e ) => {
122				if ( e.key === 'Enter' ) handleOpen( e );
123			} );
124		}
125
126		if ( !trigger.getAttribute( 'aria-label' ) && !trigger.textContent.trim() ) {
127			trigger.setAttribute( 'aria-label', 'Open modal' );
128		}
129	};
130
131	if ( args.modalTrigger === 'custom-class' && args.cssClass ) {
132		document.querySelectorAll( `.${args.cssClass}` ).forEach( setupTrigger );
133	} else if ( args.modalTrigger === 'custom-id' && args.cssId ) {
134		const trigger = document.getElementById( args.cssId );
135		if ( trigger ) setupTrigger( trigger );
136	}
137};
138
139/**
140 * Handle modal initialization events and setup appropriate triggers.
141 *
142 * @param {Event} event - Custom event with modal configuration in detail.
143 */
144const handleModalInitialization = ( event ) => {
145	const { detail } = event;
146	const args = {
147		...detail,
148		cssClass: detail.cssClass || '',
149		cssId: detail.cssId || '',
150		modalTrigger: detail.modalTrigger || 'automatic',
151		showAfterSeconds: detail.showAfterSeconds || false,
152		noOfSecondsToShow: parseInt( detail.noOfSecondsToShow || '5', 10 ),
153		hideForDays: parseInt( detail.hideForDays || '7', 10 ),
154		enableCookies: detail.enableCookies === '1' || detail.enableCookies === 'true' || detail.enableCookies === true,
155		setCookiesOn: detail.setCookiesOn || 'page-refresh'
156	};
157
158	if ( args.modalTrigger === 'custom-class' || args.modalTrigger === 'custom-id' ) {
159		attachCustomTriggers( args );
160	} else if ( args.modalTrigger === 'automatic' ) {
161		if ( args.exitIntent ) {
162			attachExitIntent( args );
163		} else if ( args.showAfterSeconds ) {
164			attachAutomaticOpen( args );
165		}
166	}
167};
168
169/**
170 * Setup global mouse handlers for exit intent detection.
171 */
172const setupGlobalHandlers = () => {
173	let isNearTop = false;
174	let moveTimeout;
175
176	document.addEventListener( 'mousemove', ( e ) => {
177		if ( moveTimeout ) return;
178		moveTimeout = setTimeout( () => { moveTimeout = null; }, 16 );
179		isNearTop = e.clientY <= EXIT_INTENT_THRESHOLD;
180	} );
181
182	document.addEventListener( 'mouseleave', () => {
183		if ( exitIntentTriggered || exitIntentModals.size === 0 || !isNearTop ) return;
184		// eslint-disable-next-line no-unused-vars
185		for ( const [blockId, modal] of exitIntentModals.entries() ) {
186			if ( canShow( modal ) ) {
187				openModal( modal );
188				exitIntentTriggered = true;
189				return;
190			}
191		}
192	} );
193};
194
195/**
196 * Process modals that are already in DOM with data-wp-context attributes.
197 */
198const processExistingModals = () => {
199	document.querySelectorAll( '[data-wp-interactive="spectra/modal"][data-wp-context]' ).forEach( modal => {
200		const contextAttr = modal.getAttribute( 'data-wp-context' );
201		const contextMatch = contextAttr?.match( /^[^:]+::(.+)$/ );
202		
203		if ( contextMatch ) {
204			try {
205				modal.dispatchEvent( new CustomEvent( 'spectra:modal:initialized', {
206					detail: JSON.parse( contextMatch[1] ),
207					bubbles: true
208				} ) );
209			} catch ( e ) {
210				// Silently fail if context parsing fails
211			}
212		}
213	} );
214};
215
216/**
217 * Initialize the modal script - setup handlers and process existing modals.
218 */
219const initialize = () => {
220	setupGlobalHandlers();
221
222	document.addEventListener( 'spectra:modal:initialized', handleModalInitialization );
223
224	document.addEventListener( 'UAGModalEditor', ( event ) => {
225		const { detail } = event;
226		handleModalInitialization( {
227			detail: {
228				blockId: 'uagb-block-' + detail.block_id,
229				modalTrigger: detail.modalTrigger || 'automatic',
230				cssClass: detail.cssClass || '',
231				cssId: detail.cssID || '',
232				exitIntent: detail.exitIntent || false,
233				showAfterSeconds: detail.showAfterSeconds || false,
234				noOfSecondsToShow: detail.noOfSecondsToShow || '5',
235				enableCookies: detail.enableCookies || false,
236				setCookiesOn: detail.setCookiesOn || 'page-refresh',
237				hideForDays: detail.hideForDays || '7'
238			}
239		} );
240	} );
241
242	if ( document.readyState === 'loading' ) {
243		document.addEventListener( 'DOMContentLoaded', processExistingModals );
244	} else {
245		processExistingModals();
246	}
247
248	setTimeout( processExistingModals, 1000 );
249};
250
251initialize();

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.