1(function() { 2 'use strict'; 3 4 // Track which forms have been spotlighted 5 const spotlightedForms = new WeakSet(); 6 let isSpotlightActive = false; 7 let activeTimeout = null; 8 let overlay = null; 9 let isCheckingForms = false; 10 let lastCheckTime = 0; 11 let spotlightInitialized = false; 12 13 // Popup modal state 14 let popupInitialized = false; 15 let popupModal = null; 16 let popupTimer = null; 17 let isPopupOpen = false; 18 let popupDelayMs = 10000; 19 20 // AJAX form submission state 21 let ajaxInitialized = false; 22 23 // Initialize spotlight functionality 24 function init() { 25 if (spotlightInitialized) return; 26 spotlightInitialized = true; 27 28 // Get or create overlay 29 overlay = document.getElementById('form-spotlight-overlay'); 30 if (!overlay) { 31 overlay = document.createElement('div'); 32 overlay.id = 'form-spotlight-overlay'; 33 overlay.className = 'spotlight-overlay'; 34 document.body.appendChild(overlay); 35 } 36 37 // Check immediately 38 setTimeout(checkForms, 100); 39 40 // Check on scroll - directly call checkForms without debounce 41 window.addEventListener('scroll', checkForms, { passive: true }); 42 43 // Also use requestAnimationFrame for continuous checking 44 startContinuousCheck(); 45 } 46 47 // Initialize popup modals 48 function initPopups() { 49 if (popupInitialized) return; 50 51 const popups = document.querySelectorAll('.emailtru-popup-modal'); 52 if (!popups || popups.length === 0) { 53 return; 54 } 55 56 popupInitialized = true; 57 popupModal = popups[0]; 58 59 // Close when clicking on the backdrop (outside the inner card) 60 popupModal.addEventListener('click', function(event) { 61 if (!isPopupOpen) return; 62 if (event.target === popupModal) { 63 hidePopup(); 64 } 65 }); 66 67 const ctaButton = popupModal.querySelector('.emailtru-popup-cta'); 68 const formContainer = popupModal.querySelector('.emailtru-popup-form-container'); 69 if (ctaButton && formContainer) { 70 ctaButton.addEventListener('click', function() { 71 // Reveal the email form and hide the initial CTA 72 ctaButton.style.display = 'none'; 73 formContainer.style.display = 'block'; 74 }); 75 } 76 77 const declineButton = popupModal.querySelector('.emailtru-popup-decline'); 78 if (declineButton) { 79 declineButton.addEventListener('click', function() { 80 hidePopup(); 81 }); 82 } 83 84 const closeButton = popupModal.querySelector('.emailtru-popup-close'); 85 if (closeButton) { 86 closeButton.addEventListener('click', function() { 87 hidePopup(); 88 }); 89 } 90 91 document.addEventListener('keydown', function(event) { 92 if (!isPopupOpen) return; 93 if (event.key === 'Escape' || event.key === 'Esc') { 94 hidePopup(); 95 } 96 }); 97 98 // Automatically open time-based popups after the configured delay (seconds). 99 // JavaScript-triggered popups remain hidden until emailtruOpenPopup() is called. 100 const triggerType = popupModal.getAttribute('data-emailtru-popup-trigger-type') || 'time_based'; 101 popupDelayMs = 10000; 102 const delayAttr = popupModal.getAttribute('data-emailtru-popup-delay'); 103 if (delayAttr !== null && delayAttr !== '') { 104 const parsed = parseInt(delayAttr, 10); 105 if (!isNaN(parsed) && parsed >= 0) { 106 popupDelayMs = parsed * 1000; 107 } 108 } 109 110 if (triggerType !== 'javascript') { 111 schedulePopupTimer(); 112 } 113 } 114 115 function initAjaxForms() { 116 if (ajaxInitialized) return; 117 ajaxInitialized = true; 118 119 document.addEventListener('submit', function(event) { 120 const form = event.target; 121 if (!form || form.nodeName !== 'FORM') return; 122 if (!form.hasAttribute('data-emailtru-ck-form-id')) return; 123 124 const ckFormId = form.getAttribute('data-emailtru-ck-form-id'); 125 if (!ckFormId || ckFormId === '0') return; 126 127 const action = (form.getAttribute('action') || '').trim(); 128 const isKitDirectSubmit = action.indexOf('https://app.kit.com/forms/') === 0 && action.indexOf('/subscriptions') !== -1; 129 130 if (isKitDirectSubmit) { 131 event.preventDefault(); 132 133 const submitButton = form.querySelector('button[type="submit"], input[type="submit"]'); 134 const previousButtonText = submitButton && submitButton.tagName === 'BUTTON' ? submitButton.textContent : null; 135 if (submitButton) { 136 submitButton.disabled = true; 137 if (submitButton.tagName === 'BUTTON') { 138 submitButton.textContent = 'Submitting...'; 139 } 140 } 141 142 function showMessage(message, kind) { 143 let messageEl = form.nextElementSibling; 144 if (!messageEl || !messageEl.classList || !messageEl.classList.contains('emailtru-form-message')) { 145 messageEl = document.createElement('div'); 146 messageEl.className = 'emailtru-form-message';
147 messageEl.setAttribute('role', kind === 'error' ? 'alert' : 'status'); 148 messageEl.setAttribute('aria-live', kind === 'error' ? 'assertive' : 'polite'); 149 form.insertAdjacentElement('afterend', messageEl); 150 } 151 messageEl.classList.toggle('emailtru-form-message--error', kind === 'error'); 152 messageEl.textContent = message; 153 } 154 155 function setButtonEnabled(enabled) { 156 if (!submitButton) return; 157 submitButton.disabled = !enabled; 158 if (submitButton.tagName === 'BUTTON' && previousButtonText !== null) { 159 submitButton.textContent = enabled ? previousButtonText : 'Submitting...'; 160 } 161 } 162 163 function showSuccessAndHide() { 164 showMessage('Success! Watch your inbox for great recipes.', 'success'); 165 form.reset(); 166 form.style.display = 'none'; 167 if (form.classList.contains('emailtru-popup-form')) { 168 setTimeout(function() { 169 hidePopup(); 170 }, 1500); 171 } 172 setButtonEnabled(true); 173 } 174 175 function showErrorAndEnable(message) { 176 showMessage(message || 'Something went wrong. Please try again.', 'error'); 177 setButtonEnabled(true); 178 } 179 180 function submitViaHiddenIframe() { 181 const iframeName = 'emailtru_kit_subscribe_' + ckFormId; 182 let iframe = document.querySelector('iframe[name="' + iframeName + '"]'); 183 if (!iframe) { 184 iframe = document.createElement('iframe'); 185 iframe.name = iframeName; 186 iframe.setAttribute('aria-hidden', 'true'); 187 iframe.tabIndex = -1; 188 iframe.style.width = '0'; 189 iframe.style.height = '0'; 190 iframe.style.border = '0'; 191 iframe.style.position = 'absolute'; 192 iframe.style.left = '-9999px'; 193 document.body.appendChild(iframe); 194 } 195 196 let handled = false; 197 const onLoad = function() { 198 if (handled) return; 199 handled = true; 200 iframe.removeEventListener('load', onLoad); 201 showSuccessAndHide(); 202 }; 203 204 iframe.addEventListener('load', onLoad); 205 form.setAttribute('target', iframeName); 206 form.submit(); 207 } 208 209 // Preferred: POST via fetch with Accept: application/json (no page redirect). 210 // If Kit blocks CORS, fall back to hidden-iframe submit (still no redirect). 211 try { 212 const directFormData = new FormData(form); 213 const params = new URLSearchParams(); 214 directFormData.forEach(function(value, key) { 215 params.append(key, value); 216 }); 217 218 fetch(action, { 219 method: 'POST', 220 headers: { 221 'Accept': 'application/json', 222 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' 223 }, 224 body: params.toString(), 225 credentials: 'omit' 226 }) 227 .then(function(response) { 228 const contentType = (response.headers && response.headers.get) ? (response.headers.get('content-type') || '') : ''; 229 if (contentType.indexOf('application/json') !== -1) { 230 return response.json() 231 .catch(function() { return null; }) 232 .then(function(payload) { 233 return { ok: response.ok, payload: payload }; 234 }); 235 } 236 237 return response.text() 238 .catch(function() { return ''; }) 239 .then(function(text) { 240 return { ok: response.ok, text: text }; 241 }); 242 }) 243 .then(function(result) { 244 if (result && result.ok) { 245 showSuccessAndHide(); 246 return; 247 } 248 249 if (result && result.payload && (result.payload.message || result.payload.error)) { 250 showErrorAndEnable(result.payload.message || result.payload.error); 251 return; 252 } 253 254 submitViaHiddenIframe(); 255 }) 256 .catch(function() { 257 submitViaHiddenIframe(); 258 }); 259 } catch (e) { 260 submitViaHiddenIframe(); 261 } 262 263 return; 264 } 265 266 event.preventDefault(); 267 268 if (!window.emailtruSpotlight || !emailtruSpotlight.ajaxUrl) return; 269 270 const formData = new FormData(form); 271 formData.append('action', 'emailtru_spotlight_subscribe'); 272 formData.append('nonce', emailtruSpotlight.nonce || ''); 273 formData.append('ck_form_id', ckFormId || ''); 274 275 const submitButton = form.querySelector('button[type="submit"], input[type="submit"]'); 276 const previousButtonText = submitButton && submitButton.tagName === 'BUTTON' ? submitButton.textContent : null; 277 if (submitButton) { 278 submitButton.disabled = true; 279 if (submitButton.tagName === 'BUTTON') { 280 submitButton.textContent = 'Submitting...'; 281 } 282 } 283 284 function showMessage(message, kind) { 285 let messageEl = form.nextElementSibling; 286 if (!messageEl || !messageEl.classList || !messageEl.classList.contains('emailtru-form-message')) { 287 messageEl = document.createElement('div'); 288 messageEl.className = 'emailtru-form-message';
289 messageEl.setAttribute('role', kind === 'error' ? 'alert' : 'status'); 290 messageEl.setAttribute('aria-live', kind === 'error' ? 'assertive' : 'polite'); 291 form.insertAdjacentElement('afterend', messageEl); 292 } 293 messageEl.classList.toggle('emailtru-form-message--error', kind === 'error'); 294 messageEl.textContent = message; 295 } 296 297 fetch(emailtruSpotlight.ajaxUrl, { 298 method: 'POST', 299 credentials: 'same-origin', 300 body: formData 301 }) 302 .then(function(response) { 303 return response.json() 304 .catch(function() { return null; }) 305 .then(function(payload) { 306 return { ok: response.ok, payload: payload }; 307 }); 308 }) 309 .then(function(result) { 310 if (result.payload && result.payload.success) { 311 const message = (result.payload.data && result.payload.data.message) ? result.payload.data.message : 'Success! Watch your inbox for great recipes.'; 312 showMessage(message, 'success'); 313 form.reset(); 314 form.style.display = 'none'; 315 if (form.classList.contains('emailtru-popup-form')) { 316 setTimeout(function() { 317 hidePopup(); 318 }, 1500); 319 } 320 return; 321 } 322 323 const errorMessage = (result.payload && result.payload.data && result.payload.data.message) 324 ? result.payload.data.message 325 : 'Something went wrong. Please try again.'; 326 showMessage(errorMessage, 'error'); 327 }) 328 .catch(function() { 329 showMessage('Something went wrong. Please try again.', 'error'); 330 }) 331 .finally(function() { 332 if (submitButton) { 333 submitButton.disabled = false; 334 if (submitButton.tagName === 'BUTTON' && previousButtonText !== null) { 335 submitButton.textContent = previousButtonText; 336 } 337 } 338 }); 339 }, true); 340 } 341 342 function showPopup() { 343 if (!popupModal || isPopupOpen) return; 344 345 // Avoid stacking with ConvertKit popups: only show when 346 // there is no .formkit-modal with data-active="true" 347 if (!canShowPopup()) { 348 schedulePopupTimer(); 349 return; 350 } 351 352 popupModal.classList.add('emailtru-popup-visible'); 353 popupModal.setAttribute('aria-hidden', 'false'); 354 isPopupOpen = true; 355 } 356 357 function hidePopup() { 358 if (!popupModal || !isPopupOpen) return; 359 popupModal.classList.remove('emailtru-popup-visible'); 360 popupModal.setAttribute('aria-hidden', 'true'); 361 isPopupOpen = false; 362 363 if (popupTimer) { 364 clearTimeout(popupTimer); 365 popupTimer = null; 366 } 367 } 368
369 // Determine if it's safe to show the popup (no active ConvertKit modal) 370 function canShowPopup() { 371 const activeFormkitModal = document.querySelector('.formkit-modal[data-active="true"]'); 372 if (activeFormkitModal) { 373 return false; 374 } 375 return true; 376 } 377 378 // Schedule or reschedule the popup timer respecting the configured delay 379 function schedulePopupTimer() { 380 if (!popupModal) return; 381 382 if ((popupModal.getAttribute('data-emailtru-popup-trigger-type') || 'time_based') === 'javascript') { 383 return; 384 } 385 386 if (popupTimer) { 387 clearTimeout(popupTimer); 388 popupTimer = null; 389 } 390 391 popupTimer = setTimeout(function() { 392 showPopup(); 393 }, popupDelayMs); 394 } 395 396 // Public API for JavaScript-triggered popups. 397 window.emailtruOpenPopup = function(formId) { 398 const selector = '[data-emailtru-popup-form-id="' + 399 String(formId).replace(/([\\"\\\\])/g, '\\$1') + '"]'; 400 const requestedModal = document.querySelector(selector); 401 if (!requestedModal) return false; 402 403 popupModal = requestedModal; 404 showPopup(); 405 return true; 406 }; 407 408 // Continuously check forms with requestAnimationFrame 409 function startContinuousCheck() { 410 function check() { 411 checkForms(); 412 requestAnimationFrame(check); 413 } 414 requestAnimationFrame(check); 415 } 416 417 // Check all forms for center position 418 function checkForms() { 419 // Throttle checks to every 50ms 420 const now = Date.now(); 421 if (now - lastCheckTime < 50) return; 422 lastCheckTime = now; 423 424 // Don't check if already active or checking 425 if (isSpotlightActive || isCheckingForms) return; 426 427 isCheckingForms = true; 428 429 try { 430 // Target any embedded EmailTru form container 431 const forms = document.querySelectorAll('.emailtru-forms'); 432 433 // Find the form closest to center that hasn't been spotlighted 434 let closestForm = null; 435 let closestDistance = Infinity; 436 437 for (let i = 0; i < forms.length; i++) { 438 const form = forms[i]; 439 440 // Skip if already spotlighted 441 if (spotlightedForms.has(form)) continue; 442 443 // Skip if this element or its parent has the no-spotlight attribute 444 if (form.hasAttribute('data-emailtru-no-spotlight') && form.getAttribute('data-emailtru-no-spotlight') === 'true') { 445 continue; 446 } 447 448 // Check parent elements for the no-spotlight flag 449 let parent = form.parentElement; 450 let shouldSkip = false; 451 while (parent && parent !== document.body) { 452 if (parent.hasAttribute('data-emailtru-no-spotlight') && parent.getAttribute('data-emailtru-no-spotlight') === 'true') { 453 shouldSkip = true; 454 break; 455 } 456 parent = parent.parentElement; 457 } 458 if (shouldSkip) continue; 459 460 const rect = form.getBoundingClientRect(); 461 const viewportHeight = window.innerHeight; 462 const viewportCenterY = viewportHeight / 2; 463 const formCenterY = rect.top + (rect.height / 2); 464 const distance = Math.abs(formCenterY - viewportCenterY); 465 466 if (distance < closestDistance) { 467 closestDistance = distance; 468 closestForm = form; 469 } 470 } 471 472 // Only spotlight if the closest form is actually centered 473 if (closestForm && isInCenter(closestForm)) { 474 showSpotlight(closestForm); 475 } 476 } finally { 477 isCheckingForms = false; 478 } 479 } 480 481 // Check if element is in center of viewport 482 function isInCenter(form) { 483 if (!form) return false; 484 485 try { 486 const rect = form.getBoundingClientRect(); 487 const viewportHeight = window.innerHeight; 488 const viewportCenterY = viewportHeight / 2; 489 490 // Check if form has dimensions 491 if (!rect || rect.height <= 0 || rect.width <= 0) return false; 492 493 // Check if form is visible in viewport 494 if (rect.bottom < 0 || rect.top > viewportHeight) return false; 495 496 // NEW APPROACH: Check if the viewport center line passes through the form 497 // This is simple: is the center Y position between form's top and bottom? 498 const formContainsCenter = rect.top <= viewportCenterY && rect.bottom >= viewportCenterY; 499 500 if (formContainsCenter) return true; 501 502 // ALTERNATIVE: Check if form center is reasonably close to viewport center 503 const formCenterY = rect.top + (rect.height / 2); 504 const distanceFromCenter = Math.abs(formCenterY - viewportCenterY);
505 const maxDistance = viewportHeight * 0.25; // Within 25% of viewport height 506 507 return distanceFromCenter <= maxDistance; 508 } catch (e) { 509 return false; 510 } 511 } 512 513 function getSpotlightDurationMs(form) { 514 var seconds = 3; 515 var node = form; 516 while (node && node !== document.body) { 517 if (node.hasAttribute && node.hasAttribute('data-emailtru-spotlight-seconds')) { 518 var parsed = parseInt(node.getAttribute('data-emailtru-spotlight-seconds'), 10); 519 if (!isNaN(parsed) && parsed >= 0) { 520 seconds = parsed; 521 } 522 break; 523 } 524 node = node.parentElement; 525 } 526 return seconds * 1000; 527 } 528 529 // Show spotlight on a form 530 function showSpotlight(form) { 531 if (!overlay || spotlightedForms.has(form) || isSpotlightActive) return; 532 533 const rect = form.getBoundingClientRect(); 534 const scrollTop = window.pageYOffset || document.documentElement.scrollTop; 535 const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; 536 537 // Position and size the spotlight 538 overlay.style.setProperty('--spotlight-top', (rect.top + scrollTop) + 'px'); 539 overlay.style.setProperty('--spotlight-left', (rect.left + scrollLeft) + 'px'); 540 overlay.style.setProperty('--spotlight-width', rect.width + 'px'); 541 overlay.style.setProperty('--spotlight-height', rect.height + 'px'); 542 543 // Show the overlay with a slight delay to ensure CSS vars are set 544 setTimeout(function() { 545 overlay.classList.add('active'); 546 }, 10); 547 548 isSpotlightActive = true; 549 spotlightedForms.add(form); 550 551 // Clear any existing timeout 552 if (activeTimeout) { 553 clearTimeout(activeTimeout); 554 activeTimeout = null; 555 } 556 557 var durationMs = getSpotlightDurationMs(form); 558 559 // Remove after spotlight_seconds (default 3s) 560 activeTimeout = setTimeout(function() { 561 overlay.classList.remove('active'); 562 // Wait for CSS transition to complete before allowing next spotlight 563 setTimeout(function() { 564 isSpotlightActive = false; 565 }, 600); // 0.5s transition + 0.1s buffer 566 activeTimeout = null; 567 }, durationMs); 568 } 569 570 // Wait for forms and/or popups to load 571 function waitForFormsAndPopups() { 572 initAjaxForms(); 573 const forms = document.querySelectorAll('.emailtru-forms'); 574 const popups = document.querySelectorAll('.emailtru-popup-modal'); 575 576 if (forms.length > 0) { 577 init(); 578 } 579 580 if (popups.length > 0) { 581 initPopups(); 582 } 583 584 if (forms.length === 0 && popups.length === 0) { 585 setTimeout(waitForFormsAndPopups, 500); 586 } 587 } 588 589 // Start when DOM is ready 590 if (document.readyState === 'loading') { 591 document.addEventListener('DOMContentLoaded', waitForFormsAndPopups); 592 } else { 593 waitForFormsAndPopups(); 594 } 595})();
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.