1/* Demo booking form validation and submission 2 * Extracted from landing_page;demo_booking_js.php for cacheability. 3 * Requires jQuery â must load after jquery-3.6.0.min.js. 4 * 5 * Translated strings are provided via window.DemoBookingI18n, which is 6 * emitted by templates/partials/landing_page;demo_booking_js.php before 7 * this script is loaded. Do NOT put PHP tags in this file â it is served 8 * statically by nginx from /resources/js/ and will not be interpreted. 9 */ 10// Common demo booking JavaScript functionality for all landing pages 11$(document).ready(function() { 12 var i18n = window.DemoBookingI18n || {}; 13 var demoSubmissionsInFlight = {}; 14 var modalTrigger = null; 15 var acquisitionMetaPixelId = '2087638994684850'; 16 17 function createMetaEventId() { 18 if (window.crypto && typeof window.crypto.randomUUID === 'function') { 19 return 'ttf-demo-' + window.crypto.randomUUID(); 20 } 21 22 return 'ttf-demo-' + Date.now() + '-' + Math.random().toString(36).slice(2); 23 } 24 25 function waitForAcquisitionConsentHandoff(callback) { 26 var consentReference = window.__ttfAcquisitionConsentReference || ''; 27 var handoffPromise = window.__ttfAcquisitionConsentHandoffPromise; 28 var shouldWait = window.__ttfAdvertisingAllowed === true 29 && !/^[a-f0-9]{64}$/.test(consentReference) 30 && handoffPromise 31 && typeof handoffPromise.then === 'function'; 32 33 if (!shouldWait) { 34 callback(); 35 return; 36 } 37 38 var completed = false; 39 var proceed = function() { 40 if (completed) return; 41 completed = true; 42 callback(); 43 }; 44 handoffPromise.then(proceed, proceed); 45 window.setTimeout(proceed, 1500); 46 } 47 48 // Form validation and submission logic 49 function initializeBookingForm(formSelector, formId) { 50 if (!$.fn || typeof $.fn.validate !== 'function') { 51 console.error('Demo booking validation is not available'); 52 return false; 53 } 54 55 $(formSelector).validate({ 56 rules: { 57 firstname: "required", 58 lastname: "required", 59 email: { 60 required: true, 61 email: true 62 }, 63 company: "required", 64 what_describes_you_best: "required" 65 }, 66 messages: { 67 firstname: i18n.please_enter_first_name, 68 lastname: i18n.please_enter_last_name, 69 email: { 70 required: i18n.please_enter_email, 71 email: i18n.required_email 72 }, 73 company: i18n.please_enter_company_name, 74 what_describes_you_best: i18n.please_select_role 75 }, 76 errorPlacement: function(error, element) { 77 if (element.attr("type") === "radio") { 78 error.insertAfter(element.closest(".radio-group")); 79 } else { 80 error.insertAfter(element); 81 } 82 }, 83 highlight: function(element, errorClass) { 84 if ($(element).attr("type") === "radio") { 85 $(element).closest('.radio-group').addClass('error'); 86 } else { 87 $(element).addClass('error'); 88 } 89 }, 90 unhighlight: function(element, errorClass) { 91 if ($(element).attr("type") === "radio") { 92 $(element).closest('.radio-group').removeClass('error'); 93 } else { 94 $(element).removeClass('error'); 95 } 96 }, 97 submitHandler: function(form, event) { 98 event.preventDefault(); 99 if (demoSubmissionsInFlight[formId]) return false; 100 101 var submissionToken = {}; 102 demoSubmissionsInFlight[formId] = submissionToken; 103 var $submitControls = $(form).find(':submit'); 104 $submitControls.prop('disabled', true); 105 var releaseSubmission = function() { 106 if (demoSubmissionsInFlight[formId] === submissionToken) { 107 delete demoSubmissionsInFlight[formId]; 108 $submitControls.prop('disabled', false); 109 } 110 }; 111 112 var submitBooking = function() { 113 // Closing the modal during the bounded handoff wait intentionally 114 // cancels that submission. Never launch a scheduler for a form the 115 // visitor has dismissed and which Chili Piper can no longer find. 116 if (!document.documentElement.contains(form)) { 117 releaseSubmission(); 118 return; 119 } 120 121 try { 122 // Capture ref cookie for Impact affiliate tracking 123 function getCookie(name) { 124 var value = '; ' + document.cookie; 125 var parts = value.split('; ' + name + '='); 126 if (parts.length === 2) return parts.pop().split(';').shift(); 127 return ''; 128 } 129 130 var advertisingAllowed = window.__ttfAdvert
130isingAllowed === true; 131 var consentReference = window.__ttfAcquisitionConsentReference || ''; 132 var acquisitionTracking = window.__ttfAcquisitionTracking || {}; 133 var metaEventId = advertisingAllowed ? createMetaEventId() : ''; 134 var irclickid = advertisingAllowed ? getCookie('irclickid') : ''; 135 if (irclickid) { 136 // Populate hidden fields with affiliate tracking data 137 $(form).find('input[name="affiliateTrackingId"]').val(irclickid); 138 $(form).find('input[name="affiliatePlatform"]').val('IMPACT'); 139 console.log('Impact tracking: irclickid captured for demo booking:', irclickid); 140 } else { 141 $(form).find( 142 'input[name="affiliateTrackingId"], input[name="affiliatePlatform"]' 143 ).remove(); 144 } 145 146 // Carry the Google Ads click id (gclid) through Chili Piper so the 147 // server webhook can report the demo booking to Google Ads as an 148 // offline conversion. The field is server-rendered from the current 149 // URL; when the demo is booked on a later page we recover it from 150 // Google's own first-party _gcl_aw cookie (format GCL.<ts>.<gclid>), 151 // the same source a gtag conversion reads. Scope the lookup to the 152 // submitted form so the cloned modal populates its own field. 153 (function() { 154 var advertisingAllowed = window.__ttfAdvertisingAllowed === true 155 && /^[a-f0-9]{64}$/.test(consentReference); 156 var advertisingFields = [ 157 'gclid', 158 'fbclid', 159 'fbc', 160 'advertisingConsentReference', 161 'gads_lp_id', 162 'utm_source', 163 'utm_medium', 164 'utm_campaign', 165 'utm_term', 166 'utm_content' 167 ]; 168 169 if (!advertisingAllowed) { 170 advertisingFields.forEach(function(name) { 171 $(form).find('input[name="' + name + '"]').remove(); 172 }); 173 return; 174 } 175 176 var $gclidField = $(form).find('input[name="gclid"]'); 177 var gclid = $gclidField.length ? $gclidField.val() : ''; 178 if (!gclid) { 179 try { 180 gclid = new URLSearchParams(window.location.search).get('gclid') || ''; 181 } catch (e) { 182 gclid = ''; 183 } 184 } 185 // Our own GoogleAdsClickTracking middleware persists the click id in a 186 // first-party, readable `gclid` cookie (90 days) on the acquisition 187 // landing pages â durable across pages and unaffected by gtag/consent 188 // blocking, so prefer it over Google's _gcl_aw. 189 if (!gclid) { 190 gclid = getCookie('gclid') || ''; 191 } 192 if (!gclid) { 193 var gcl = getCookie('_gcl_aw'); 194 if (gcl) { 195 var segs = decodeURIComponent(gcl).split('.'); 196 // GCL.<timestamp>.<gclid>; the gclid is opaque and may itself 197 // contain dots, so take everything after the second segment 198 // (matching gtag's own extraction), not just the last segment. 199 if (segs.length >= 3) gclid = segs.slice(2).join('.'); 200 } 201 } 202 if (gclid) { 203 // Not every demo form template server-renders the hidden gclid input 204 // (the cold-traffic v9 variant, for one). Create it so the id reaches
205 // Chili Piper on every form this script drives, not only the ones that 206 // pre-declare the field. 207 if (!$gclidField.length) { 208 $gclidField = $('<input>', { type: 'hidden', name: 'gclid' }).appendTo(form); 209 } 210 $gclidField.val(gclid); 211 } 212 213 // Demo conversions use the same durable consent proof and exact 214 // landing bundle as signup conversions. These values are populated 215 // by landing-cta.js only after the shared advertising grant. 216 $(form).find('input[name="fbclid"], input[name="fbc"], input[name="fbp"]').remove(); 217 var demoFields = { 218 advertisingConsentReference: consentReference, 219 metaEventId: metaEventId, 220 gads_lp_id: acquisitionTracking.gads_lp_id || '', 221 utm_source: acquisitionTracking.utm_source || '', 222 utm_medium: acquisitionTracking.utm_medium || '', 223 utm_campaign: acquisitionTracking.utm_campaign || '', 224 utm_term: acquisitionTracking.utm_term || '', 225 utm_content: acquisitionTracking.utm_content || '' 226 }; 227 Object.keys(demoFields).forEach(function(name) { 228 var $field = $(form).find('input[name="' + name + '"]'); 229 if (!demoFields[name]) { 230 $field.remove(); 231 return; 232 } 233 if (!$field.length) { 234 $field = $('<input>', { type: 'hidden', name: name }).appendTo(form); 235 } 236 $field.val(demoFields[name]); 237 }); 238 })(); 239 240 (function() { 241 var handoffs = window.__ttfLandingExperienceCompletionHandoffs || {}; 242 var token = typeof handoffs.demo_booking === 'string' 243 && /^[a-f0-9]{64}$/.test(handoffs.demo_booking) 244 ? handoffs.demo_booking 245 : ''; 246 var $field = $(form).find('input[name="landingExperienceCompletionToken"]'); 247 248 if (!token) { 249 $field.remove(); 250 return; 251 } 252 253 if (!$field.length) { 254 $field = $('<input>', { 255 type: 'hidden', 256 name: 'landingExperienceCompletionToken' 257 }).appendTo(form); 258 } 259 $field.val(token); 260 })(); 261 262 if (typeof ChiliPiper !== 'undefined') { 263 const chilipiper_options = { 264 title: i18n.demo_call_time_title, 265 formId: formId, 266 injectRootCss: true, 267 onSuccess: function(data) { 268 releaseSubmission(); 269 // Delete irclickid affiliate tracking cookie after successful demo booking 270 (function() { 271 var host = window.location.hostname; 272 var parts = host.split('.'); 273 var cookieDomain = (parts.length > 1 && !host.endsWith('localhost')) 274 ? '.' + parts.slice(-2).join('.') 275 : host; 276 document.cookie = 'irclickid=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=' + cookieDomain + '; secure; samesite=Lax'; 277 })(); 278 if (formId === 'bookNowFormModal') { 279 closeModal(); 280 } 281 // Track conversions 282 if (typeof dataLayer !== 'undefined') { 283 dataLayer.push({'event': 'Demo booking'}); 284 }
285 if (window.__ttfAdvertisingAllowed === true && typeof fbq !== 'undefined') { 286 fbq('trackSingle', acquisitionMetaPixelId, 'Schedule', {}, {eventID: metaEventId}); 287 fbq('trackSingleCustom', acquisitionMetaPixelId, 'CustomerBookedDemoOrCreatedBrand', {}, {eventID: metaEventId}); 288 fbq('trackSingleCustom', acquisitionMetaPixelId, 'CustomerBookedDemoSuccessfully', {}, {eventID: metaEventId}); 289 } 290 if (window.__ttfAdvertisingAllowed === true && typeof rdt !== 'undefined') { 291 rdt('track', 'SignUp'); 292 } 293 if (window.__ttfAdvertisingAllowed === true && typeof twq !== 'undefined') { 294 twq('track', 'SignUp'); 295 } 296 if (window.__ttfAdvertisingAllowed === true && typeof oaiq !== 'undefined') { 297 oaiq("measure", "appointment_scheduled", { type: "customer_action" }); 298 } 299 } 300 }; 301 ChiliPiper.submit( 302 "theticketfairy", 303 "inbound-router", 304 chilipiper_options 305 ); 306 // The handoff is now owned by Chili Piper. Keep the form 307 // guarded briefly against double-clicks, then allow a retry 308 // if the visitor dismisses its scheduler without booking. 309 window.setTimeout(releaseSubmission, 3000); 310 } else { 311 releaseSubmission(); 312 console.error('ChiliPiper is not loaded'); 313 alert(i18n.booking_system_error); 314 } 315 } catch (error) { 316 releaseSubmission(); 317 console.error('Demo booking could not start', error); 318 alert(i18n.booking_system_error); 319 } 320 }; 321 322 waitForAcquisitionConsentHandoff(submitBooking); 323 return false; 324 } 325 }); 326 return true; 327 } 328 329 // Modal handling 330 function openModal(trigger) { 331 if (!$.fn || typeof $.fn.validate !== 'function') { 332 var fallbackUrl = trigger && trigger.href ? trigger.href : ''; 333 if (fallbackUrl) { 334 window.location.assign(fallbackUrl); 335 } 336 return; 337 } 338 339 var $modal = $('#demoModal'); 340 var $modalFormContainer = $('#modal-form-container'); 341 var $originalForm = $('#bookNowForm'); 342 343 modalTrigger = trigger || document.activeElement; 344 $modal.attr('aria-hidden', 'false'); 345 $modal.addClass('show'); 346 $('body').css('overflow', 'hidden'); 347 348 if ($originalForm.length && $modalFormContainer.length) { 349 // Clone the entire demo-booking-form div, not just the form 350 var $demoBookingDiv = $('#demo-booking-form').clone(false); 351 $demoBookingDiv.attr('id', 'demo-booking-form-modal'); 352 353 // Update form ID 354 var $formClone = $demoBookingDiv.find('form'); 355 $formClone.attr('id', 'bookNowFormModal'); 356 357 // Update IDs to avoid conflicts 358 $demoBookingDiv.find('[id]').each(function() { 359 var oldId = $(this).attr('id'); 360 if (oldId && oldId !== 'bookNowFormModal') { 361 $(this).attr('id', oldId + '-modal'); 362 } 363 }); 364 365 // Update onclick handlers to reference the modal IDs 366 $demoBookingDiv.find('input[type="radio"]').each(function() { 367 var onclick = $(this).attr('onclick'); 368 if (onclick) { 369 onclick = onclick.replace('buy-or-resell-tickets-message', 'buy-or-resell-tickets-message-modal'); 370 onclick = onclick.replace('no-events-message', 'no-events-message-modal'); 371 onclick = onclick.replace('book-now-form-submit-btn', 'book-now-form-submit-btn-modal'); 372 $(this).attr('onclick', onclick); 373 } 374 }); 375 376 carryEnteredEmail($demoBookingDiv); 377 378 $modalFormContainer.empty().append($demoBookingDiv); 379 initializeBookingForm('#bookNowFormModal', 'bookNowFormModal'); 380 } 381 382 window.setTimeout(function() { 383 var focusTarget = $modal.find('input, button, [href], [tabindex]:not
383([tabindex="-1"])') 384 .filter(':visible') 385 .first(); 386 if (focusTarget.length) focusTarget.trigger('focus'); 387 }, 0); 388 } 389 390 /** 391 * A page whose hero asks for an email address before it offers the demo 392 * has already been given one by the time this modal opens. Carry it in, 393 * so nobody is asked for the same address twice. 394 * 395 * The visitor's own typing in the modal always wins: this only fills a 396 * field that is empty. 397 */ 398 function carryEnteredEmail($form) { 399 var entered = $('form.js-signup-form input[name="email"]').val(); 400 var $email = $form.find('input[name="email"]'); 401 402 if (!entered || !$email.length || $email.val()) return; 403 404 $email.val($.trim(entered)); 405 } 406 407 function closeModal() { 408 var $modal = $('#demoModal'); 409 delete demoSubmissionsInFlight.bookNowFormModal; 410 $modal.removeClass('show'); 411 $modal.attr('aria-hidden', 'true'); 412 $('body').css('overflow', ''); 413 $('#modal-form-container').empty(); 414 if (modalTrigger && typeof modalTrigger.focus === 'function') { 415 modalTrigger.focus(); 416 } 417 modalTrigger = null; 418 } 419 420 // Bind modal controls before form initialization. If a validation dependency 421 // ever fails to load, visitors must still be able to open and close the modal. 422 $(document).on('click', '.open-modal', function(e) { 423 e.preventDefault(); 424 openModal(this); 425 }); 426 427 $('#demoModal .close').on('click', closeModal); 428 429 $(document).on('click', function(e) { 430 if ($(e.target).is('#demoModal')) { 431 closeModal(); 432 } 433 }); 434 435 $(document).on('keydown', function(e) { 436 var $modal = $('#demoModal'); 437 if (!$modal.hasClass('show')) return; 438 439 if (e.key === 'Escape') { 440 closeModal(); 441 return; 442 } 443 444 if (e.key === 'Tab') { 445 var $focusable = $modal 446 .find('input, button, select, textarea, [href], [tabindex]:not([tabindex="-1"])') 447 .filter(':visible') 448 .filter(':enabled') 449 .filter(function () { 450 // Only one radio per group is in the tab sequence: the 451 // checked one, or the first when none is checked. Counting 452 // the rest would put the wrap on an element Tab never 453 // reaches, and focus would leave the open dialog. 454 if (this.type !== 'radio' || !this.name) { 455 return true; 456 } 457 458 var name = this.name; 459 var $group = $modal.find('input[type="radio"]').filter(function () { 460 return this.name === name; 461 }); 462 var $checked = $group.filter(':checked'); 463 464 return this === ($checked.length ? $checked.get(0) : $group.get(0)); 465 }); 466 if (!$focusable.length) { 467 e.preventDefault(); 468 return; 469 } 470 471 var first = $focusable.get(0); 472 var last = $focusable.get($focusable.length - 1); 473 if (e.shiftKey && document.activeElement === first) { 474 e.preventDefault(); 475 last.focus(); 476 } else if (!e.shiftKey && document.activeElement === last) { 477 e.preventDefault(); 478 first.focus(); 479 } 480 } 481 }); 482 483 // Initialize the main form after modal controls are available. 484 if ($('#bookNowForm').length) { 485 initializeBookingForm('#bookNowForm', 'bookNowForm'); 486 } 487 488 // Handle sticky header visibility 489 var $stickyCta = $('.sticky-ctas'); 490 var headerHeight = $('header').outerHeight() || 100; // Use header height or default to 100px 491 492 $(window).scroll(function() { 493 if ($(window).scrollTop() > headerHeight) { 494 $stickyCta.addClass('show'); 495 } else { 496 $stickyCta.removeClass('show'); 497 } 498 }); 499});
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.