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.