PageSourceSearch

https://www.ticketfairy.com/resources/js/demo-booking.js?v=12

js ticketfairy.com collected 2026-10-01 10:42:04 UTC 23,385 bytes, 499 lines download raw bytes

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.