1/** 2 * Double Opt-In â Universal Error Notification 3 * 4 * Listens for form submission events from any supported form plugin 5 * and checks the server-side AJAX endpoint for OptIn creation errors. 6 * Displays a toast notification if an error is found. 7 * 8 * @since 4.2.0 9 */ 10(function () { 11 'use strict'; 12 13 if ( typeof doiErrorNotification === 'undefined' ) { 14 return; 15 } 16 17 var config = doiErrorNotification; 18 var checking = false; 19 var cooldown = false; 20 21 /** 22 * Check the AJAX endpoint for a stored submission error. 23 */ 24 function checkForError() { 25 if ( checking || cooldown ) { 26 return; 27 } 28 checking = true; 29 30 var xhr = new XMLHttpRequest(); 31 xhr._doiInternal = true; // Prevent the XHR interceptor from re-triggering scheduleCheck 32 xhr.open( 'POST', config.ajaxUrl, true ); 33 xhr.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' ); 34 35 xhr.onreadystatechange = function () { 36 if ( xhr.readyState !== 4 ) { 37 return; 38 } 39 checking = false; 40 41 if ( xhr.status !== 200 ) { 42 return; 43 } 44 45 try { 46 var response = JSON.parse( xhr.responseText ); 47 } catch ( e ) { 48 return; 49 } 50 51 if ( response.success && response.data && response.data.error ) { 52 // Hide form confirmation messages when validation error should be visible 53 if ( response.data.error.hide_confirmation ) { 54 hideFormConfirmations(); 55 } 56 57 // Redirect to error page if configured, otherwise show toast 58 if ( response.data.redirect_url ) { 59 window.location.href = response.data.redirect_url; 60 return; 61 } 62 63 showNotification( response.data.error.message, 'error' ); 64 65 // Prevent duplicate checks for 5 seconds 66 cooldown = true; 67 setTimeout( function () { 68 cooldown = false; 69 }, 5000 ); 70 } else if ( response.success && response.data && response.data.success_message ) { 71 // A "sent â please confirm" message stashed server-side by an 72 // integration that die()s early (Avada) and so can't show the 73 // form plugin's own confirmation. Render it as a success toast. 74 showNotification( response.data.success_message, 'success' ); 75 76 cooldown = true; 77 setTimeout( function () { 78 cooldown = false; 79 }, 5000 ); 80 } 81 }; 82 83 xhr.send( 84 'action=doi_check_submission_error&nonce=' + encodeURIComponent( config.nonce ) 85 ); 86 } 87 88 /** 89 * Schedule an error check with a short delay to ensure 90 * the server-side form processing has completed. 91 */ 92 function scheduleCheck() { 93 setTimeout( checkForError, 800 ); 94 } 95 96 /** 97 * Hide form plugin confirmation/success messages. 98 * 99 * Called when a validation error should be visible (block/redirect mode) 100 * so the user does not see a contradictory success message. 101 */ 102 function hideFormConfirmations() { 103 // WPForms confirmation containers 104 var wpfSelectors = '.wpforms-confirmation-container-full, .wpforms-confirmation-container'; 105 document.querySelectorAll( wpfSelectors ).forEach( function ( el ) { 106 el.style.display = 'none'; 107 } ); 108 109 // Gravity Forms confirmation wrapper 110 document.querySelectorAll( '.gform_confirmation_wrapper' ).forEach( function ( el ) { 111 el.style.display = 'none'; 112 } ); 113 114 // CF7 success response (only hide the success state, not error state) 115 document.querySelectorAll( '.wpcf7-response-output.wpcf7-mail-sent-ok' ).forEach( function ( el ) { 116 el.style.display = 'none'; 117 } ); 118 119 // Elementor Forms success message 120 document.querySelectorAll( '.elementor-message.elementor-message-success' ).forEach( function ( el ) { 121 el.style.display = 'none'; 122 } ); 123 } 124 125 /** 126 * Show a toast notification. 127 * 128 * @param {string} message The message to display. 129 * @param {string} [type] 'error' (default) or 'success' â controls colour + icon. 130 */ 131 function showNotification( message, type ) { 132 type = type === 'success' ? 'success' : 'error'; 133 134 var existing = document.querySelector( '.doi-error-notification' ); 135 if ( existing ) { 136 existing.remove(); 137 } 138 139 var notification = document.createElement( 'div' ); 140 notification.className = 'doi-error-notification'; 141 if ( type === 'success' ) { 142 notification.className += ' doi-error-notification--success'; 143 } 144 notification.setAttribute( 'role', type === 'success' ? 'status' : 'alert' ); 145 146 var content = document.createElement( 'div' ); 147 content.className = 'doi-error-notification__content'; 148 149 var icon = document.createElement( 'span' ); 150 icon.className = 'doi-error-notification__icon'; 151 // Check mark for success, warning triangle for error. 152 icon.innerHTML = type === 'success' ? '✓' : '⚠'; 153 154 var text = document.createElement( 'p' ); 155 text.className = 'doi-error-notification__message';
156 text.textContent = message; 157 158 var closeBtn = document.createElement( 'button' ); 159 closeBtn.className = 'doi-error-notification__close'; 160 closeBtn.setAttribute( 'type', 'button' ); 161 closeBtn.innerHTML = '×'; 162 closeBtn.addEventListener( 'click', function () { 163 dismiss( notification ); 164 } ); 165 166 content.appendChild( icon ); 167 content.appendChild( text ); 168 content.appendChild( closeBtn ); 169 notification.appendChild( content ); 170 document.body.appendChild( notification ); 171 172 // Trigger enter animation 173 requestAnimationFrame( function () { 174 notification.classList.add( 'doi-error-notification--visible' ); 175 } ); 176 177 // Auto-dismiss after 10 seconds 178 setTimeout( function () { 179 dismiss( notification ); 180 }, 10000 ); 181 } 182 183 /** 184 * Dismiss a notification with exit animation. 185 * 186 * @param {HTMLElement} el The notification element. 187 */ 188 function dismiss( el ) { 189 if ( ! el || ! el.parentNode ) { 190 return; 191 } 192 el.classList.remove( 'doi-error-notification--visible' ); 193 el.classList.add( 'doi-error-notification--exit' ); 194 setTimeout( function () { 195 if ( el.parentNode ) { 196 el.parentNode.removeChild( el ); 197 } 198 }, 300 ); 199 } 200 201 // ------------------------------------------------------------------------- 202 // Form plugin event listeners 203 // ------------------------------------------------------------------------- 204 205 // Contact Form 7 (native DOM events) 206 document.addEventListener( 'wpcf7mailsent', scheduleCheck ); 207 document.addEventListener( 'wpcf7invalid', scheduleCheck ); 208 document.addEventListener( 'wpcf7spam', scheduleCheck ); 209 document.addEventListener( 'wpcf7mailfailed', scheduleCheck ); 210 211 // WPForms + Gravity Forms (jQuery events â not caught by native addEventListener) 212 if ( typeof jQuery !== 'undefined' ) { 213 jQuery( document ).on( 'wpformsAjaxSubmitSuccess', scheduleCheck ); 214 jQuery( document ).on( 'wpformsAjaxSubmitError', scheduleCheck ); 215 jQuery( document ).on( 'gform_confirmation_loaded', scheduleCheck ); 216 } 217 218 // ------------------------------------------------------------------------- 219 // Universal: catch ANY form submission on the page (capture phase) 220 // This is the most reliable way to detect form submissions regardless 221 // of which form plugin is used. It covers AJAX, standard, and custom flows. 222 // ------------------------------------------------------------------------- 223 document.addEventListener( 'submit', function () { 224 // Use a longer delay for standard (non-AJAX) submissions 225 // because the server needs time to process the request. 226 setTimeout( checkForError, 1500 ); 227 }, true ); 228 229 // ------------------------------------------------------------------------- 230 // Generic AJAX interception 231 // Catches all AJAX calls to detect form submissions from plugins 232 // that don't fire specific events (e.g., Avada, Elementor). 233 // ------------------------------------------------------------------------- 234 235 // Intercept fetch() 236 if ( typeof window.fetch === 'function' ) { 237 var originalFetch = window.fetch; 238 window.fetch = function () { 239 // Capture URL before the async call â `arguments` inside .then() 240 // refers to the callback's own arguments, not the fetch arguments. 241 var fetchUrl = ( typeof arguments[0] === 'string' ) 242 ? arguments[0] 243 : ( arguments[0] && arguments[0].url ) || ''; 244 245 return originalFetch.apply( this, arguments ).then( function ( response ) { 246 if ( fetchUrl && fetchUrl.indexOf( 'admin-ajax.php' ) !== -1 ) { 247 scheduleCheck(); 248 } 249 return response; 250 } ); 251 }; 252 } 253 254 // Intercept XMLHttpRequest for jQuery.ajax based form plugins 255 var origOpen = XMLHttpRequest.prototype.open; 256 XMLHttpRequest.prototype.open = function ( method, url ) { 257 // Skip our own internal error-check requests to prevent infinite polling loops 258 if ( typeof url === 'string' && url.indexOf( 'admin-ajax.php' ) !== -1 && ! this._doiInternal ) { 259 this.addEventListener( 'load', scheduleCheck ); 260 } 261 return origOpen.apply( this, arguments ); 262 }; 263 264})();
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.