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.