PageSourceSearch

https://www.childrensmuseum.org/assets/js/purchase-paths.js

js childrensmuseum.org collected 2026-09-24 08:37:38 UTC 86,647 bytes, 2,607 lines download raw bytes

1import {startCartTimer} from "/assets/js/cart-timer.js"
2
3const CONFIG =
4{
5    MAX_TICKETS: 50,
6    BULK_MIN_TICKETS: 50,
7    BULK_HH_MIN_TICKETS: 20,
8    BULK_MAX_TICKETS: 500,
9    FREE_DAY_MAX: 7,
10    MAX_CHILDREN: 10,
11    MEMBERSHIP_UPSELL_BREAKPOINT: 225,
12    MEMBERSHIP_UPSELL_OFFSET: 100,
13    MOBILE_BREAKPOINT: 1200,
14    FOOTER_STICKY_GAP: 15,
15    PATHS_WITH_EXPIRATION: ["admission", "ticketedEvent", "hauntedHouse", "bulkTickets", "bulkHauntedHouse"],
16    URLS: {
17        zipCode: "/ajax/modules/tnew/zip-code",
18        stepSave: "/modules/tnew/purchase-path-step-save",
19        stepGet: "/modules/tnew/purchase-path-step",
20        cart: "/modules/tnew/purchase-path-cart",
21        promo: "/modules/tnew/purchase-path-promo",
22        clearPromo: "/modules/tnew/purchase-path-clear-promo",
23        giftCert: "/modules/tnew/purchase-path-gift-cert",
24        removeGiftCert: "/modules/tnew/purchase-path-remove-gift-cert",
25        removeItem: "/modules/tnew/purchase-path-remove-item",
26        removeDate: "/modules/tnew/purchase-path-remove-date",
27        donation: "/modules/tnew/purchase-path-donation",
28        switchView: "/modules/tnew/purchase-path-switch-view",
29        membershipUpsell: "/cart/membership?group=classic&selected=",
30        thankYou: "/cart/thank-you",
31        payment: "/cart/payment",
32    }
33}
34
35const analytics =
36{
37    _send(event, params)
38    {
39        if (typeof gtag === "function") gtag("event", event, params)
40    },
41    addToCart(items, value)
42    {
43        this._send("add_to_cart", { currency: "USD", value, items })
44    },
45    removeFromCart(items)
46    {
47        this._send("remove_from_cart", { items })
48    },
49    viewItem(items, value)
50    {
51        this._send("view_item", { currency: "USD", value, items })
52    },
53    beginCheckout()
54    {
55        this._send("begin_checkout", {})
56    },
57    viewPromotion(name)
58    {
59        this._send("view_promotion", { promotion_name: name })
60    },
61    selectPromotion(name, items)
62    {
63        this._send("select_promotion", { promotion_name: name, items })
64    },
65}
66
67const purchasePath = $("#purchasePath").html()
68
69let purchasePathStep = $("#purchasePathStep").html()
70
71let calendarView = $("#purchasePathCalendarView").html()
72
73let scrollPosition = 0
74
75let adjustCalendarGridRows = () =>
76{
77    if($(".purchase-path-ticket-calendar").length)
78    {
79        const $days = $(".purchase-path-ticket-calendar .day")
80        if ($days.length <= 35)
81        {
82            $(".purchase-path-ticket-calendar").css("grid-template-rows", "repeat(5, 1fr)")
83        }
84        else
85        {
86            $(".purchase-path-ticket-calendar").css("grid-template-rows", "repeat(6, 1fr)")
87        }
88    }
89}
90
91let setupMobileSelectToggle = ($selectButton, calendarSelector, deselectOtherDaysValue = false) =>
92{
93    $selectButton.off("click")
94
95    if($(calendarSelector).filter(".selected").length > 0)
96    {
97        $selectButton.html("selected").addClass("selected")
98    }
99    else
100    {
101        $selectButton.html("select").removeClass("selected")
102    }
103
104    $selectButton.on("click", function()
105    {
106        const $calBtns = $(calendarSelector)
107        if($(this).hasClass("selected"))
108        {
109            $(this).removeClass("selected").html("select")
110            $calBtns.removeClass("selected").attr("aria-checked", "false")
111        }
112        else
113        {
114            $(this).addClass("selected").html("selected")
115            $calBtns.addClass("selected").attr("aria-checked", "true")
116            if(deselectOtherDaysValue !== false)
117            {
118                const $otherBtns = $(`.calendar-day-button:not([data-day="${deselectOtherDaysValue}"])`)
119                $otherBtns.removeClass("selected").attr("aria-checked", "false")
120            }
121        }
122    })
123}
124
125let handleMobileHauntedHouseClick = ($clicked) =>
126{
127    let friendly = $clicked
128    if ($clicked.data("subType") === "fright")
129    {
130        const $friendlyForDay = $(`.calendar-day-button[data-day="${$clicked.data("day")}"][data-sub-type="friendly"]`)
131        friendly = $friendlyForDay.length ? $friendlyForDay : friendly
132    }
133
134    const $partOne = $(".mobile-select-container .day-part.one")
135    if(friendly.hasClass("times-over"))
136    {
137        $partOne.hide()
138    }
139    else
140    {
141        $partOne.show()
142        $partOne.find(".day-time").html(friendly.data("time"))
143        $partOne.find(".day-title").html(friendly.data("title"))
144        if(friendly.data("name"))
145        {
146            $partOne.find(".day-name").html(friendly.data("name"))
147        }
148        else
149        {
150            $partOne.find(".day-name").hide()
151        }
152    }
153
154    const friendlyDay = friendly.data("day")
155    setupMobileSelectToggle(
156        $partOne.find("button"),
157        `.calendar-day-button[data-day="${friendlyDay}"][data-sub-type="friendly"]`,
158        friendlyDay
159    )
160
161    const $frightEl = $(`.half.second[data-day="${$clicked.data("day")}"]`)
162    const $partTwo = $(".mobile-select-container .day-part.two")
163    if($frightEl.length > 0)
164    {
165        if($frightEl.data("name"))
166        {
167            $partTwo.find(".day-name").html($frightEl.data("name"))
168        }
169        else
170        {
171            $partTwo.find(".day-name").hide()
172        }
173        $partTwo.find(".day-time").html($frightEl.data("time"))
174        $partTwo.find(".day-title").html($frightEl.data("title"))
175
176        setupMobileSelectToggle(
177            $partTwo.find("button"),
178            `.calendar-day-button[data-day="${$frightEl.data("day")}"][data-sub-type="${$frightEl.data("subType")}"]`,
179            $frightEl.data("day")
180        )
181        $partTwo.show()
182    }
183    else
184    {
185        $partTwo.hide()
186    }
187
188    if($clicked.data("price-adult"))
189    {
190        $("#mobile-price-adult").html(`$${$clicked.data("price-adult")} Adult`)
191    }
192    if($clicked.data("price-youth"))
193    {
194        $("#mobile-price-youth").html(`$${$clicked.data("price-youth")} Youth`)
195    }
196    if($clicked.data("price"))
197    {
198        $(".mobile-price-guest").html(`$${$clicked.data("price")} Guest`)
199    }
200}
201
202let handleMobileStandardClick = ($clicked) =>
203{
204    const $partOne = $(".mobile-select-container .day-part.one")
205    const $partTwo = $(".mobile-select-container .day-part.two")
206    $partTwo.hide()
207
208    $partOne.find(".day-time").html($clicked.data("time"))
209    $partOne.find(".day-title").html($clicked.data("title"))
210    if($clicked.data("name"))
211    {
212        $partOne.find(".day-name").html($clicked.data("name"))
213    }
214    else
215    {
216        $partOne.find(".day-name").hide()
217    }
218
219    if($clicked.data("special-event") != "0")
220    {
221        $("#mobile-calendar-special-event").find(".body-copy-small").html($clicked.data("special-event"))
222        $("#mobile-calendar-special-event").show()
223    }
224    else
225    {
226        $("#mobile-calendar-special-event").hide()
227    }
228
229    const dayValue = $clicked.data("day")
230
231    if($clicked.data("firstThursday"))
232    {
233        const $firstThursday = $(`.half.second[data-day="${dayValue}"]`)
234        $partTwo.find(".day-time").html($firstThursday.data("time"))
235        $partTwo.find(".day-title").html($firstThursday.data("name"))
236
237        $("#mobile-price-adult-2").html(`$${$firstThursday.data("price")} Adult`)
238        $("#mobile-price-youth-2").html(`$${$firstThursday.data("price")} Youth`)
239
240        setupMobileSelectToggle(
241            $partTwo.find("button"),
242            `.calendar-day-button[data-day="${dayValue}"][data-sub-type="firstThursday"]`
243        )
244        $partTwo.show()
245    }
246
247    if($clicked.data("price-adult") !== undefined)
248    {
249        $("#mobile-price-adult").html(`$${$clicked.data("price-adult")} Adult`)
250    }
251    if($clicked.data("price-youth") !== undefined)
252    {
253        $("#mobile-price-youth").html(`$${$clicked.data("price-youth")} Youth`)
254    }
255    if($clicked.data("price"))
256    {
257        $("#mobile-price-guest").html(`$${$clicked.data("price")} Guest`)
258    }
259
260    setupMobileSelectToggle(
261        $partOne.find("button"),
262        `.calendar-day-button[data-day="${dayValue}"][data-type="admission"]:not([data-sub-type="firstThursday"])`
263    )
264}
265
266let handleDesktopCalendarClick = ($clicked) =>
267{
268    if($clicked.hasClass("selected"))
269    {
270        $clicked.removeClass("selected")
271        $clicked.attr("aria-checked", "false")
272    }
273    else if(!$clicked.hasClass("closed"))
274    {
275        if(purchasePath == "hauntedHouse")
276        {
277            const $otherBtns = $(`.calendar-day-button:not([data-day="${$clicked.data("day")}"])`)
278            $otherBtns.removeClass("selected")
279            $otherBtns.attr("aria-checked", "false")
280        }
281        $clicked.addClass("selected")
282        $clicked.attr("aria-checked", "true")
283    }
284}
285
286let setupCalendarMonthSelect = () =>
287{
288    $("#purchase-path-calendar-month-select").on("change", async function()
289    {
290        if($(".calendar-day-button.selected").length > 0)
291        {
292            let days = collectSelectedDays()
293            if(days && days["selectedDays"] && Object.keys(days["selectedDays"]).length > 0)
294            {
295                await sendStepData("selectDays", days, false)
296                const data = await fetchNextStep()
297                applyStep(data)
298            }
299        }
300        else
301        {
302            const data = await fetchNextStep()
303            applyStep(data)
304        }
305    })
306}
307
308let setupCalendarViewSwitch = () =>
309{
310    $("#calendar-switch-view").on("click", function()
311    {
312        calendarSwitchView()
313    })
314}
315
316let initMobileCalendarState = () =>
317{
318    if ($(window).width() < CONFIG.MOBILE_BREAKPOINT)
319    {
320        $(".purchase-path-ticket-calendar .day .calendar-day-button").addClass("mobile")
321    }
322    else
323    {
324        $(".purchase-path-ticket-calendar .day .calendar-day-button").removeClass("mobile")
325    }
326    let selectedMonth = 0;
327    if($("#purchase-path-calendar-month-select").length)
328    {
329        selectedMonth = $("#purchase-path-calendar-month-select").val().split("-")[0]
330    }
331    if($(".calendar-day-button.mobile").length && $(`.calendar-day-button.selected[data-month="${selectedMonth}"]`).length == 0)
332    {
333        $(".calendar-day-button:not(.closed)").first().focus()
334        $(".calendar-day-button:not(.closed)").first().trigger("click")
335    }
336    else if($(".calendar-day-button.mobile.selected").length > 0)
337    {
338        if($(".calendar-day-button.selected:not(.closed)").first().data("subType") == "firstThursday")
339        {
340            setTimeout(function()
341            {
342                $(".calendar-day-button.selected:not(.closed)").first().parent().find(".calendar-day-button").first().focus()
343                $(".calendar-day-button.selected:not(.closed)").first().parent().find(".calendar-day-button").first().trigger("click")
344            }, 200)
345        }
346        else
347        {
348            $(".calendar-day-button.selected:not(.closed)").first().focus()
349            $(".calendar-day-button.selected:not(.closed)").first().trigger("click")
350        }
351    }
352}
353
354let applyCalendarClickEvents = () =>
355{
356    adjustCalendarGridRows()
357
358    $(".calendar-day-button").on("click", function()
359    {
360        if($(this).hasClass("mobile"))
361        {
362            if(purchasePath == "hauntedHouse")
363            {
364                handleMobileHauntedHouseClick($(this))
365            }
366            else
367            {
368                handleMobileStandardClick($(this))
369            }
370        }
371        else
372        {
373            handleDesktopCalendarClick($(this))
374        }
375    })
376
377    setupCalendarMonthSelect()
378    setupCalendarViewSwitch()
379    initMobileCalendarState()
380    resize()
381}
382
383let applyExtraCollapseEvents = () =>
384{
385    $(".purchase-path-ticket-rolling-week.add-extras .day").on("click keydown", function(event)
386    {
387        if(event.type === 'click' || (event.type === 'keydown' && event.keyCode === 13))
388        {
389            let groups = $(`.ticket-group[data-day="${$(this).data("day")}"], .ticket-group-header[data-day="${$(this).data("day")}"]`)
390            if($(this).hasClass("uncollapsed"))
391            {
392                $(this).removeClass("uncollapsed")
393                groups.attr("hidden", true)
394            }
395            else
396            {
397                $(this).addClass("uncollapsed")
398                if($("#accessPass").length)
399                {
400                    groups = groups.not(".access-pass-additional-tickets:not
400(.unlocked)")
401                }
402                groups.removeAttr("hidden")
403            }
404        }
405    })
406    $(".purchase-path-extras-event-header").on("click keydown", function(event)
407    {
408        if(event.type === 'click' || (event.type === 'keydown' && event.keyCode === 13))
409        {
410            let event = $(this).parent()
411            if(event.hasClass("uncollapsed"))
412            {
413                event.removeClass("uncollapsed")
414            }
415            else
416            {
417                event.addClass("uncollapsed")
418            }
419        }
420    })
421}
422
423let unlockAdditionalTickets = () =>
424{
425    $(".ticket-group.access-pass-additional-tickets").addClass("unlocked").each(function()
426    {
427        let day = $(this).data("day")
428        if(!day || $(`.purchase-path-ticket-rolling-week.add-extras .day[data-day="${day}"]:not(.ticket-group-header)`).hasClass("uncollapsed"))
429        {
430            $(this).removeAttr("hidden")
431        }
432    })
433}
434
435let applyTicketSelectEvents = () =>
436{
437    $(".ticket-container input").on("input", function()
438    {
439        let ticketContainer = $(this).parent().parent()
440        ticketContainer.data("qty", $(this).val())
441    })
442    $(".ticket-container button").on("click", function()
443    {
444        let ticketContainer = $(this).parent().parent()
445        if($(this).data("action") == "plus")
446        {
447            if(ticketContainer.data("accessPass"))
448            {
449                let accessPassMax = parseInt($("#accessPassMax").html())
450                if(purchasePathStep == "reviewCart")
451                {
452                    if(countTicketsByAttribute("access-pass", $(this).data("performanceId")) >= accessPassMax)
453                    {
454                        displayStepError(`You are limited to ${accessPassMax} Access Pass tickets per event.`)
455                    }
456                    else if(ticketContainer.data("qty") >= CONFIG.MAX_TICKETS)
457                    {
458                        displayStepError("Max ticket count reached.")
459                    }
460                    else
461                    {
462                        ticketContainer.data("qty", ticketContainer.data("qty") + 1)
463                    }
464                }
465                else
466                {
467                    if(countTicketsByAttribute("access-pass") >= accessPassMax)
468                    {
469                        displayStepError("Too many access pass tickets selected. Additional admission tickets are offered at regular price.")
470                        unlockAdditionalTickets()
471                    }
472                    else if(ticketContainer.data("qty") >= CONFIG.MAX_TICKETS)
473                    {
474                        displayStepError("Max ticket count reached.")
475                    }
476                    else
477                    {
478                        ticketContainer.data("qty", ticketContainer.data("qty") + 1)
479                    }
480                }
481            }
482            else if(ticketContainer.data("freeDay"))
483            {
484                let remaining = ticketContainer.data("freeDayRemaining") !== undefined ? parseInt(ticketContainer.data("freeDayRemaining")) : CONFIG.FREE_DAY_MAX
485                if(countTicketsByAttribute("free-day", ticketContainer.data("performanceId")) >= remaining)
486                {
487                    displayStepError(`You are limited to ${CONFIG.FREE_DAY_MAX} free tickets per free admission day. You have already claimed ${CONFIG.FREE_DAY_MAX - remaining}.`)
488                }
489                else if(ticketContainer.data("qty") >= CONFIG.MAX_TICKETS)
490                {
491                    displayStepError("Max ticket count reached.")
492                }
493                else
494                {
495                    ticketContainer.data("qty", ticketContainer.data("qty") + 1)
496                }
497            }
498            else
499            {
500                if(ticketContainer.data("qty") < CONFIG.MAX_TICKETS)
501                {
502                    ticketContainer.data("qty", ticketContainer.data("qty") + 1)
503                }
504                else
505                {
506                    displayStepError("Max ticket count reached.")
507                }
508                if($("#membership-upsell").length && totalAdmissionPrice() + CONFIG.MEMBERSHIP_UPSELL_OFFSET > CONFIG.MEMBERSHIP_UPSELL_BREAKPOINT)
509                {
510                    $("#save-message").html($("#save-message").html().replace("%AMOUNT%", CONFIG.MEMBERSHIP_UPSELL_BREAKPOINT - totalAdmissionPrice()))
511                    $("#default-message").attr("hidden", true)
512                    $("#save-message").removeAttr("hidden")
513                }
514            }
515        }
516        else if($(this).data("action") == "minus")
517        {
518            if(ticketContainer.data("qty") > 0)
519            {
520                ticketContainer.data("qty", ticketContainer.data("qty") - 1)
521            }
522        }
523        $(this).parent().find(".ticket-qty-value").html(ticketContainer.data("qty"))
524        let ticketLabel = ticketContainer.find(".ticket-info .h5").first().text()
525            || ticketContainer.find(".ticket-type span").first().text()
526            || "Ticket"
527        announceQuantityChange(ticketLabel.trim(), ticketContainer.data("qty"))
528    })
529    $("#zip-code-enter").on("click", async function()
530    {
531        if($("#zip-code").val().length == 5 && !isNaN($("#zip-code").val()))
532        {
533            let fd = new FormData();
534            fd.append('zipCode', $("#zip-code").val())
535            try
536            {
537                const data = await $.ajax({
538                    method: "POST",
539                    url: CONFIG.URLS.zipCode,
540                    processData: false,
541                    contentType: false,
542                    dataType: "json",
543                    data: fd
544                })
545                if(data.isResident)
546                {
547                    $(`.ticket-container`).each(function()
548                    {
549                        if($(this).is(":visible") && !$(this).data("freeDay") && $(this).data("inStatePriceTypeId"))
550                        {
551                            $(this).data("priceTypeId", $(this).data("inStatePriceTypeId"))
552                            let oldPrice = parseFloat($(this).data("price"))
553                            let newPrice = parseFloat($(this).data("inStatePrice"))
554                            let $priceEl = $(this).find(".ticket-info .ticket-price")
555                            $({val: oldPrice}).animate({val: newPrice}, {
556                                duration: 600,
557                                easing: "swing",
558                                step: function(now)
559                                {
560                                    $priceEl.html(`$${now.toFixed(2)}`)
561                                },
562                                complete: function()
563                                {
564                                    $priceEl.html(`$${newPrice.toFixed(2)}`)
565                                    $priceEl.addClass("price-updated")
566                                    setTimeout(() => $priceEl.removeClass("price-updated"), 1000)
567                                }
568                            })
569                        }
570                    })
571                }
572                else
573                {
574                    // zip code not eligible — no discount applied
575                }
576                $("#ticket-zip-code").slideUp()
577            }
578            catch (err)
579            {
580                displayStepError("There was a problem checking your zip code. Please try again later.");
581            }
582        }
583        else
584        {
585            displayStepError("Invalid zip code.");
586        }
587    })
588    $(".ticket-group-remove-date").on("click", async function()
589    {
590        let fd = new FormData();
591        fd.append('date', $(this).data("date"))
592        try
593        {
594            await $.ajax({
595                method: "POST",
596                url: CONFIG.URLS.removeDate,
597                processData: false,
598                contentType: false,
599                data: fd
600            })
601            const data = await fetchNextStep()
602            applyStep(data)
603        }
604        catch (err)
605        {
606            displayStepError("There was a problem removing this date. Please try again later.");
607        }
608    })
609    $("#upsell-uncollapse-button").on("click", function(e)
610    {
611        e.preventDefault()
612        $(".membership-upsell").removeAttr("hidden")
613        $(this).hide()
614        analytics.viewPromotion("Membership Upsell")
615    })
616    $(".membership-upsell-select button").on("click", function(e)
617    {
618        e.preventDefault()
619        analytics.selectPromotion("Membership Upsell", [{
620            item_id: $(this).data("id"),
621            item_category: $(this).data("group"),
622            item_name: $(this).data("name"),
623        }])
624        window.location = `${CONFIG.URLS.membershipUpsell}${$(this).data("id")}`
625    })
626    $(".ticket-container").each(function()
627    {
628        if($(this).is(":visible"))
629        {
630            analytics.viewItem([{
631                item_id: $(this).data("performanceId"),
632                item_name: "Performance",
633                item_category: "Performance",
634                price: $(this).data("price")
635            }], $(this).data("price"))
636        }
637    })
638}
639
640let countTicketsByAttribute = (attrName, performanceId = false) =>
641{
642    let count = 0
643    let selectorString = `.ticket-container[data-${attrName}="1"]`
644    if(performanceId)
645    {
646        selectorString = `${selectorString}[data-performance-id="${performanceId}"]`
647    }
648    $(selectorString).each(function()
649    {
650        if ($(this).is(":visible"))
651        {
652            count += $(this).data("qty")
653        }
654    })
655    return count
656}
657
658let totalAdmissionPrice = () =>
659{
660    let price = 0
661    $(`.ticket-container[data-type="admission"`).each(function()
662    {
663        price += parseFloat($(this).data("price")) * parseInt($(this).data("qty"))
664    })
665    return price
666}
667
668let applyFeeSelectEvents = () =>
669{
670    $(".extras-fee button").on("click", function()
671    {
672        let feeContainer = $(this).parent().parent()
673        if($(this).data("action") == "plus")
674        {
675            feeContainer.data("qty", feeContainer.data("qty") + 1)
676        }
677        else if($(this).data("action") == "minus")
678        {
679            if(feeContainer.data("qty") > 0)
680            {
681                feeContainer.data("qty", feeContainer.data("qty") - 1)
682            }
683        }
684        $(this).parent().find(".fee-qty-display h4").html(feeContainer.data("qty"))
685        let feeLabel = feeContainer.find(".extras-fee-info h5").first().text() || "Item"
686        announceQuantityChange(feeLabel.trim(), feeContainer.data("qty"))
687    })
688}
689
690let applyFormEvents = () =>
691{
692    $(".form-group input, .form-group select").off()
693    $(".form-group input, .form-group select").each(function()
694    {
695        if($(this).val().length != 0)
696        {
697            $(this).closest(".form-group").addClass("filled")
698        }
699    })
700    $(".form-group input, .form-group select").on("change", function()
701    {
702        if($(this).val() != "")
703        {
704            $(this).closest(".form-group").addClass("filled")
705        }
706        else
707        {
708            $(this).closest(".form-group").removeClass("filled")
709        }
710    })
711}
712
713let applyConfirmMembershipClickEvents = () =>
714{
715    $(".purchase-path-membership-choice input[type='radio']").on("change", function()
716    {
717        $(".purchase-path-membership-choice input[type='radio']").not($(this)).removeAttr("checked")
718    })
719    $("#membership-show-all").on("click", function (e)
720    {
721        e.preventDefault()
722        $(".purchase-path-membership").removeAttr("hidden")
723        $("#membership-show-all").attr("hidden", "")
724    })
725}
726
727let applyMemberInfoFormEvents = () =>
728{
729    applyRemoveChildEvents()
730    if($("#add-a2-button").length > 0)
731    {
732        applyRemoveAdultEvents("a2")
733    }
734    applyRemoveAdultEvents("a3")
735    $("#add-child-button").on('click keydown', function(event)
736    {
737        if(event.type === 'click' || (event.type === 'keydown' && event.keyCode === 13))
738        {
739            if(countChildren() < CONFIG.MAX_CHILDREN)
740            {
741                let nextChildNum = 1
742                if($(".membership-child").last().length > 0)
743                {
744                    //nextChildNum = $(".membership-child").last().data("child") + 1
745                    for(let i = 1;i <= CONFIG.MAX_CHILDREN;i++)
746                    {
747                        if(!$(`.membership-child[data-child="${i}"]`).length)
748                        {
749                            nextChildNum = i
750                            break
751                        }
752                    }
753                }
754                $("#child-container").append(
755                `
756                    <div class="uk-grid membership-child new-child" data-child="${nextChildNum}" uk-grid>
757                        <div class="uk-width-1-1 uk-width-1-4@l">
758                            <div class="form-group">
759                                <label class="form-label" for="child-${nextChildNum}-first-name">First Name*</label>
760                                <input id="child-${nextChildNum}-first-name" name="child-${nextChildNum}-first-name" class="form-input" type="text" required>
761                                <div class="form-input-helper-text">Enter the first name of your child.</div>
762                            </div>
763                        </div>
764                        <div class="uk-width-1-1 uk-width-1-4@l">
765                            <div class="form-group">
766                                <label class="form-label" for="child--last-name">Last Name*</label>
767                                <input id="child-${nextChildNum}-last-name" name="child-${nextChildNum}-last-name" class="form-input" type="text" required>
768                                <div class="form-input-helper-text">Enter the last name of your child.</div>
769                            </div>
770                        </div>
771                        <div class="uk-width-1-1 uk-width-1-4@l">
772                            <div class="form-group ">
773                                <label class="form-label" for="child-${nextChildNum}-birthdate">Date of Birth*</label>
774                                <input id="child-${nextChildNum}-birthdate" name="child-${nextChildNum}-birthdate" class="form-input" type="date" required>
775                                <div class="form-input-helper-text">MM / DD / YYYY</div>
776                            </div>
777                        </div>
778                        <input type="hidden" name="child-${nextChildNum}-affiliation-id" value="0">
779                        <input type="hidden" name="child-${nextChildNum}-type" value="new">
780                        <input type="hidden" name="child-${nextChildNum}-removed" value="0">
781                        <input type="hidden" name="child-${nextChildNum}-id" value="0">
782                        <input type="hidden" name="child-${nextChildNum}-birthdate-exists" value="0">
783                        <div class="uk-width-1-1 uk-width-1-4@l uk-flex uk-flex-middle">
784                            <a class="membership-remove-button remove-child-button body-copy-small" data-child="${nextChildNum}">Remove Child</a>
785                        </div>
786                    </div>
787                `)
788                applyRemoveChildEvents()
789            }
790            else
791            {
792                displayStepError(`You can have a max of ${CONFIG.MAX_CHILDREN} children on your membership.`);
793            }
794        }
795    })
796    if($("#add-a2-button").length > 0)
797    {
798        $("#add-a2-button").on('click keydown', function(event)
799        {
800            if(event.type === 'click' || (event.type === 'keydown' && event.keyCode === 13))
801            {
802                $("#new-a2-container").removeAttr("hidden")
803                $("#add-a2-button-container").attr("hidden", "")
804                $("#remove-a2-button-container").removeAttr("hidden")
805            }
806        })
807    }
808    $("#add-a3-button").on('click keydown', function(event)
809    {
810        if(event.type === 'click' || (event.type === 'keydown' && event.keyCode === 13))
811        {
812            $("#new-a3-container").removeAttr("hidden")
813            $("#add-a3-button-container").attr("hidden", "")
814            $("#remove-a3-button-container").removeAttr("hidden")
815        }
816    })
817    $(".preCheckoutFileUpload").on("change", function()
818    {
819        let fileName = $(this).val().split('\\').pop()
820        $('label[for="document"]').find("span").text(fileName)
821        $('label[for="document"]').addClass("complete")
822    })
823}
824
825let applyRemoveAdultEvents = (key) =>
826{
827    $(`.remove-${key}-button`).off()
828    $(`.remove-${key}-button`).on("click keydown", function(event)
829    {
830        if(event.type === 'click' || (event.type === 'keydown' && event.keyCode === 13))
831        {
832            if($(`#existing-${key}-container`).length > 0)
833            {
834                $(`#existing-${key}-container`).attr("hidden", "")
835                $(`#removed-${key}-container`).removeAttr("hidden")
836            }
837            $(`#new-${key}-container`).attr("hidden", "")
838            $(`#remove-${key}-button-container`).attr("hidden", "")
839            $(`#add-${key}-button-container`).removeAttr("hidden")
840        }
841    })
842}
843
844let applyRemoveChildEvents = () =>
845{
846    $(`.remove-child-button`).off()
847    $(`.remove-child-button`).on("click keydown", function(event)
848    {
849        if(event.type === 'click' || (event.type === 'keydown' && event.keyCode === 13))
850        {
851            let currentChild = $(this).closest(".membership-child")
852            if(currentChild.hasClass("new-child"))
853            {
854                // if it's a new child, just remove the entire thing
855                currentChild.remove()
856            }
857            else
858            {
859                currentChild.addClass("removed-child")
860                let childNum = currentChild.data("child")
861                let childFirstName = $(this).parent().parent().find("input[data-input-type='first']").val()
862                let childLastName = $(this).parent().parent().find("input[data-input-type='last']").val()
863                let affiliationId = $(this).parent().parent().find("input[data-input-type='affiliation']").val()
864                let id = $(this).parent().parent().find("input[data-input-type='id']").val()
865                // if it's an existing child, display the "reason" input
866                currentChild.html(`
867                    <div class="uk-width-1-1">
868                        <div class="form-group">
869                            <label class="form-label" for="child-${childNum}-reason">Child Removal Reason (optional)</label>
870                            <input id="child-${childNum}-reason" name="child-${childNum}-reason" class="form-input" type="text">
871                            <div class="form-input-helper-text">Child removal reason for ${childFirstName} ${childLastName}</div>
872                        </div>
873                    </div>
874                    <input type="hidden" name="child-${childNum}-first-name" value="${childFirstName}">
875                    <input type="hidden" name="child-${childNum}-last-name" value="${childLastName}">
876                    <input type="hidden" name="child-${childNum}-affiliation-id" value="${affiliationId}">
877                    <input type="hidden" name="child-${childNum}-id" value="${id}">
878                    <input type="hidden" name="child-${childNum}-type" value="existing">
879                    <input type="hidden" name="child-${childNum}-removed" value="1">
880                `)
881                applyFormEvents()
882            }
883        }
884    })
885    applyFormEvents()
886}
887
888let countChildren = () =>
889{
890    return $(".membership-child:not(.removed-child)").length
891}
892
893let announceQuantityChange = (label, qty) =>
894{
895    const el = $("#purchase-path-qty-status")
896    el.text("")
897    setTimeout(() => el.text(`${label}, quantity ${qty}`), 100)
898}
899
900let displayStepError = (text) =>
901{
902    $(".next-step").removeClass("loading")
903    const errorContainer = $(`.error-message[data-step="${purchasePathStep}"]`)
904    errorContainer.find("span").html(text)
905    errorContainer.removeAttr("hidden")
906    const alert = $("#purchase-path-alert")
907    alert.text("")
908    setTimeout(() => alert.text(text), 100)
909}
910
911let clearStepError = () =>
912{
913    $(`.error-message[data-step="${purchasePathStep}"]`).attr("hidden", true)
914    $("#purchase-path-alert").text("");
915}
916
917let applyGiftCertRecipientEvents = () =>
918{
919    $("#gift-cert-method").on("click keydown", function(event)
920    {
921        if(event.type === 'click' || (event.type === 'keydown' && event.keyCode === 13))
922        {
923            let method = $(this).val()
924            $(`div[data-input-category='gift-cert-method']`).attr("hidden", "")
925            $(`div[data-section='${method}']`).removeAttr("hidden")
926        }
927    })
928}
929
930// for select inputs that show/hide other fields/elements
931let applyDataToggle = () =>
932{
933    $(document).on("change click keydown", "[data-toggle='fields']", function(event)
934    {
935        if (event.type !== "change" && !(event.type === "click" || (event.type === "keydown" && event.keyCode === 13)))
936        {
937            return
938        }
939        let $ctrl = $(this)
940        let current = String($ctrl.val())
941
942        let group = $ctrl.data("toggleGroup")
943        let groupAttr = $ctrl.data("targetGroupAttr") || "data-input-category"
944        let valueAttr = $ctrl.data("targetValueAttr") || "data-section"
945
946        if (!group) return
947
948        let $all = $(`[${groupAttr}='${group}']`)
949        // Hide everything in the group
950        $all.attr("hidden", "")
951
952        // Show items whose valueAttr matches the current select value
953        let $show = $all.filter(function()
954        {
955            return $(this).attr(valueAttr) === current
956        })
957        $show.removeAttr("hidden")
958    })
959
960    $("[data-toggle='fields']").each(function()
961    {
962        $(this).trigger("change")
963    })
964}
965
966let applyDataCopy = () =>
967{
968    let doCopy = (group, sourcePrefix, targetPrefix, suffixes) =>
969    {
970        suffixes.forEach(suffix =>
971        {
972            let sourceName = `${sourcePrefix}${suffix}`
973            let targetName = `${targetPrefix}${suffix}`
974
975            let $source = $(`[name='${sourceName}']`)
976            let $target = $(`[name='${targetName}']`)
977
978            if ($source.length && $target.length)
979            {
980                if ($source.is(":checkbox"))
981                {
982                    $target.prop("checked", $source.prop("checked")).trigger("change")
983                }
984                else if ($source.is(":radio"))
985                {
986                    let val = $source.filter(":checked").val()
987                    $target.filter(`[value='${val ?? ""}']`).prop("checked", true).trigger("change")
988                }
989                else
990                {
991                    $target.val($source.val()).trigger("change")
992                }
993            }
994        })
995    }
996
997    let unbindLive = group =>
998    {
999        $(`[data-copy-source-listener='${group}']`).each(function()
1000        {
1001            $(this).off(".copy_" + group).removeAttr("data-copy-source-listener")
1002        })
1003    }
1004
1005    let bindLive = (group, sourcePrefix, targetPrefix, suffixes) =>
1006    {
1007        suffixes.forEach(suffix =>
1008        {
1009            let sourceName = `${sourcePrefix}${suffix}`
1010            let $source = $(`[name='${sourceName}']`)
1011            if ($source.length)
1012            {
1013                $source.attr("data-copy-source-listener", group)
1014                    .on(`input.copy_${group} change.copy_${group}`, function()
1015                    {
1016                        // Mirror only while the checkbox is still checked
1017                        let $toggle = $(`[data-copy='fields'][data-copy-group='${group}']`)
1018                        if (!$toggle.is(":checked")) return
1019
1020                        let targetName = `${targetPrefix}${suffix}`
1021                        let $target = $(`[name='${targetName}']`)
1022
1023                        if ($(this).is(":checkbox"))
1024                        {
1025                            $target.prop("checked", $(this).prop("checked")).trigger("change")
1026                        }
1027                        else if ($(this).is(":radio"))
1028                        {
1029                            let val = $source.filter(":checked").val()
1030                            $target.filter(`[value='${val ?? ""}']`).prop("checked", true).trigger("change")
1031                        }
1032                        else
1033                        {
1034                            $target.val($(this).val()).trigger("change")
1035                        }
1036                    })
1037            }
1038        })
1039    }
1040
1041    let setLocked = (targetPrefix, suffixes, locked) =>
1042    {
1043        suffixes.forEach(suffix =>
1044        {
1045            let targetName = `${targetPrefix}${suffix}`
1046            let $target = $(`[name='${targetName}']`)
1047            if ($target.is(":checkbox,:radio"))
1048            {
1049                $target.prop("disabled", locked)
1050            }
1051            else
1052            {
1053                $target.prop("readonly", locked)
1054            }
1055        })
1056    }
1057
1058    $(document).on("change", "[data-copy='fields']", function()
1059    {
1060        let $cb = $(this)
1061        let group = $cb.data("copyGroup")
1062        if (!group) return
1063
1064        let sourcePrefix = $cb.data("copySourcePrefix") || ""
1065        let targetPrefix = $cb.data("copyTargetPrefix") || ""
1066        let suffixes = String($cb.data("copyFields") || "")
1067            .split(",")
1068            .map(s => s.trim())
1069            .filter(Boolean)
1070
1071        unbindLive(group)
1072
1073        if ($cb.is(":checked"))
1074        {
1075            doCopy(group, sourcePrefix, targetPrefix, suffixes)
1076            bindLive(group, sourcePrefix, targetPrefix, suffixes)
1077            setLocked(targetPrefix, suffixes, true)
1078        }
1079        else
1080        {
1081            setLocked(targetPrefix, suffixes, false)
1082        }
1083    })
1084
1085    $("[data-copy='fields']").each(function()
1086    {
1087        $(this).trigger("change")
1088    })
1089}
1090
1091let applyAttendeeEvents = () =>
1092{
1093    $(".attendee-select").on("change", function()
1094    {
1095        let attendeeId = $(this).data("attendee")
1096        if($(this).val() == "other")
1097        {
1098            $(`.form-group[data-attendee="${attendeeId}"]`).removeAttr("hidden")
1099        }
1100        else
1101        {
1102            $(`.form-group[data-attendee="${attendeeId}"]`).attr("hidden", true)
1103        }
1104    })
1105}
1106
1107let applyClearPromoCodeEvents = () =>
1108{
1109    $(".clear-promo").on("click", async function()
1110    {
1111        try
1112        {
1113            await $.ajax({
1114                method: "GET",
1115                url: CONFIG.URLS.clearPromo,
1116                dataType: "json",
1117            })
1118            getCartSidebar()
1119            $(".promo-applied").html("")
1120        }
1121        catch (err)
1122        {
1123            displayStepError("There was a problem clearing your promo code. Please try again later.");
1124        }
1125    })
1126}
1127
1128
1129let applyClearGiftCertEvents = () =>
1130{
1131    $(".clear-gift-cert").on("click", async function()
1132    {
1133        let giftCertCode = $(this).data("giftCertCode")
1134        let formData = new FormData()
1135        formData.append("code", giftCertCode)
1136        try
1137        {
1138            await $.ajax({
1139                method: "POST",
1140                url: CONFIG.URLS.removeGiftCert,
1141                data: formData,
1142                processData: false,
1143                contentType: false,
1144            })
1145            getCartSidebar()
1146            $(".gift-cert-applied").html("")
1147        }
1148        catch (err)
1149        {
1150            displayStepError("There was a problem removing your gift certificate. Please try again later.");
1151        }
1152    })
1153}
1154
1155let applyDonationSelectEvents = () =>
1156{
1157    $('input[type="radio"][name="checkout-donate"]').on("change", async function(e)
1158    {
1159        let formData = new FormData()
1160        let amount = $(this).val()
1161        formData.append("donation", amount)
1162        try
1163        {
1164            const data = await $.ajax({
1165                method: "POST",
1166                url: CONFIG.URLS.donation,
1167                data: formData,
1168                processData: false,
1169                contentType: false,
1170                dataType: "json",
1171            })
1172            if(data.nextStep)
1173            {
1174                getCartSidebar()
1175                analytics.addToCart([{ item_name: "Checkout Donation" }], amount)
1176            }
1177        }
1178        catch (err)
1179        {
1180            displayStepError("There was a problem saving your donation selection. Please try again later.");
1181        }
1182    })
1183}
1184
1185let applyPromoCodeEvents = () =>
1186{
1187    $(".submit-promo").on("click", async function(e)
1188    {
1189        e.preventDefault()
1190        const button = $(this)
1191        button.addClass("loading")
1192        let formGroup = button.closest(".form-group")
1193        let input = formGroup.find(".promo-code-input")
1194        if(input.val().length > 0)
1195        {
1196            let fd = new FormData();
1197            fd.append('promo', input.val())
1198            try
1199            {
1200                const data = await $.ajax({
1201                    method: "POST",
1202                    url: CONFIG.URLS.promo,
1203                    processData: false,
1204                    contentType: false,
1205                    dataType: "json",
1206                    data: fd
1207                })
1208                if(data.successPromoCode)
1209                {
1210                    getCartSidebar()
1211                    let html = $("#clear-promo-html").html()
1212                    $(".promo-applied").html(`
1213                        <div class="promo-code-tag button">
1214                            <span>${input.val()}</span>
1215                            ${html}
1216                        </div>
1217                    `)
1218                    applyClearPromoCodeEvents()
1219                    button.removeClass("loading")
1220                }
1221                else if(data.error)
1222                {
1223                    formGroup.find(".form-input-helper-text").html(data.error)
1224                    formGroup.addClass("error")
1225                }
1226            }
1227            catch (err)
1228            {
1229                button.removeClass("loading")
1230                displayStepError("There was a problem applying your promo code. Please try again later");
1231            }
1232        }
1233        else
1234        {
1235            button.removeClass("loading")
1236            displayStepError("Invalid promo code.");
1237        }
1238    })
1239    applyClearPromoCodeEvents()
1240}
1241
1242let applyGiftCertCodeEvents = () =>
1243{
1244    $(".submit-gift-cert").on("click", async function(e)
1245    {
1246        e.preventDefault()
1247        const button = $(this)
1248        let formGroup = button.closest(".form-group")
1249        let input = formGroup.find(".gift-cert-input").val()
1250        if(input.includes("#"))
1251        {
1252            input = input.replace("#", "")
1253        }
1254        if(input.length > 0)
1255        {
1256            button.addClass("loading")
1257            let fd = new FormData();
1258            fd.append('gift-cert', input)
1259            try
1260            {
1261                const data = await $.ajax({
1262                    method: "POST",
1263                    url: CONFIG.URLS.giftCert,
1264                    processData: false,
1265                    contentType: false,
1266                    dataType: "json",
1267                    data: fd
1268                })
1269                if(data.successGiftCert)
1270                {
1271                    getCartSidebar()
1272                    if(data.giftCerts)
1273                    {
1274                        $(".gift-cert-applied").html("")
1275                        for (const [key, value] of Object.entries(data.giftCerts))
1276                        {
1277                            $(".gift-cert-applied").append(`
1278                                <div class="gift-cert-tag button">
1279                                    <span>#${value.GiftCertificateNumber} - $${value.Amount}</span>
1280                                    <span class="clear-gift-cert" data-gift-cert-code="${value.GiftCertificateNumber}">
1281                                        <svg width="10" height="11" viewB
1281ox="0 0 10 11" fill="none" xmlns="http://www.w3.org/2000/svg">
1282                                            <path d="M8.07129 1.96484L1.00022 9.03591" stroke="#08245E" style="stroke:#08245E;stroke:color(display-p3 0.0314 0.1412 0.3686);stroke-opacity:1;" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1283                                            <path d="M1.00098 1.96484L8.07204 9.03591" stroke="#08245E" style="stroke:#08245E;stroke:color(display-p3 0.0314 0.1412 0.3686);stroke-opacity:1;" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1284                                        </svg>
1285                                    </span>
1286                                </div>
1287                            `)
1288                        }
1289                    }
1290                    applyClearGiftCertEvents()
1291                    button.removeClass("loading")
1292                }
1293                else if(data.error)
1294                {
1295                    button.removeClass("loading")
1296                    displayStepError(data.error);
1297                }
1298            }
1299            catch (err)
1300            {
1301                button.removeClass("loading")
1302                displayStepError("There was a problem applying your gift certificate code. Please try again later");
1303            }
1304        }
1305    })
1306    applyClearGiftCertEvents()
1307}
1308
1309let applyTicketCalendar = html =>
1310{
1311    $("#ticket-calendar-container").html(html)
1312    $("#ticket-calendar-container").fadeTo(1000, 100, function(){})
1313    applyCalendarClickEvents()
1314    if ($(window).width() < CONFIG.MOBILE_BREAKPOINT)
1315    {
1316        $(".purchase-path-ticket-calendar .day .calendar-day-button").addClass("mobile")
1317    }
1318    else
1319    {
1320        $(".purchase-path-ticket-calendar .day .calendar-day-button").removeClass("mobile")
1321    }
1322    if($(".calendar-day-button.mobile").length > 0 && $(".calendar-day-button.selected").length == 0)
1323    {
1324        $(".calendar-day-button").first().focus()
1325        $(".calendar-day-button").first().trigger("click")
1326    }
1327    else if($(".calendar-day-button.mobile").length > 0 && $(".calendar-day-button.selected").length > 0)
1328    {
1329        $(".calendar-day-button.selected").first().focus()
1330        $(".calendar-day-button.selected").first().trigger("click")
1331    }
1332}
1333
1334let applyReviewAmountEvents = () =>
1335{
1336    $(".gift-cert-amount-button").on("click", function(e)
1337    {
1338        e.preventDefault()
1339        $("#amount").val($(this).data("amount"))
1340        $("#amount").parent().addClass("filled")
1341    })
1342}
1343
1344// MARK: step setups
1345
1346const STEP_SETUP = {
1347    selectDays: () =>
1348    {
1349        applyCalendarClickEvents()
1350        applyNextStepButtonClickEvents()
1351
1352        // selecting date automatically if in url params
1353        const urlParams = new URLSearchParams(window.location.search)
1354        const dateParam = urlParams.get('date')
1355        if(dateParam)
1356        {
1357            const parts = dateParam.split('-')
1358            if(parts.length === 3)
1359            {
1360                const targetMonth = parseInt(parts[0], 10)
1361                const targetDay = parseInt(parts[1], 10)
1362                const targetYear = parseInt(parts[2], 10)
1363
1364                const $dayBtn = $(".calendar-day-button").filter(function()
1365                {
1366                    return $(this).data("day") == targetDay &&
1367                           $(this).data("month") == targetMonth &&
1368                           $(this).data("year") == targetYear
1369                })
1370
1371                if($dayBtn.length && !$dayBtn.hasClass("closed") && !$dayBtn.hasClass("selected"))
1372                {
1373                    $dayBtn.trigger("click")
1374                    if($dayBtn.hasClass("selected"))
1375                    {
1376                        $(".next-step").trigger("click")
1377                    }
1378                }
1379            }
1380        }
1381    },
1382    reviewAmount: () =>
1383    {
1384        applyFormEvents()
1385        applyNextStepButtonClickEvents()
1386        applyReviewAmountEvents()
1387    },
1388    recipientInfo: () =>
1389    {
1390        applyFormEvents()
1391        applyGiftCertRecipientEvents()
1392        applyNextStepButtonClickEvents()
1393    },
1394    selectTickets: () =>
1395    {
1396        applyTicketSelectEvents()
1397        applyNextStepButtonClickEvents()
1398    },
1399    bulkTicketsSelect: () =>
1400    {
1401        applyTicketSelectEvents()
1402        applyNextStepButtonClickEvents()
1403    },
1404    addExtras: () =>
1405    {
1406        applyTicketSelectEvents()
1407        applyExtraCollapseEvents()
1408        applyFeeSelectEvents()
1409        applyNextStepButtonClickEvents()
1410    },
1411    confirmMembership: () =>
1412    {
1413        applyConfirmMembershipClickEvents()
1414        applyNextStepButtonClickEvents()
1415    },
1416    memberInfo: () =>
1417    {
1418        applyMemberInfoFormEvents()
1419        applyFormEvents()
1420        applyNextStepButtonClickEvents()
1421    },
1422    checkout: () =>
1423    {
1424        applyFormEvents()
1425        applyNextStepButtonClickEvents()
1426        applyPromoCodeEvents()
1427        applyGiftCertCodeEvents()
1428        applyDonationSelectEvents()
1429        applyAttendeeEvents()
1430        analytics.beginCheckout()
1431    },
1432    reviewCart: () =>
1433    {
1434        applyFeeSelectEvents()
1435        applyTicketSelectEvents()
1436        applyNextStepButtonClickEvents()
1437    },
1438}
1439
1440const STEP_ADVANCE = {
1441    selectDays: () =>
1442    {
1443        let days = collectSelectedDays()
1444        if(days && days["selectedDays"])
1445        {
1446            advanceStep("selectDays", days)
1447        }
1448        else
1449        {
1450            displayStepError("No days selected.")
1451        }
1452    },
1453    selectTickets: () =>
1454    {
1455        let tickets = collectSelectedTickets()
1456        if(tickets)
1457        {
1458            for (const [key, value] of Object.entries(tickets.selectedTickets))
1459            {
1460                analytics.addToCart([{
1461                    item_id: value.performanceId,
1462                    item_category: "Performance",
1463                    item_category2: "Admission",
1464                    quantity: value.qty
1465                }], value.price)
1466            }
1467            advanceStep("selectTickets", tickets)
1468        }
1469        else
1470        {
1471            displayStepError("No tickets selected.")
1472        }
1473    },
1474    bulkTicketsSelect: () =>
1475    {
1476        let tickets = collectSelectedTickets()
1477        if(tickets)
1478        {
1479            for (const [key, value] of Object.entries(tickets.selectedTickets))
1480            {
1481                analytics.addToCart([{
1482                    item_id: value.performanceId,
1483                    item_category: "Performance",
1484                    item_category2: "Admission - Bulk",
1485                    quantity: value.qty
1486                }], value.price)
1487            }
1488            advanceStep("bulkTicketsSelect", tickets)
1489        }
1490    },
1491    addExtras: () =>
1492    {
1493        let extras = collectSelectedExtras()
1494        if(extras)
1495        {
1496            for (const [key, value] of Object.entries(extras.selectedExtras))
1497            {
1498                analytics.addToCart([{
1499                    item_id: value.performanceId,
1500                    item_category: "Performance",
1501                    item_category2: "Event",
1502                    quantity: value.qty
1503                }], value.price)
1504            }
1505            for (const [key, value] of Object.entries(extras.selectedFees))
1506            {
1507                if(!value.preselected)
1508                {
1509                    analytics.addToCart([{
1510                        item_id: value.feeId,
1511                        item_category: "Fee",
1512                        item_name: value.description,
1513                        quantity: value.qty
1514                    }], value.price * value.qty)
1515                }
1516            }
1517            advanceStep("addExtras", extras)
1518        }
1519    },
1520    reviewAmount: () =>
1521    {
1522        let amount = false
1523        if($("#amount").val().length > 0 && !isNaN($("#amount").val()))
1524        {
1525            amount = $("#amount").val()
1526        }
1527        else
1528        {
1529            displayStepError("Amount is required.")
1530        }
1531        if(amount)
1532        {
1533            analytics.addToCart([{
1534                item_category: "Gift Certificate",
1535                item_name: "Gift Certificate"
1536            }], amount)
1537            advanceStep("reviewAmount", {amount: amount})
1538        }
1539    },
1540    confirmMembership: () =>
1541    {
1542        let membership = collectSelectedMembership()
1543        analytics.addToCart([{
1544            item_id: membership.membershipId,
1545            item_category: "Membership",
1546            item_name: membership.description
1547        }], membership.price)
1548        if(membership)
1549        {
1550            advanceStep("confirmMembership", membership)
1551        }
1552        else
1553        {
1554            displayStepError("Select a membership to continue.")
1555        }
1556    },
1557    memberInfo: () =>
1558    {
1559        let file = false
1560        let memberInfoForm = collectMemberInfoForm()
1561        if($(".preCheckoutFileUpload").length)
1562        {
1563            const files = $(".preCheckoutFileUpload")[0].files
1564            if (files.length > 0)
1565            {
1566                file = files[0]
1567            }
1568            else
1569            {
1570                displayStepError("File upload is required.")
1571                return
1572            }
1573        }
1574        if(memberInfoForm)
1575        {
1576            advanceStep("memberInfo", memberInfoForm, {
1577                file,
1578                onErrors(errors) {
1579                    processFormErrors(errors)
1580                    if (errors.document) displayStepError(errors.document)
1581                }
1582            })
1583        }
1584        else
1585        {
1586            displayStepError("Correct the errors above before continuing.")
1587        }
1588    },
1589    recipientInfo: () =>
1590    {
1591        let giftCertForm = collectGiftCertForm()
1592        if(giftCertForm)
1593        {
1594            advanceStep("recipientInfo", giftCertForm, {
1595                onErrors: processFormErrors
1596            })
1597        }
1598        else
1599        {
1600            displayStepError("Please correct the errors above.")
1601        }
1602    },
1603    checkout: async (nextButton) =>
1604    {
1605        let checkoutForm = collectCheckoutForm()
1606        if(checkoutForm)
1607        {
1608            const data = await sendStepData("checkout", checkoutForm)
1609            if (!data) return
1610            if(data.giftCertApplied)
1611            {
1612                $("#gift-cert-group").addClass("valid")
1613                $("#gift-cert-group input").prop("disabled", true)
1614                $("#gift-cert-group .form-input-helper-text").html("Gift certificate applied")
1615            }
1616            if(data.checkoutOk)
1617            {
1618                if(data.invoice &&
1619                (purchasePath == "bulkTickets" || purchasePath == "bulkHauntedHouse"))
1620                {
1621                    window.location = CONFIG.URLS.thankYou;
1622                }
1623                else
1624                {
1625                    window.location = CONFIG.URLS.payment;
1626                }
1627            }
1628            else if(data.errors)
1629            {
1630                processFormErrors(data.errors)
1631                if(data.errors.mustReload)
1632                {
1633                    nextButton.addClass("disabled")
1634                    nextButton.attr("disabled", true)
1635                }
1636                $(".next-step").removeClass("loading")
1637            }
1638        }
1639        else
1640        {
1641            $(".next-step").removeClass("loading")
1642        }
1643    },
1644}
1645STEP_ADVANCE.reviewCart = STEP_ADVANCE.addExtras
1646
1647// MARK: applyStep
1648
1649let applyStep = data =>
1650{
1651    $(`.step-container[data-step="${data.step}"]`).html(data.html)
1652    const setup = STEP_SETUP[purchasePathStep]
1653    if (setup) setup()
1654    if(CONFIG.PATHS_WITH_EXPIRATION.includes(purchasePath))
1655    {
1656        startCartTimer({
1657            container: "#purchase-path-container",
1658            onError: displayStepError,
1659            beforeExpire: () =>
1660            {
1661                if($("#cart-container").hasClass("open"))
1662                {
1663                    toggleMobileSummary()
1664                }
1665            },
1666        })
1667        applyDataToggle()
1668        applyDataCopy()
1669    }
1670    getCartSidebar()
1671}
1672
1673let smoothScrollToElement = element =>
1674{
1675    $("html, body").animate(
1676    {
1677        scrollTop: $(element).offset().top - 200
1678    }, 500)
1679}
1680
1681let updateStep = step =>
1682{
1683    purchasePathStep = step
1684    const activeStep = $(`.purchase-path-step[data-step="${step}"]`)
1685    $(`.purchase-path-step:not([data-step="${step}"])`).addClass("collapsed")
1686    activeStep.removeClass("collapsed")
1687    $(`.purchase-path-step`).removeClass("completed")
1688    $(`.purchase-path-step[data-step="${step}"]`).prevAll('.purchase-path-step').addClass('completed')
1689    $(`.purchase-path-step`).removeClass("locked").removeClass("show-message")
1690    $(`.purchase-path-step[data-step="${step}"]`).nextAll('.purchase-path-step').addClass('locked')
1691    $(`.purchase-path-step .purchase-path-step-header`).removeAttr("role tabindex")
1692    $(`.purchase-path-step.locked .purchase-path-step-header`).attr({"role": "button", "tabindex": 0})
1693    $(`.stepper-item`).removeClass("completed").removeClass("active").removeAttr("aria-current").removeAttr("role tabindex")
1694    $(`.stepper-item[data-step="${step}"]`).addClass("active").attr("aria-current", "step")
1695    $(`.stepper-item[data-step="${step}"]`).prevAll('.stepper-item').addClass('completed').attr({"role": "button", "tabindex": 0})
1696    smoothScrollToElement(activeStep)
1697    const stepTitle = activeStep.find(".purchase-path-step-header h2").text()
1698    if (stepTitle)
1699    {
1700        const status = $("#purchase-path-status")
1701        status.text("")
1702        setTimeout(() => status.text(`Now on step: ${stepTitle}`), 100)
1703    }
1704}
1705
1706let jumpToStepInFlight = false
1707
1708let jumpToStep = async (targetStep) =>
1709{
1710    if (!targetStep || targetStep === purchasePathStep || jumpToStepInFlight) return
1711    jumpToStepInFlight = true
1712    let formData = new FormData()
1713    formData.append("targetStep", targetStep)
1714    try
1715    {
1716        const data = await $.ajax({
1717            method: "POST",
1718            url: CONFIG.URLS.stepSave,
1719            data: formData,
1720            processData: false,
1721            contentType: false,
1722            dataType: "json",
1723        })
1724        if (data && data.nextStep)
1725        {
1726            updateStep(data.nextStep)
1727            const stepData = await fetchNextStep()
1728            applyStep(stepData)
1729            const $activeStep = $(`.purchase-path-step[data-step="${data.nextStep}"]`)
1730            const $heading = $activeStep.find(".purchase-path-step-header h2").first()
1731            if ($heading.length)
1732            {
1733                $heading.attr("tabindex", "-1").focus()
1734            }
1735        }
1736    }
1737    catch (err)
1738    {
1739        displayStepError("There was a problem processing your request. Please try again later.")
1740    }
1741    finally
1742    {
1743        jumpToStepInFlight = false
1744    }
1745}
1746
1747let calendarSwitchView = async () =>
1748{
1749    try
1750    {
1751        const data = await $.ajax({
1752            method: "GET",
1753            url: CONFIG.URLS.switchView,
1754            dataType: "json",
1755        })
1756        calendarView = data.view
1757        if($(".calendar-day-button.selected").length > 0)
1758        {
1759            let days = collectSelectedDays()
1760            if(days && days["selectedDays"])
1761            {
1762                await sendStepData("selectDays", days, false)
1763                const data = await fetchNextStep()
1764                applyStep(data)
1765            }
1766        }
1767        else
1768        {
1769            const data = await fetchNextStep()
1770            applyStep(data)
1771        }
1772    }
1773    catch (err)
1774    {
1775        displayStepError("There was a problem switching the calendar view. Please try again later.");
1776    }
1777}
1778
1779// MARK: collect funcs
1780
1781let collectSelectedDays = () =>
1782{
1783    let data = {}
1784    if($(".calendar-day-button.selected").length > 0)
1785    {
1786        data.selectedDays = {}
1787        $(`.calendar-day-button.selected`).each(function()
1788        {
1789            if(!(`${$(this).data("month")}-${$(this).data("day")}-${$(this).data("year")}` in data.selectedDays))
1790            {
1791                data.selectedDays[`${$(this).data("month")}-${$(this).data("day")}-${$(this).data("year")}`] = []
1792            }
1793            data.selectedDays[`${$(this).data("month")}-${$(this).data("day")}-${$(this).data("year")}`].push({
1794                day: $(this).data("day"),
1795                month: $(this).data("month"),
1796                year: $(this).data("year"),
1797                subType: $(this).data("subType") ? $(this).data("subType") : false,
1798                performanceId: $(this).data("performanceId"),
1799                freeDay: $(this).data("freeDay"),
1800                start: $(this).data("start"),
1801                end: $(this).data("end")
1802            })
1803        })
1804        return data
1805    }
1806    return false
1807}
1808
1809let collectSelectedTickets = () =>
1810{
1811    let data = {}
1812    data["selectedTickets"] = []
1813    let ticketQty = 0
1814    $(".ticket-container").each(function()
1815    {
1816        if($(this).data("qty") > 0 && $(this).is(":visible"))
1817        {
1818            data["selectedTickets"].push({
1819                priceTypeId: $(this).data("priceTypeId"),
1820                performanceId: $(this).data("performanceId"),
1821                qty: $(this).data("qty"),
1822                price: $(this).data("price") * $(this).data("qty")
1823            })
1824            ticketQty += parseInt($(this).data("qty"))
1825        }
1826    })
1827    if(ticketQty > CONFIG.BULK_MAX_TICKETS && (purchasePath == "bulkTickets" || purchasePath == "bulkHauntedHouse"))
1828    {
1829        displayStepError(`You can purchase a maximum of ${CONFIG.BULK_MAX_TICKETS} tickets.`);
1830    }
1831    else if(ticketQty < CONFIG.BULK_MIN_TICKETS && purchasePath == "bulkTickets")
1832    {
1833        displayStepError(`You must purchase at least ${CONFIG.BULK_MIN_TICKETS} tickets.`);
1834    }
1835    else if(ticketQty < CONFIG.BULK_HH_MIN_TICKETS && purchasePath == "bulkHauntedHouse")
1836    {
1837        displayStepError(`You must purchase at least ${CONFIG.BULK_HH_MIN_TICKETS} tickets.`);
1838    }
1839    else if(ticketQty > 0)
1840    {
1841        return data
1842    }
1843    return false
1844}
1845
1846let collectSelectedExtras = () =>
1847{
1848    let data = {}
1849    data["selectedExtras"] = []
1850    data["selectedFees"] = []
1851    let ticketQty = 0
1852    $(`.ticket-container`).each(function()
1853    {
1854        data["selectedExtras"].push({
1855            priceTypeId: $(this).data("priceTypeId"),
1856            performanceId: $(this).data("performanceId"),
1857            qty: $(this).data("qty"),
1858            price: $(this).data("price") * $(this).data("qty")
1859        })
1860        ticketQty++
1861    })
1862    $(`.extras-fee`).each(function()
1863    {
1864        data["selectedFees"].push({
1865            associatedPerf: $(this).data("associatedPerf"),
1866            feeId: $(this).data("feeId"),
1867            qty: $(this).data("qty"),
1868            date: $(this).data("date"),
1869            description: $(this).data("description"),
1870            price: $(this).data("price"),
1871            preselected: $(this).data("preselected") ? true : false
1872        })
1873    })
1874        return data
1875}
1876
1877let collectSelectedMembership = () =>
1878{
1879    let membership = {}
1880    $(".purchase-path-membership input[type='radio']").each(function()
1881    {
1882        if($(this).is(":checked") && Object.keys(membership).length == 0)
1883        {
1884            membership.fundId = $(this).data("fundId")
1885            membership.membershipId = $(this).data("membershipId")
1886            membership.description = $(this).data("membershipDescription")
1887            membership.price = $(this).data("price")
1888            if($(`input[type="number"][data-membership-id="${$(this).data("membershipId")}"]`).length)
1889            {
1890                membership.guildCustomPrice = parseInt($(`input[type="number"][data-membership-id="${$(this).data("membershipId")}"]`).val())
1891            }
1892            else if($(`input[type="number"][data-membership-id-2="${$(this).data("membershipId")}"]`).length)
1893            {
1894                membership.guildCustomPrice = parseInt($(`input[type="number"][data-membership-id-2="${$(this).data("membershipId")}"]`).val())
1895            }
1896        }
1897    })
1898    return membership
1899}
1900
1901let collectFormInputs = (containerSelector, inputsKey, options = {}) =>
1902{
1903    let inputs = {}
1904    let errors = 0
1905    inputs[inputsKey] = {}
1906    const exclusions = options.checkboxExclusions || []
1907
1908    $(`${containerSelector} input, ${containerSelector} select`).each(function()
1909    {
1910        const type = $(this).attr("type")
1911
1912        if(options.skipFileInputs && type == "file")
1913        {
1914            return
1915        }
1916
1917        if(type == "checkbox" && !exclusions.includes($(this).attr("id")))
1918        {
1919            if($(this).is(":checked"))
1920            {
1921                if(typeof inputs[inputsKey][$(this).attr("name")] == "undefined")
1922                {
1923                    inputs[inputsKey][$(this).attr("name")] = []
1924                }
1925                inputs[inputsKey][$(this).attr("name")].push($(this).val())
1926            }
1927        }
1928        else
1929        {
1930            if($(this).attr("required") && $(this).val().trim().length == 0 && $(this).is(":visible"))
1931            {
1932                $(this).parent().addClass("error")
1933                if(options.scrollToFirstError && errors == 0)
1934                {
1935                    smoothScrollToElement($(this).parent())
1936                }
1937                if(!$(this).parent().find(".form-input-helper-text").data("helperText"))
1938                {
1939                    $(this).parent().find(".form-input-helper-text").data("helperText", $(this).parent().find(".form-input-helper-text").html())
1940                }
1941                $(this).parent().find(".form-input-helper-text").html("This field cannot be blank")
1942                errors++
1943            }
1944            else if($(this).is(":visible") ||
1945                   (type == "hidden" && (options.strictVisibility ? $(this).parent().is(":visible") : true)) ||
1946                   (options.strictVisibility && $(this).data("inputForce")))
1947            {
1948                inputs[inputsKey][$(this).attr("name")] = $(this).val()
1949            }
1950            if($(this).parent().find(".form-input-helper-text").data("helperText") && $(this).val().trim().length > 0 && type != "hidden")
1951            {
1952                $(this).parent().removeClass("error")
1953                $(this).parent().find(".form-input-helper-text").html($(this).parent().find(".form-input-helper-text").data("helperText"))
1954            }
1955        }
1956    })
1957    if(errors == 0)
1958    {
1959        return inputs
1960    }
1961    return false
1962}
1963
1964let collectMemberInfoForm = () => collectFormInputs("#member-info", "member-info-inputs", {
1965    skipFileInputs: true,
1966    checkboxExclusions: ["member-email-opt-in"],
1967    scrollToFirstError: true,
1968    strictVisibility: true
1969})
1970
1971let collectGiftCertForm = () => collectFormInputs("#gift-cert", "gift-cert-inputs")
1972
1973let collectCheckoutForm = () =>
1974{
1975    let inputs = {}
1976    let errors = 0
1977    inputs["checkout-inputs"] = {}
1978
1979    $("#checkout input, #checkout select, #checkout textarea").each(function()
1980    {
1981        const element  = $(this)
1982        const name     = element.attr("name")
1983        if (!name) return
1984
1985        const type     = (element.attr("type") || "").toLowerCase()
1986        const visible  = element.is(":visible")
1987        const required = !!element.prop("required")
1988
1989        let raw = element.val()
1990        let value = Array.isArray(raw) ? raw : (raw == null ? "" : String(raw).trim())
1991        const isEmpty = Array.isArray(value) ? value.length === 0 : value === ""
1992
1993        if (required && isEmpty && visible)
1994        {
1995            const parent = element.parent()
1996            parent.addClass("error")
1997
1998            const helper = parent.find(".form-input-helper-text")
1999            if (!helper.data("helperText"))
2000            {
2001                helper.data("helperText", helper.html())
2002            }
2003            helper.html("This field cannot be blank")
2004            errors++
2005        }
2006        else if (visible || type === "hidden")
2007        {
2008            if (element.is(":checkbox") || element.is(":radio"))
2009            {
2010                if (this.checked)
2011                {
2012                    inputs["checkout-inputs"][name] = element.val()
2013                }
2014            }
2015            else
2016            {
2017                inputs["checkout-inputs"][name] = Array.isArray(raw) ? raw : element.val()
2018            }
2019        }
2020        if (element.parent().find(".form-input-helper-text").data("helperText") && !isEmpty && type !== "hidden")
2021        {
2022            const parent = element.parent()
2023            const helper = parent.find(".form-input-helper-text")
2024            parent.removeClass("error")
2025            helper.html(helper.data("helperText"))
2026        }
2027    })
2028    if (errors === 0)
2029    {
2030        return inputs
2031    }
2032
2033    displayStepError("Correct the errors above before continuing.")
2034    return false
2035}
2036
2037
2038let processFormErrors = errors =>
2039{
2040    let errorCount = 0
2041    for (const [key, value] of Object.entries(errors))
2042    {
2043        if(key == "step")
2044        {
2045            displayStepError(value)
2046            continue
2047        }
2048        let input = $(`input[name="${key}"]`)
2049        if(input.length == 0 && $(`select[name="${key}"]`).length > 0)
2050        {
2051            input = $(`select[name="${key}"]`)
2052        }
2053        if(input.length > 0)
2054        {
2055            input.parent().addClass("error")
2056            input.parent().find(".form-input-helper-text").html(value)
2057            if(errorCount == 0)
2058            {
2059                smoothScrollToElement(input.parent())
2060            }
2061            errorCount++
2062        }
2063    }
2064}
2065
2066let applySidebarClickEvents = () =>
2067{
2068    $(".item .handle").on("click keydown", function(event)
2069    {
2070        if(event.type === 'click' || (event.type === 'keydown' && event.keyCode === 13))
2071        {
2072            if($(this).parent().hasClass("collapsed"))
2073            {
2074                $(this).parent().removeClass("collapsed")
2075            }
2076            else
2077            {
2078                $(this).parent().addClass("collapsed")
2079            }
2080        }
2081    })
2082    $(".actions .remove, .purchase-path-review-cart-event .remove").on("click keydown", async function(event)
2083    {
2084        if(event.type === 'click' || (event.type === 'keydown' && event.keyCode === 13))
2085        {
2086            showSpinner($(".item-container"))
2087            let item = {}
2088            let formData = new FormData()
2089            for (const [key, value] of Object.entries($(this).data()))
2090            {
2091                formData.append(key, value)
2092                item[key] = value
2093            }
2094            try
2095            {
2096                const data = await $.ajax({
2097                    method: "POST",
2098                    url: CONFIG.URLS.removeItem,
2099                    data: formData,
2100                    processData: false,
2101                    contentType: false,
2102                    dataType: "json",
2103                })
2104                analytics.removeFromCart([{
2105                    item_id: item.itemId,
2106                    item_category: item.itemType,
2107                    quantity: item.qty
2108                }])
2109                if(data.nextStep)
2110                {
2111                    updateStep(data.nextStep)
2112                    const stepData = await fetchNextStep()
2113                    applyStep(stepData)
2114                }
2115            }
2116            catch (err)
2117            {
2118                displayStepError("There was a problem removing this item. Please try again later.");
2119            }
2120        }
2121    })
2122}
2123
2124let lastCartHtml = null
2125
2126let getCartSidebar = async () =>
2127{
2128    $(".cart-sticky").addClass("loading")
2129    try
2130    {
2131        const data = await $.ajax({
2132            method: "GET",
2133            url: CONFIG.URLS.cart,
2134            dataType: "json",
2135        })
2136        const cartChanged = lastCartHtml !== null && lastCartHtml !== data.html
2137        lastCartHtml = data.html
2138        $("#cart-container").html(data.html)
2139        if (cartChanged)
2140        {
2141            $("#cart-container .order-summary").addClass("cart-updated")
2142        }
2143        $("#cart-container").toggleClass("empty", !!data.isEmpty)
2144        $(".checkout-dino").toggleClass("small", !data.isEmpty)
2145        applySidebarClickEvents()
2146        clearStepError()
2147        resizeSidebar()
2148        updateOrderSummaryToggleState()
2149        $("#cart-container .order-summary, #cart-container .close").on("click keydown", function(e)
2150        {
2151            const isClick = e.type === "click"
2152            const isEnterOrSpace = e.type === "keydown" && (e.key === "Enter" || e.key === " ")
2153            if (!isClick && !isEnterOrSpace) return
2154            if ($(window).width() >= CONFIG.MOBILE_BREAKPOINT) return
2155            const isOpen = $("#cart-container").hasClass("open")
2156            if ($(this).hasClass("close"))
2157            {
2158                e.preventDefault()
2159                e.stopPropagation()
2160                if (isOpen) toggleMobileSummary()
2161            }
2162            else if (!isOpen)
2163            {
2164                e.preventDefault()
2165                toggleMobileSummary()
2166            }
2167        })
2168        return data
2169    }
2170    catch (err)
2171    {
2172        displayStepError("There was a problem loading your cart. Please try again later.");
2173    }
2174}
2175
2176let fetchNextStep = async () =>
2177{
2178    let stepData = {}
2179    if(purchasePathStep == "selectDays")
2180    {
2181        const urlParams = new URLSearchParams(window.location.search)
2182        const dateParam = urlParams.get('date')
2183
2184        if($("#purchase-path-calendar-month-select").length > 0 && calendarView != "rolling")
2185        {
2186            stepData.month = $("#purchase-path-calendar-month-select").val().split("-")[0]
2187            stepData.year = $("#purchase-path-calendar-month-select").val().split("-")[1]
2188        }
2189        else if(dateParam)
2190        {
2191            const parts = dateParam.split('-')
2192            if(parts.length === 3)
2193            {
2194                stepData.month = parts[0]
2195                stepData.year = parts[2]
2196            }
2197        }
2198        stepData.template = `calendar-${calendarView}`
2199    }
2200    showSpinner($(`.step-container[data-step="${purchasePathStep}"]`))
2201
2202    try
2203    {
2204        const data = await $.ajax({
2205            method: "GET",
2206            url: `${CONFIG.URLS.stepGet}?${getFormattedUrlParams()}`,
2207            data: stepData,
2208            dataType: "json",
2209        })
2210        if(data.redirect)
2211        {
2212            window.location = `${window.location.origin}/${data.redirect}`
2213        }
2214        else if(data.goBackStep)
2215        {
2216            updateStep(data.step)
2217            const nextData = await fetchNextStep()
2218            applyStep(nextData)
2219        }
2220        return data
2221    }
2222    catch (err)
2223    {
2224        displayStepError("There was a problem loading this step. Please try again later.");
2225    }
2226}
2227
2228let sendStepData = async (step, data, completeStep = true, file = false) =>
2229{
2230    $(".next-step").addClass("loading")
2231    let formData = new FormData()
2232    formData.append("step", step)
2233    if (completeStep)
2234    {
2235        formData.append("completeCurrentStep", completeStep);
2236    }
2237    for (const [key, value] of Object.entries(data))
2238    {
2239        formData.append(key, JSON.stringify(value))
2240    }
2241    if (file)
2242    {
2243        formData.append("document", file)
2244    }
2245    try
2246    {
2247        return await $.ajax({
2248            method: "POST",
2249            url: CONFIG.URLS.stepSave,
2250            data: formData,
2251            processData: false,
2252            contentType: false,
2253            dataType: "json",
2254        })
2255    }
2256    catch (err)
2257    {
2258        let parsed = err && err.responseJSON ? err.responseJSON : null
2259        if (!parsed && err && err.responseText)
2260        {
2261            try { parsed = JSON.parse(err.responseText) } catch (_) {}
2262        }
2263        if (parsed && parsed.errors)
2264        {
2265            return parsed
2266        }
2267        displayStepError("There was a problem saving your information. Please try again later.");
2268    }
2269}
2270
2271async function advanceStep(stepName, data, options = {})
2272{
2273    const parsed = await sendStepData(stepName, data, options.completeStep ?? true, options.file)
2274    $(".next-step").removeClass("loading")
2275    if (!parsed) return
2276    if (parsed.errors)
2277    {
2278        if (options.onErrors) options.onErrors(parsed.errors)
2279        else displayStepError(parsed.errors[0] || "An error occurred.")
2280        return parsed
2281    }
2282    if (parsed.nextStep)
2283    {
2284        updateStep(parsed.nextStep)
2285        const stepData = await fetchNextStep()
2286        applyStep(stepData)
2287
2288        // Move focus to the heading of the step
2289        const $activeStep = $(`.purchase-path-step[data-step="${parsed.nextStep}"]`)
2290        const $heading = $activeStep.find(".purchase-path-step-header h2").first()
2291        if ($heading.length)
2292        {
2293            $heading.attr("tabindex", "-1").focus()
2294        }
2295    }
2296    return parsed
2297}
2298
2299let showSpinner = target =>
2300{
2301    target.html(
2302    `
2303    <div class="loading-spinner">
2304        <div class="spinner"></div>
2305    </div>
2306    `)
2307}
2308
2309let applyNextStepButtonClickEvents = () =>
2310{
2311    $(".next-step").on("click", function(event)
2312    {
2313        event.preventDefault()
2314        const nextButton = $(this)
2315        if(nextButton.hasClass("loading"))
2316        {
2317            return
2318        }
2319        const step = nextButton.data("step")
2320        const handler = STEP_ADVANCE[step]
2321        if (handler) handler(nextButton)
2322    })
2323    $(".back-step-button").on("click", async function(event)
2324    {
2325        event.preventDefault()
2326        let formData = new FormData()
2327        formData.append("goBackStep", true)
2328        try
2329        {
2330            const data = await $.ajax({
2331                method: "POST",
2332                url: CONFIG.URLS.stepSave,
2333                data: formData,
2334                processData: false,
2335                contentType: false,
2336                dataType: "json",
2337            })
2338            updateStep(data.nextStep)
2339            const stepData = await fetchNextStep()
2340            applyStep(stepData)
2341
2342            // Move focus to the heading of step
2343            const $activeStep = $(`.purchase-path-step[data-step="${data.nextStep}"]`)
2344            const $heading = $activeStep.find(".purchase-path-step-header h2").first()
2345            if ($heading.length)
2346            {
2347                $heading.attr("tabindex", "-1").focus()
2348            }
2349        }
2350        catch (err)
2351        {
2352            displayStepError("There was a problem processing your request. Please try again later.");
2353        }
2354    })
2355}
2356
2357let getFormattedUrlParams = () =>
2358{
2359    const urlParams = new URLSearchParams(window.location.search)
2360    const formattedParams = []
2361    urlParams.forEach((value, key) =>
2362    {
2363        formattedParams.push(`${key}=${encodeURIComponent(value)}`)
2364    })
2365    return formattedParams.join('&')
2366}
2367
2368let resizeSidebar = () =>
2369{
2370    const cartEmpty = $("#cart-container").hasClass("empty")
2371    const vh = (window.visualViewport && window.visualViewport.height) || $(window).height()
2372    if ($(window).width() < CONFIG.MOBILE_BREAKPOINT)
2373    {
2374        $('.cart-content').css("max-height", "initial")
2375        $('.cart-content').css("height", "initial")
2376        $("#cart-container").css("height", "")
2377        if (cartEmpty)
2378        {
2379            $("#cart-container .item-container").css("height", "initial")
2380            return
2381        }
2382        const headerHeight = $(".order-summary").outerHeight(true)
2383        const totalDivHeight = $(".cart-total").outerHeight(true)
2384        $("#cart-container .item-container").css("height", vh - headerHeight - totalDivHeight)
2385        $("#cart-container .cart-content").css("height", vh - totalDivHeight)
2386        //$("#cart-container .item-container").css("margin-bottom", totalDivHeight)
2387    }
2388    else
2389    {
2390        if($("#cart-container").hasClass("open"))
2391        {
2392            toggleMobileSummary()
2393        }
2394        let height = 0
2395        $(".purchase-path-step").each(function()
2396        {
2397            height += $(this).outerHeight()
2398        })
2399        const sidebarOffset = 200
2400        if (!cartEmpty)
2401        {
2402            const totalHeight = $('.cart-total').outerHeight()
2403            const adjustedHeight = vh - totalHeight - sidebarOffset
2404            $('.cart-content').css('max-height', adjustedHeight + 'px')
2405            $('.cart-content').css("overflow-y", "scroll")
2406        }
2407        let containerHeight = height + $(".cart-sticky").height() + sidebarOffset
2408        const $footer = $("#footer")
2409        if ($footer.length)
2410        {
2411            $("#cart-container").css("height", "")
2412            const maxHeight = $footer.offset().top - CONFIG.FOOTER_STICKY_GAP - $("#cart-container").offset().top
2413            containerHeight = Math.min(containerHeight, maxHeight)
2414        }
2415        $("#cart-container").css("height", containerHeight)
2416    }
2417}
2418
2419let resize = () =>
2420{
2421    if ($(window).width() < CONFIG.MOBILE_BREAKPOINT)
2422    {
2423        $(".purchase-path-ticket-calendar .day .calendar-day-button").addClass("mobile")
2424    }
2425    else
2426    {
2427        $(".purchase-path-ticket-calendar .day .calendar-day-button").removeClass("mobile")
2428    }
2429    if($(".calendar-day-button.mobile").length > 0 && $(".calendar-day-button.selected").length == 0)
2430    {
2431        $(".calendar-day-button:not(.closed)").first().focus()
2432        $(".calendar-day-button:not(.closed)").first().trigger("click")
2433    }
2434    else if($(".calendar-day-button.mobile").length > 0 && $(".calendar-day-button.selected").length > 0)
2435    {
2436        $(".calendar-day-button:not(.closed).selected").first().focus()
2437        $(".calendar-day-button:not(.closed).selected").first().trigger("click")
2438    }
2439    resizeSidebar()
2440    updateOrderSummaryToggleState()
2441}
2442
2443let toggleMobileSummary = () =>
2444{
2445
2446    const $container = $("#cart-container")
2447    const isOpen = $container.hasClass("open")
2448    const $close = $container.find(".close")
2449
2450    if (!isOpen)
2451    {
2452        scrollPosition = window.scrollY || window.pageYOffset
2453        lockScroll()
2454        $container.addClass("open")
2455        $close.attr("aria-expanded", "true").removeAttr("hidden").trigger("focus")
2456        updateOrderSummaryToggleState()
2457        updateMobileCartHeights()
2458    }
2459    else
2460    {
2461        $container.removeClass("open")
2462        $close.attr("aria-expanded", "false").attr("hidden", true)
2463        updateOrderSummaryToggleState()
2464        updateMobileCartHeights()
2465        unlockScroll()
2466        $container.find(".order-summary").trigger("focus")
2467    }
2468}
2469
2470let updateOrderSummaryToggleState = () =>
2471{
2472    const $container = $("#cart-container")
2473    const $summary = $container.find(".order-summary")
2474    if ($(window).width() < CONFIG.MOBILE_BREAKPOINT && !$container.hasClass("open"))
2475    {
2476        $summary.attr({
2477            "role": "button",
2478            "tabindex": 0,
2479            "aria-label": "open order summary",
2480            "aria-controls": "cart-container",
2481            "aria-expanded": "false"
2482        })
2483    }
2484    else
2485    {
2486        $summary.removeAttr("role tabindex aria-label aria-controls aria-expanded")
2487    }
2488}
2489
2490let lockScroll = () =>
2491{
2492    $("body").addClass("no-scroll").css({
2493        position: "fixed",
2494        left: 0,
2495        right: 0,
2496        width: "100%"
2497    })
2498    document.body.style.setProperty("top", `-${scrollPosition}px`, "important")
2499    $("html").addClass("no-scroll")
2500}
2501
2502let unlockScroll = () =>
2503{
2504    $("body").removeClass("no-scroll").attr("style", "")
2505    $("html").removeClass("no-scroll")
2506    window.scrollTo(0, scrollPosition)
2507}
2508
2509
2510function updateMobileCartHeights()
2511{
2512    if ($(window).width() < CONFIG.MOBILE_BREAKPOINT)
2513    {
2514        const $cart = $("#cart-container")
2515        if (!$cart.hasClass("open"))
2516        {
2517            $cart.css("height", "")
2518            $('.cart-content').css("max-height", "initial")
2519            $('.cart-content').css("height", "initial")
2520            $("#cart-container .item-container").css("height", "initial")
2521            $("#cart-container .item-container").css("margin-bottom", "initial")
2522        }
2523        else
2524        {
2525            const vh = window.visualViewport.height
2526            const $nav = $(".navbar-mobile")
2527            const headerBottom = $nav.length ? Math.max(0, Math.round($nav[0].getBoundingClientRect().bottom)) : 0
2528            const drawerHeight = vh - headerBottom
2529            const headerHeight = $(".order-summary").outerHeight()
2530            const totalDivHeight = $(".cart-total").outerHeight()
2531            const usableHeight = Math.floor(drawerHeight - headerHeight - totalDivHeight)
2532
2533            $cart.css("height", drawerHeight)
2534            $(".cart-content").css("height", usableHeight)
2535            $(".item-container").css({
2536                height: usableHeight,
2537                "margin-bottom": totalDivHeight
2538            })
2539        }
2540    }
2541
2542}
2543
2544
2545$(document).ready(async function()
2546{
2547    resize()
2548    const data = await fetchNextStep()
2549    applyStep(data)
2550
2551    $(document).on("click", ".edit-selection-button", function(event)
2552    {
2553        event.preventDefault()
2554        const targetStep = $(this).data("step")
2555        jumpToStep(targetStep)
2556    })
2557
2558    $(document).on("click keydown", ".stepper-item.completed", function(event)
2559    {
2560        if(event.type === "click" || (event.type === "keydown" && (event.key === "Enter" || event.key === " ")))
2561        {
2562            event.preventDefault()
2563            const targetStep = $(this).data("step")
2564            jumpToStep(targetStep)
2565        }
2566    })
2567
2568    // future steps can't be skipped to. Clicking them briefly reveals
2569    // a "Please complete the previous section." message on the step
2570    $(document).on("click keydown", ".purchase-path-step.locked .purchase-path-step-header", function(event)
2571    {
2572        if(event.type === "click" || (event.type === "keydown" && (event.key === "Enter" || event.key === " ")))
2573        {
2574            event.preventDefault()
2575            const $step = $(this).closest(".purchase-path-step.locked")
2576            if (!$step.length) return
2577            if ($step.data("lockedMsgTimeout"))
2578            {
2579                clearTimeout($step.data("lockedMsgTimeout"))
2580            }
2581            $step.addClass("show-message")
2582            const alert = $("#purchase-path-alert")
2583            alert.text("")
2584            setTimeout(() => alert.text("Please complete the previous section."), 100)
2585            const timeout = setTimeout(() =>
2586            {
2587                $step.removeClass("show-message")
2588                $step.removeData("lockedMsgTimeout")
2589            }, 2000)
2590            $step.data("lockedMsgTimeout", timeout)
2591        }
2592    })
2593
2594    $(window).resize(function()
2595    {
2596        resize()
2597    })
2598    $(window).on("scroll resize", updateMobileCartHeights)
2599    if (window.visualViewport)
2600    {
2601        window.visualViewport.addEventListener("resize", function()
2602        {
2603            resize()
2604            updateMobileCartHeights()
2605        })
2606    }
2607})

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.