1(function () { 2 'use strict'; 3 4 var root = document.querySelector('.wht-home-code-build-root, .wht-home-code'); 5 6 if (!root) { 7 return; 8 } 9 10 var bodyHadHelperClass = document.body.classList.contains('wht-home-code-page'); 11 document.body.classList.add('wht-home-code-page'); 12 13 var video = root.querySelector('[data-wht-hero-video]'); 14 var videoModal = root.querySelector('[data-wht-video-modal]'); 15 var videoFrame = root.querySelector('[data-wht-video-frame]'); 16 var videoModalTitle = root.querySelector('[data-wht-video-title]'); 17 var videoCloseButton = root.querySelector('button[data-wht-video-close]'); 18 var videoTriggers = root.querySelectorAll('[data-wht-video-trigger]'); 19 var reviewCarousel = root.querySelector('[data-wht-review-carousel]'); 20 var requestForm = root.querySelector('[data-wht-request-form]'); 21 var tourGrid = root.querySelector('.wht-home-code__tour-grid'); 22 var cardDebugMarker = root.querySelector('[data-wht-card-debug-marker]'); 23 var formConfig = window.whtHomeCodeBuild || {}; 24 var pluginVersion = formConfig.pluginVersion || '0.2.66'; 25 var cssUrl = formConfig.cssUrl || ''; 26 var jsUrl = formConfig.jsUrl || ''; 27 var cssVersion = formConfig.cssVersion || pluginVersion; 28 var jsAssetVersion = formConfig.jsVersion || pluginVersion; 29 var jsVersion = pluginVersion; 30 var criticalCssVersion = formConfig.criticalCssVersion || pluginVersion; 31 var previewMode = formConfig.previewMode || 'normal_page'; 32 var previewPageId = typeof formConfig.pageId !== 'undefined' ? formConfig.pageId : ''; 33 var previewId = typeof formConfig.previewId !== 'undefined' ? formConfig.previewId : ''; 34 var previewNoncePresent = !!formConfig.previewNoncePresent; 35 var thumbnailIdPresent = !!formConfig.thumbnailIdPresent; 36 var debugQueryDetected = window.location.search.indexOf('wht_debug_form=1') !== -1; 37 var isDebugMode = !!formConfig.debugForm || debugQueryDetected; 38 var mobileMarkerVisible = !!formConfig.mobileMarkerVisible || debugQueryDetected; 39 var mobileDebugVisible = !!formConfig.mobileDebugVisible || mobileMarkerVisible || debugQueryDetected; 40 var rootWrapperExists = root.classList.contains('wht-home-code-build-root'); 41 var lastVideoTrigger = null; 42 43 if (isDebugMode) { 44 root.classList.add('wht-home-code--debug'); 45 } 46 47 function normalizeText(value) { 48 return (value || '').replace(/\s+/g, ' ').trim().toLowerCase(); 49 } 50 51 function cleanHeaderNavigation() { 52 var header = document.querySelector('header, .site-header, #masthead'); 53 var navList; 54 var existingToursGroup; 55 var existingPrivateGroup; 56 var groupItem; 57 var groupButton; 58 var subMenu; 59 var privateGroupItem; 60 var privateGroupButton; 61 var privateSubMenu; 62 var hasContact = false; 63 var routes = [ 64 { 65 label: 'M\u00e1laga Wine Tour', 66 url: window.location.origin + '/tour/wine-tour-malaga/' 67 }, 68 { 69 label: 'Organic Olive Oil & Wine Tour', 70 url: window.location.origin + '/tour/olive-oil-wine-tour-malaga/' 71 }, 72 { 73 label: 'Mijas Vineyard Wine & Olive Oil Tasting', 74 url: window.location.origin + '/tour/top-mijas-vineyard-tasting/' 75 }, 76 { 77 label: 'M\u00e1laga City Wine Tasting', 78 url: window.location.origin + '/tour/malaga-city-wine-tasting/' 79 } 80 ]; 81 var privateRoutes = [ 82 { 83 label: 'Private tours', 84 url: window.location.origin + '/private-tours/' 85 }, 86 { 87 label: 'Business tours', 88 url: '#wht-home-request' 89 } 90 ]; 91 var hiddenLabels = [ 92 'regular tours', 93 'small-group tours', 94 'small group tours', 95 'tours & tastings', 96 'tours and tastings', 97 'mijas vineyard wine & olive oil tasting', 98 'vineyard wine & olive oil tasting', 99 'm\u00e1laga city wine tasting', 100 'malaga city wine tasting', 101 'wine tour from m\u00e1laga', 102 'wine tour from malaga', 103 'm\u00e1laga wine tour', 104 'malaga wine tour', 105 'organic olive oil & wine tour from m\u00e1laga', 106 'organic olive oil & wine tour from malaga', 107 'organic wine & olive oil tour from m\u00e1laga', 108 'organic wine & olive oil tour from malaga', 109 'organic wine & olive oil tour', 110 'olive oil & wine tour' 111 ]; 112 113 if (!header) { 114 return; 115 } 116 117 Array.prototype.forEach.call(header.querySelectorAll('a'), function (link) { 118 var linkText = normalizeText(link.textContent); 119 120 if (linkText === 'other experiences' || linkText.indexOf('other experiences') === 0) { 121 (link.closest('li, .menu-item') || link).classList.add('wht-home-header-hidden-item'); 122 } 123 124 if (hiddenLabels.indexOf(linkText) !== -1) { 125 (link.closest('li, .menu-item') || link).classList.add('wht-home-header-hidden-item'); 126 } 127 128 if (linkText === 'private & business tours' || linkText === 'private and business tours' || linkText.indexOf('private & business tours') === 0 || linkText.indexOf('private and business tours') === 0) { 129 (link.closest('li, .menu-item') || link).classList.add('wht-home-header-hidden-item'); 130 } 131 132 if (linkText === 'private tours / events' || linkText === 'private tours & events' || linkText === 'private tours and events') { 133 (link.closest('li, .menu-item') || link).classList.add('wht-home-header-hidden-item'); 134 } 135 136 if (linkText === 'wine shop' || linkText === 'shop') { 137 link.classList.add('wht-home-code-wine-shop-link'); 138 link.setAttribute('href', '/shop/'); 139 } 140 141 if (linkText === 'contact' || link.getAttribute('href') === '#wht-home-request') { 142 hasContact = true; 143 } 144 }); 145 146 navList = header.querySelector('nav ul, .main-navigation ul, .ast-builder
146-menu ul'); 147 existingToursGroup = header.querySelector('.wht-home-code-menu-group'); 148 existingPrivateGroup = header.querySelector('.wht-home-code-private-menu-group'); 149 150 if (!navList) { 151 return; 152 } 153 154 if (!existingToursGroup) { 155 groupItem = document.createElement('li'); 156 groupItem.className = 'menu-item wht-home-code-menu-group'; 157 158 groupButton = document.createElement('button'); 159 groupButton.className = 'wht-home-code-menu-button'; 160 groupButton.type = 'button'; 161 groupButton.setAttribute('aria-haspopup', 'true'); 162 groupButton.textContent = 'Tours & Tastings'; 163 164 subMenu = document.createElement('ul'); 165 subMenu.className = 'wht-home-code-submenu'; 166 subMenu.setAttribute('aria-label', 'Tours and tastings'); 167 168 routes.forEach(function (route) { 169 var item = document.createElement('li'); 170 var link = document.createElement('a'); 171 172 link.href = route.url; 173 link.textContent = route.label; 174 item.appendChild(link); 175 subMenu.appendChild(item); 176 }); 177 178 groupItem.appendChild(groupButton); 179 groupItem.appendChild(subMenu); 180 navList.insertBefore(groupItem, navList.firstChild); 181 } 182 183 if (!existingPrivateGroup) { 184 privateGroupItem = document.createElement('li'); 185 privateGroupItem.className = 'menu-item wht-home-code-menu-group wht-home-code-private-menu-group'; 186 187 privateGroupButton = document.createElement('button'); 188 privateGroupButton.className = 'wht-home-code-menu-button'; 189 privateGroupButton.type = 'button'; 190 privateGroupButton.setAttribute('aria-haspopup', 'true'); 191 privateGroupButton.textContent = 'Private Tours & Events'; 192 193 privateSubMenu = document.createElement('ul'); 194 privateSubMenu.className = 'wht-home-code-submenu'; 195 privateSubMenu.setAttribute('aria-label', 'Private tours and events'); 196 197 privateRoutes.forEach(function (route) { 198 var item = document.createElement('li'); 199 var link = document.createElement('a'); 200 201 link.href = route.url; 202 link.textContent = route.label; 203 item.appendChild(link); 204 privateSubMenu.appendChild(item); 205 }); 206 207 privateGroupItem.appendChild(privateGroupButton); 208 privateGroupItem.appendChild(privateSubMenu); 209 210 if (groupItem && groupItem.parentNode === navList) { 211 navList.insertBefore(privateGroupItem, groupItem.nextSibling); 212 } else { 213 navList.insertBefore(privateGroupItem, navList.firstChild); 214 } 215 } 216 217 if (!hasContact) { 218 var contactItem = document.createElement('li'); 219 var contactLink = document.createElement('a'); 220 221 contactItem.className = 'menu-item wht-home-code-contact-item'; 222 contactLink.href = '#wht-home-request'; 223 contactLink.textContent = 'Contact'; 224 contactLink.setAttribute('aria-label', 'Contact White Houses Tours'); 225 contactItem.appendChild(contactLink); 226 navList.appendChild(contactItem); 227 } 228 } 229 230 function fallbackMobileMenuRoutes() { 231 return [ 232 { 233 label: 'M\u00e1laga Wine Tour', 234 url: window.location.origin + '/tour/wine-tour-malaga/' 235 }, 236 { 237 label: 'Organic Olive Oil & Wine Tour', 238 url: window.location.origin + '/tour/olive-oil-wine-tour-malaga/' 239 }, 240 { 241 label: 'Mijas Vineyard Wine & Olive Oil Tasting', 242 url: window.location.origin + '/tour/top-mijas-vineyard-tasting/' 243 }, 244 { 245 label: 'M\u00e1laga City Wine Tasting', 246 url: window.location.origin + '/tour/malaga-city-wine-tasting/' 247 }, 248 { 249 label: 'Private & Business Tours', 250 url: '#wht-home-private' 251 }, 252 { 253 label: 'Other Experiences', 254 url: '#wht-home-request' 255 }, 256 { 257 label: 'Wine Shop', 258 url: '/shop/' 259 } 260 ]; 261 } 262 263 function buildFallbackMobileHeaderLayer() { 264 var layer = document.createElement('div'); 265 var panel = document.createElement('nav'); 266 var closeButton = document.createElement('button'); 267 var closeIcon = document.createElement('span'); 268 var list = document.createElement('ul'); 269 270 layer.className = 'wht-home-code-mobile-header-layer'; 271 layer.setAttribute('data-wht-mobile-header-layer', ''); 272 layer.setAttribute('data-wht-mobile-menu-rendered-by', 'js-fallback'); 273 layer.setAttribute('data-wht-plugin-version', pluginVersion); 274 275 if (mobileDebugVisible) { 276 var debug = document.createElement('div'); 277 debug.className = 'wht-home-code-mobile-debug-proof'; 278 debug.setAttribute('data-wht-mobile-menu-debug', ''); 279 debug.setAttribute('data-wht-mobile-version-marker', ''); 280 debug.innerHTML = 'WHT compact menu debug · plugin <span data-wht-mobile-debug-plugin>' + pluginVersion + '</span> · breakpoint compact · dedicated panel <span data-wht-mobile-debug-dom>js-fallback</span> · links <span data-wht-mobile-debug-count>0</span> · old panel hidden <span data-wht-mobile-debug-old-hidden>yes</span> · hamburger connected <span data-wht-mobile-debug-connected>pending</span> · open <span data-wht-mobile-debug-open>no</span>'; 281 layer.appendChild(debug); 282 } 283 284 panel.id = 'wht-home-code-mobile-menu'; 285 panel.className = 'wht-home-code-mobile-panel'; 286 panel.setAttribute('data-wht-mobile-menu-panel', ''); 287 panel.setAttribute('data-wht-mobile-menu-source', 'js-fallback'); 288 panel.setAttribute('aria-label', 'White Houses Tours mobile menu'); 289 panel.setAttribute('aria-hidden', 'true'); 290 panel.setAttribute('hidden', ''); 291 292 closeButton.className = 'wht-home-code-mobile-panel__close'; 293 closeButton.type = 'button'; 294 closeButton.setAttribute('data-wht-mobile-menu-close', ''); 295 closeButton.setAttribute('aria-label', 'Close menu'); 296 297 closeIcon.setAttribute('aria-hidden', 'true'); 298 closeIcon.innerHTML = '×'; 299 closeButton.appendChild(closeIcon); 300 301 list.className = 'wht-home-code-mobile-panel__list'; 302 list.setAttribute('data-wht-mobile-links-count', '7'); 303
304 fallbackMobileMenuRoutes().forEach(function (route) { 305 var item = document.createElement('li'); 306 var link = document.createElement('a'); 307 308 link.href = route.url; 309 link.textContent = route.label; 310 item.appendChild(link); 311 list.appendChild(item); 312 }); 313 314 panel.appendChild(closeButton); 315 panel.appendChild(list); 316 layer.appendChild(panel); 317 document.body.appendChild(layer); 318 319 return layer; 320 } 321 322 function ensureMobileHeaderLayer() { 323 var layer = document.querySelector('[data-wht-mobile-header-layer]'); 324 325 if (!layer) { 326 layer = buildFallbackMobileHeaderLayer(); 327 } 328 329 return layer; 330 } 331 332 function ensureMobileHeaderPanel() { 333 var header = document.querySelector('header, .site-header, #masthead'); 334 var layer = ensureMobileHeaderLayer(); 335 var panel = layer ? layer.querySelector('[data-wht-mobile-menu-panel]') : null; 336 337 if (header && layer && layer.parentNode !== header) { 338 header.appendChild(layer); 339 } 340 341 return panel; 342 } 343 344 function isMobileViewport() { 345 return window.matchMedia && window.matchMedia('(max-width: 1199px)').matches; 346 } 347 348 function isThemeMobileMenuOpen(header) { 349 var expandedToggle; 350 351 if (!header) { 352 return false; 353 } 354 355 expandedToggle = header.querySelector('.menu-toggle[aria-expanded="true"], .main-header-menu-toggle[aria-expanded="true"], .ast-mobile-menu-trigger-minimal[aria-expanded="true"]'); 356 357 return !!expandedToggle 358 || !!header.querySelector('.ast-mobile-popup-drawer.active, .ast-mobile-popup-drawer.show, .main-header-bar-navigation.toggle-on, .main-header-bar-navigation.toggled, .main-navigation.toggled, .main-navigation.toggle-on') 359 || document.body.classList.contains('ast-main-header-nav-open'); 360 } 361 362 function updateMobilePanelPosition() { 363 var header = document.querySelector('header, .site-header, #masthead'); 364 var rect; 365 var top = 88; 366 367 if (header && header.getBoundingClientRect) { 368 rect = header.getBoundingClientRect(); 369 top = Math.max(0, Math.round(rect.bottom)); 370 } 371 372 document.documentElement.style.setProperty('--wht-mobile-panel-top', top + 'px'); 373 document.body.style.setProperty('--wht-mobile-panel-top', top + 'px'); 374 } 375 376 function updateMobileHeaderDebug(connectedCount) { 377 var panel = document.querySelector('[data-wht-mobile-menu-panel]'); 378 var debug = document.querySelector('[data-wht-mobile-menu-debug]'); 379 var links = panel ? panel.querySelectorAll('a') : []; 380 var toggles = document.querySelectorAll('[data-wht-mobile-menu-bridge="true"]'); 381 var connected = typeof connectedCount === 'number' ? connectedCount : toggles.length; 382 383 if (!debug) { 384 return; 385 } 386 387 var pluginNode = debug.querySelector('[data-wht-mobile-debug-plugin]');
388 var domNode = debug.querySelector('[data-wht-mobile-debug-dom]'); 389 var countNode = debug.querySelector('[data-wht-mobile-debug-count]'); 390 var connectedNode = debug.querySelector('[data-wht-mobile-debug-connected]'); 391 var openNode = debug.querySelector('[data-wht-mobile-debug-open]'); 392 393 if (pluginNode) { 394 pluginNode.textContent = pluginVersion; 395 } 396 397 if (domNode) { 398 domNode.textContent = panel ? 'yes' : 'no'; 399 } 400 401 if (countNode) { 402 countNode.textContent = String(links.length); 403 } 404 405 if (connectedNode) { 406 connectedNode.textContent = connected > 0 ? 'yes' : 'no'; 407 } 408 409 if (openNode) { 410 openNode.textContent = document.body.classList.contains('wht-mobile-menu-is-open') ? 'true' : 'false'; 411 } 412 } 413 414 function mobileToggleSelector() { 415 return '.menu-toggle, .main-header-menu-toggle, .ast-mobile-menu-trigger-minimal, .ast-mobile-popup-drawer .menu-toggle-close, .ast-button-wrap button, button[aria-label*="Menu"], button[aria-label*="menu"], [aria-controls*="menu"], [class*="menu-toggle"]'; 416 } 417 418 function closestMobileToggle(target) { 419 var header = document.querySelector('header, .site-header, #masthead'); 420 var toggle = target && target.closest ? target.closest(mobileToggleSelector()) : null; 421 422 if (!toggle || !header || !header.contains(toggle) || toggle.closest('[data-wht-mobile-menu-panel]')) { 423 return null; 424 } 425 426 return toggle; 427 } 428 429 function setMobileToggleState(isOpen) { 430 var header = document.querySelector('header, .site-header, #masthead'); 431 432 if (!header) { 433 return; 434 } 435 436 Array.prototype.forEach.call(header.querySelectorAll('[data-wht-mobile-menu-bridge="true"], ' + mobileToggleSelector()), function (toggle) { 437 if (!header.contains(toggle)) { 438 return; 439 } 440 441 toggle.setAttribute('aria-controls', 'wht-home-code-mobile-menu'); 442 toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); 443 toggle.classList.toggle('wht-home-code-mobile-toggle-open', !!isOpen); 444 }); 445 } 446 447 function setMobileHeaderPanelOpen(isOpen) { 448 var header = document.querySelector('header, .site-header, #masthead'); 449 var panel = ensureMobileHeaderPanel(); 450 451 if (!header || !panel) { 452 updateMobileHeaderDebug(0); 453 return; 454 } 455 456 if (isOpen && isMobileViewport()) { 457 updateMobilePanelPosition(); 458 document.body.classList.add('wht-home-code-mobile-menu-open'); 459 document.body.classList.add('wht-mobile-menu-is-open'); 460 panel.removeAttribute('hidden'); 461 panel.setAttribute('aria-hidden', 'false'); 462 setMobileToggleState(true); 463 updateMobileHeaderDebug(); 464 return; 465 } 466 467 document.body.classList.remove('wht-home-code-mobile-menu-open'); 468 document.body.classList.remove('wht-mobile-menu-is-open'); 469 panel.setAttribute('hidden', ''); 470 panel.setAttribute('aria-hidden', 'true'); 471 setMobileToggleState(false); 472 updateMobileHeaderDebug(); 473 } 474 475 function syncMobileHeaderPanel() { 476 var header = document.querySelector('header, .site-header, #masthead'); 477 478 if (!isMobileViewport()) { 479 setMobileHeaderPanelOpen(false); 480 return; 481 } 482 483 if (document.body.classList.contains('wht-mobile-menu-is-open')) { 484 updateMobilePanelPosition(); 485 updateMobileHeaderDebug(); 486 return; 487 } 488 489 setMobileHeaderPanelOpen(isThemeMobileMenuOpen(header)); 490 } 491 492 function connectMobileHeaderToggles(header) { 493 var connectedCount = 0; 494 495 if (!header) { 496 return connectedCount; 497 } 498 499 Array.prototype.forEach.call(header.querySelectorAll(mobileToggleSelector() + ', [aria-controls="wht-home-code-mobile-menu"]'), function (toggle) { 500 if (toggle.getAttribute('data-wht-mobile-menu-bridge') === 'true') { 501 connectedCount += 1; 502 return; 503 } 504 505 toggle.setAttribute('data-wht-mobile-menu-bridge', 'true'); 506 toggle.setAttribute('aria-controls', 'wht-home-code-mobile-menu'); 507 toggle.addEventListener('click', function (event) { 508 var wasOpen = document.body.classList.contains('wht-mobile-menu-is-open'); 509 510 if (isMobileViewport()) { 511 event.preventDefault(); 512 event.stopPropagation(); 513 if (event.stopImmediatePropagation) { 514 event.stopImmediatePropagation(); 515 } 516 } 517 518 setMobileHeaderPanelOpen(!wasOpen); 519 }); 520 connectedCount += 1; 521 }); 522 523 updateMobileHeaderDebug(connectedCount); 524 525 return connectedCount; 526 } 527 528 function installMobileHeaderPanel() { 529 var header = document.querySelector('header, .site-header, #masthead'); 530 var observer; 531 532 if (!header) { 533 return; 534 } 535 536 ensureMobileHeaderPanel(); 537 updateMobilePanelPosition(); 538 connectMobileHeaderToggles(header); 539 540 if (document.documentElement.getAttribute('data-wht-mobile-capture-installed') !== 'true') { 541 document.documentElement.setAttribute('data-wht-mobile-capture-installed', 'true'); 542 document.addEventListener('click', function (event) { 543 var toggle; 544 var wasOpen; 545 var mobilePanel; 546 547 if (!isMobileViewport()) { 548 return; 549 } 550 551 toggle = closestMobileToggle(event.target); 552 553 if (!toggle) { 554 mobilePanel = document.querySelector('[data-wht-mobile-menu-panel]'); 555 if ( 556 document.body.classList.contains('wht-mobile-menu-is-open') && 557 !header.contains(event.target) && 558 (!mobilePanel || !mobilePanel.contains(event.target)) 559 ) { 560 setMobileHeaderPanelOpen(false); 561 } 562 return; 563 } 564 565 wasOpen = document.body.classList.contains('wht-mobile-menu-is-open'); 566 event.preventDefault(); 567 event.stopPropagation(); 568 569 if (event.stopImmediatePropagation) { 570 event.stopImmediatePropagation(); 571 } 572 573 toggle.setAttribute('data-wht-mobile-menu-bridge', 'true'); 574 setMobileHeaderPanelOpen(!wasOpen); 575 }, true); 576 } 577 578 header.addEventListener('click', function (event) { 579 if (event.target.closest('[data-wht-mobile-menu-close]')) { 580 event.preventDefault(); 581 setMobileHeaderPanelOpen(false); 582 return; 583 } 584 585 if (event.target.closest('[data-wht-mobile-menu-panel] a')) { 586 setMobileHeaderPanelOpen(false); 587 } 588 }); 589 590 document.addEventListener('keydown', function (event) { 591 if (event.key === 'Escape') { 592 setMobileHeaderPanelOpen(false); 593 } 594 }); 595 596 if (window.MutationObserver) { 597 observer = new MutationObserver(function () { 598 window.setTimeout(function () { 599 connectMobileHeaderToggles(header); 600 updateMobilePanelPosition(); 601 updateMobileHeaderDebug(); 602 }, 0); 603 }); 604 observer.observe(header, { 605 attributes: true, 606 subtree: true, 607 attributeFilter: ['class', 'aria-expanded'] 608 }); 609 } 610 611 window.addEventListener('resize', function () { 612 updateMobilePanelPosition(); 613 syncMobileHeaderPanel(); 614 }); 615 syncMobileHeaderPanel(); 616 updateMobileHeaderDebug(); 617 } 618 619 function positionHeaderSubmenus() { 620 var header = document.querySelector('header, .site-header, #masthead'); 621 var viewportPadding = 16; 622 623 if (!header) { 624 return; 625 } 626 627 Array.prototype.forEach.call(header.querySelectorAll('.wht-home-code-submenu'), function (subMenu) { 628 var rect; 629 var shift = 0; 630 631 subMenu.style.setProperty('--wht-submenu-shift', '0px'); 632 rect = subMenu.getBoundingClientRect(); 633 634 if (rect.left < viewportPadding) { 635 shift += viewportPadding - rect.left; 636 } 637 638 if (rect.right + shift > window.innerWidth - viewportPadding) { 639 shift -= rect.right + shift - (window.innerWidth - viewportPadding); 640 } 641 642 subMenu.style.setProperty('--wht-submenu-shift', shift.toFixed(0) + 'px'); 643 }); 644 } 645 646 cleanHeaderNavigation(); 647 installMobileHeaderPanel(); 648 positionHeaderSubmenus(); 649 window.setTimeout(positionHeaderSubmenus, 0); 650 window.addEventListener('resize', positionHeaderSubmenus); 651 652 function ensureCardDebugMarker() { 653 if (!isDebugMode || cardDebugMarker) { 654 return; 655 } 656 657 cardDebugMarker = document.createElement('div'); 658 cardDebugMarker.className = 'wht-home-code__card-debug-marker'; 659 cardDebugMarker.setAttribute('data-wht-card-debug-marker', ''); 660 cardDebugMarker.setAttribute('data-wht-card-selector', '.wht-home-code__tour-card'); 661 662 if (tourGrid && tourGrid.parentNode) { 663 tourGrid.parentNode.insertBefore(cardDebugMarker, tourGrid); 664 return; 665 } 666 667 root.insertBefore(cardDebugMarker, root.firstChild); 668 } 669 670 function updateCardDebugMarker() { 671 var cardSelector = '.wht-home-code__tour-card'; 672 var cards; 673 var bulletLists; 674 var heights = []; 675 var overflows = []; 676 var topGaps = []; 677 var actionBottomGaps = []; 678 var bulletBottomGaps = []; 679 var bodyPaddingTopValues = []; 680 var bodyPaddingBottomValues = []; 681 var ctaFoundCount = 0; 682 var ctaClipped = false;
683 var ctaClipDetails = []; 684 var cssSourceMarker = 'unknown'; 685 var criticalCssLoaded = criticalCssVersion; 686 var microfactsStyle = 'unknown'; 687 var microfactsLineStripCount = 0; 688 var microfactsDetails = []; 689 var mediaDimensions = []; 690 var mediaStable = true; 691 var allEqual; 692 var withinAcceptedHeight; 693 var bulletsSingleColumn = true; 694 var bulletLayoutDetails = []; 695 696 if (!isDebugMode) { 697 return; 698 } 699 700 ensureCardDebugMarker(); 701 702 if (!cardDebugMarker) { 703 return; 704 } 705 706 cards = root.querySelectorAll(cardSelector); 707 bulletLists = root.querySelectorAll(cardSelector + ' .wht-home-code__bullet-list'); 708 if (window.getComputedStyle) { 709 var rootStyles = window.getComputedStyle(root); 710 cssSourceMarker = (rootStyles.getPropertyValue('--wht-card-css-source') || 'unknown').replace(/["']/g, '').trim(); 711 criticalCssLoaded = (rootStyles.getPropertyValue('--wht-card-critical-css-version') || criticalCssVersion || 'unknown').replace(/["']/g, '').trim(); 712 microfactsStyle = (rootStyles.getPropertyValue('--wht-card-microfacts-style') || 'unknown').trim(); 713 } 714 715 Array.prototype.forEach.call(cards, function (card, index) { 716 var rect = card.getBoundingClientRect(); 717 var firstText = card.querySelector('.wht-home-code__card-label'); 718 var actions = card.querySelector('.wht-home-code__tour-actions'); 719 var cta = actions ? actions.querySelector('.wht-home-code__text-link') : null; 720 var bullets = card.querySelector('.wht-home-code__bullet-list'); 721 var facts = card.querySelector('.wht-home-code__tour-facts'); 722 var factList = card.querySelector('.wht-home-code__fact-list'); 723 var media = card.querySelector('.wht-home-code__tour-image-wrap'); 724 var body = card.querySelector('.wht-home-code__tour-body'); 725 var bodyStyles = body ? window.getComputedStyle(body) : null; 726 var factsStyles = facts ? window.getComputedStyle(facts) : null; 727 var factItem = factList ? factList.querySelector('li') : null; 728 var factItemStyles = factItem ? window.getComputedStyle(factItem) : null; 729 var height = Math.round(rect.height); 730 var contentOverflows = card.scrollHeight >
730 card.clientHeight + 1 || card.scrollWidth > card.clientWidth + 1; 731 var firstTextTop = firstText ? Math.round(firstText.getBoundingClientRect().top - rect.top) : 0; 732 var actionBottom = actions ? Math.round(rect.bottom - actions.getBoundingClientRect().bottom) : 0; 733 var bulletBottom = bullets ? Math.round(rect.bottom - bullets.getBoundingClientRect().bottom) : 0; 734 var bodyPaddingTop = bodyStyles ? Math.round(parseFloat(bodyStyles.paddingTop) || 0) : 0; 735 var bodyPaddingBottom = bodyStyles ? Math.round(parseFloat(bodyStyles.paddingBottom) || 0) : 0; 736 var mediaRect = media ? media.getBoundingClientRect() : null; 737 var mediaWidth = mediaRect ? Math.round(mediaRect.width) : 0; 738 var mediaHeight = mediaRect ? Math.round(mediaRect.height) : 0; 739 var ctaRect; 740 var ctaIsClipped = false; 741 742 heights.push(height); 743 overflows.push(contentOverflows); 744 topGaps.push(firstTextTop); 745 actionBottomGaps.push(actionBottom); 746 bulletBottomGaps.push(bulletBottom); 747 bodyPaddingTopValues.push(bodyPaddingTop); 748 bodyPaddingBottomValues.push(bodyPaddingBottom); 749 mediaDimensions.push(mediaWidth + 'x' + mediaHeight); 750 751 if (!mediaRect || mediaWidth <= 0 || mediaHeight <= 0 || Math.abs(mediaHeight - height) > 18) { 752 mediaStable = false; 753 } 754 755 if (cta) { 756 ctaFoundCount += 1; 757 ctaRect = cta.getBoundingClientRect(); 758 ctaIsClipped = ctaRect.left < rect.left || ctaRect.right > rect.right || ctaRect.top < rect.top || ctaRect.bottom > rect.bottom || cta.scrollWidth > cta.clientWidth + 1 || cta.scrollHeight > cta.clientHeight + 1; 759 if (ctaIsClipped) { 760 ctaClipped = true; 761 } 762 ctaClipDetails.push('card ' + (index + 1) + ': ' + (ctaIsClipped ? 'clipped' : 'ok')); 763 } else { 764 ctaClipped = true; 765 ctaClipDetails.push('card ' + (index + 1) + ': missing'); 766 } 767 768 if (factsStyles && factItemStyles) { 769 var hasStripLines = parseFloat(factsStyles.borderTopWidth) > 0 && parseFloat(factsStyles.borderBottomWidth) > 0; 770 var hasNoChip = factItemStyles.backgroundColor === 'rgba(0, 0, 0, 0)' && (factItemStyles.borderTopStyle === 'none' || parseFloat(factItemStyles.borderTopWidth) === 0); 771 if (hasStripLines && hasNoChip) { 772 microfactsLineStripCount += 1; 773 } 774 microfactsDetails.push('card ' + (index + 1) + ': ' + (hasStripLines && hasNoChip ? 'line-strip' : 'check-css')); 775 } 776 777 card.setAttribute('data-wht-card-debug-label', 'card ' + (index + 1) + ' \u00b7 ' + height + 'px'); 778 }); 779 780 Array.prototype.forEach.call(bulletLists, function (list, index) { 781 var styles = window.getComputedStyle(list); 782 var gridColumns = styles.gridTemplateColumns || 'none'; 783 var columnCount = styles.columnCount || 'auto'; 784 var display = styles.display || ''; 785 var flexWrap = styles.flexWrap || '';
786 var gridColumnCount = gridColumns.split(' ').filter(Boolean).length; 787 var isSingle = columnCount !== '2' && gridColumnCount <= 1 && !(display.indexOf('flex') !== -1 && flexWrap !== 'nowrap'); 788 789 if (!isSingle) { 790 bulletsSingleColumn = false; 791 } 792 793 bulletLayoutDetails.push('card ' + (index + 1) + ': ' + gridColumns + ', columns ' + columnCount + ', display ' + display + ', wrap ' + flexWrap); 794 }); 795 796 allEqual = heights.length === 4 && heights.every(function (height) { 797 return height === heights[0]; 798 }); 799 withinAcceptedHeight = heights.length === 4 && heights.every(function (height) { 800 return height >= 336 && height <= 350; 801 }); 802 803 cardDebugMarker.hidden = false; 804 cardDebugMarker.textContent = 'WHT card debug \u00b7 plugin ' + pluginVersion + ' \u00b7 preview_mode ' + previewMode + ' \u00b7 page_id ' + previewPageId + ' \u00b7 preview_id ' + previewId + ' \u00b7 preview_nonce present ' + (previewNoncePresent ? 'yes' : 'no') + ' \u00b7 _thumbnail_id present ' + (thumbnailIdPresent ? 'yes' : 'no') + ' \u00b7 css url ' + (cssUrl || 'n/a') + ' \u00b7 css ' + cssVersion + ' \u00b7 critical css ' + criticalCssLoaded + ' \u00b7 js url ' + (jsUrl || 'n/a') + ' \u00b7 js ' + jsAssetVersion + ' \u00b7 root wrapper exists ' + (rootWrapperExists ? 'yes' : 'no') + ' \u00b7 body helper existed before JS ' + (bodyHadHelperClass ? 'yes' : 'no') + ' \u00b7 body helper class exists ' + (document.body.classList.contains('wht-home-code-page') ? 'yes' : 'no') + ' \u00b7 css source ' + cssSourceMarker + ' \u00b7 microfacts ' + microfactsStyle + ' \u00b7 microfacts loaded: ' + (microfactsLineStripCount === 4 ? 'line-strip' : 'check-css') + ' \u00b7 height selector ' + cardSelector + ' \u00b7 card heights: ' + (heights.length ? heights.join(' / ') : 'none') + ' px \u00b7 equal: ' + (allEqual ? 'yes' : 'no') + ' \u00b7 height 336-350px: ' + (withinAcceptedHeight ? 'yes' : 'no') + ' \u00b7 media: ' + (mediaDimensions.length ? mediaDimensions.join(' / ') : 'none') + ' \u00b7 media stable: ' + (mediaStable ? 'yes' : 'no') + ' \u00b7 content top padding: ' + (bodyPaddingTopValues.length ? bodyPaddingTopValues.join(' / ') : 'none') + ' px \u00b7 content bottom padding: ' + (bodyPaddingBottomValues.length ? bodyPaddingBottomValues.join(' / ') : 'none') + ' px \u00b7 first text top gaps: ' + (topGaps.length ? topGaps.join(' / ') : 'none') + ' px \u00b7 CTA found: ' + ctaFoundCount + '/4 \u00b7 CTA clipped: ' + (ctaClipped ? 'yes' : 'no') + ' \u00b7 CTA details: ' + (ctaClipDetails.length ? ctaClipDetails.join(' | ') : 'none') + ' \u00b7 CTA bottom gaps: ' + (actionBottomGaps.length ? actionBottomGaps.join(' / ') : 'none') + ' px \u00b7 bullet bottom gaps: ' + (bulletBottomGaps.length ? bulletBottomGaps.join(' / ') : 'none') + ' px \u00b7 bullet layout: ' + (bulletsSingleColumn ? 'single-column' : 'not single-column') + ' \u00b7 microfacts details: ' + (microfactsDetails.length ? microfactsDetails.join(' | ') : 'none') + ' \u00b7 bullet selector .wht-home-code__bullet-list \u00b7 bullet computed: ' + (bulletLayoutDetails.length ? bulletLayoutDetails.join(' | ') : 'none') + ' \u00b7 overflow: ' + (overflows.indexOf(true) === -1 ? 'no' : 'yes'); 805 } 806 807 if (isDebugMode) { 808 updateCardDebugMarker(); 809 window.setTimeout(updateCardDebugMarker, 0); 810 window.setTimeout(updateCardDebugMarker, 500); 811 window.addEventListener('load', updateCardDebugMarker); 812 window.addEventListener('resize', updateCardDebugMarker); 813 } 814 815 function buildYoutubeEmbedUrl(videoId) { 816 return 'https://www.youtube-nocookie.com/embed/' + encodeURIComponent(videoId) + '?rel=0&modestbranding=1&playsinline=1'; 817 } 818 819 function openVideoModal(trigger) { 820 var videoId = trigger.getAttribute('data-wht-video-id'); 821 var videoTitle = trigger.getAttribute('data-wht-video-title') || 'Experience preview'; 822 823 if (!videoModal || !videoFrame || !videoId) { 824 return; 825 } 826 827 lastVideoTrigger = trigger; 828 videoFrame.setAttribute('src', buildYoutubeEmbedUrl(videoId)); 829 videoFrame.setAttribute('title', videoTitle); 830 831 if (videoModalTitle) { 832 videoModalTitle.textContent = videoTitle; 833 } 834 835 videoModal.hidden = false;
836 videoModal.setAttribute('aria-hidden', 'false'); 837 document.body.classList.add('wht-home-code-modal-open'); 838 839 if (videoCloseButton) { 840 videoCloseButton.focus(); 841 } 842 } 843 844 function closeVideoModal() { 845 if (!videoModal || videoModal.hidden) { 846 return; 847 } 848 849 videoModal.hidden = true; 850 videoModal.setAttribute('aria-hidden', 'true'); 851 document.body.classList.remove('wht-home-code-modal-open'); 852 853 if (videoFrame) { 854 videoFrame.setAttribute('src', ''); 855 videoFrame.setAttribute('title', 'Experience preview'); 856 } 857 858 if (videoModalTitle) { 859 videoModalTitle.textContent = 'Experience preview'; 860 } 861 862 if (lastVideoTrigger) { 863 lastVideoTrigger.focus(); 864 lastVideoTrigger = null; 865 } 866 } 867 868 if (videoModal && videoFrame && videoTriggers.length) { 869 Array.prototype.forEach.call(videoTriggers, function (trigger) { 870 trigger.addEventListener('click', function () { 871 openVideoModal(trigger); 872 }); 873 }); 874 875 videoModal.addEventListener('click', function (event) { 876 var closeTarget = event.target.closest ? event.target.closest('[data-wht-video-close]') : null; 877 878 if (closeTarget) { 879 closeVideoModal(); 880 } 881 }); 882 883 document.addEventListener('keydown', function (event) { 884 if (event.key === 'Escape') { 885 closeVideoModal(); 886 } 887 }); 888 } 889 890 if (reviewCarousel) { 891 var reviewTrack = reviewCarousel.querySelector('[data-wht-review-track]'); 892 var reviewPrev = root.querySelector('[data-wht-review-prev]'); 893 var reviewNext = root.querySelector('[data-wht-review-next]'); 894 var reviewScrollTimer; 895 896 function updateReviewControls() { 897 if (!reviewTrack || !reviewPrev || !reviewNext) { 898 return; 899 } 900 901 var maxScroll = reviewTrack.scrollWidth - reviewTrack.clientWidth - 2; 902 903 reviewPrev.disabled = reviewTrack.scrollLeft <= 2; 904 reviewNext.disabled = reviewTrack.scrollLeft >= maxScroll; 905 } 906 907 function scrollReviews(direction) { 908 if (!reviewTrack) { 909 return; 910 } 911 912 reviewTrack.scrollBy({ 913 left: direction * Math.max(260, reviewTrack.clientWidth * 0.9), 914 behavior: 'smooth' 915 }); 916 } 917 918 if (reviewPrev && reviewNext && reviewTrack) { 919 reviewPrev.addEventListener('click', function () { 920 scrollReviews(-1); 921 }); 922 923 reviewNext.addEventListener('click', function () { 924 scrollReviews(1); 925 }); 926 927 reviewTrack.addEventListener('scroll', function () { 928 window.clearTimeout(reviewScrollTimer); 929 reviewScrollTimer = window.setTimeout(updateReviewControls, 80); 930 }); 931 932 window.addEventListener('resize', updateReviewControls); 933 updateReviewControls(); 934 } 935 } 936 937 if (requestForm && requestForm.dataset.whtRequestBound !== '1') { 938 requestForm.dataset.whtRequestBound = '1'; 939 940 var formMessage = requestForm.querySelector('[data-wht-form-message]'); 941 var debugMarker = requestForm.querySelector('[data-wht-js-form-debug-marker]'); 942 var debugPanel = requestForm.querySelector('[data-wht-form-debug-panel]'); 943 var selfTestResult = requestForm.querySelector('[data-wht-self-test-result]'); 944 var mailSelfTestButton = requestForm.querySelector('[data-wht-mail-self-test]'); 945 var ajaxSelfTestButton = requestForm.querySelector('[data-wht-ajax-self-test]'); 946 var copyDiagnosticButton = requestForm.querySelector('[data-wht-copy-diagnostic]'); 947 var submitButton = requestForm.querySelector('button[type="submit"]'); 948 var nameField = requestForm.querySelector('[name="wht_home_code_name"]'); 949 var emailField = requestForm.querySelector('[name="wht_home_code_email"]'); 950 var messageField = requestForm.querySelector('[name="wht_home_code_message"]'); 951 var pageUrlField = requestForm.querySelector('[name="wht_home_code_page_url"]'); 952 var nonceField = requestForm.querySelector('[name="wht_home_code_nonce"]'); 953 var requestIdField = requestForm.querySelector('[name="wht_home_code_request_id"]'); 954 var originalSubmitHtml = submitButton ? submitButton.innerHTML : ''; 955 var isFormDebug = isDebugMode; 956 var formMatchCount = root.querySelectorAll('[data-wht-request-form]').length; 957 var handlerAttachCount = parseInt(requestForm.getAttribute('data-wht-handler-count') || '0', 10); 958 var isSubmitting = false; 959 var activeRequestId = ''; 960 var diagnosticState = { 961 lastSubmit: null, 962 lastSelfTest: null, 963 jsErrors: [], 964 unhandledRejections: [] 965 }; 966 967 if (isNaN(handlerAttachCount)) { 968 handlerAttachCount = 0; 969 } 970 971 handlerAttachCount += 1;
972 requestForm.setAttribute('data-wht-handler-count', String(handlerAttachCount)); 973 974 if (isFormDebug && !debugMarker) { 975 debugMarker = document.createElement('div'); 976 debugMarker.className = 'wht-home-code__form-debug-marker wht-home-code__form-debug-marker--js'; 977 debugMarker.setAttribute('data-wht-js-form-debug-marker', ''); 978 requestForm.insertBefore(debugMarker, formMessage || requestForm.firstChild); 979 } 980 981 if (isFormDebug && !debugPanel) { 982 debugPanel = document.createElement('div'); 983 debugPanel.className = 'wht-home-code__form-debug-panel'; 984 debugPanel.setAttribute('data-wht-form-debug-panel', ''); 985 debugPanel.hidden = true; 986 987 if (formMessage && formMessage.nextSibling) { 988 requestForm.insertBefore(debugPanel, formMessage.nextSibling); 989 } else { 990 requestForm.insertBefore(debugPanel, requestForm.firstChild); 991 } 992 } 993 994 function yesNo(value) { 995 return value ? 'yes' : 'no'; 996 } 997 998 function boolDebugValue(value) { 999 if (value === true) { 1000 return 'true'; 1001 } 1002 1003 if (value === false) { 1004 return 'false'; 1005 } 1006 1007 return 'n/a'; 1008 } 1009 1010 function responseLooksBlocked(value) { 1011 var lowered = String(value || '').toLowerCase(); 1012 1013 return lowered.indexOf('apbct') !== -1 || lowered.indexOf('cleantalk') !== -1 || lowered.indexOf('blocked') !== -1 || lowered.indexOf('forbidden') !== -1 || lowered.indexOf('spam check') !== -1; 1014 } 1015 1016 function containsSubmittedPii(value) { 1017 var lowered = String(value || '').toLowerCase();
1018 var fieldValues = [nameField, emailField, messageField].map(function (field) { 1019 return field && field.value ? String(field.value).trim().toLowerCase() : ''; 1020 }).filter(function (fieldValue) { 1021 return fieldValue.length >= 4; 1022 }); 1023 1024 return fieldValues.some(function (fieldValue) { 1025 return lowered.indexOf(fieldValue) !== -1; 1026 }); 1027 } 1028 1029 function safeRawResponsePreview(value) { 1030 var text = String(value || '').trim(); 1031 1032 if (!text || containsSubmittedPii(text)) { 1033 return 'not shown'; 1034 } 1035 1036 return text.replace(/\s+/g, ' ').slice(0, 360); 1037 } 1038 1039 function safeDiagnosticText(value) { 1040 var text = String(value || '').trim(); 1041 1042 if (!text || containsSubmittedPii(text)) { 1043 return 'not shown'; 1044 } 1045 1046 return text.replace(/\s+/g, ' ').slice(0, 240); 1047 } 1048 1049 function describeError(error) { 1050 if (!error) { 1051 return 'unknown'; 1052 } 1053 1054 if (typeof error === 'string') { 1055 return safeDiagnosticText(error); 1056 } 1057 1058 if (error.message) { 1059 return safeDiagnosticText(error.name ? error.name + ': ' + error.message : error.message); 1060 } 1061 1062 return safeDiagnosticText(String(error)); 1063 } 1064 1065 function recordJsDiagnostic(type, error) { 1066 var entry = { 1067 type: type, 1068 message: describeError(error), 1069 timestamp: new Date().toISOString() 1070 }; 1071 1072 if (type === 'unhandledrejection') { 1073 diagnosticState.unhandledRejections.push(entry); 1074 diagnosticState.unhandledRejections = diagnosticState.unhandledRejections.slice(-5); 1075 } else { 1076 diagnosticState.jsErrors.push(entry); 1077 diagnosticState.jsErrors = diagnosticState.jsErrors.slice(-5); 1078 } 1079 1080 return entry; 1081 } 1082 1083 function latestDiagnosticEntry(list) { 1084 return list && list.length ? list[list.length - 1] : null; 1085 } 1086 1087 if (isFormDebug && !window.whtHomeCodeBuildDebugErrorsBound) { 1088 window.whtHomeCodeBuildDebugErrorsBound = true; 1089 window.addEventListener('error', function (event) { 1090 recordJsDiagnostic('window.onerror', event.error || event.message || 'window error'); 1091 }); 1092 window.addEventListener('unhandledrejection', function (event) { 1093 recordJsDiagnostic('unhandledrejection', event.reason || 'unhandled rejection'); 1094 }); 1095 } 1096 1097 function escapeHtml(value) { 1098 return String(value == null ? '' : value) 1099 .replace(/&/g, '&') 1100 .replace(/</g, '<') 1101 .replace(/>/g, '>') 1102 .replace(/"/g, '"') 1103 .replace(/'/g, '''); 1104 } 1105 1106 function showFormMessage(type, message) { 1107 if (!formMessage) { 1108 return; 1109 } 1110 1111 formMessage.hidden = false; 1112 formMessage.classList.remove('wht-home-code__form-message--success'); 1113 formMessage.classList.remove('wht-home-code__form-message--error'); 1114 formMessage.classList.add('wht-home-code__form-message--' + type); 1115 formMessage.textContent = message; 1116 } 1117 1118 function clearDebugPanel() { 1119 if (!debugPanel) { 1120 return; 1121 } 1122 1123 debugPanel.hidden = true; 1124 debugPanel.textContent = ''; 1125 } 1126 1127 function updateDebugMarker() { 1128 if (!isFormDebug || !debugMarker) { 1129 return; 1130 } 1131 1132 debugMarker.hidden = false; 1133 debugMarker.textContent = 'WHT JS form debug active \u00b7 js ' + jsVersion + ' \u00b7 preview_mode ' + previewMode + ' \u00b7 submit handler attached \u00b7 forms found: ' + formMatchCount + ' \u00b7 handlers: ' + handlerAttachCount + ' \u00b7 css asset ' + cssVersion + ' \u00b7 js asset ' + jsAssetVersion + ' \u00b7 critical css ' + criticalCssVersion + ' \u00b7 debug param detected by JS: ' + (debugQueryDetected ? 'yes' : 'no') + ' \u00b7 admin self-tests: ' + (formConfig.canRunSelfTests ? 'available' : 'not available') + ' \u00b7 ajax config: ' + (formConfig.ajaxUrl ? 'admin_ajax yes' : 'admin_ajax missing') + ', ' + (formConfig.fallbackUrl ? 'fallback yes' : 'fallback missing') + ', ' + (formConfig.nonce ? 'nonce yes' : 'nonce missing'); 1134 } 1135 1136 function setFormLoading(isLoading) { 1137 requestForm.classList.toggle('is-loading', isLoading); 1138 requestForm.setAttribute('aria-busy', isLoading ? 'true' : 'false'); 1139 1140 if (submitButton) { 1141 submitButton.disabled = isLoading; 1142 submitButton.innerHTML = isLoading ? 'Sending...' : originalSubmitHtml; 1143 } 1144 } 1145 1146 function createRequestId() { 1147 var randomPart = Math.random().toString(36).slice(2, 10); 1148 1149 if (window.crypto && typeof window.crypto.getRandomValues === 'function') { 1150 var bytes = new Uint32Array(2); 1151 window.crypto.getRandomValues(bytes); 1152 randomPart = bytes[0].toString(36) + bytes[1].toString(36); 1153 } 1154 1155 return 'wht-' + Date.now().toString(36) + '-' + randomPart; 1156 } 1157 1158 function endpointTypeFor(url) { 1159 if (!url) { 1160 return 'unknown'; 1161 } 1162 1163 if (formConfig.fallbackUrl && url === formConfig.fallbackUrl) { 1164 return 'fallback'; 1165 } 1166 1167 if (url.indexOf('admin-ajax.php') !== -1) { 1168 return 'admin_ajax'; 1169 } 1170 1171 return 'unknown'; 1172 } 1173 1174 function buildFormData(requestId) { 1175 if (pageUrlField) { 1176 pageUrlField.value = window.location.href; 1177 } 1178 1179 if (nonceField && formConfig.nonce) { 1180 nonceField.value = formConfig.nonce; 1181 } 1182 1183 var formData = new FormData(requestForm); 1184 formData.set('action', 'wht_home_code_request'); 1185 formData.set('wht_home_code_debug', isFormDebug ? '1' : '0'); 1186 formData.set('wht_home_code_request_id', requestId); 1187 1188 if (requestIdField) { 1189 requestIdField.value = requestId; 1190 } 1191 1192 return formData; 1193 } 1194 1195 function getFieldValue(name) { 1196 var field = requestForm.querySelector('[name="' + name + '"]'); 1197 1198 return field ? (field.value || '').trim() : ''; 1199 } 1200 1201 function isValidEmail(value) { 1202 return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); 1203 } 1204 1205 function validateBeforeSubmit() { 1206 var name = getFieldValue('wht_home_code_name'); 1207 var email = getFieldValue('wht_home_code_email'); 1208 var message = getFieldValue('wht_home_code_message'); 1209 1210 if (messageField && typeof messageField.setCustomValidity === 'function') { 1211 messageField.setCustomValidity(''); 1212 messageField.removeAttribute('aria-invalid'); 1213 } 1214 1215 if (!name || !email || !message) { 1216 return { 1217 valid: false, 1218 message: 'Please add your name, email and tell us what we can do for you.', 1219 clientValidation: 'missing_fields' 1220 }; 1221 } 1222 1223 if (!isValidEmail(email)) { 1224 return { 1225 valid: false, 1226 message: 'Please add a valid email address.',
1227 clientValidation: 'invalid_email' 1228 }; 1229 } 1230 1231 if (message.length < 20) { 1232 if (messageField && typeof messageField.setCustomValidity === 'function') { 1233 messageField.setCustomValidity('Please enter at least 20 characters.'); 1234 messageField.setAttribute('aria-invalid', 'true'); 1235 } 1236 1237 return { 1238 valid: false, 1239 message: 'Please check the highlighted fields and try again.', 1240 fieldMessage: 'Please enter at least 20 characters.', 1241 field: messageField, 1242 clientValidation: 'client_validation_message_too_short' 1243 }; 1244 } 1245 1246 return { 1247 valid: true, 1248 message: '', 1249 clientValidation: '' 1250 }; 1251 } 1252 1253 if (messageField) { 1254 messageField.addEventListener('input', function () { 1255 if (typeof messageField.setCustomValidity === 'function') { 1256 messageField.setCustomValidity(''); 1257 } 1258 messageField.removeAttribute('aria-invalid'); 1259 }); 1260 } 1261 1262 function createRequestStats(requestId) { 1263 return { 1264 requestId: requestId, 1265 requestsFired: 0, 1266 endpoints: [], 1267 fallbackAttempted: false, 1268 clientValidation: '' 1269 }; 1270 } 1271 1272 function noteRequest(stats, endpoint, fallbackUsed) { 1273 if (!stats) { 1274 return; 1275 } 1276 1277 stats.requestsFired += 1; 1278 stats.fallbackAttempted = stats.fallbackAttempted || !!fallbackUsed; 1279 stats.endpoints.push(endpointTypeFor(endpoint) + ': ' + endpoint); 1280 } 1281 1282 function renderDebugPanel(responseData, transport, stats, clientValidation) { 1283 var debug; 1284 var rows; 1285 var notes = []; 1286 var endpoints; 1287 var serverCode; 1288 var requestId; 1289 var html; 1290 var latestError; 1291 var latestRejection; 1292 1293 if (!isFormDebug || !debugPanel) { 1294 return; 1295 } 1296 1297 responseData = responseData || {}; 1298 debug = responseData.debug || {}; 1299 transport = transport || {}; 1300 stats = stats || createRequestStats(responseData.request_id || activeRequestId || 'unknown'); 1301 endpoints = stats.endpoints && stats.endpoints.length ? stats.endpoints.join(' | ') : (transport.endpoint || 'none'); 1302 serverCode = clientValidation || responseData.code || debug.server_code || 'unknown'; 1303 requestId = responseData.request_id || transport.requestId || stats.requestId || activeRequestId || 'unknown'; 1304 latestError = latestDiagnosticEntry(diagnosticState.jsErrors); 1305 latestRejection = latestDiagnosticEntry(diagnosticState.unhandledRejections); 1306 1307 rows = [ 1308 ['request_id', requestId], 1309 ['plugin version', debug.plugin_version || responseData.plugin_version || pluginVersion], 1310 ['js version', jsVersion], 1311 ['js asset version', jsAssetVersion], 1312 ['css asset version', cssVersion], 1313 ['critical css version', criticalCssVersion], 1314 ['preview mode', previewMode], 1315 ['page_id', previewPageId], 1316 ['preview_id', previewId], 1317 ['preview_nonce present', yesNo(previewNoncePresent)], 1318 ['_thumbnail_id present', yesNo(thumbnailIdPresent)], 1319 ['submit handler', 'attached'], 1320 ['handlers attached', handlerAttachCount], 1321 ['forms found', formMatchCount], 1322 ['debug query param', yesNo(debugQueryDetected)], 1323 ['ajax config', (formConfig.ajaxUrl ? 'admin_ajax yes' : 'admin_ajax missing') + ', ' + (formConfig.fallbackUrl ? 'fallback yes' : 'fallback missing') + ', ' + (formConfig.nonce ? 'nonce yes' : 'nonce missing')], 1324 ['endpoint used', endpoints], 1325 ['endpoint type', transport.endpointType || debug.endpoint_type || 'unknown'], 1326 ['requests fired', stats.requestsFired || 0], 1327 ['fallback attempted', yesNo(stats.fallbackAttempted || transport.fallbackUsed)], 1328 ['HTTP status', typeof transport.httpStatus !== 'undefined' ? transport.httpStatus : 'n/a'], 1329 ['response content type', transport.contentType || 'n/a'], 1330 ['response shape', transport.responseType || 'unknown'], 1331 ['CleanTalk/APBCT detected', yesNo(!!transport.cleantalkDetected || serverCode === 'cleantalk_blocked')], 1332 ['JS parsed response.success', boolDebugValue(transport.parsedSuccess)], 1333 ['server code', serverCode], 1334 ['handler reached', yesNo(!!debug.handler_reached)], 1335 ['wp_mail called', yesNo(!!debug.wp_mail_called)], 1336 ['wp_mail returned', boolDebugValue(debug.wp_mail_returned)], 1337 ['JS errors captured', diagnosticState.jsErrors.length], 1338 ['latest JS error', latestError ? latestError.type + ' ' + latestError.message : 'none'], 1339 ['unhandled promise rejections captured', diagnosticState.unhandledRejections.length], 1340 ['latest unhandled rejection', latestRejection ? latestRejection.message : 'none'], 1341 ['raw response preview', transport.rawResponsePreview || 'not shown'] 1342 ]; 1343 1344 diagnosticState.lastSubmit = { 1345 requestId: requestId, 1346 responseData: responseData, 1347 transport: transport, 1348 stats: stats, 1349 serverCode: serverCode, 1350 clientValidation: clientValidation || '' 1351 }; 1352 1353 if (clientValidation) { 1354 rows.push(['client validation', clientValidation]); 1355 notes.push('Client validation stopped the request before admin-ajax, fallback or wp_mail.'); 1356 } 1357 1358 if (serverCode === 'cleantalk_blocked') { 1359 notes.push('Root cause: CleanTalk/WAF interception. PHP handler reached: no. wp_mail called: no.'); 1360 notes.push('Next action: whitelist this specific staging form endpoint/action or adju
1360st CleanTalk settings minimally. Do not disable CleanTalk globally.'); 1361 } else if (serverCode === 'mail_failed') { 1362 notes.push('Root cause: wp_mail returned false or the mail transport rejected the message.'); 1363 notes.push('Next action: check SMTP/Hostinger mail transport configuration.'); 1364 } else if (serverCode === 'handler_not_reached') { 1365 notes.push('Root cause: WordPress AJAX handler was not reached.'); 1366 notes.push('Next action: inspect endpoint, nonce, cache, WAF or unexpected HTML/WordPress 0 response.'); 1367 } else if (serverCode === 'wp_zero') { 1368 notes.push('Root cause: WordPress returned 0, which usually means the AJAX action did not reach the expected handler.'); 1369 } else if (serverCode === 'html_response') { 1370 notes.push('Root cause: endpoint returned HTML instead of WordPress JSON.'); 1371 } else if (serverCode === 'invalid_json') { 1372 notes.push('Root cause: endpoint returned non-JSON output, usually HTML, PHP warning/notice, BOM or security plugin output.'); 1373 } else if (serverCode === 'ajax_network_error') { 1374 notes.push('Root cause: browser could not complete the fetch request or no AJAX endpoint/config was available.'); 1375 } else if (serverCode === 'js_exception') { 1376 notes.push('Root cause: JavaScript exception interrupted the submit flow after the handler attached.'); 1377 } else if (serverCode === 'sent_but_ui_failed') { 1378 notes.push('Root cause: server reported success but the UI response did not include the expected sent/mail flag.'); 1379 } 1380 1381 debugPanel.hidden = false; 1382 html = '<p class="wht-home-code__form-debug-panel-title">WHT FORM DEBUG ACTIVE</p><dl class="wht-home-code__form-debug-list">'; 1383 rows.forEach(function (row) { 1384 html += '<div class="wht-home-code__form-debug-row"><dt>' + escapeHtml(row[0]) + '</dt><dd>' + escapeHtml(row[1]) + '</dd></div>'; 1385 }); 1386 html += '</dl>'; 1387 1388 if (notes.length) { 1389 html += '<p class="wht-home-code__form-debug-note">' + escapeHtml(notes.join(' ')) + '</p>'; 1390 } 1391 1392 debugPanel.innerHTML = html; 1393 } 1394 1395 function renderReadyDebugPanel() { 1396 var requestId = 'not_submitted'; 1397 var endpoint = formConfig.ajaxUrl || formConfig.fallbackUrl || ''; 1398 var readyPayload; 1399 var readyStats; 1400 1401 if (!isFormDebug || !debugPanel) { 1402 return; 1403 } 1404 1405 readyPayload = failedPayload('WHT form debug is ready. Submit the form to capture request diagnostics.', 'unknown', requestId, { 1406 endpoint_type: endpointTypeFor(endpoint), 1407 handler_reached: false, 1408 wp_mail_called: false, 1409 wp_mail_returned: null 1410 }); 1411 readyStats = createRequestStats(requestId); 1412 1413 renderDebugPanel( 1414 readyPayload.data, 1415 { 1416 endpoint: endpoint || 'none', 1417 endpointType: endpointTypeFor(endpoint), 1418 httpStatus: 'n/a', 1419 contentType: 'n/a', 1420 responseType: 'ready', 1421 fallbackUsed: false, 1422 requestId: requestId, 1423 parsedSuccess: null, 1424 rawResponsePreview: 'not shown' 1425 }, 1426 readyStats 1427 ); 1428 } 1429 1430 function buildDebugErrorMessage(message, responseData, transport) { 1431 var parts = []; 1432 1433 if (!isFormDebug) { 1434 return message; 1435 } 1436 1437 responseData = responseData || {}; 1438 transport = transport || {}; 1439 1440 parts.push('request_id: ' + (responseData.request_id || transport.requestId || activeRequestId || 'unknown')); 1441 parts.push('endpoint_type: ' + (transport.endpointType || 'unknown')); 1442 1443 if (typeof transport.httpStatus !== 'undefined') { 1444 parts.push('status: ' + transport.httpStatus); 1445 } 1446 1447 parts.push('response: ' + (transport.responseType || (transport.validJson === false ? 'invalid_json' : 'unknown'))); 1448 parts.push('server: ' + (responseData.code || 'unknown_error')); 1449 1450 if (transport.fallbackUsed) { 1451 parts.push('fallback: yes'); 1452 } 1453 1454 if (responseData.debug && responseData.debug.handler) { 1455 parts.push('handler: ' + responseData.debug.handler); 1456 } 1457 1458 if (responseData.debug && typeof responseData.debug.mail_sent !== 'undefined') { 1459 parts.push('mail: ' + (responseData.debug.mail_sent ? 'true' : 'false')); 1460 } 1461 1462 return message + ' WHT FORM DEBUG ACTIVE panel below has request_id, endpoint, HTTP status and server code.'; 1463 } 1464 1465 function buildTransport(endpoint, fallbackUsed, requestId, response) { 1466 return { 1467 endpoint: endpoint, 1468 endpointType: endpointTypeFor(endpoint), 1469 httpStatus: response ? response.status : 0, 1470 responseOk: response ? response.ok : false, 1471 contentType: response && response.headers && typeof response.headers.get === 'function' ? (response.headers.get('content-type') || '') : '', 1472 responseType: 'json', 1473 validJson: true, 1474 fallbackUsed: !!fallbackUsed, 1475 requestId: requestId, 1476 parsedSuccess: null, 1477 rawResponsePreview: 'not shown', 1478 cleantalkDetected: false 1479 }; 1480 } 1481 1482 function failedPayload(message, code, requestId, debugData) { 1483 var debug = { 1484 request_id: requestId, 1485 plugin_version: pluginVersion, 1486 server_code: code, 1487 endpoint_type: 'unknown', 1488 handler_reached: false, 1489 wp_mail_called: false, 1490 wp_mail_returned: null, 1491 mail_sent: false 1492 }; 1493 1494 if (debugData) {
1495 Object.keys(debugData).forEach(function (key) { 1496 debug[key] = debugData[key]; 1497 }); 1498 } 1499 1500 return { 1501 success: false, 1502 data: { 1503 message: message || 'Sorry, something went wrong. Please email us directly at [email protected].', 1504 code: code, 1505 mail_sent: false, 1506 request_id: requestId, 1507 plugin_version: pluginVersion, 1508 debug: debug 1509 } 1510 }; 1511 } 1512 1513 function textMentionsCleanTalk(value) { 1514 return responseLooksBlocked(value); 1515 } 1516 1517 function parseAjaxResponse(response, endpoint, fallbackUsed, requestId) { 1518 return response.text().then(function (text) { 1519 var transport = buildTransport(endpoint, fallbackUsed, requestId, response); 1520 var trimmed = (text || '').trim(); 1521 var lowered = trimmed.toLowerCase(); 1522 var parsed; 1523 1524 transport.rawResponsePreview = safeRawResponsePreview(trimmed); 1525 transport.cleantalkDetected = textMentionsCleanTalk(trimmed); 1526 1527 if (trimmed === '0') { 1528 transport.validJson = false; 1529 transport.responseType = 'wp_zero'; 1530 transport.parsedSuccess = null; 1531 1532 return { 1533 payload: failedPayload(null, 'wp_zero', requestId, { 1534 endpoint_type: transport.endpointType 1535 }), 1536 transport: transport 1537 }; 1538 } 1539 1540 if (lowered.indexOf('<html') !== -1 || lowered.charAt(0) === '<') { 1541 transport.validJson = false; 1542 transport.responseType = 'html'; 1543 transport.parsedSuccess = null; 1544 1545 return { 1546 payload: failedPayload(null, textMentionsCleanTalk(lowered) ? 'cleantalk_blocked' : 'html_response', requestId, { 1547 endpoint_type: transport.endpointType 1548 }), 1549 transport: transport 1550 }; 1551 } 1552 1553 try { 1554 parsed = JSON.parse(text); 1555 transport.parsedSuccess = parsed && typeof parsed.success === 'boolean' ? parsed.success : null; 1556 1557 if ( 1558 (parsed && parsed.apbct && parsed.apbct.blocked) || 1559 textMentionsCleanTalk(JSON.stringify(parsed)) 1560 ) { 1561 return { 1562 payload: failedPayload(null, 'cleantalk_blocked', requestId, { 1563 endpoint_type: transport.endpointType 1564 }), 1565 transport: transport 1566 }; 1567 } 1568 1569 if (!parsed || typeof parsed.success === 'undefined' || typeof parsed.data === 'undefined') { 1570 return { 1571 payload: failedPayload(null, 'handler_not_reached', requestId, { 1572 endpoint_type: transport.endpointType 1573 }), 1574 transport: transport 1575 }; 1576 } 1577 1578 if (parsed.data && !parsed.data.request_id) { 1579 parsed.data.request_id = requestId; 1580 } 1581 1582 return { 1583 payload: parsed, 1584 transport: transport 1585 }; 1586 } catch (error) { 1587 transport.validJson = false; 1588 transport.responseType = textMentionsCleanTalk(lowered) ? 'html' : 'invalid_json'; 1589 transport.parsedSuccess = null; 1590 1591 return { 1592 payload: failedPayload(null, transport.responseType === 'html' ? 'cleantalk_blocked' : 'invalid_json', requestId, { 1593 endpoint_type: transport.endpointType 1594 }), 1595 transport: transport 1596 }; 1597 } 1598 }); 1599 } 1600 1601 function shouldTryFallback(result) { 1602 var payload = result && result.payload ? result.payload : {}; 1603 var responseData = payload && payload.data ? payload.data : {}; 1604 var transport = result && result.transport ? result.transport : {}; 1605 var safeCodes = ['cleantalk_blocked']; 1606 1607 if (transport.responseType === 'wp_zero' && responseData.code === 'wp_zero') { 1608 return true; 1609 } 1610 1611 return safeCodes.indexOf(responseData.code) !== -1; 1612 } 1613 1614 function sendForm(endpoint, allowFallback, fallbackUsed, requestId, stats) { 1615 function fetchEndpoint(url) { 1616 noteRequest(stats, url, fallbackUsed); 1617 1618 return fetch(url, { 1619 method: 'POST', 1620 credentials: 'same-origin', 1621 body: buildFormData(requestId) 1622 }).then(function (response) { 1623 return parseAjaxResponse(response, url, fallbackUsed, requestId); 1624 }); 1625 } 1626 1627 return fetchEndpoint(endpoint) 1628 .catch(function (error) { 1629 return { 1630 payload: failedPayload(null, 'ajax_network_error', requestId, { 1631 endpoint_type: endpointTypeFor(endpoint) 1632 }), 1633 transport: { 1634 endpoint: endpoint, 1635 endpointType: endpointTypeFor(endpoint), 1636 httpStatus: 0, 1637 responseOk: false, 1638 contentType: '', 1639 responseType: 'network_error', 1640 validJson: false, 1641 fallbackUsed: !!fallbackUsed, 1642 networkError: true, 1643 requestId: requestId, 1644 parsedSuccess: null, 1645 rawResponsePreview: 'not shown' 1646 } 1647 }; 1648 }) 1649 .then(function (result) { 1650 if ( 1651 allowFallback && 1652 result && 1653 result.payload && 1654 !result.payload.success && 1655 shouldTryFallback(result) && 1656 formConfig.fallbackUrl && 1657 endpoint !== formConfig.fallbackUrl 1658 ) { 1659 return sendForm(formConfig.fallbackUrl, false, true, requestId, stats); 1660 } 1661 1662 return result; 1663 }); 1664 } 1665 1666 function setSelfTestLoading(button, isLoading) { 1667 if (!button) { 1668 return; 1669 } 1670 1671 button.disabled = isLoading; 1672 button.setAttribute('aria-busy', isLoading ? 'true' : 'false'); 1673 } 1674
1675 function renderSelfTestResult(title, result, stats) { 1676 var payload = result && result.payload ? result.payload : {}; 1677 var responseData = payload && payload.data ? payload.data : {}; 1678 var debug = responseData.debug || {}; 1679 var transport = result && result.transport ? result.transport : {}; 1680 var serverCode = responseData.server_code || responseData.code || debug.server_code || 'unknown'; 1681 var requestId = responseData.self_test_request_id || responseData.request_id || debug.request_id || (stats && stats.requestId) || transport.requestId || 'unknown'; 1682 var rows; 1683 var html; 1684 var notes = []; 1685 1686 if (!selfTestResult) { 1687 return; 1688 } 1689 1690 rows = [ 1691 ['test', title], 1692 ['request_id', requestId], 1693 ['plugin version', responseData.plugin_version || debug.plugin_version || pluginVersion], 1694 ['js version', jsVersion], 1695 ['css version', cssVersion], 1696 ['critical css version', criticalCssVersion], 1697 ['preview mode', previewMode], 1698 ['endpoint', transport.endpoint || (formConfig.ajaxUrl || 'none')], 1699 ['endpoint type', transport.endpointType || debug.endpoint_type || 'unknown'], 1700 ['requests fired', stats && stats.requestsFired ? stats.requestsFired : 0], 1701 ['HTTP status', typeof transport.httpStatus !== 'undefined' ? transport.httpStatus : 'n/a'], 1702 ['response content type', transport.contentType || 'n/a'], 1703 ['response shape', transport.responseType || 'unknown'], 1704 ['response.success parsed', boolDebugValue(transport.parsedSuccess)], 1705 ['server code', serverCode], 1706 ['handler reached', yesNo(!!debug.handler_reached)], 1707 ['wp_mail called', yesNo(!!debug.wp_mail_called || !!responseData.wp_mail_called)], 1708 ['wp_mail returned', boolDebugValue(typeof debug.wp_mail_returned !== 'undefined' ? debug.wp_mail_returned : responseData.wp_mail_returned)], 1709 ['timestamp', responseData.timestamp || debug.timestamp || 'n/a'], 1710 ['CleanTalk/APBCT detected', yesNo(!!transport.cleantalkDetected || serverCode === 'cleantalk_blocked')], 1711 ['raw response preview', transport.rawResponsePreview || 'not shown'] 1712 ]; 1713 1714 diagnosticState.lastSelfTest = { 1715 requestId: requestId, 1716 responseData: responseData, 1717 transport: transport, 1718 stats: stats, 1719 serverCode: serverCode, 1720 title: title 1721 }; 1722 1723 if (serverCode === 'cleantalk_blocked') { 1724 notes.push('CleanTalk/WAF intercepted this request before the plugin handler could complete.'); 1725 } 1726 1727 if (serverCode === 'mail_failed') { 1728 notes.push('wp_mail returned false for this test; check SMTP/Hostinger mail transport.'); 1729 } 1730 1731 selfTestResult.hidden = false; 1732 html = '<p class="wht-home-code__self-test-result-title">' + escapeHtml(title) + '</p><dl class="wht-home-code__form-debug-list">'; 1733 rows.forEach(function (row) { 1734 html += '<div class="wht-home-code__form-debug-row"><dt>' + escapeHtml(row[0]) + '</dt><dd>' + escapeHtml(row[1]) + '</dd></div>'; 1735 }); 1736 html += '</dl>'; 1737 1738 if (notes.length) { 1739 html += '<p class="wht-home-code__form-debug-note">' + escapeHtml(notes.join(' ')) + '</p>'; 1740 } 1741 1742 selfTestResult.innerHTML = html; 1743 } 1744 1745 function buildMailSelfTestData(requestId) { 1746 var formData = new FormData(); 1747 1748 formData.set('action', formConfig.mailSelfTestAction || 'wht_home_code_mail_self_test'); 1749 formData.set('wht_home_code_debug', '1'); 1750 formData.set('wht_home_code_request_id', requestId); 1751 formData.set('wht_home_code_self_test_nonce', formConfig.selfTestNonce || ''); 1752 1753 return formData; 1754 } 1755 1756 function buildAjaxHandlerSelfTestData(requestId) { 1757 var formData = new FormData(); 1758 1759 formData.set('action', 'wht_home_code_request'); 1760 formData.set('wht_home_code_action', 'request'); 1761 formData.set('wht_home_code_nonce', formConfig.nonce || ''); 1762 formData.set('wht_home_code_debug', '1'); 1763 formData.set('wht_home_code_admin_self_test', '1'); 1764 formData.set('wht_home_code_self_test_nonce', formConfig.selfTestNonce || ''); 1765 formData.set('wht_home_code_request_id', requestId); 1766 formData.set('wht_home_code_name', 'WHT QA Self-Test'); 1767 formData.set('wht_home_code_email', '[email protected]'); 1768 formData.set('wht_home_code_message', 'This is a staging admin-only AJAX handler self-test for the WHT Home contact form. Please ignore this message.'); 1769 formData.set('wht_home_code_page_url', window.location.href); 1770 formData.set('wht_home_code_company', ''); 1771 1772 return formData; 1773 } 1774 1775 function runAdminSelfTest(button, title, buildData) { 1776 var endpoint = formConfig.ajaxUrl || ''; 1777 var requestId = createRequestId(); 1778 var stats = createRequestStats(requestId); 1779 1780 if (!formConfig.canRunSelfTests || !formConfig.selfTestNonce || !endpoint || !window.fetch || !window.FormData) { 1781 renderSelfTestResult(title, { 1782 payload: failedPayload('Self-test unavailable in this browser/session.', 'ajax_network_error', requestId, { 1783 endpoint_type: endpointTypeFor(endpoint) 1784 }), 1785 transport: { 1786 endpoint: endpoint || 'none', 1787 endpointType: endpointTypeFor(endpoint), 1788 httpStatus: 0, 1789 contentType: '', 1790 responseType: 'network_error', 1791 validJson: false, 1792 requestId: requestId, 1793 parsedSuccess: null, 1794 rawResponsePreview: 'not shown' 1795 } 1796 }, stats); 1797 return; 1798 } 1799 1800 setSelfTestLoading(button, true); 1801 noteRequest(stats, endpoint, false); 1802 1803 fetch(endpoint, { 1804 method: 'POST', 1805 credentials: 'same-origin', 1806 body: buildData(requestId) 1807 }) 1808 .then(function (response) { 1809 return parseAjaxResponse(response, endpoint, false, requestId); 1810 }) 1811 .catch(function () { 1812 return { 1813 payload: failedPayload('Self-test request could not be completed.', 'ajax_network_error', requestId, { 1814 endpoint_type: endpointTypeFor(endpoint) 1815 }), 1816 transport: { 1817 endpoint: endpoint, 1818 endpointType: endpointTypeFor(endpoint), 1819 httpStatus: 0, 1820 contentType: '', 1821 responseType: 'network_error', 1822 validJson: false, 1823 requestId: requestId, 1824 parsedSuccess: null, 1825 rawResponsePreview: 'not shown' 1826 } 1827 }; 1828 }) 1829 .then(function (result) { 1830 renderSelfTestResult(title, result, stats); 1831 }) 1832 .finally(function () { 1833 setSelfTestLoading(button, false); 1834 }); 1835 } 1836 1837 function buildCopyDiagnosticReport() { 1838 var last = diagnosticState.lastSubmit || diagnosticState.lastSelfTest || {}; 1839 var responseData = last.responseData || {}; 1840 var debug = responseData.debug || {}; 1841 var transport = last.transport || {}; 1842 var stats = last.stats || {}; 1843 var latestError = latestDiagnosticEntry(diagnosticState.jsErrors); 1844 var latestRejection = latestDiagnosticEntry(diagnosticState.unhandledRejections); 1845 var serverCode = last.serverCode || responseData.code || debug.server_code || 'unknown'; 1846 var requestId = last.requestId || responseData.request_id || debug.request_id || 'not_submitted'; 1847 var lines = [ 1848 'WHT form diagnostic', 1849 'plugin version: ' + pluginVersion, 1850 'css version: ' + cssVersion, 1851 'critical css version: ' + criticalCssVersion, 1852 'js version: ' + jsVersion, 1853 'preview mode detected: ' + previewMode, 1854 'page_id: ' + previewPageId, 1855 'preview_id: ' + previewId, 1856 'preview_nonce present: ' + yesNo(previewNoncePresent), 1857 '_thumbnail_id present: ' + yesNo(thumbnailIdPresent), 1858 'root wrapper exists: ' + yesNo(rootWrapperExists), 1859 'body helper existed before JS: ' + yesNo(bodyHadHelperClass), 1860 'body helper class exists: ' + yesNo(document.body.classList.contains('wht-home-code-page')), 1861 'endpoint: ' + (transport.endpoint || formConfig.ajaxUrl || 'none'), 1862 'endpoint type: ' + (transport.endpointType || debug.endpoint_type || endpointTypeFor(formConfig.ajaxUrl || '')), 1863 'nonce present: ' + yesNo(!!formConfig.nonce), 1864 'submit handler attached: yes', 1865 'handlers attached: ' + handlerAttachCount, 1866 'forms found: ' + formMatchCount, 1867 'last submit request_id: ' + requestId, 1868 'requests fired: ' + (stats.requestsFired || 0), 1869 'fallback attempted: ' + yesNo(stats.fallbackAttempted || transport.fallbackUsed), 1870 'HTTP status: ' + (typeof transport.httpStatus !== 'undefined' ? transport.httpStatus : 'n/a'), 1871 'response type: ' + (transport.responseType || 'unknown'), 1872 'response.success parsed: ' + boolDebugValue(transport.parsedSuccess), 1873 'server code: ' + serverCode, 1874 'handler reached: ' + yesNo(!!debug.handler_reached), 1875 'wp_mail called: ' + yesNo(!!debug.wp_mail_called || !!responseData.wp_mail_called), 1876 'wp_mail returned: ' + boolDebugValue(typeof debug.wp_mail_returned !== 'undefined' ? debug.wp_mail_returned : responseData.wp_mail_returned), 1877 'JS errors captured: ' + diagnosticState.jsErrors.length, 1878 'latest JS error: ' + (latestError ? latestError.type + ' ' + latestError.message : 'none'), 1879 'unhandled promise rejections captured: ' + diagnosticState.unhandledRejections.length, 1880 'latest unhandled rejection: ' + (latestRejection ? latestRejection.message : 'none'), 1881 'CleanTalk detected: ' + yesNo(!!transport.cleantalkDetected || serverCode === 'cleantalk_blocked'), 1882 'raw response preview: ' + (transport.rawResponsePreview || 'not shown') 1883 ]; 1884 1885 return lines.map(safeDiagnosticText).join('\n'); 1886 } 1887 1888 function showCopiedDiagnostic(report, copied) { 1889 if (!selfTestResult) { 1890 return; 1891 } 1892 1893 selfTestResult.hidden = false;
1894 selfTestResult.innerHTML = '<p class="wht-home-code__self-test-result-title">' + (copied ? 'Diagnostic copied' : 'Diagnostic ready') + '</p><pre class="wht-home-code__diagnostic-pre">' + escapeHtml(report) + '</pre>'; 1895 } 1896 1897 function copyDiagnosticReport() { 1898 var report = buildCopyDiagnosticReport(); 1899 1900 if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { 1901 navigator.clipboard.writeText(report).then(function () { 1902 showCopiedDiagnostic(report, true); 1903 }).catch(function (error) { 1904 recordJsDiagnostic('clipboard_error', error); 1905 showCopiedDiagnostic(report, false); 1906 }); 1907 return; 1908 } 1909 1910 recordJsDiagnostic('clipboard_unavailable', 'navigator.clipboard unavailable'); 1911 showCopiedDiagnostic(report, false); 1912 } 1913 1914 if (mailSelfTestButton) { 1915 mailSelfTestButton.addEventListener('click', function () { 1916 runAdminSelfTest(mailSelfTestButton, 'WHT wp_mail self-test', buildMailSelfTestData); 1917 }); 1918 } 1919 1920 if (ajaxSelfTestButton) { 1921 ajaxSelfTestButton.addEventListener('click', function () { 1922 runAdminSelfTest(ajaxSelfTestButton, 'WHT AJAX handler self-test', buildAjaxHandlerSelfTestData); 1923 }); 1924 } 1925 1926 if (copyDiagnosticButton) { 1927 copyDiagnosticButton.addEventListener('click', copyDiagnosticReport); 1928 } 1929 1930 updateDebugMarker(); 1931 renderReadyDebugPanel(); 1932 1933 requestForm.addEventListener('submit', function (event) { 1934 event.preventDefault(); 1935 event.stopPropagation(); 1936 1937 try { 1938 if (isSubmitting) { 1939 return; 1940 } 1941 1942 var requestId = createRequestId(); 1943 activeRequestId = requestId; 1944 1945 var stats = createRequestStats(requestId); 1946 var validation = validateBeforeSubmit(); 1947 1948 clearDebugPanel(); 1949 1950 if (!validation.valid) { 1951 stats.clientValidation = validation.clientValidation; 1952 showFormMessage('error', validation.message); 1953 if (validation.field && typeof validation.field.reportValidity === 'function') { 1954 validation.field.reportValidity(); 1955 } 1956 renderDebugPanel( 1957 { 1958 message: validation.message, 1959 code: 'validation_failed', 1960 mail_sent: false, 1961 request_id: requestId, 1962 plugin_version: pluginVersion, 1963 debug: { 1964 request_id: requestId, 1965 plugin_version: pluginVersion, 1966 server_code: 'validation_failed', 1967 endpoint_type: 'unknown', 1968 handler_reached: false, 1969 wp_mail_called: false, 1970 wp_mail_returned: null, 1971 mail_sent: false 1972 } 1973 }, 1974 { 1975 endpoint: '', 1976 endpointType: 'unknown', 1977 httpStatus: 'n/a', 1978 contentType: 'n/a', 1979 responseType: 'client_validation', 1980 fallbackUsed: false, 1981 requestId: requestId, 1982 parsedSuccess: null, 1983 rawResponsePreview: 'not shown' 1984 }, 1985 stats, 1986 validation.clientValidation 1987 ); 1988 return; 1989 } 1990 1991 if (!window.fetch || !window.FormData || (!formConfig.ajaxUrl && !formConfig.fallbackUrl)) { 1992 var noFetchPayload = failedPayload( 1993 'Sorry, something went wrong. Please email us directly at [email protected].', 1994 'ajax_network_error', 1995 requestId, 1996 { 1997 endpoint_type: formConfig.ajaxUrl ? 'admin_ajax' : (formConfig.fallbackUrl ? 'fallback' : 'unknown') 1998 } 1999 ); 2000 var noFetchTransport = { 2001 endpoint: formConfig.ajaxUrl || formConfig.fallbackUrl || '', 2002 endpointType: formConfig.ajaxUrl ? 'admin_ajax' : (formConfig.fallbackUrl ? 'fallback' : 'unknown'), 2003 httpStatus: 0, 2004 contentType: '', 2005 responseType: 'network_error', 2006 validJson: false, 2007 requestId: requestId, 2008 parsedSuccess: null, 2009 rawResponsePreview: 'not shown' 2010 }; 2011 2012 showFormMessage('error', buildDebugErrorMessage(noFetchPayload.data.message, noFetchPayload.data, noFetchTransport)); 2013 renderDebugPanel(noFetchPayload.data, noFetchTransport, stats); 2014 return; 2015 } 2016 2017 isSubmitting = true; 2018 setFormLoading(true); 2019 2020 sendForm(formConfig.ajaxUrl || formConfig.fallbackUrl, true, false, requestId, stats) 2021 .then(function (result) { 2022 if (requestId !== activeRequestId) { 2023 return; 2024 } 2025 2026 var payload = result && result.payload ? result.payload : {}; 2027 var transport = result && result.transport ? result.transport : {}; 2028 var responseData = payload && payload.data ? payload.data : {}; 2029 var message = responseData.message || 'Sorry, something went wrong. Please email us directly at [email protected].'; 2030 2031 if ( 2032 responseData.code === 'validation_failed' && 2033 responseData.field_message && 2034 messageField && 2035 typeof messageField.setCustomValidity === 'function' 2036 ) { 2037 messageField.setCustomValidity(responseData.field_message); 2038 messageField.setAttribute('aria-invalid', 'true'); 2039 if (typeof messageField.reportValidity === 'function') { 2040 messageField.reportValidity(); 2041 } 2042 } 2043 2044 if (payload && payload.success && responseData.mail_sent) { 2045 showFormMessage('success', message); 2046 renderDebugPanel(responseData, transport, stats); 2047 requestForm.reset(); 2048 if (pageUrlField) { 2049 pageUrlField.value = window.location.href; 2050 } 2051 return; 2052 } 2053 2054 if (payload && payload.success && !responseData.mail_sent) { 2055 responseData.code = responseData.code || 'sent_but_ui_failed'; 2056 responseData.message = responseData.message || 'Sorry, something went wrong. Please email us directly at [email protected].'; 2057 if (responseData.debug) { 2058 responseData.debug.server_code = responseData.debug.server_code || 'sent_but_ui_failed'; 2059 } 2060 message = responseData.message; 2061 } 2062 2063 showFormMessage('error', buildDebugErrorMessage(message, responseData, transport)); 2064 renderDebugPanel(responseData, transport, stats); 2065 }) 2066 .catch(function (error) { 2067 if (requestId !== activeRequestId) { 2068 return; 2069 } 2070
2071 recordJsDiagnostic('submit_promise_catch', error); 2072 var caughtPayload = failedPayload( 2073 'Sorry, something went wrong. Please email us directly at [email protected].', 2074 'ajax_network_error', 2075 requestId, 2076 { 2077 endpoint_type: formConfig.ajaxUrl ? 'admin_ajax' : (formConfig.fallbackUrl ? 'fallback' : 'unknown') 2078 } 2079 ); 2080 var caughtTransport = { 2081 endpoint: formConfig.ajaxUrl || formConfig.fallbackUrl || '', 2082 endpointType: formConfig.ajaxUrl ? 'admin_ajax' : (formConfig.fallbackUrl ? 'fallback' : 'unknown'), 2083 httpStatus: 0, 2084 contentType: '', 2085 responseType: 'network_error', 2086 validJson: false, 2087 requestId: requestId, 2088 parsedSuccess: null, 2089 rawResponsePreview: 'not shown' 2090 }; 2091 2092 showFormMessage('error', buildDebugErrorMessage(caughtPayload.data.message, caughtPayload.data, caughtTransport)); 2093 renderDebugPanel(caughtPayload.data, caughtTransport, stats); 2094 }) 2095 .finally(function () { 2096 if (requestId === activeRequestId) { 2097 isSubmitting = false; 2098 setFormLoading(false); 2099 } 2100 }); 2101 } catch (error) { 2102 var exceptionRequestId = activeRequestId || createRequestId(); 2103 var exceptionStats = createRequestStats(exceptionRequestId); 2104 var exceptionEntry = recordJsDiagnostic('submit_exception', error); 2105 var exceptionPayload = failedPayload( 2106 'Sorry, something went wrong. Please email us directly at [email protected].', 2107 'js_exception', 2108 exceptionRequestId, 2109 { 2110 endpoint_type: formConfig.ajaxUrl ? 'admin_ajax' : (formConfig.fallbackUrl ? 'fallback' : 'unknown'), 2111 handler_reached: false, 2112 wp_mail_called: false, 2113 wp_mail_returned: null 2114 } 2115 ); 2116 var exceptionTransport = { 2117 endpoint: formConfig.ajaxUrl || formConfig.fallbackUrl || '', 2118 endpointType: formConfig.ajaxUrl ? 'admin_ajax' : (formConfig.fallbackUrl ? 'fallback' : 'unknown'), 2119 httpStatus: 0, 2120 contentType: '', 2121 responseType: 'js_exception', 2122 validJson: false, 2123 requestId: exceptionRequestId, 2124 parsedSuccess: null, 2125 rawResponsePreview: exceptionEntry.message || 'not shown' 2126 }; 2127 2128 isSubmitting = false; 2129 setFormLoading(false); 2130 showFormMessage('error', buildDebugErrorMessage(exceptionPayload.data.message, exceptionPayload.data, exceptionTransport)); 2131 renderDebugPanel(exceptionPayload.data, exceptionTransport, exceptionStats); 2132 } 2133 }); 2134 } 2135 2136 if (!video) { 2137 return; 2138 } 2139 2140 var source = video.querySelector('source[data-src]'); 2141 var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); 2142 var desktopMedia = window.matchMedia('(min-width: 768px)'); 2143 2144 function unloadVideo() { 2145 video.pause(); 2146 2147 if (source) { 2148 source.removeAttribute('src'); 2149 } 2150 2151 video.classList.remove('is-loaded'); 2152 video.load(); 2153 } 2154 2155 function loadVideo() { 2156 if (!source || source.getAttribute('src')) { 2157 return; 2158 } 2159 2160 source.setAttribute('src', source.getAttribute('data-src')); 2161 video.load(); 2162 } 2163 2164 function syncHeroVideo() { 2165 if (reduceMotion.matches || !desktopMedia.matches) { 2166 unloadVideo(); 2167 return; 2168 } 2169 2170 loadVideo(); 2171 2172 var playRequest = video.play(); 2173 2174 if (playRequest && typeof playRequest.catch === 'function') { 2175 playRequest.catch(function () {}); 2176 } 2177 } 2178 2179 video.addEventListener('loadeddata', function () { 2180 video.classList.add('is-loaded'); 2181 }); 2182 2183 syncHeroVideo(); 2184 2185 if (typeof reduceMotion.addEventListener === 'function') { 2186 reduceMotion.addEventListener('change', syncHeroVideo); 2187 desktopMedia.addEventListener('change', syncHeroVideo); 2188 } else if (typeof reduceMotion.addListener === 'function') { 2189 reduceMotion.addListener(syncHeroVideo); 2190 desktopMedia.addListener(syncHeroVideo); 2191 } 2192})();
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.