PageSourceSearch

https://www.georgiaaquarium.org/wp-content/plugins/tessitura-gai/js/ticketwidget.js?ver=20261001185938

js georgiaaquarium.org collected 2026-10-02 09:51:45 UTC 39,579 bytes, 885 lines download raw bytes

1var gaiPYVHours = (function ($){
2    var t = {};
3
4    t.busy = false;
5    t.visitDate = convertDate(new Date());
6    t.guestCount = 0;
7    // Plan Your Visit Datepicker
8    t.ticketWidgetWrapper = $('.plan-visit-widget__content-ga-field-input-wrapper')
9    t.ticketWidgetDatepicker = $('#plan-visit-widget__datepicker');
10    t.ticketWidgetDatepickerContainer = $('.plan-visit-widget__datepicker');
11
12    // OpenHours Datepicker
13    t.openHoursWrapper = $('.plan-visit-widget .hours-schedule__datepicker-wrapper');
14    t.button = $('.js-hours-datepicker',t.openHoursWrapper);
15    t.reset_button = t.button.siblings('.js-reset-datepicker');
16    t.hsdpContainer = t.button.siblings('.hours-schedule__datepicker-container');
17    t.openHoursDatepicker = t.hsdpContainer.find('input');
18    debug_log("[PYV][OPEN HOURS] Initializing hours datepicker for PYV");
19
20    /**
21     * Set initial state of form fields
22     */
23    t.initFieldState = function() {
24        // Initially only date is enabled
25        const $timeInput = $('#plan-visit-widget__content-ga-field-time');
26        const $timeField = $('.plan-visit-widget__content-ga-field--time');
27        const $guestField = $('.plan-visit-widget__content-ga-field--guests');
28        const $decreaseBtn = $('.plan-visit-widget__content-ga-field-guest-counter .decrease');
29        const $increaseBtn = $('.plan-visit-widget__content-ga-field-guest-counter .increase');
30        const $buyBtn = $('#plan-visit-widget-buy-btn');
31
32        // debug_log($buyBtn);
33        
34        // Disable time field
35        $timeInput.prop('disabled', true);
36        $timeInput.val('');
37        $timeField.addClass('disabled');
38        
39        // Disable guest counter
40        $decreaseBtn.prop('disabled', true);
41        $increaseBtn.prop('disabled', true);
42        $guestField.addClass('disabled');
43
44        //Disable the buy button
45        $buyBtn.prop('disabled', true);
46
47        $('.plan-visit-widget__content-ga-field-guest-count').text('0');
48    }
49    
50    /**
51     * Reset time and guest fields when date changes
52     */
53    t.resetTimeAndGuestFields = function() {
54        // debug_log("[PYV]resetTimeAndGuestFields");
55        // Reset time field
56        const $timeInput = $('#plan-visit-widget__content-ga-field-time');
57        $timeInput.val('');
58        
59        // Reset guest count to 0
60        $('.plan-visit-widget__content-ga-field-guest-count').text('0');
61        
62        // Update the subtotal
63        if (typeof updateSubtotal === 'function') {
64            t.updateSubtotal();
65        }
66    }
67    
68    /**
69     * Reset guest field when time changes
70     */
71    t.resetGuestField = function() {
72
73        // Reset guest count to 0
74        $('.plan-visit-widget__content-ga-field-guest-count').text('0');
75
76        // Disable the decrease button
77        $('.plan-visit-widget__content-ga-field-guest-counter .decrease').prop('disabled',true);
78
79        // Disable the buy button
80        $('#plan-visit-widget-buy-btn').prop('disabled',true);
81
82        
83        // Set the max of the quantity picker to 14 or the seats remaining at the selected time, whichever is lower.
84        debug_log("[PYV]Setting the quantity picker max to: ",Math.min(14,parseInt($('.plan-visit-widget__time-option.active').data('seats'))));
85        $('.plan-visit-widget__content-ga-field-guest-count').data('max', Math.min(14,parseInt($('.plan-visit-widget__time-option.active').data('seats'))));
86
87
88        // Update the subtotal
89        if (typeof updateSubtotal === 'function') {
90            t.updateSubtotal();
91        }
92    }
93    
94    /**
95     * Initialize datepicker functionality
96     */
97    t.initDatepicker = function() {
98        // Store a reference to the PYV datepicker container for more specific targeting
99        const $pyvDatepickerInput = $('#plan-visit-widget__datepicker');
100        
101        // Initialize the datepicker on the input
102        const tpMinDate = tooLate() ? moment().add(1,'day').toDate() : new Date(); // Cannot select dates in the past
103        const tpMaxDate = moment().endOf('year').startOf('day').toDate(); // 6 MONTH THRESHOLD
104        t.tpDP = new AirDatepicker(t.ticketWidgetDatepicker[0],{
105            classes: 'plan-visit-widget__datepicker', // Add custom class for styling
106            language: 'en',
107            minDate: tpMinDate,
108            maxDate: tpMaxDate,
109            autoClose: false,
110            inline: true,
111            navTitles: {
112                days: 'MMMM yyyy'
113            },
114            prevHtml: '<svg width="6" height="11" viewBox="0 0 6 11" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.89691 5.5L0 1.5625L1.05155 0.5L6 5.5L1.05155 10.5L0 9.4375L3.89691 5.5Z" fill="black"/></svg>',
115            nextHtml: '<svg width="6" height="11" viewBox="0 0 6 11" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.89691 5.5L0 1.5625L1.05155 0.5L6 5.5L1.05155 10.5L0 9.4375L3.89691 5.5Z" fill="black"/></svg>',
116            minView: 'days',
117            view: 'days',
118            selectOtherMonths: false,
119            locale: altDatepickerLocale,
120            dateFormat: 'MMMM d, yyyy',
121            onSelect: function({date, formattedDate, inst}) {
122                // First reset and disable dependent fields
123                t.resetTimeAndGuestFields();
124                
125                // Fetch data for the selected date
126                t.fetchDateData(date);
127                
128                // Trigger change event to update any dependent fields
129                $pyvDatepickerInput.trigger('change');
130                
131                // Hide datepicker container after selection
132                $('.plan-visit-widget__datepicker').removeClass('is-visible');
133            },
134        });
135
136        // Overflow dates from adjacent months: pointer-events re-enabled via SCSS; AirDatepicker skips disabled clicks so only this handler fires.
137        // AirDatepicker stamps each cell with its real date via data-year/data-month/data-date, so read those
138        // directly (instead of guessing the month from grid position) and advance the view to that month so the
139        // "July 2026" -> "August 2026" header rotation happens even for the trailing overflow days.
140        // stopPropagation: the document-level "close on outside click" handler checks the OLD v2 cell class,
141        // so it treats this v3 cell click as an outside click and resets the view before the selection lands.
142        t.ticketWidgetWrapper.on('click', '.air-datepicker-cell.-other-month-', function(e) {
143            e.stopPropagation();
144            const $cell = $(this);
145            const targetDate = new Date(
146                parseInt($cell.attr('data-year'), 10),
147                parseInt($cell.attr('data-month'), 10),
148                parseInt($cell.attr('data-date'), 10)
149            );
150            if (targetDate < tpMinDate || targetDate > tpMaxDate) return;
151            t.tpDP.setViewDate(targetDate);
152            t.tpDP.selectDate(targetDate);
153        });
154
155        // Handle clicks on the datepicker input to show the datepicker
156        $pyvDatepickerInput.on('click', function(e) {
157            e.preventDefault();
158            e.stopPropagation();
159            
160            // Toggle is-visible class on the datepicker container
161            $('.plan-visit-widget__datepicker').toggleClass('is-visible');
162        });
163        
164        // Close datepicker when clicking outside (only for plan-visit-widget datepicker)
165        $(document).on('click', function(e) {
166            const $target = $(e.target);
167            const $datepickerContainer = $('.plan-visit-widget__datepicker');
168            
169            // Only handle the plan-visit-widget datepicker in this handler
170            if (!$pyvDatepickerInput.is(e.target) && 
171                $pyvDatepickerInput.has(e.target).length === 0 && 
172                !$datepickerContainer.is(e.target) && 
173                $datepickerContainer.has(e.target).length === 0 &&
174                !$target.closest('.datepicker--nav-action, .datepicker--cell').length) {
175                $datepickerContainer.removeClass('is-visible');
176            }
177        });
178    }
179    
180    /**
181     * Fetch admission data for the selected date
182     * @param {Date} date - Selected date object
183     */
184    t.fetchDateData = function(date) {
185        // Show loading indicator
186        $('.plan-visit-widget').addClass('loading');
187        
188        // Format date for API (YYYY-MM-DD)
189        const apiDateFormat = ('0' + (date.getMonth() + 1)).slice(-2) + '/' + ('0' + date.getDate()).slice(-2) + '/' + date.getFullYear();
190        
191        const uri = apiBaseURI + 'getTimes',
192            fields = { 
193                visitDate: apiDateFormat,
194                noGenAdOK: false
195            };
196
197        debug_log("[PYV][TICKET WIDGET] getTimes request",fields);
198        // Make AJAX call to our custom handler
199        $.ajax({
200            url: uri,
201            method: 'POST',
202            data: JSON.stringify(fields),
203            contentType: 'application/json',
204            timeout: 30000, // 30 seconds
205            dataType: 'json',
206            beforeSend: function(xhr) {
207                $('.plan-visit-widget .error-msg').text('').removeClass('active');
208                $('.plan-visit-widget__content-ga-field-input-wrapper').css({'border-color':'#000'});
209            },
210            success: function(response) {
211                debug_log("[PYV][TICKET WIDGET] getTimes response:",response);
212                if ((env.live || !confirm("Fake failure to get ticket data?")) && (response.status == 'success')) {
213                    // Update time options with data from response
214
215                    // Only one of these should resolve.
216                    let sectionNames = ['Daytime','Promotion'], hasTimes = false;
217
218                    $.each(sectionNames, function (i, v) {
219                        if (response.results[v] && (response.results[v] !== 'None found.')) {
220                            //debug_log("[PYV]Populating the time options of:",response.results[v])
221                            hasTimes = t.populateTimeOptions(response.results[v]);
222                            debug_log("[PYV]hasTimes:",hasTimes);
223                            return false; // Break out of the loop
224                        }
225                    });
226
227                    if (hasTimes == true && (env.live || !confirm("Fake no times remaining this day?"))) {
228                        // Enable the time field now that we have options
229                        t.enableTimeField();
230
231                        // If there's only one time slot, select it automatically.
232                        if ($('.plan-visit-widget__time-option').length == 1) {
233                            $('.plan-visit-widget__time-option').click();
234                            $('.plan-visit-widget__content-ga-field--time').hide();
235                            debug_log("[PYV] Only one time slot available, auto-selecting it.");
236                            //t.enableGuestCounter();
237                        }
238                        else {
239                            $('.plan-visit-widget__content-ga-field--time').show();
240                        }
241
242                    }
243                    else {
244                        // If no times available, show a message
245                        $('.plan-visit-widget__content-ga-field--time').show();
246                        $('.plan-visit-widget__content-ga-field--date .error-msg').text('This date is unavailable. Please select a new date.').addClass('active');
247                        $('.plan-visit-widget__content-ga-field-input-wrapper').css({'border-color':'#d32300'});
248                        $('.plan-visit-widget__time-dropdown').addClass('no-times');
249                    }
250                    
251                } else {
252                    console.error('Error fetching admission data:', response.message || 'Unknown error');
253
254                    $('.plan-visit-widget__content-ga-field--date .error-msg').html('Sorry, there was an error. Please try again later.').addClass('active');
255                    $('.plan-visit-widget__content-ga-field-input-wrapper').css({'border-color':'#d32300'});
256                    
257                }
258                
259                // Remove loading indicator
260                $('.plan-visit-widget').removeClass('loading');
261                t.currentDate = apiDateFormat;
262            },
263            error: function(xhr, status, error) {
264                console.error('AJAX error:', error);
265                
266                $('.plan-visit-widget__content-ga-field--date .error-msg').text('Sorry, there was an error. Please try again later.').addClass('active');
267
268                // Remove loading indicator
269                $('.plan-visit-widget').removeClass('loading');
270            }
271        });
272    }
273    
274    /**
275     * Populate time options dropdown with available times
276     * @param {Array} times - Array of available time slots
277     */
278    t.populateTimeOptions = function(ticketData) {
279        const $timeDropdown = $('.plan-visit-widget__time-dropdown');
280        debug_log("[PYV]ticketData: ",ticketData);
281        if (('Zones' in ticketData) && (ticketData.Zones.length > 0)) {
282            // Add each available time as an option
283            $timeDropdown.empty();
284            
285            ticketData.Zones.forEach(zone => {
286                let price = zone.Price || ticketData.Prices.Adult;
287                if (zone.Seats > 0) {
288                    $timeDropdown.append(`<div data-seats="${zone.Seats}" data-zid="${zone.Id}" data-price="${price}" class="plan-visit-widget__time-option">${zone.Description}</div>`);
289                    window.selectedDatePricing = price; // Will get overwritten by the selected time option
290                }
291            });
292
293            return true;
294        } else {
295            $('.plan-visit-widget__content-ga-field--time').show();
296            // If no times available, show a message
297            return false;
298        }
299
300    }
301    
302    /**
303     * Enable the time field
304     */
305    t.enableTimeField = function() {
306        const $timeInput = $('#plan-visit-widget__content-ga-field-time');
307        const $timeField = $('.plan-visit-widget__content-ga-field--time');
308        
309        $timeInput.prop('disabled', false);
310        $timeField.removeClass('disabled');
311        
312        // Make sure guest field is disabled
313        t.disableGuestCounter();
314    }
315    
316    /**
317     * Disable the guest counter
318     */
319    t.disableGuestCounter = function() {
320        const $guestField = $('.plan-visit-widget__content-ga-field--guests');
321        const $decreaseBtn = $('.plan-visit-widget__content-ga-field-guest-counter .decrease');
322        const $increaseBtn = $('.plan-visit-widget__content-ga-field-guest-counter .increase');
323        
324        $decreaseBtn.prop('disabled', true);
325        $increaseBtn.prop('disabled', true);
326        $guestField.addClass('disabled');
327
328        // Disable the buy button
329        $('#plan-visit-widget-buy-btn').prop('disabled',true);
330    }
331    
332    /**
333     * Enable the guest counter
334     */
335    t.enableGuestCounter = function() {
336        const $guestField = $('.plan-visit-widget__content-ga-field--guests');
337        const $decreaseBtn = $('.plan-visit-widget__content-ga-field-guest-counter .decrease');
338        const $increaseBtn = $('.plan-visit-widget__content-ga-field-guest-counter .increase');
339        const $counterContainer = $('.plan-visit-widget__content-ga-field-guest-counter');
340        const $count = $counterContainer.find('.plan-visit-widget__content-ga-field-guest-count');
341
342        $decreaseBtn.prop('disabled', true);
343        $increaseBtn.prop('disabled', false);
344        $guestField.removeClass('disabled');
345        
346        // Reset guest count to 1 when enabling
347        t.guestCount = 1;
348        t.updateCount($count);
349        
350        // Update subtotal with new guest count
351        if (typeof updateSubtotal === 'function') {
352            t.updateSubtotal();
353        }
354
355        // Enable the buy button
356        $('#plan-visit-widget-buy-btn').prop('disabled',false);
357    }
358    
359    /**
360     * Initialize time picker functionality
361     */
362    t.initTimepicker = function() {
363        const $timeInput = $('#plan-visit-widget__content-ga-field-time');
364        
365        // If the time input doesn't exist, exit early
366        if ($timeInput.length === 0) {
367            return;
368        }
369        
370        // Sample available times (this would typically come from an API)
371        const availableTimes = [
372            '9:00 AM', '10:00 AM', '11:00 AM', '12:00 PM', 
373            '1:00 PM', '2:00 PM', '3:00 PM', '4:00 PM','5:00 PM'
374        ];
375        
376        // Create time dropdown if it doesn't exist yet
377        if ($('.plan-visit-widget__time-dropdown').length === 0) {
378            let $timeDropdown = $('<div class="plan-visit-widget__time-dropdown"></div>');
379            
380            availableTimes.forEach(time => {
381                $timeDropdown.append(`<div class="plan-visit-widget__time-option">${time}</div>`);
382            });
383            
384            // Append dropdown to the time field container
385            $timeInput.closest('.plan-visit-widget__content-ga-field-input-wrapper').append($timeDropdown);
386        }
387        
388        // Show dropdown on input focus
389        $timeInput.on('focus', function() {
390            if (!$(this).prop('disabled')) {
391                $('.plan-visit-widget__time-dropdown').addClass('active');
392            }
393        });
394        
395        // Handle time selection
396        $('.plan-visit-widget__time-dropdown').on('click', '.plan-visit-widget__time-option', function() {
397            debug_log("[PYV]Time option clicked:", $(this).text());
398            const selectedTime = $(this).text();
399            $timeInput.val(selectedTime);
400            debug_log("[PYV]Selected time:", $(this).data());
401            window.selectedDatePricing = $(this).data('price') || window.selectedDatePricing; // Use the price from the selected time option
402            
403            $(this).siblings().removeClass('active');
404            $(this).addClass('active');
405            $('.plan-visit-widget__time-dropdown').removeClass('active');
406            $timeInput.trigger('change');
407            
408            // Enable guest counter when time is selected
409            t.enableGuestCounter();
410            
411            // In a real application, this would be where you'd fetch availability for this time slot
412            // Example: fetchAvailabilityForTimeSlot(selectedTime);
413        });
414        
415        // Hide dropdown when clicking outside
416        $(document).on('click', function(e) {
417            if (!$(e.target).closest('.plan-visit-widget__content-ga-field--time').length) {
418                $('.plan-visit-widget__time-dropdown').removeClass('active');
419            }
420        });
421    }
422    
423    // Update count display
424    t.updateCount = function($count) {
425
426        $count.text(t.guestCount);
427        
428        // If we need to update subtotal when guest count changes
429        if (typeof t.updateSubtotal === 'function') {
430            t.updateSubtotal();
431        }
432    }
433
434    /**
435     * Initialize guest counter
436     */
437    t.initGuestCounter = function() {
438        const $counterContainer = $('.plan-visit-widget__content-ga-field-guest-counter');
439        const $count = $counterContainer.find('.plan-visit-widget__content-ga-field-guest-count');
440        const $decreaseBtn = $counterContainer.find('.decrease');
441        const $increaseBtn = $counterContainer.find('.increase');
442        
443        // Set initial count to 0 (will be updated to 1 when enabled)
444        $count.text('0');
445        t.guestCount = 0;
446        // Start with the decrease button disabled
447        $decreaseBtn.prop('disabled', true);
448
449        // Decrease button
450        $decreaseBtn.on('click', function(e) {
451            e.preventDefault();
452            if (!$(this).prop('disabled') && t.guestCount > 1) {
453                debug_log("[PYV]Decrement");
454                t.guestCount--;
455                t.updateCount($count);
456                if (t.guestCount == 1) {
457                    $(this).prop('disabled', true);
458                }
459                $increaseBtn.prop('disabled', false);
460            }
461        });
462        
463        // Increase button
464        $increaseBtn.on('click', function(e) {
465            e.preventDefault();
466            if (!$(this).prop('disabled') && (t.guestCount < ($count.data('max') * 1))) { 
467                debug_log("[PYV]Increment");
468                t.guestCount++;
469                t.updateCount($count);
470                if (t.guestCount == ($count.data('max') * 1)) {
471                    $(this).prop('disabled', true);
472                }
473                $decreaseBtn.prop('disabled', false);
474            }
475        });
476    }
477    
478    /**
479     * Update the subtotal based on selected options
480     */
481    t.updateSubtotal = function() {
482        // Get the price from selected date or use default
483        const basePrice = (window.selectedDatePricing) || 44.99;
484        
485        const guestCount = parseInt($('.plan-visit-widget__content-ga-field-guest-count').text()) || 0;
486        const subtotal = (basePrice * guestCount).toFixed(2);
487        
488        // Update subtotal display
489        $('.plan-visit-widget__content-ga-subtotal-col-price').text('$' + subtotal);
490
491
492
493    }
494
495
496    // Event listeners for the Ticket Flow page.
497    t.ticketWidgetEvents = function() {
498
499        // Add change event listener for datepicker
500        $('#plan-visit-widget__datepicker').on('change', function() {
501            t.resetTimeAndGuestFields();
502        });
503        
504        // Add change event listener for time input
505        $('#plan-visit-widget__content-ga-field-time').on('change', function() {
506            t.resetGuestField();
507        });
508
509        $('.plan-visit-block__open-btn').on('click', function() {
510            debug_log('[PYV] clicked');
511            $('.plan-visit-widget').addClass('active');
512            $('body').addClass('overflow-hidden');
513        });
514
515        $('.plan-visit-widget__close-btn').on('click', function() {
516            $('.plan-visit-widget').removeClass('active');
517            if($(window).width() >= 1200) {
518                $('body').removeClass('overflow-hidden');
519            }
520        });
521
522        $('body').on('click',function(e){
523            if ($(e.target).closest('.datepicker--nav-action').length) {
524                //debug_log("[PYV]Prevent datepicker from activating disabled nav links?");
525                if($(e.target).closest('.datepicker--nav-action').hasClass('-disabled-')) {
526                    e.stopImmediatePropagation();
527                }
528            }
529        });
530
531        $('#plan-visit-widget-buy-btn').on('click', function(e) {
532            const guestCount = parseInt($('.plan-visit-widget__content-ga-field-guest-count').text()) || 0;
533            e.preventDefault();
534
535            // date, time and quantity must all be set for this to work.
536            if ((t.currentDate !== '') && ($('.plan-visit-widget__time-option.active').length > 0) || (guestCount > 0)) {
537                window.location.href = `/tickets?ticketType=single-day&VisitDate=${t.currentDate}&Timezone=${$('.plan-visit-widget__time-option.active').data('zid')}
537&QtyAdult=${guestCount}`;
538            }
539
540
541        });
542
543    };
544
545    t.initHoursDatepicker = function() {
546        debug_log("[PYV] Attaching hours datepicker on the input",t.openHoursDatepicker[0]);
547        // Initialize datepicker on the hidden input
548        const ohMinDate = tooLate() ? moment().add(1,'day').toDate() : new Date(); // Cannot select dates in the past
549        const ohMaxDate = moment().add(2,'months').endOf('month').startOf('day').toDate(); // 2 month THRESHOLD
550        t.ohDP = new AirDatepicker(t.openHoursDatepicker[0],{
551            classes: 'plan-visit-widget__datepicker plan-visit-widget__datepicker--hours', // Add custom class for styling
552            position: 'bottom left',
553            language: 'en',
554            minDate: ohMinDate,
555            maxDate: ohMaxDate,
556            autoClose: false, // We do this by hiding its container instead - inline datepicker hide is buggy.
557            inline: true,
558            visible: true,
559            navTitles: {
560                days: 'MMMM yyyy'
561            },
562            prevHtml: '<svg width="6" height="11" viewBox="0 0 6 11" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.89691 5.5L0 1.5625L1.05155 0.5L6 5.5L1.05155 10.5L0 9.4375L3.89691 5.5Z" fill="black"/></svg>',
563            nextHtml: '<svg width="6" height="11" viewBox="0 0 6 11" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.89691 5.5L0 1.5625L1.05155 0.5L6 5.5L1.05155 10.5L0 9.4375L3.89691 5.5Z" fill="black"/></svg>',
564            minView: 'days',
565            view: 'days',
566            buttons: [
567                {
568                    content: 'Today\'s Hours',
569                    className: 'today-button',
570                    onClick: function(dp, $el) {
571                        dp.selectDate(new Date());
572                        $reset_button.attr('aria-hidden', 'true');
573                    }
574                }
575            ],
576            selectOtherMonths: false,
577            locale: altDatepickerLocale,
578            dateFormat: 'yyyy-MM-d',
579            onShow: function(isCompleted) {
580                t.hsdpContainer.addClass('is-visible');
581                debug_log("[PYV] OH datepicker shown",t.openHoursDatepicker.val());
582                if (t.openHoursDatepicker.val() && moment(t.openHoursDatepicker.val()).isAfter(moment())) {
583                    $('.plan-visit-widget__sidebar-hours-schedule .air-datepicker--buttons').show();
584                }
585                else {
586                    $('.plan-visit-widget__sidebar-hours-schedule .air-datepicker--buttons').hide();
587                }
588                
589            },
590            onHide: function(isCompleted) {
591                t.hsdpContainer.removeClass('is-visible');
592                if (t.openHoursDatepicker.val()) {
593                    debug_log("[PYV]Setting view date to ",t.openHoursDatepicker.val());
594                    t.ohDP.setViewDate(t.openHoursDatepicker.val());    
595                }
596                else {
597                    debug_log("[PYV]Setting view date to today");
598                    t.ohDP.setViewDate(new Date());        
599                }
600            },
601            onSelect: function({date, formattedDate, inst}) {
602                debug_log("[PYV]OH datepicker selected",date,formattedDate,inst);
603                // Get today's date and reset time components for accurate comparison
604                const today = new Date();
605                today.setHours(0, 0, 0, 0);
606                
607                // Reset time components of selected date for accurate comparison
608                const selectedDate = new Date(date);
609                selectedDate.setHours(0, 0, 0, 0);
610                
611                // Check if selected date is different from today
612                if (selectedDate.getTime() !== today.getTime()) {
613                    t.reset_button.attr('aria-hidden', 'false');
614                } else {
615                    t.reset_button.attr('aria-hidden', 'true');
616                }
617                
618                // Close the datepicker only when a date is selected
619                //inst.hide();
620                t.hsdpContainer.removeClass('is-visible');
621
622                // Update the "Today's Hours" title to the selected date immediately, independent of the
623                // getOpenHours fetch (which only fills in the hours line). This matches the on-page Hours &
624                // Schedule block, whose title updates on select even when the hours request returns nothing.
625                let displayDate = moment(formattedDate);
626                if (displayDate.isAfter(moment())) {
627                    $('.plan-visit-widget .hours-schedule__schedule-title-text').text(displayDate.format('MMM Do, YYYY'));
628                }
629                else {
630                    $('.plan-visit-widget .hours-schedule__schedule-title-text').text('Today\'s Hours');
631                }
632
633                t.openHoursDatepicker.change();
634
635            },
636        });
637
638        // Overflow dates from adjacent months: pointer-events re-enabled via SCSS; AirDatepicker skips disabled clicks so only this handler fires.
639        // AirDatepicker stamps each cell with its real date via data-year/data-month/data-date, so read those
640        // directly (instead of guessing the month from grid position) and advance the view to that month so the
641        // "July 2026" -> "August 2026" header rotation happens even for the trailing overflow days.
642        // Stop propagation: the document-level "close on outside click" handler below checks the OLD v2 cell
643        // class (.datepicker--cell), so it treats this v3 (.air-datepicker-cell) click as an outside click,
644        // hides the calendar and resets the view to today before selectDate's async input-write lands — wiping
645        // the selection. Keeping the event from bubbling lets our setViewDate/selectDate run in isolation.
646        t.openHoursWrapper.on('click', '.air-datepicker-cell.-other
646-month-', function(e) {
647            e.stopPropagation();
648            const $cell = $(this);
649            const targetDate = new Date(
650                parseInt($cell.attr('data-year'), 10),
651                parseInt($cell.attr('data-month'), 10),
652                parseInt($cell.attr('data-date'), 10)
653            );
654            if (targetDate < ohMinDate || targetDate > ohMaxDate) return;
655            t.ohDP.setViewDate(targetDate);
656            t.ohDP.selectDate(targetDate);
657        });
658
659        // Collapse the initially-rendered (visible:true) calendar on load via the container
660        // class — NOT t.ohDP.hide(): this is an inline AirDatepicker (see opts above) whose
661        // hide() crashes in _finishHide ("this.$container.removeChild is not a function").
662        // Matches the onSelect close path (line ~620) and the line 556 comment.
663        t.hsdpContainer.removeClass('is-visible');
664
665    }
666
667    t.fetchHoursDatepickerData = function(date) {
668        return new Promise((resolve,reject) => {
669            debug_log(`[PYV OPEN HOURS] getOpenHours called on ${date}`);
670            let uri = apiBaseURIv2 + 'getOpenHours',
671                fields = { 
672                    date: date,
673                };
674        
675            debug_log("[PYV OPEN HOURS] ",uri," the following: ",JSON.stringify(fields));
676    
677            $.ajax({
678                type: "POST",
679                url: uri,
680                data: JSON.stringify(fields),
681                //async: false, // Just to keep things orderly for now.
682                contentType: 'application/json',
683                timeout: 30000, // 30 seconds
684                dataType: 'json',
685                beforeSend: function() {
686                    // Clear my section's error and show its loader.
687                    clearError($('.hours-schedule__datepicker-wrapper .error-msg'));
688                    $('.hours-schedule__datepicker-wrapper .error-msg').removeClass('active');
689                    $('.plan-visit-widget__content-ga-field-input-wrapper').css('border-color','black');
690                },
691                success: function (data) {
692                    let fakeError = !env.live && !suppressDebug && confirm('Pretend OpenHours fetch returned error?'); 	
693                    let resp = safeJSONParse(data);
694                    debug_log("[PYV][OpenHours] Grabbed data: ",resp);
695                    if ('0' in resp && !fakeError) {
696                        resolve(resp[0]);
697                    }
698                    else {
699                        pushException('getOpenHours-errorResponse');
700                        showError($('.hours-schedule__datepicker-wrapper .error-msg'),'custom',resp.message);
701                        $('.hours-schedule__datepicker-wrapper .error-msg').addClass('active');
702                        reject(resp);
703                    }
704                },
705                error: function (jqXHR, status, errorThrown) {
706                    pushException('getOpenHours-badResponse');
707                    showError($('.hours-schedule__datepicker-wrapper .error-msg'),status,errorThrown);
708                    $('.hours-schedule__datepicker-wrapper .error-msg').addClass('active');
709                    reject(errorThrown);
710                },
711                complete: function (jqXHR, status) {
712                }
713            });
714    
715        });
716    }
717
718    t.initHoursEvents = function() {
719        t.openHoursDatepicker.on('change',function() {
720            const selectedDate = $(this).val();
721            debug_log("[PYV]Hours datepicker changed: " + selectedDate);
722            t.fetchHoursDatepickerData(selectedDate)
723                .then((data) => {
724
725                    let displayDate = moment(selectedDate);
726                    if (displayDate.isAfter(moment())) {
727                        $('.plan-visit-widget .hours-schedule__schedule-title-text').text(displayDate.format('MMM Do, YYYY'));
728                    }
729                    else {
730                        $('.plan-visit-widget .hours-schedule__schedule-title-text').text('Today\'s Hours');
731                    }
732                    
733                    // Handle success
734                    debug_log("[PYV][OpenHours] Successfully fetched data: ", data);
735                    // Update the UI with the fetched data
736                    $('.hours-schedule__hours').html(`${data.openHours}-${data.closeHours}`);
737                })
738                .catch((error) => {
739                    // Handle error
740                    debug_log("[PYV][OpenHours] Error fetching data: ", error);
741                });
742    
743        });
744        
745        // Handle button click to show datepicker
746        t.button.on('click', function(e) {
747            e.preventDefault();
748            if (t.hsdpContainer.hasClass('is-visible')) {
749                // Close via the container class only — t.ohDP.hide() crashes on this inline
750                // datepicker (_finishHide -> this.$container.removeChild). Same pattern as onSelect.
751                t.hsdpContainer.removeClass('is-visible');
752            }
753            else {
754                t.ohDP.show();
755                t.hsdpContainer.addClass('is-visible');
756            }
757            debug_log("[PYV][dp] Showing the datepicker",t.ohDP);
758        });
759
760        // Handle reset button click
761        t.reset_button.on('click', function(e) {
762            e.preventDefault();
763            //const datepickerInstance = t.openHoursDatepicker.datepicker().data('datepicker');
764            t.ohDP.selectDate(new Date());
765            t.reset_button.attr('aria-hidden', 'true');
766        });
767
768        // Handle keyboard interaction
769        t.button.on('keydown', function(e) {
770            if (e.key === 'Enter' || e.key === ' ') {
771                e.preventDefault();
772
773                debug_log("[PYV][dp] Hiding the datepicker",t.ohDP);
774                t.hsdpContainer.addClass('is-visible');
775                t.ohDP.show();
776                //t.openHoursDatepicker.datepicker().data('datepicker').show();
777            }
778        });
779
780        // Close hours datepicker when clicking outside (specific handler for hours datepicker)
781        $(document).on('click', function(e) {
782            const $target = $(e.target);
783            const isDatepickerControl = $target.closest('.datepicker--nav-action, .datepicker--nav-control
783s').length > 0;
784            const isDatepickerCell = $target.closest('.datepicker--cell').length > 0;
785            
786            // Only handle if we're interacting with the hours datepicker elements
787            // Check specifically if we're dealing with the hours datepicker container
788            const isHoursDatepicker = $target.closest('.hours-schedule__datepicker-container').length > 0 ||
789                                     $target.closest('.hours-schedule__datepicker-wrapper').length > 0;
790            
791            // Only close the hours datepicker in this handler, not the plan-visit-widget datepicker
792            if (!t.openHoursWrapper.is(e.target) && 
793                t.openHoursWrapper.has(e.target).length === 0 && 
794                !isDatepickerControl && 
795                !isDatepickerCell &&
796                // Only handle if we're interacting with the hours datepicker or clicking outside any datepicker
797                (isHoursDatepicker || (!$target.closest('.plan-visit-widget__datepicker').length && 
798                                       !$target.closest('#plan-visit-widget__datepicker').length))) {
799                
800                debug_log("[PYV][dp] Hiding the hours datepicker", t.ohDP);
801                t.hsdpContainer.removeClass('is-visible');
802                
803                if (t.openHoursDatepicker.val()) {
804                    debug_log("[PYV]Setting view date to ", t.openHoursDatepicker.val());
805                    t.ohDP.setViewDate(t.openHoursDatepicker.val());    
806                } else {
807                    debug_log("[PYV]Setting view date to today");
808                    t.ohDP.setViewDate(new Date());        
809                }
810            }
811        });
812    }
813
814    // Main initializer and detection of page context.
815    t.init = function() {
816        
817        $(document).ready(function(){            
818            if (t.ticketWidgetWrapper.length > 0) {
819                debug_log("[PYV] Ticket Widget");
820                t.initFieldState();
821                t.initDatepicker();
822                t.initTimepicker();
823                t.initGuestCounter();
824    
825                t.ticketWidgetEvents();
826            }
827
828            if (t.openHoursWrapper.length > 0) {
829                debug_log("[PYV] Hours");
830                t.initHoursDatepicker();
831                t.initHoursEvents();
832            }
833    
834        });
835
836    };
837/*
838    function arrowNavigation() {
839        // Use setTimeout to ensure the datepicker is fully rendered
840        // Get navigation elements
841        const $nav = $('.datepicker--nav');
842        const $title = $nav.find('.datepicker--nav-title');
843        const $actions = $nav.find('.datepicker--nav-action');
844        
845        // Customize the arrow HTML for each action
846        $actions.each(function() {
847            const $action = $(this);
848            // Clear existing content
849            $action.empty();
850            
851            if ($action.hasClass('datepicker--nav-action[data-action="prev"]')) {
852                // Add custom previous arrow
853                $action.html('<svg width="6" height="11" viewBox="0 0 6 11" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2.10309 5.5L6 1.5625L4.94845 0.5L0 5.5L4.94845 10.5L6 9.4375L2.10309 5.5Z" fill="black"/></svg>');
854            } else {
855                // Add custom next arrow
856                $action.html('<svg width="6" height="11" viewBox="0 0 6 11" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.89691 5.5L0 1.5625L1.05155 0.5L6 5.5L1.05155 10.5L0 9.4375L3.89691 5.5Z" fill="black"/></svg>');
857            }
858        });
859        
860        // Reset any previous modifications
861        $nav.find('.datepicker--nav-controls').remove();
862        
863        // Ensure proper order: prev button, title, next button
864        const $prevButton = $nav.find('.datepicker--nav-action[data-action="prev"]');
865        const $nextButton = $nav.find('.datepicker--nav-action[data-action="next"]');
866        
867        // Reorder elements
868        $nav.prepend($prevButton);
869        $nav.append($nextButton);
870        
871        // Center the title
872        $title.css({
873            'position': 'absolute',
874            'left': '50%',
875            'transform': 'translateX(-50%)'
876        });
877    }
878*/
879    
880
881
882    return t;
883})(jQuery);
884
885jQuery(function() { gaiPYVHours.init(); });

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.