PageSourceSearch

https://www.bewhalewise.org/wp-content/plugins/date-time-picker-field/public/js/avdp-public.js?ver=3.0

js bewhalewise.org collected 2026-09-24 09:45:40 UTC 37,880 bytes, 827 lines download raw bytes

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.