1/** 2 * Public JavaScript for Availability Datepicker (Base) 3 * 4 * @package Availability_Datepicker 5 * @since 2.4.0.13 6 */ 7 8(function ($) { 9 'use strict'; 10 11 // ----- Passive diagnostics (stored in localStorage for Support page) ----- 12 13 var _avdpSession = { 14 url: window.location.href, 15 ts: Date.now(), 16 matched: {}, 17 errors: [] 18 }; 19 20 // Capture uncaught errors that originate from plugin scripts. 21 window.addEventListener('error', function (e) { 22 if ( e.filename && e.filename.indexOf('avdp') !== -1 ) { 23 _avdpSession.errors.push({ 24 msg: e.message, 25 file: e.filename.split('/').pop(), 26 line: e.lineno, 27 col: e.colno, 28 ts: Date.now() 29 }); 30 } 31 }); 32 33 /** 34 * Persist the current session diagnostics to localStorage (rolling, last 5). 35 */ 36 function _avdpSaveDiagnostics() { 37 try { 38 var stored = JSON.parse( localStorage.getItem('avdp_diagnostics') || '[]' ); 39 if ( !Array.isArray(stored) ) { stored = []; } 40 stored.unshift(_avdpSession); 41 localStorage.setItem( 'avdp_diagnostics', JSON.stringify( stored.slice(0, 5) ) ); 42 } catch (e) {} 43 } 44 45 // ------------------------------------------------------------------------- 46 47 $(document).ready(function () { 48 initDatepickers(); 49 }); 50 51 /** 52 * Initialize datepickers based on CSS selectors 53 */ 54 function initDatepickers() { 55 if (!avdpPublic || !avdpPublic.selectors || !avdpPublic.config) { 56 return; 57 } 58 59 var config = avdpPublic.config; 60 var selectors = avdpPublic.selectors; 61 62 var $fields = $(); 63 64 // Collect all fields matching any selector and record match counts. 65 $.each(selectors, function (key, sel) { 66 if (sel) { 67 var $matched = $(sel); 68 _avdpSession.matched[key] = $matched.length; 69 $fields = $fields.add($matched); 70 } 71 }); 72 73 _avdpSaveDiagnostics(); 74 75 if ($fields.length === 0) { 76 return; 77 } 78 79 $fields.each(function () { 80 var $field = $(this); 81 if ($field.hasClass('avdp-initialized')) { 82 return; 83 } 84 85 initLibrary($field, config); 86 87 $field.addClass('avdp-initialized'); 88 89 // Add inline mode class to parent for CSS styling 90 if (config.inline) { 91 $field.parent().addClass('avdp-inline-mode'); 92 } 93 }); 94 95 $(document).on('click', '[data-avdp-picker] .xdsoft_date:not(.xdsoft_disabled)', function() { 96 var $cell = $(this); 97 var $picker = $cell.closest('.xdsoft_datetimepicker'); 98 if ($picker.length) { 99 $picker.find('.xdsoft_date').removeClass('xdsoft_selected'); 100 $cell.addClass('xdsoft_selected'); 101 } 102 }); 103 } 104 105 var parseFlexibleDate = AVDPAvailabilityCore.parseFlexibleDate; 106 var getFirstAvailableDate = AVDPAvailabilityCore.getFirstAvailableDate; 107 var calculateSlots = AVDPAvailabilityCore.calculateSlots; 108 var calculateSlotsFromCustomRange = AVDPAvailabilityCore.calculateSlotsFromCustomRange; 109 var canStartSequentialBooking = AVDPAvailabilityCore.canStartSequentialBooking; 110 var validateSequence = AVDPAvailabilityCore.validateSequence; 111 var countAvailableDays = AVDPAvailabilityCore.countAvailableDays; 112 var filterTodaySlots = AVDPAvailabilityCore.filterTodaySlots; 113 114 function initLibrary($field, config) { 115 var library = (config && config.library) || 'xdsoft'; 116 117 if (library === 'xdsoft') { 118 initXdsoft($field, config); 119 } 120 } 121 122 /** 123 * Initialize XDSoft DateTimePicker on a field 124 */ 125 function initXdsoft($field, config) { 126 // Calculate options 127 var format = config.format || 'Y-m-d H:i'; 128 var timepicker = config.timepicker !== false; // Default true unless specified 129 var datepicker = true; 130 131 // Determine type based on selector match 132 if ($field.is(avdpPublic.selectors.start_date) || $field.is(avdpPublic.selectors.end_date)) { 133 // Date Only 134 format = config.formatDate || 'Y-m-d'; 135 timepicker = false;
136 } else if ($field.is(avdpPublic.selectors.start_time) || $field.is(avdpPublic.selectors.end_time)) { 137 // Time Only 138 format = config.formatTime || 'H:i'; 139 datepicker = false; 140 } 141 142 // Calculate the first available date (not disabled weekday, not blocked date) 143 var firstAvailableDate = getFirstAvailableDate(config); 144 145 var minDateObj = false; 146 var maxDateObj = false; 147 148 if (config.minDate) { 149 var minParts = config.minDate.split('-'); 150 if (minParts.length === 3) { 151 minDateObj = new Date(parseInt(minParts[0]), parseInt(minParts[1]) - 1, parseInt(minParts[2])); 152 } 153 } 154 155 if (config.maxDate) { 156 var maxParts = config.maxDate.split('-'); 157 if (maxParts.length === 3) { 158 maxDateObj = new Date(parseInt(maxParts[0]), parseInt(maxParts[1]) - 1, parseInt(maxParts[2])); 159 } 160 } 161 162 // Snapshot original bounds â beforeShowDay uses these to keep non-exempted gap dates disabled 163 var bookingWindowMin = minDateObj ? new Date(minDateObj.getTime()) : false; 164 var bookingWindowMax = maxDateObj ? new Date(maxDateObj.getTime()) : false; 165 166 // Extend minDate/maxDate to cover exempted dates so XDSoft doesn't block them independently 167 if (config.exemptedDates && config.exemptedDates.length > 0) { 168 config.exemptedDates.forEach(function (ds) { 169 var ep = ds.split('-'); 170 if (ep.length !== 3) { return; } 171 var ed = new Date(parseInt(ep[0]), parseInt(ep[1]) - 1, parseInt(ep[2])); 172 ed.setHours(0, 0, 0, 0); 173 if (maxDateObj && ed > maxDateObj) { maxDateObj = ed; } 174 if (minDateObj && ed < minDateObj) { minDateObj = ed; } 175 }); 176 } 177 178 // Check if there's a pending minDate from start date selection (for end date pickers) 179 var pendingMinDate = $field.data('avdp-pending-min-date'); 180 if (pendingMinDate && pendingMinDate instanceof Date && !isNaN(pendingMinDate.getTime())) { 181 // Use the pending minDate if it's later than the config minDate 182 if (!minDateObj || pendingMinDate > minDateObj) { 183 minDateObj = pendingMinDate; 184 } 185 // Clear the pending minDate 186 $field.removeData('avdp-pending-min-date'); 187 } 188 189 var xdsoftOptions = { 190 format: format, 191 formatTime: config.formatTime || 'H:i', 192 formatDate: config.formatDate || 'Y-m-d', 193 timepicker: timepicker, 194 datepicker: datepicker, 195 minDate: minDateObj, 196 maxDate: maxDateObj, 197 step: config.step || 30, 198 inline: config.inline || false, 199 dayOfWeekStart: 1, // Monday 200 lazyInit: true, 201 202 // Set default date to first available date (not today if today is disabled) 203 defaultDate: firstAvailableDate, 204 defaultSelect: false, // Don't auto-select, just show this date 205 206 validateOnBlur: false, 207 208 // Keep picker open until time is selected (for datetime mode) 209 closeOnDateSelect: timepicker ? false : true, 210 211 // Close on time selection in datetime mode 212 closeOnTimeSelect: timepicker && datepicker, 213 214 // Initial allowTimes from weekly_hours (onShow/onSelectDate update when date changes) 215 allowTimes: (config.weekly_hours && timepicker) ? calculateSlots(firstAvailableDate, config) : [], 216 217 // Dynamic Logic 218 beforeShowDay: function (date) { 219 var y = date.getFullYear(); 220 var m = String(date.getMonth() + 1).padStart(2, '0'); 221 var d = String(date.getDate()).padStart(2, '0'); 222 var dateStr = y + '-' + m + '-' + d; 223 224 var dayOfWeek = date.getDay(); 225 var checkDate = new Date(date); 226 checkDate.setHours(0, 0, 0, 0); 227 228 // Get start and end field references 229 var $startField = null; 230 var $endField = null; 231 var isEndPicker = $field.is(avdpPublic.selectors.end_date) || $field.is(avdpPublic.selectors.end_datetime); 232 var isStartPicker = $field.is(avdpPublic.selectors.start_date) || $field.is(avdpPublic.selectors.start_datetime); 233 234 // Scope to the same form to handle multiple forms on the page 235 var $form = $field.closest('form'); 236 237 if ($field.is(avdpPublic.selectors.end_date) || $field.is(avdpPublic.selectors.start_date)) { 238 $startField = $form.length ? $form.find(avdpPublic.selectors.start_date) : $(avdpPublic.selectors.start_date); 239 $endField = $form.length ? $form.find(avdpPublic.selectors.end_date) : $(avdpPublic.selectors.end_date); 240 } else if ($field.is(avdpPublic.selectors.end_datetime) || $field.is(avdpPublic.selectors.start_datetime)) { 241 $startField = $form.length ? $form.find(avdpPublic.selectors.start_datetime) : $(avdpPublic.selectors.start_datetime); 242 $endField = $form.length ? $form.find(avdpPublic.selectors.end_datetime) : $(avdpPublic.selectors.end_datetime); 243 } 244 245 // RANGE LOGIC: For end date pickers, disable dates before start date 246 if (isEndPicker && $startField && $startField.length && $startField.val()) { 247 var startDate = parseFlexibleDate($startField.val(), config.formatDate); 248 if (startDate && !isNaN(startDate.getTime())) { 249 startDate.setHours(0, 0, 0, 0); 250 if (checkDate < startDate) {
251 return [false, 'avdp-before-start']; 252 } 253 } 254 } 255 256 var isAvailable = true; 257 var cssClass = ''; 258 259 // Exempted dates override - always available regardless of booking window 260 if (config.exemptedDates && config.exemptedDates.includes(dateStr)) { 261 cssClass = 'avdp-exempted'; 262 } 263 else if (bookingWindowMax && checkDate > bookingWindowMax) { 264 isAvailable = false; 265 } 266 else if (bookingWindowMin && checkDate < bookingWindowMin) { 267 isAvailable = false; 268 } 269 else if (config.disabledDates && config.disabledDates.includes(dateStr)) { 270 isAvailable = false; 271 } 272 else if (config.disabledWeekDays && config.disabledWeekDays.length > 0) { 273 var disabledDays = config.disabledWeekDays.map(function (dd) { 274 return parseInt(dd, 10); 275 }); 276 if (disabledDays.indexOf(dayOfWeek) !== -1) { 277 isAvailable = false; 278 } 279 } 280 281 // For timepicker fields: disable today if all time slots have already passed 282 if (isAvailable && timepicker) { 283 var nowBSD = new Date(); 284 var isTodayBSD = (checkDate.getFullYear() === nowBSD.getFullYear() && 285 checkDate.getMonth() === nowBSD.getMonth() && 286 checkDate.getDate() === nowBSD.getDate()); 287 if (isTodayBSD) { 288 var todaySlots = calculateSlots(checkDate, config); 289 var noticeMinBSD = parseInt(config.minimum_notice) || 0; 290 var remainingBSD = filterTodaySlots(todaySlots, noticeMinBSD, nowBSD); 291 if (remainingBSD.length === 0) { 292 isAvailable = false; 293 cssClass = 'avdp-no-times-today'; 294 } 295 } 296 } 297 298 // For START pickers in Day Based mode: check if this date can lead to 299 // min_days consecutive available days. Flexible Range uses duration (hours) 300 // constraints instead â min_days is not applicable there. 301 if (isAvailable && isStartPicker && config.booking_type !== 'flexible') { 302 var minDays = parseInt(config.min_days) || 0; 303 if (minDays > 0 && !canStartSequentialBooking(checkDate, minDays, config)) { 304 isAvailable = false; 305 cssClass = 'avdp-insufficient-days'; 306 } 307 } 308 309 // Range highlighting: add xdsoft_current class to dates within selected range 310 if (isAvailable && $startField && $endField && $startField.length && $endField.length) { 311 var startVal = $startField.val(); 312 var endVal = $endField.val(); 313 314 if (startVal && endVal) { 315 var rangeStart = parseFlexibleDate(startVal, config.formatDate); 316 var rangeEnd = parseFlexibleDate(endVal, config.formatDate); 317 318 if (rangeStart && rangeEnd && !isNaN(rangeStart.getTime()) && !isNaN(rangeEnd.getTime())) { 319 rangeStart.setHours(0, 0, 0, 0); 320 rangeEnd.setHours(0, 0, 0, 0); 321 322 if (checkDate >= rangeStart && checkDate <= rangeEnd) { 323 cssClass = (cssClass ? cssClass + ' ' : '') + 'xdsoft_current'; 324 } 325 } 326 } 327 } 328 329 return [isAvailable, cssClass]; 330 }, 331 332 onShow: function (ct) { 333 $(this).attr('data-avdp-picker', '1'); 334 335 var pickerInstance = $field.data('xdsoft_datetimepicker'); 336 337 // For date pickers, ensure visual state matches field value when reopening 338 if (!timepicker && pickerInstance && $field.val()) { 339 var fieldValue = $field.val(); 340 var parsedDate = parseFlexibleDate(fieldValue, config.formatDate); 341 if (parsedDate && !isNaN(parsedDate.getTime())) { 342 setTimeout(function() { 343 if (pickerInstance) { 344 pickerInstance.setOptions({ value: fieldValue }); 345 var $picker = $('.xdsoft_datetimepicker:visible').last(); 346 if ($picker.length) { 347 var targetDay = parsedDate.getDate(); 348 $picker.find('.xdsoft_date').removeClass('xdsoft_selected'); 349 $picker.find('.xdsoft_date').each(function() { 350 var $cell = $(this); 351 if (!$cell.hasClass('xdsoft_disabled') && parseInt($cell.text().trim(), 10) === targetDay) { 352 $cell.addClass('xdsoft_selected'); 353 return false;
354 } 355 }); 356 } 357 } 358 }, 100); 359 } 360 } 361 362 if (timepicker) { 363 // For time-only fields (separate date/time), get the date from the corresponding date field 364 if ($field.is(avdpPublic.selectors.start_time) || $field.is(avdpPublic.selectors.end_time)) { 365 var $dateField = null; 366 if ($field.is(avdpPublic.selectors.start_time)) { 367 $dateField = $(avdpPublic.selectors.start_date); 368 } else if ($field.is(avdpPublic.selectors.end_time)) { 369 $dateField = $(avdpPublic.selectors.end_date); 370 } 371 372 if ($dateField && $dateField.length && $dateField.val()) { 373 var dateStr = $dateField.val(); 374 var selectedDate = parseFlexibleDate(dateStr, config.formatDate); 375 if (selectedDate && !isNaN(selectedDate.getTime())) { 376 var instance = $field.data('xdsoft_datetimepicker'); 377 updateAllowTimes(selectedDate, instance || this, config); 378 return; 379 } 380 } 381 // If no date selected yet, use today as fallback 382 var instance = $field.data('xdsoft_datetimepicker'); 383 updateAllowTimes(new Date(), instance || this, config); 384 } else { 385 // For datetime fields, ct represents what the user is viewing 386 var instance = $field.data('xdsoft_datetimepicker'); 387 updateAllowTimes(ct, instance || this, config); 388 } 389 } 390 391 // For end date pickers, navigate to the start date when opening 392 var isEndPicker = $field.is(avdpPublic.selectors.end_date) || $field.is(avdpPublic.selectors.end_datetime); 393 394 if (isEndPicker) { 395 var $startField = null; 396 var $form = $field.closest('form'); 397 398 if ($field.is(avdpPublic.selectors.end_date)) { 399 $startField = $form.length ? $form.find(avdpPublic.selectors.start_date) : $(avdpPublic.selectors.start_date); 400 } else if ($field.is(avdpPublic.selectors.end_datetime)) { 401 $startField = $form.length ? $form.find(avdpPublic.selectors.start_datetime) : $(avdpPublic.selectors.start_datetime); 402 } 403 404 var startDate = null; 405 406 // First, check if there's a pending minDate stored 407 var pendingMinDate = $field.data('avdp-pending-min-date'); 408 409 if (pendingMinDate && pendingMinDate instanceof Date && !isNaN(pendingMinDate.getTime())) { 410 startDate = new Date(pendingMinDate.getFullYear(), pendingMinDate.getMonth(), pendingMinDate.getDate()); 411 $field.removeData('avdp-pending-min-date'); 412 } else if ($startField && $startField.length && $startField.val()) { 413 var parsedStartDate = parseFlexibleDate($startField.val(), config.formatDate); 414 if (parsedStartDate && !isNaN(parsedStartDate.getTime())) { 415 startDate = new Date(parsedStartDate.getFullYear(), parsedStartDate.getMonth(), parsedStartDate.getDate()); 416 } 417 } 418 419 if (startDate && pickerInstance) { 420 try { 421 var currentMinDate = null; 422 try { 423 currentMinDate = pickerInstance.getOptions('minDate'); 424 } catch (e) {} 425 426 var finalMinDate = startDate; 427 if (currentMinDate && currentMinDate > startDate) { 428 finalMinDate = currentMinDate; 429 } 430 431 pickerInstance.setOptions({ 432 minDate: finalMinDate, 433 defaultDate: !$field.val() ? startDate : null 434 }); 435 436 // Force calendar refresh 437 setTimeout(function() { 438 pickerInstance.trigger('xchange.xdsoft'); 439 }, 50); 440 } catch (e) {} 441 } 442 } 443 }, 444 445 onSelectDate: function (ct, $input) { 446 var pickerInstance = $field.data('xdsoft_datetimepicker'); 447 448 // Validate BEFORE setting any date values. 449 // For end date pickers, prevent selection of dates before start date. 450 if (!timepicker && ct && ($field.is(avdpPublic.selectors.end_date) || $field.is(avdpPublic.selectors.end_datetime))) { 451 var $startField = null; 452 var $form = $field.closest('form'); 453 if ($field.is(avdpPublic.selectors.end_date)) { 454 $startField = $form.length ? $form.find(avdpPublic.selectors.start_date) : $(avdpPublic.selectors.start_date); 455 } else if ($field.is(avdpPublic.selectors.end_datetime)) { 456 $startField = $form.length ? $form.find(avdpPublic.selectors.start_datetime) : $(avdpPublic.selectors.start_datetime); 457 } 458 459 if ($startField && $startField.length && $startField.val()) { 460 var parsedStartDate = parseFlexibleDate($startField.val(), config.formatDate); 461 if (parsedStartDate && !isNaN(parsedStartDate.getTime())) { 462 var startDate = new Date(parsedStartDate.getFullYear(), parsedStartDate.getMonth(), parsedStartDate.getDate()); 463 var selectedDate = new Date(ct.getFullYear(), ct.getMonth(), ct.getDate()); 464 465 if (selectedDate < startDate) { 466 $field.val(''); 467 return false; // Prevent further processing 468 } 469 } 470 } 471 } 472 473 // When user selects a date in the calendar, update allowed times 474 if (timepicker) { 475 var instance = $field.data('xdsoft_datetimepicker'); 476 var pickerToUpdate = instance || this; 477 var ctSnapshot = ct ? new Date(ct.getTime()) : new Date(); 478 setTimeout(function () { 479 updateAllowTimes(ctSnapshot, pickerToUpdate, config); 480 }, 0); 481 } else if (!$field.is(avdpPublic.selectors.start_time) && !$field.is(avdpPublic.selectors.end_time)) { 482 // For date-only fields, update the corresponding time picker 483 updateTimePickerForDateField($field, ct, config); 484 } 485 486 // Update dependent pickers: if start date is selected, update end date minDate 487 if ($field.is(avdpPublic.selectors.start_date) || $field.is(avdpPublic.selectors.start_datetime)) { 488 updateDependentPickers($field, ct, config); 489 } 490 }, 491 492 onChangeDateTime: function (dp, $input) { 493 var pickerInstance = $field.data('xdsoft_datetimepicker'); 494 var fieldValue = $field.val(); 495 var pickerDate = pickerInstance ? pickerInstance.getValue() : null; 496 497 // For end date pickers, ensure selected date is not before start date 498 if (!timepicker && pickerDate && ($field.is(avdpPublic.selectors.end_date) || $field.is(avdpPublic.selectors.end_datetime))) { 499 var $startField = null; 500 var $form = $field.closest('form'); 501 if ($field.is(avdpPublic.selectors.end_date)) { 502 $startField = $form.length ? $form.find(avdpPublic.selectors.start_date) : $(avdpPublic.selectors.start_date); 503 } else if ($field.is(avdpPublic.selectors.end_datetime)) { 504 $startField = $form.length ? $form.find(avdpPublic.selectors.start_datetime) : $(avdpPublic.selectors.start_datetime); 505 } 506 507 if ($startField && $startField.length && $startField.val()) { 508 var parsedStartDate = parseFlexibleDate($startField.val(), config.formatDate); 509 if (parsedStartDate && !isNaN(parsedStartDate.getTime())) { 510 var startDate = new Date(parsedStartDate.getFullYear(), parsedStartDate.getMonth(), parsedStartDate.getDate()); 511 var selectedDate = new Date(pickerDate.getFullYear(), pickerDate.getMonth(), pickerDate.getDate()); 512 513 if (selectedDate < startDate) { 514 $field.val(''); 515 return; 516 } 517 } 518 } 519 } 520 521 // For date-only pickers, ensure visual selection state is updated 522 if (!timepicker && pickerInstance && fieldValue) { 523 var parsedDate = parseFlexibleDate(fieldValue, config.formatDate); 524 if (parsedDate && !isNaN(parsedDate.getTime())) { 525 pickerInstance.setOptions({ value: fieldValue }); 526 } 527 } 528 529 // If date changed, update allowed times 530 if (timepicker) { 531 var selectedDate = this.getValue(); 532 if (selectedDate) { 533 updateAllowTimes(selectedDate, this, config); 534 } 535 } else if (!$field.is(avdpPublic.selectors.start_time) && !$field.is(avdpPublic.selectors.end_time)) { 536 var selectedDate = this.getValue(); 537 if (selectedDate) { 538 updateTimePickerForDateField($field, selectedDate, config); 539 } 540 } 541 542 // Update dependent pickers: if start date is selected, update end date minDate 543 if ($field.is(avdpPublic.selectors.start_date) || $field.is(avdpPublic.selectors.start_datetime)) { 544 var selectedDate = this.getValue(); 545 if (selectedDate) { 546 updateDependentPickers($field, selectedDate, config); 547 } 548 } 549 550 validateSelection($input, config); 551 } 552 }; 553 554 if (avdpPublic.config.lang) { 555 $.datetimepicker.setLocale(avdpPublic.config.lang); 556 } 557 558 try { 559 $field.datetimepicker(xdsoftOptions); 560 } catch (e) { 561 _avdpSession.errors.push({ 562 msg: e.message || String(e), 563 file: 'avdp-public.js', 564 line: 0, 565 col: 0, 566 ts: Date.now() 567 }); 568 _avdpSaveDiagnostics(); 569 } 570 571 // Add direct change event listener for end date fields to validate against start date 572 if (!timepicker && ($field.is(avdpPublic.selectors.end_date) || $field.is(avdpPublic.selectors.end_datetime))) { 573 $field.on('change', function() { 574 var $endField = $(this); 575 var endValue = $endField.val(); 576 if (!endValue) { 577 return; 578 } 579 580 var pickerInstance = $endField.data('xdsoft_datetimepicker'); 581 var pickerDate = pickerInstance ? pickerInstance.getDate() : null; 582 if (!pickerDate) { 583 return; 584 } 585 586 var $form = $endField.closest('form'); 587 var $startField = null; 588 if ($endField.is(avdpPublic.selectors.end_date)) { 589 $startField = $form.length ? $form.find(avdpPublic.selectors.start_date) : $(avdpPublic.selectors.start_date); 590 } else if ($endField.is(avdpPublic.selectors.end_datetime)) { 591 $startField = $form.length ? $form.find(avdpPublic.selectors.start_datetime) : $(avdpPublic.selectors.start_datetime); 592 } 593 594 if ($startField && $startField.length && $startField.val()) { 595 var parsedStartDate = parseFlexibleDate($startField.val(), config.formatDate); 596 if (parsedStartDate && !isNaN(parsedStartDate.getTime())) { 597 var startDate = new Date(parsedStartDate.getFullYear(), parsedStartDate.getMonth(), parsedStartDate.getDate()); 598 var selectedDate = new Date(pickerDate.getFullYear(), pickerDate.getMonth(), pickerDate.getDate()); 599 600 if (selectedDate < startDate) { 601 $endField.val(''); 602 } 603 } 604 } 605 }); 606 } 607 } 608 609 /** 610 * Update dependent pickers (e.g. Start Date sets End Date minDate) 611 */ 612 function updateDependentPickers($startField, selectedDate, config) { 613 if (!selectedDate || !(selectedDate instanceof Date) || isNaN(selectedDate.getTime())) { 614 return; 615 } 616 617 var $form = $startField.closest('form'); 618 var $endField = null; 619 if ($startField.is(avdpPublic.selectors.start_date)) { 620 $endField = $form.length ? $form.find(avdpPublic.selectors.end_date) : $(avdpPublic.selectors.end_date); 621 } else if ($startField.is(avdpPublic.selectors.start_datetime)) { 622 $endField = $form.length ? $form.find(avdpPublic.selectors.end_datetime) : $(avdpPublic.selectors.end_datetime); 623 } 624 625 if (!$endField || !$endField.length) { 626 return; 627 } 628 629 var endPickerInstance = $endField.data('xdsoft_datetimepicker'); 630 631 var year = selectedDate.getFullYear();
632 var month = selectedDate.getMonth(); 633 var day = selectedDate.getDate(); 634 var minDate = new Date(year, month, day); 635 636 if (!endPickerInstance) { 637 $endField.data('avdp-pending-min-date', minDate); 638 return; 639 } 640 641 var opts = endPickerInstance.data('options') || {}; 642 var currentMinDate = opts.minDate || false; 643 644 var finalMinDate = minDate; 645 if (currentMinDate && currentMinDate > minDate) { 646 finalMinDate = currentMinDate; 647 } 648 649 endPickerInstance.setOptions({ 650 minDate: finalMinDate 651 }); 652 653 var endDate = endPickerInstance.getValue(); 654 if (endDate && endDate < finalMinDate) { 655 $endField.val(''); 656 } 657 } 658 659 /** 660 * Validate selection against Min/Max limits. 661 * Uses parseFlexibleDate so non-ISO date formats (e.g. d/m/Y) work correctly. 662 */ 663 function validateSelection($input, config) { 664 // Daily Validation (Days) â Count only AVAILABLE days 665 if ($input.is(avdpPublic.selectors.start_date) || $input.is(avdpPublic.selectors.end_date)) { 666 var $start = $(avdpPublic.selectors.start_date); 667 var $end = $(avdpPublic.selectors.end_date); 668 669 if ($start.length && $end.length && $start.val() && $end.val()) { 670 var d1 = parseFlexibleDate($start.val(), config.formatDate); 671 var d2 = parseFlexibleDate($end.val(), config.formatDate); 672 673 if (!d1 || !d2 || isNaN(d1.getTime()) || isNaN(d2.getTime())) { 674 return; 675 } 676 677 // Validate sequence â check for blocked dates that break the booking 678 var seqResult = validateSequence(d1, d2, config); 679 if (!seqResult.valid) { 680 alert('Your selected range contains an unavailable date (' + seqResult.blockedDate + ') that breaks the booking sequence. Please select a continuous range of available dates.'); 681 $input.val(''); 682 return; 683 } 684 685 // Flexible Range with date-only fields: enforce min/max duration in hours. 686 // Two midnight timestamps give exact 24h-per-night arithmetic. 687 if (config.booking_type === 'flexible') { 688 var diffHours = Math.abs(d2 - d1) / (1000 * 60 * 60); 689 var minDur = parseFloat(config.min_duration) || 0; 690 var maxDur = parseFloat(config.max_duration) || 0; 691 692 if (minDur > 0 && diffHours < minDur) { 693 alert('Minimum booking is ' + (minDur / 24) + ' night(s). Please select a longer stay.'); 694 $input.val(''); 695 return; 696 } else if (maxDur > 0 && diffHours > maxDur) { 697 alert('Maximum booking is ' + (maxDur / 24) + ' night(s). Please select a shorter stay.'); 698 $input.val(''); 699 return; 700 } 701 } else { 702 var availableDays = countAvailableDays(d1, d2, config); 703 704 var min = parseInt(config.min_days) || 0; 705 var max = parseInt(config.max_days) || 0; 706 707 if (min > 0 && availableDays < min) { 708 alert('Minimum booking is ' + min + ' available days. Your selection has only ' + availableDays + ' available days.'); 709 $input.val(''); 710 } else if (max > 0 && availableDays > max) { 711 alert('Maximum booking is ' + max + ' available days. Your selection has ' + availableDays + ' available days.'); 712 $input.val(''); 713 } 714 } 715 } 716 } 717 718 // Flexible Validation (Duration in Hours) 719 if ($input.is(avdpPublic.selectors.start_datetime) || $input.is(avdpPublic.selectors.end_datetime)) { 720 var $start = $(avdpPublic.selectors.start_datetime); 721 var $end = $(avdpPublic.selectors.end_datetime); 722 723 if ($start.length && $end.length && $start.val() && $end.val()) { 724 var d1 = parseFlexibleDate($start.val(), config.formatDate); 725 var d2 = parseFlexibleDate($end.val(), config.formatDate); 726 727 if (!d1 || !d2 || isNaN(d1.getTime()) || isNaN(d2.getTime())) { 728 return; 729 } 730 731 var seqResult = validateSequence(d1, d2, config); 732 if (!seqResult.valid) { 733 alert('Your selected range contains an unavailable date (' + seqResult.blockedDate + ') that breaks the booking sequence. Please select a continuous range of available dates.'); 734 $input.val(''); 735 return; 736 } 737 738 var diffTime = Math.abs(d2 - d1); 739 var diffHours = diffTime / (1000 * 60 * 60); 740 741 var min = parseFloat(config.min_duration) || 0; 742 var max = parseFloat(config.max_duration) || 0; 743 744 if (min > 0 && diffHours < min) { 745 alert('Minimum duration is ' + min + ' hours.'); 746 $input.val(''); 747 } else if (max > 0 && diffHours > max) { 748 alert('Maximum duration is ' + max + ' hours.'); 749 $input.val(''); 750 } 751 } 752 } 753 } 754 755 /** 756 * Update the time picker when a date-only field changes. 757 * Used for separate date/time field integrations. 758 */ 759 function updateTimePickerForDateField($dateField, selectedDate, config) { 760 if (!selectedDate || !(selectedDate instanceof Date) || isNaN(selectedDate.getTime())) { 761 return; 762 } 763 764 var $timeField = null; 765 if ($dateField.is(avdpPublic.selectors.start_date)) { 766 $timeField = $(avdpPublic.selectors.start_time); 767 } else if ($dateField.is(avdpPublic.selectors.end_date)) { 768 $timeField = $(avdpPublic.selectors.end_time); 769 } 770 771 if (!$timeField || !$timeField.length) { 772 return; 773 } 774 775 var timePickerInstance = $timeField.data('xdsoft_datetimepicker'); 776 if (!timePickerInstance) { 777 return; 778 } 779 780 updateAllowTimes(selectedDate, timePickerInstance, config); 781 } 782 783 /** 784 * Update allowed times based on Weekly Hours, Buffers, and Minimum Notice. 785 * 786 * Logic: 787 * - IF TODAY: filter out slots before max(now + notice, now) 788 * - IF NOT TODAY: show all available slots â notice doesn't apply 789 */ 790 function updateAllowTimes(date, pickerInstance, config) { 791 if (!config.weekly_hours) return; 792 793 if (!date || !(date instanceof Date) || isNaN(date.getTime())) { 794 date = new Date(); 795 } 796 797 var now = new Date(); 798 var isToday = (date.getFullYear() === now.getFullYear() && 799 date.getMonth() === now.getMonth() && 800 date.getDate() === now.getDate()); 801 802 var availableSlots = calculateSlots(date, config); 803 var noticeMinutes = parseInt(config.minimum_notice) || 0; 804 805 // When selecting TODAY, filter out past slots (and any within minimum_notice). 806 // filterTodaySlots is a pure function from avdp-availability-core.js. 807 if (isToday && availableSlots.length > 0) { 808 availableSlots = filterTodaySlots(availableSlots, noticeMinutes, now); 809 } 810 811 if (availableSlots.length === 0) { 812 return; 813 } 814 815 pickerInstance.setOptions({ 816 allowTimes: availableSlots 817 }); 818 819 if (pickerInstance && pickerInstance.trigger) { 820 var _capturedInstance = pickerInstance; 821 setTimeout(function () { 822 _capturedInstance.trigger('afterOpen.xdsoft'); 823 }, 25); 824 } 825 } 826 827})(jQuery);
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.