PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.14
VikBooking Hotel Booking Engine & PMS v1.8.14
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
vikbooking / site / resources / datesrangepicker.js

datesrangepicker.js in VikBooking Hotel Booking Engine & PMS 1.8.14, at site/resources/datesrangepicker.js

1,613 lines 48.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * VikBooking - DatesRangePicker v1.2.3.
3 * Copyright (C) 2025 E4J s.r.l. All Rights Reserved.
4 * http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
5 * https://vikwp.com | https://e4j.com | https://e4jconnect.com
6 *
7 * Forked from MultiDatesPicker v1.6.6
8 * https://dubrox.github.io/Multiple-Dates-Picker-for-jQuery-UI
9 */
10 (function(factory) {
11 if (typeof define === 'function' && define.amd) {
12 define(['jquery', 'jquery-ui-dist'], factory);
13 } else {
14 factory(jQuery);
15 }
16 }(function($) {
17 $.extend($.ui, { vboMultiDatesPicker: { version: '1.6.6' } });
18
19 $.fn.vboMultiDatesPicker = function(method) {
20 var mdp_arguments = arguments;
21 var ret = this;
22 var today_date = new Date;
23 var day_zero = new Date(0);
24 var mdp_events = {};
25
26 function removeDate(date, type) {
27 if (!type) {
28 type = 'picked';
29 }
30 date = dateConvert.call(this, date);
31 for (var i = 0; i < this.vboMultiDatesPicker.dates[type].length; i++)
32 if (!methods.compareDates(this.vboMultiDatesPicker.dates[type][i], date)) {
33 return this.vboMultiDatesPicker.dates[type].splice(i, 1).pop();
34 }
35 }
36 function removeIndex(index, type) {
37 if (!type) {
38 type = 'picked';
39 }
40 return this.vboMultiDatesPicker.dates[type].splice(index, 1).pop();
41 }
42 function addDate(date, type, no_sort) {
43 if (!type) {
44 type = 'picked';
45 }
46 date = dateConvert.call(this, date);
47
48 date.setHours(0);
49 date.setMinutes(0);
50 date.setSeconds(0);
51 date.setMilliseconds(0);
52
53 if (methods.gotDate.call(this, date, type) === false) {
54 this.vboMultiDatesPicker.dates[type].push(date);
55 if (!no_sort) {
56 this.vboMultiDatesPicker.dates[type].sort(methods.compareDates);
57 }
58 }
59 }
60 function sortDates(type) {
61 if (!type) {
62 type = 'picked';
63 }
64 this.vboMultiDatesPicker.dates[type].sort(methods.compareDates);
65 }
66 function dateConvert(date, desired_type, date_format) {
67 if (!desired_type) {
68 desired_type = 'object';
69 }
70 return methods.dateConvert.call(this, date, desired_type, date_format);
71 }
72
73 var methods = {
74 init: function(options) {
75 var $this = $(this);
76 this.vboMultiDatesPicker.changed = false;
77
78 var mdp_events = {
79 beforeShow: function(input, inst) {
80 this.vboMultiDatesPicker.changed = false;
81 if (this.vboMultiDatesPicker.originalBeforeShow) {
82 this.vboMultiDatesPicker.originalBeforeShow.call(this, input, inst);
83 }
84 },
85 onSelect: function(dateText, inst) {
86 var $this = $(this);
87 this.vboMultiDatesPicker.changed = true;
88
89 if (dateText) {
90 $this.vboMultiDatesPicker('toggleDate', dateText);
91 this.vboMultiDatesPicker.changed = true;
92 }
93
94 if (this.vboMultiDatesPicker.mode == 'normal' && this.vboMultiDatesPicker.pickableRange) {
95 if (this.vboMultiDatesPicker.dates.picked.length > 0) {
96 var min_date = this.vboMultiDatesPicker.dates.picked[0],
97 max_date = new Date(min_date.getTime());
98
99 methods.sumDays(max_date, this.vboMultiDatesPicker.pickableRange-1);
100
101 // counts the number of disabled dates in the range
102 if (this.vboMultiDatesPicker.adjustRangeToDisabled) {
103 var c_disabled,
104 disabled = this.vboMultiDatesPicker.dates.disabled.slice(0);
105 do {
106 c_disabled = 0;
107 for (var i = 0; i < disabled.length; i++) {
108 if (disabled[i].getTime() <= max_date.getTime()) {
109 if ((min_date.getTime() <= disabled[i].getTime()) && (disabled[i].getTime() <= max_date.getTime()) ) {
110 c_disabled++;
111 }
112 disabled.splice(i, 1);
113 i--;
114 }
115 }
116 max_date.setDate(max_date.getDate() + c_disabled);
117 } while(c_disabled != 0);
118 }
119
120 if (this.vboMultiDatesPicker.maxDate && (max_date > this.vboMultiDatesPicker.maxDate)) {
121 max_date = this.vboMultiDatesPicker.maxDate;
122 }
123
124 $this.datepicker('option', 'minDate', min_date).datepicker('option', 'maxDate', max_date);
125 } else {
126 $this.datepicker('option', 'minDate', this.vboMultiDatesPicker.minDate).datepicker('option', 'maxDate', this.vboMultiDatesPicker.maxDate);
127 }
128 }
129
130 if (this.vboMultiDatesPicker.originalOnSelect && dateText) {
131 this.vboMultiDatesPicker.originalOnSelect.call(this, dateText, inst);
132 }
133 },
134 beforeShowDay: function(date) {
135 var $this = $(this),
136 gotThisDate = $this.vboMultiDatesPicker('gotDate', date) !== false,
137 isDisabledCalendar = $this.datepicker('option', 'disabled'),
138 isDisabledDate = $this.vboMultiDatesPicker('gotDate', date, 'disabled') !== false,
139 areAllSelected = this.vboMultiDatesPicker.maxPicks <= this.vboMultiDatesPicker.dates.picked.length;
140
141 var bsdReturn = [true, '', null];
142 if (this.vboMultiDatesPicker.originalBeforeShowDay) {
143 bsdReturn = this.vboMultiDatesPicker.originalBeforeShowDay.call(this, date);
144 }
145
146 bsdReturn[1] = gotThisDate ? 'ui-state-highlight ' + bsdReturn[1] : bsdReturn[1];
147 bsdReturn[0] = bsdReturn[0] && !(isDisabledCalendar || isDisabledDate || (areAllSelected && !bsdReturn[1]));
148 return bsdReturn;
149 }
150 };
151
152 // value needs to be extracted before datepicker is initiated
153 if ($this.val()) {
154 var inputDates = $this.val();
155 }
156
157 if (options) {
158 // value needs to be extracted before datepicker is initiated
159 if (options.separator) {
160 this.vboMultiDatesPicker.separator = options.separator;
161 }
162 if (!this.vboMultiDatesPicker.separator) {
163 this.vboMultiDatesPicker.separator = ', ';
164 }
165
166 this.vboMultiDatesPicker.originalBeforeShow = options.beforeShow;
167 this.vboMultiDatesPicker.originalOnSelect = options.onSelect;
168 this.vboMultiDatesPicker.originalBeforeShowDay = options.beforeShowDay;
169 this.vboMultiDatesPicker.originalOnClose = options.onClose;
170
171 // datepicker init
172 $this.datepicker(options);
173
174 this.vboMultiDatesPicker.minDate = $.datepicker._determineDate(this, options.minDate, null);
175 this.vboMultiDatesPicker.maxDate = $.datepicker._determineDate(this, options.maxDate, null);
176 if (options.addDates) {
177 methods.addDates.call(this, options.addDates);
178 }
179
180 if (options.addDisabledDates) {
181 methods.addDates.call(this, options.addDisabledDates, 'disabled');
182 }
183
184 methods.setMode.call(this, options);
185 } else {
186 $this.datepicker();
187 }
188 $this.datepicker('option', mdp_events);
189
190 // adds any dates found in the input or alt field
191 if (inputDates) $this.vboMultiDatesPicker('value', inputDates);
192
193 // generates the new string of added dates
194 var inputs_values = $this.vboMultiDatesPicker('value');
195
196 // fills the input field back with all the dates in the calendar
197 $this.val(inputs_values);
198
199 // Fixes the altField filled with defaultDate by default
200 var altFieldOption = $this.datepicker('option', 'altField');
201 if (altFieldOption) $(altFieldOption).val(inputs_values);
202
203 // Updates the calendar view
204 $this.datepicker('refresh');
205 },
206 compareDates: function(date1, date2) {
207 date1 = dateConvert.call(this, date1);
208 date2 = dateConvert.call(this, date2);
209 // return > 0 means date1 is later than date2
210 // return == 0 means date1 is the same day as date2
211 // return < 0 means date1 is earlier than date2
212 var diff = date1.getFullYear() - date2.getFullYear();
213 if (!diff) {
214 diff = date1.getMonth() - date2.getMonth();
215 if (!diff) {
216 diff = date1.getDate() - date2.getDate();
217 }
218 }
219 return diff;
220 },
221 sumDays: function(date, n_days) {
222 var origDateType = typeof date;
223 obj_date = dateConvert.call(this, date);
224 obj_date.setDate(obj_date.getDate() + n_days);
225 return dateConvert.call(this, obj_date, origDateType);
226 },
227 dateConvert: function(date, desired_format, dateFormat) {
228 var from_format = typeof date;
229 var $this = $(this);
230
231 if (from_format == desired_format) {
232 if (from_format == 'object') {
233 try {
234 date.getTime();
235 } catch (e) {
236 $.error('Received date is in a non supported format!');
237 return false;
238 }
239 }
240 return date;
241 }
242
243 if (typeof date == 'undefined') {
244 date = new Date(0);
245 }
246
247 if (desired_format != 'string' && desired_format != 'object' && desired_format != 'number')
248 $.error('Date format "'+ desired_format +'" not supported!');
249
250 if (!dateFormat) {
251 var dp_dateFormat = $this.datepicker('option', 'dateFormat');
252 if (dp_dateFormat) {
253 dateFormat = dp_dateFormat;
254 } else {
255 dateFormat = $.datepicker._defaults.dateFormat;
256 }
257 }
258
259 // converts to object as a neutral format
260 switch (from_format) {
261 case 'object': break;
262 case 'string': date = $.datepicker.parseDate(dateFormat, date); break;
263 case 'number': date = new Date(date); break;
264 default: $.error('Conversion from "'+ from_format +'" format not allowed on jQuery.vboMultiDatesPicker');
265 }
266 // then converts to the desired format
267 switch (desired_format) {
268 case 'object': return date;
269 case 'string': return $.datepicker.formatDate(dateFormat, date);
270 case 'number': return date.getTime();
271 default: $.error('Conversion to "'+ desired_format +'" format not allowed on jQuery.vboMultiDatesPicker');
272 }
273 return false;
274 },
275 gotDate: function(date, type) {
276 if (!type) {
277 type = 'picked';
278 }
279 for (var i = 0; i < this.vboMultiDatesPicker.dates[type].length; i++) {
280 if (methods.compareDates.call(this, this.vboMultiDatesPicker.dates[type][i], date) === 0) {
281 return i;
282 }
283 }
284 return false;
285 },
286 value: function(value) {
287 if (value && typeof value == 'string') {
288 methods.addDates.call(this, value.split(this.vboMultiDatesPicker.separator));
289 } else {
290 var dates = methods.getDates.call(this, 'string');
291 return dates.length
292 ? dates.join(this.vboMultiDatesPicker.separator)
293 : '';
294 }
295 },
296 getDates: function(format, type) {
297 if (!format) {
298 format = 'string';
299 }
300 if (!type) {
301 type = 'picked';
302 }
303 switch (format) {
304 case 'object':
305 return this.vboMultiDatesPicker.dates[type];
306 case 'string':
307 case 'number':
308 var o_dates = [];
309 for (var i = 0; i < this.vboMultiDatesPicker.dates[type].length; i++)
310 o_dates.push(
311 dateConvert.call(
312 this,
313 this.vboMultiDatesPicker.dates[type][i],
314 format
315 )
316 );
317 return o_dates;
318
319 default: $.error('Format "'+format+'" not supported!');
320 }
321 },
322 addDates: function(dates, type) {
323 if (dates.length > 0) {
324 if (!type) {
325 type = 'picked';
326 }
327 switch (typeof dates) {
328 case 'object':
329 case 'array':
330 if (dates.length) {
331 for (var i = 0; i < dates.length; i++)
332 addDate.call(this, dates[i], type, true);
333 sortDates.call(this, type);
334 break;
335 } // else does the same as 'string'
336 case 'string':
337 case 'number':
338 addDate.call(this, dates, type);
339 break;
340 default:
341 $.error('Date format "'+ typeof dates +'" not allowed on jQuery.vboMultiDatesPicker');
342 }
343 } else {
344 $.error('Empty array of dates received.');
345 }
346 },
347 removeDates: function(dates, type) {
348 if (!type) {
349 type = 'picked';
350 }
351 var removed = [];
352 if (Object.prototype.toString.call(dates) === '[object Array]') {
353 dates.sort(function(a,b) {return b-a});
354 for (var i = 0; i < dates.length; i++) {
355 removed.push(removeDate.call(this, dates[i], type));
356 }
357 } else {
358 removed.push(removeDate.call(this, dates, type));
359 }
360 return removed;
361 },
362 removeIndexes: function(indexes, type) {
363 if (!type) {
364 type = 'picked';
365 }
366 var removed = [];
367 if (Object.prototype.toString.call(indexes) === '[object Array]') {
368 indexes.sort(function(a,b) {return b-a});
369 for (var i = 0; i < indexes.length; i++) {
370 removed.push(removeIndex.call(this, indexes[i], type));
371 }
372 } else {
373 removed.push(removeIndex.call(this, indexes, type));
374 }
375 return removed;
376 },
377 resetDates: function (type) {
378 if (!type) {
379 type = 'picked';
380 }
381 this.vboMultiDatesPicker.dates[type] = [];
382 },
383 toggleDate: function(date, type) {
384 if (!type) {
385 type = 'picked';
386 }
387 switch (this.vboMultiDatesPicker.mode) {
388 case 'daysRange':
389 this.vboMultiDatesPicker.dates[type] = []; // deletes all picked/disabled dates
390 var end = this.vboMultiDatesPicker.autoselectRange[1];
391 var begin = this.vboMultiDatesPicker.autoselectRange[0];
392 if (end < begin) {
393 end = this.vboMultiDatesPicker.autoselectRange[0];
394 begin = this.vboMultiDatesPicker.autoselectRange[1];
395 }
396 for (var i = begin; i < end; i++) {
397 methods.addDates.call(this, methods.sumDays.call(this,date, i), type);
398 }
399 break;
400 default:
401 if (methods.gotDate.call(this, date) === false) {
402 methods.addDates.call(this, date, type);
403 } else {
404 methods.removeDates.call(this, date, type);
405 }
406 break;
407 }
408 },
409 setMode: function(options) {
410 var $this = $(this);
411 if (options.mode) {
412 this.vboMultiDatesPicker.mode = options.mode;
413 }
414
415 switch (this.vboMultiDatesPicker.mode) {
416 case 'normal':
417 for (var option in options) {
418 switch (option) {
419 case 'maxPicks':
420 case 'minPicks':
421 case 'pickableRange':
422 case 'adjustRangeToDisabled':
423 this.vboMultiDatesPicker[option] = options[option];
424 break;
425 // default: $.error('Option ' + option + ' ignored for mode "'.options.mode.'".');
426 }
427 }
428 break;
429 case 'daysRange':
430 case 'weeksRange':
431 var mandatory = 1;
432 for (option in options) {
433 switch (option) {
434 case 'autoselectRange':
435 mandatory--;
436 case 'pickableRange':
437 case 'adjustRangeToDisabled':
438 this.vboMultiDatesPicker[option] = options[option];
439 break;
440 // default: $.error('Option ' + option + ' does not exist for setMode on jQuery.vboMultiDatesPicker');
441 }
442 }
443 if (mandatory > 0) {
444 $.error('Some mandatory options not specified!');
445 }
446 break;
447 }
448
449 if (mdp_events.onSelect) {
450 mdp_events.onSelect();
451 }
452 },
453 destroy: function() {
454 this.vboMultiDatesPicker = null;
455 $(this).datepicker('destroy');
456 }
457 };
458
459 this.each(function() {
460 var $this = $(this);
461 if (!this.vboMultiDatesPicker) {
462 this.vboMultiDatesPicker = {
463 dates: {
464 picked: [],
465 disabled: []
466 },
467 mode: 'normal',
468 adjustRangeToDisabled: true
469 };
470 }
471
472 if (methods[method]) {
473 var exec_result = methods[method].apply(this, Array.prototype.slice.call(mdp_arguments, 1));
474 switch (method) {
475 case 'removeDates':
476 case 'removeIndexes':
477 case 'resetDates':
478 case 'toggleDate':
479 case 'addDates':
480 var altField = $this.datepicker('option', 'altField');
481 var dates_string = methods.value.call(this);
482 if (altField !== undefined && altField != '') {
483 $(altField).val(dates_string);
484 }
485 $this.val(dates_string);
486
487 $.datepicker._refreshDatepicker(this);
488 }
489 switch (method) {
490 case 'removeDates':
491 case 'getDates':
492 case 'gotDate':
493 case 'sumDays':
494 case 'compareDates':
495 case 'dateConvert':
496 case 'value':
497 ret = exec_result;
498 }
499 return exec_result;
500 } else if (typeof method === 'object' || !method) {
501 return methods.init.apply(this, mdp_arguments);
502 } else {
503 $.error('Method ' + method + ' does not exist on jQuery.vboMultiDatesPicker');
504 }
505 return false;
506 });
507
508 return ret;
509 };
510
511 $.vboMultiDatesPicker = {version: false};
512 $.vboMultiDatesPicker.initialized = false;
513 $.vboMultiDatesPicker.uuid = new Date().getTime();
514 $.vboMultiDatesPicker.version = $.ui.vboMultiDatesPicker.version;
515
516 /**
517 * Allows MDP not to hide everytime a date is picked.
518 */
519 $(function() {
520 /**
521 * Use a constant instead of a property object to break the loop caused
522 * my modal (AJAX) duplicate rendering.
523 */
524 // $.vboMultiDatesPicker._hideDatepicker = $.datepicker._hideDatepicker;
525 const vboMultiDatesPicker_hideDatepicker = $.datepicker._hideDatepicker;
526 $.datepicker._hideDatepicker = function() {
527 /**
528 * Prevent errors with inline datepickers when _curInst is null.
529 *
530 * @see modified from original source code
531 */
532 if (!this._curInst) {
533 return;
534 }
535
536 var target = this._curInst.input[0];
537 var mdp = target.vboMultiDatesPicker;
538 if (!mdp || (this._curInst.inline === false && !mdp.changed)) {
539 return vboMultiDatesPicker_hideDatepicker.apply(this, arguments);
540 } else {
541 mdp.changed = false;
542 $.datepicker._refreshDatepicker(target);
543 return;
544 }
545 };
546 });
547
548 /**
549 * VikBooking - DatesRangePicker declaration.
550 *
551 * @param string|object checkin Check-in input field selector or element.
552 * @param string|object checkout Check-out input field selector or element.
553 * @param object options Datepicker options.
554 */
555 $.vboDatesRangePicker = function(checkin, checkout, options) {
556 if (typeof checkin === 'string') {
557 checkin = $(checkin);
558 }
559 if (typeof checkout === 'string') {
560 checkout = $(checkout);
561 }
562
563 if (!checkin || !checkout || !checkin.length || !checkout.length) {
564 throw new Error('Invalid vboDatesRangePicker check-in/check-out selectors.');
565 }
566
567 // ensure the current jQuery version is supported (v1.x is NOT supported)
568 let jq_version = $.fn.jquery || '3';
569 if (typeof jq_version === 'string' && jq_version.substring(0, 1) === '1') {
570 // fallback to regular datepicker
571 $(checkin).datepicker(options);
572 $(checkout).datepicker(options);
573 // abort
574 throw new Error('Unsupported jQuery version');
575 }
576
577 if (typeof options !== 'object') {
578 options = {};
579 }
580
581 // get the input fields for check-in and check-out
582 const inputCheckin = options?.altFields?.checkin ? $(options.altFields.checkin) : checkin;
583 const inputCheckout = options?.altFields?.checkout ? $(options.altFields.checkout) : checkout;
584
585 // get currently populated dates, if any
586 let prevCheckinDate = inputCheckin.val();
587 let prevCheckoutDate = inputCheckout.val();
588 const dates = [prevCheckinDate, prevCheckoutDate].filter(d => d);
589
590 // date range picker configuration
591 // 3 picks needed to rectify the completed selection
592 options.maxPicks = 3;
593 options.addDates = dates.length ? dates : null;
594
595 if (typeof options.altFormat !== 'undefined') {
596 // option not supported
597 delete options.altFormat;
598 }
599
600 // default configuration arguments
601 if (typeof options.numberOfMonths === 'undefined') {
602 options.numberOfMonths = 2;
603 }
604
605 if (typeof options.minDate === 'undefined') {
606 options.minDate = new Date;
607 } else {
608 // ensure minDate is an object
609 options.minDate = $(this).vboDatesRangePicker('convertPeriod', options.minDate);
610 }
611
612 const _onSelect = options.onSelect ?? null;
613 options.onSelect = function(date, instance) {
614 let pickedDates = $(this).vboMultiDatesPicker('getDates');
615
616 setTimeout(() => {
617 // clear the active status to prevent conflicts in case of deselection
618 let dateObj = $.datepicker.parseDate($(this).datepicker('option', 'dateFormat'), date);
619 const dayCell = $('.ui-datepicker').find('td.date-' + dateObj.getFullYear() + '-' + dateObj.getMonth() + '-' + dateObj.getDate());
620 dayCell.find('.ui-state-active').removeClass('ui-state-active');
621 dayCell.removeClass('ui-datepicker-current-day');
622 // resolve possible conflicts with tooltip
623 dayCell.trigger('mouseenter');
624 }, 10);
625
626 if (pickedDates.length > 2) {
627 // remove all the dates from the selection, except for the last picked one
628 $(this).vboMultiDatesPicker('removeDates', pickedDates.filter(d => d != date));
629
630 // adjust the picked dates
631 pickedDates = [date];
632 }
633
634 // populate checkin and checkout fields
635 if (pickedDates.length == 1) {
636 inputCheckin.val(pickedDates[0]);
637 inputCheckout.val('');
638 } else if (pickedDates.length == 2) {
639 inputCheckin.val(pickedDates[0]);
640 inputCheckout.val(pickedDates[1]);
641 } else {
642 inputCheckin.val('');
643 inputCheckout.val('');
644 }
645
646 /**
647 * @see Natively inline datepickers not rendered on input fields cannot be hid!
648 */
649
650 // prevent the datepicker auto-hiding after selecting a date
651 instance.inline = true;
652
653 setTimeout(() => {
654 // remove the inline state after completing the selection process,
655 // this aims to re-allow the datepicker closure when clicking outside
656 instance.inline = false;
657 });
658
659 if (_onSelect) {
660 // propagate selection behavior
661 _onSelect(date, instance);
662 }
663
664 if (pickedDates.length == 2) {
665 setTimeout(() => {
666 // prepare the very next click outside to hide the datepicker rather than 2 next clicks
667 checkin.datepicker('hide');
668 });
669
670 if (options?.environment?.autoHide) {
671 // do not allow to rectify the selection, but rather hide the DRP on selection completed
672 setTimeout(() => {
673 checkin.datepicker('hide');
674 checkin.trigger('blur');
675 }, (options?.environment?.autoHideDelay || 100));
676 }
677 }
678 };
679
680 const _beforeShowDay = options.beforeShowDay ?? null;
681 options.beforeShowDay = function(date) {
682 // get the DRP configuration
683 let config = $(this).vboDatesRangePicker('drpoption');
684
685 // current dates
686 const pickedDates = $(this).vboMultiDatesPicker('getDates');
687 const isPicked = pickedDates.some(dt => dt == date);
688
689 // in case the date has been selected, use it as min date
690 // in case the date has been deselected, use the first date in the array (config min date if empty selection)
691 let minDate = isPicked ? date : (pickedDates[0] || config.minDate);
692 // in case we have 0 or 2 selected dates, ignore the minimum date
693 // in case we have 1 or 3 selected dates, use the new date
694 minDate = pickedDates.length % 2 ? minDate : config.minDate;
695
696 // class identifier to easily select the table cell from a date object
697 const dateIdClass = 'date-' + date.getFullYear() + '-' + date.getMonth() + '-' + date.getDate();
698
699 // in case of past date, disable cell
700 if ($(this).vboDatesRangePicker('compareDates', date, minDate) < 0) {
701 return [false, 'date-past ' + dateIdClass, null];
702 }
703
704 // build date return values
705 let returnVal = [true, '', null];
706
707 // whether the date is known
708 let knownDate = false;
709
710 if (pickedDates.length > 0) {
711 // date format
712 let dateFormat = $(this).datepicker('option', 'dateFormat');
713
714 // get check-in date
715 let checkinDate = $.datepicker.parseDate(dateFormat, pickedDates[0]);
716
717 if ($(this).vboDatesRangePicker('compareDates', checkinDate, date) == 0) {
718 // parsing the check-in date
719 let checkinDayTitle = config?.labels?.checkin || null;
720 if (pickedDates.length == 1 && parseInt((config?.checkoutConstraints?.minStayNights || 0)) > 1) {
721 if (typeof config?.labels?.minStayNights === 'function') {
722 checkinDayTitle = config.labels.minStayNights.call(null, config.checkoutConstraints.minStayNights);
723 } else if (typeof config?.labels?.minStayNights === 'string') {
724 checkinDayTitle = config.labels.minStayNights;
725 }
726 }
727 returnVal = [true, 'checkin-date' + (pickedDates.length == 1 ? ' without-checkout-date' : ''), checkinDayTitle];
728 knownDate = true;
729 } else if (pickedDates.length == 2) {
730 // get check-out date
731 let checkoutDate = $.datepicker.parseDate(dateFormat, pickedDates[1]);
732
733 if ($(this).vboDatesRangePicker('compareDates', checkoutDate, date) == 0) {
734 // parsing the check-out date
735 let checkoutDayTitle = config?.labels?.checkout || null;
736 returnVal = [true, 'checkout-date', checkoutDayTitle];
737 knownDate = true;
738 } else if (checkinDate < date && date < checkoutDate) {
739 // parsing a date between the check-in and the check-out
740 returnVal = [true, 'checkin-checkout-inner', null];
741 knownDate = true;
742 }
743 }
744 }
745
746 if (_beforeShowDay && !knownDate) {
747 // call the registered methods for validating the current date
748 returnVal = _beforeShowDay(date);
749 }
750
751 if (returnVal[2]) {
752 // in case of tooltip title text, append a specific class
753 returnVal[1] += ' date-tooltip';
754 if (returnVal[2].length > 20) {
755 // this is a large tooltip text
756 returnVal[1] += ' date-tooltip-large';
757 }
758 // determine the tooltip position
759 let firstwday = $(this).vboDatesRangePicker('option', 'firstDay');
760 if (date.getDay() == firstwday) {
761 // first day of week
762 returnVal[1] += ' date-tooltip-firstwday';
763 } else {
764 // calculate last week-day index
765 let lastwday = firstwday - 1;
766 lastwday = lastwday < 0 ? 6 : lastwday;
767 if (date.getDay() == lastwday) {
768 // last day of week
769 returnVal[1] += ' date-tooltip-lastwday';
770 }
771 }
772 }
773
774 // append the class for this exact day and trim
775 returnVal[1] += ' ' + dateIdClass;
776 returnVal[1] = returnVal[1].trim();
777
778 return returnVal;
779 }
780
781 /**
782 * The beforeShow function will NOT be called for inline datepickers.
783 */
784 const _beforeShow = options.beforeShow ?? null;
785 const _mouseEnter = (inlineElement) => {
786 // determine the proper datepicker container
787 let dpContainer = typeof inlineElement === 'undefined' ? $('.ui-datepicker').not('.ui-datepicker-inline') : $(inlineElement).find('.ui-datepicker');
788
789 dpContainer.on('mouseenter', 'td', function() {
790 let pickedDates = [];
791
792 try {
793 pickedDates = $(checkin).vboMultiDatesPicker('getDates');
794 } catch (e) {
795 // silently abort in case of elements not being controlled through DRP
796 return;
797 }
798
799 if ($(this).hasClass('date-tooltip')) {
800 let title = $(this).attr('title');
801 if (title) {
802 // replace native title attribute with data attribute for tooltip
803 $(this).attr('title', '');
804 $(this).attr('data-title', title);
805 }
806 }
807
808 if (pickedDates.length == 1) {
809 // get the proper date format in case it differs from regional values
810 let dateFormat = $(checkin).datepicker('option', 'dateFormat') || options.dateFormat;
811 const checkinDate = $.datepicker.parseDate(dateFormat, pickedDates[0]);
812
813 const checkoutDate = new Date;
814 // ensure to set the date to 1 first, in case the month to set does not have this day (i.e Jan 30, Feb 30 not existing)
815 // without doing so, the Date object constructed would get an extra month, and so the date would not be the desired one
816 checkoutDate.setDate(1);
817 checkoutDate.setFullYear($(this).data('year'));
818 checkoutDate.setMonth($(this).data('month'));
819 checkoutDate.setDate($(this).text());
820
821 $('.checkout-date').removeClass('checkout-date date-will');
822 $('.checkin-checkout-inner').removeClass('checkin-checkout-inner');
823
824 if ($(checkin).vboDatesRangePicker('compareDates', checkoutDate, checkinDate) == 0) {
825 return;
826 }
827
828 $('.date-' + checkoutDate.getFullYear() + '-' + checkoutDate.getMonth() + '-' + checkoutDate.getDate()).addClass('checkout-date date-will');
829 checkoutDate.setDate(checkoutDate.getDate() - 1);
830
831 while ($(checkin).vboDatesRangePicker('compareDates', checkoutDate, checkinDate) > 0) {
832 $('.date-' + checkoutDate.getFullYear() + '-' + checkoutDate.getMonth() + '-' + checkoutDate.getDate()).addClass('checkin-checkout-inner');
833 checkoutDate.setDate(checkoutDate.getDate() - 1);
834 }
835 }
836 });
837 }
838 options.beforeShow = function(input, instance) {
839 // register mouseenter event on date cells
840 _mouseEnter();
841
842 if (_beforeShow) {
843 // propagate show behavior
844 _beforeShow(input, instance);
845 }
846 };
847
848 /**
849 * The onUpdateDatepicker function will NOT be called for inline datepickers.
850 */
851 const _onUpdateDatepicker = options.onUpdateDatepicker ?? null;
852 options.onUpdateDatepicker = function(instance) {
853 // get the DRP configuration
854 let config = $(this).vboDatesRangePicker('drpoption');
855
856 if (typeof config?.bottomCommands === 'object') {
857 if ($('.vbo-drp-commands-bottom').length) {
858 return;
859 }
860
861 // build bottom commands
862 let btmCommands = $('<div></div>').addClass('vbo-drp-commands-bottom');
863
864 // clear dates
865 let clearCommand = $('<div></div>')
866 .addClass('vbo-drp-command vbo-drp-command-clear')
867 .append(
868 $('<a></a>')
869 .attr('href', 'JavaScript: void(0);')
870 .text((config.bottomCommands?.clear || 'Clear dates'))
871 .on('click', () => {
872 $(this).vboDatesRangePicker('setDates', []);
873 $('#ui-datepicker-div').find('.ui-state-active').removeClass('ui-state-active');
874 $('#ui-datepicker-div').find('.ui-datepicker-current-day').removeClass('ui-datepicker-current-day');
875 if (typeof config.bottomCommands?.onClear === 'function') {
876 // invoke the provided method in case something needs to be cleared from the UI
877 config.bottomCommands.onClear.call(this);
878 }
879 })
880 );
881 btmCommands.append(clearCommand);
882
883 // close DRP
884 let closeCommand = $('<div></div>')
885 .addClass('vbo-drp-command vbo-drp-command-close')
886 .append(
887 $('<button></button>')
888 .addClass('btn btn-small ' + (config?.environment?.section === 'admin' ? 'vbo-dark-btn' : 'vbo-pref-color-btn'))
889 .attr('type', 'button')
890 .text((config.bottomCommands?.close || 'Close'))
891 .on('click', () => {
892 $(this).datepicker('hide');
893 })
894 );
895 btmCommands.append(closeCommand);
896
897 // append bottom commands to DRP
898 $('#ui-datepicker-div').append(btmCommands);
899 }
900
901 if (_onUpdateDatepicker) {
902 // propagate onUpdateDatepicker behavior
903 _onUpdateDatepicker(instance);
904 }
905 };
906
907 /**
908 * Define the onClose behavior.
909 */
910 const _onClose = options.onClose ?? null;
911 options.onClose = function(dateText, instance) {
912 // unregister mouseenter event from every non-inline datepicker
913 $('.ui-datepicker').not('.ui-datepicker-inline').off('mouseenter', 'td');
914
915 if (_onClose) {
916 // propagate close behavior
917 _onClose(dateText, instance);
918 }
919 };
920
921 // check for regional default settings
922 if (options?.environment?.section === 'admin') {
923 let regionalDefaults = $.datepicker.regional['vikbooking'];
924 if (typeof regionalDefaults === 'object') {
925 // merge regional settings with DRP options
926 options = Object.assign(regionalDefaults, options);
927 }
928 }
929
930 // determine whether the datepicker is inline
931 let isInline = !$(checkin).is('input[type="text"]');
932
933 // instantiate multi-dates picker for a single range of dates
934 $(checkin).vboMultiDatesPicker(options);
935
936 // trigger datepicker opening when focusing the check-out field too
937 $(checkout).on('focus', () => {
938 $(checkin).datepicker('show');
939 });
940
941 // restore the initial check-in value
942 inputCheckin.val(prevCheckinDate);
943
944 if (isInline) {
945 // manually register the mouseenter event since the beforeShow won't run
946 setTimeout(() => {
947 _mouseEnter(checkin);
948 }, 100);
949 } else {
950 // add class to the check-out field to identify the trigger
951 $(checkout).addClass('vbo-drp-inp-trigger');
952 }
953 }
954
955 /**
956 * VikBooking - DatesRangePicker jQuery plugin.
957 *
958 * @param any method Either string for getter/setter, or object to start the DRP.
959 * @param any options Initialization object options, or mixed value for setter.
960 * @param any setvalue The value to set within the datepicker in case of "option" setter.
961 */
962 $.fn.vboDatesRangePicker = function(method, options, setvalue) {
963 if (!method) {
964 // initialize the DRP
965 method = {};
966 }
967
968 // immediately exit in case of no elements found
969 if ($(this).length == 0) {
970 return this;
971 }
972
973 /**
974 * Initializes the DRP calendar.
975 */
976 const init = (options) => {
977 if (!options.dateFormat) {
978 options.dateFormat = 'yy-mm-dd';
979 }
980
981 // set DRP cloned configuration data
982 drpConfig(Object.assign({}, options));
983
984 // element selector
985 let that = $(this);
986
987 // handle native datepicker method beforeShowDay
988 if (options.beforeShowDay) {
989 // get registered callbacks
990 let beforeShowDayCheckin = options.beforeShowDay.checkin;
991 let beforeShowDayCheckout = options.beforeShowDay.checkout;
992
993 // delete native property
994 delete options.beforeShowDay;
995
996 // register native property
997 options.beforeShowDay = (date) => {
998 // get currently selected dates
999 let pickedDates = that.vboMultiDatesPicker('getDates');
1000
1001 // default date selectable state
1002 let isSelectable = true;
1003 let className = '';
1004 let tooltipText = null;
1005
1006 // invoke callbacks by injecting the proper arguments
1007 if ((!pickedDates.length || pickedDates.length == 2) && typeof beforeShowDayCheckin === 'function') {
1008 // validate cell for check-in selection
1009 let validation = beforeShowDayCheckin.call(that, date);
1010 // update cell states
1011 isSelectable = validation[0];
1012 className = validation[1] || className;
1013 tooltipText = validation[2] || tooltipText;
1014 } else if (pickedDates.length == 1 && typeof beforeShowDayCheckout === 'function') {
1015 // validate cell for check-out selection
1016 let validation = beforeShowDayCheckout.call(that, date);
1017 // update cell states
1018 isSelectable = validation[0];
1019 className = validation[1] || className;
1020 tooltipText = validation[2] || tooltipText;
1021 }
1022
1023 if (!pickedDates.length) {
1024 // unset any previously set checkout constraints when no dates selected
1025 unsetCheckoutConstraints(date);
1026 } else if (pickedDates.length == 1) {
1027 // before showing the check-out day, validate the constraints, if any
1028 let constrainData = validateCheckoutConstraints(date, [isSelectable, className, tooltipText], pickedDates[0]);
1029 // update cell states
1030 isSelectable = constrainData[0];
1031 className = constrainData[1];
1032 tooltipText = constrainData[2];
1033 }
1034
1035 return [
1036 // whether it's selectable
1037 (isSelectable ? true : false),
1038 // CSS class name to add
1039 className,
1040 // tooltip text
1041 tooltipText,
1042 ];
1043 };
1044 }
1045
1046 // handle native datepicker method onSelect
1047 if (options.onSelect) {
1048 // get registered callbacks
1049 let onSelectCheckin = options.onSelect.checkin;
1050 let onSelectCheckout = options.onSelect.checkout;
1051
1052 // delete native property
1053 delete options.onSelect;
1054
1055 // register native property
1056 options.onSelect = (selectedDate) => {
1057 // get currently selected dates
1058 let pickedDates = that.vboMultiDatesPicker('getDates');
1059
1060 // invoke callbacks by injecting the proper arguments
1061 if (pickedDates.length == 1 && typeof onSelectCheckin === 'function') {
1062 // call the registered check-in function
1063 onSelectCheckin.call(that, selectedDate);
1064 } else if (pickedDates.length == 2 && typeof onSelectCheckout === 'function') {
1065 // call the registered check-out function
1066 onSelectCheckout.call(that, selectedDate);
1067 }
1068 };
1069 }
1070
1071 // handle datepicker alternate field
1072 if (options?.altFields?.checkin) {
1073 // set native alternate field
1074 options.altField = options.altFields.checkin;
1075 }
1076
1077 // render DRP
1078 $.vboDatesRangePicker(that, $(options.checkout), options);
1079
1080 // threshold for responsiveness
1081 let thresholdWidth = options?.responsiveNumMonths?.threshold || 860;
1082
1083 // handle responsive number of months
1084 if (options?.responsiveNumMonths && (options?.numberOfMonths || 1) > 1) {
1085 // configure responsive number of months
1086 options._onResizeWindow = () => {
1087 let windowWidth = window.innerWidth;
1088 if (windowWidth && windowWidth <= thresholdWidth) {
1089 // just one month
1090 that.datepicker('option', 'numberOfMonths', 1);
1091 } else {
1092 // use the number of months configured
1093 that.datepicker('option', 'numberOfMonths', options.numberOfMonths);
1094 }
1095 };
1096
1097 // remove duplicate event listeners
1098 window.removeEventListener('resize', options._onResizeWindow);
1099
1100 // register event listener
1101 window.addEventListener('resize', options._onResizeWindow);
1102
1103 // call method on init
1104 options._onResizeWindow();
1105 }
1106
1107 // handle input fields focus/blur on mobile devices
1108 if (options?.environment?.section === 'admin' && that.is('input[type="text"]')) {
1109 // disable input fields focus on small screen resolutions
1110 let windowWidth = window.innerWidth;
1111 if (windowWidth && windowWidth <= thresholdWidth) {
1112 // disable focus on check-in input field
1113 that.on('focus', function() {
1114 $(this).blur();
1115 });
1116
1117 if ($(options.checkout).is('input[type="text"]')) {
1118 // disable focus on check-out input field
1119 $(options.checkout).on('focus', function() {
1120 $(this).blur();
1121 });
1122 }
1123 }
1124 }
1125
1126 return this;
1127 }
1128
1129 /**
1130 * Setter or getter for the DRP calendar.
1131 */
1132 const drpConfig = (options) => {
1133 if (typeof options === 'undefined') {
1134 // GETTER: return DRP configuration.
1135 // Clone the object in order to prevent manual edits to
1136 // the configuration properties.
1137 return Object.assign({}, $(this).data('vboDrpConfig'));
1138 }
1139
1140 // SETTER: update DRP configuration
1141 return $(this).data('vboDrpConfig', options);
1142 }
1143
1144 /**
1145 * Converts a period string like "+1d" into a Date object.
1146 *
1147 * @param string period The date period string.
1148 * @param Date fromDate Optional Date object from.
1149 *
1150 * @return Date
1151 */
1152 const convertPeriod = (period, fromDate) => {
1153 if (!fromDate || !(fromDate instanceof Date)) {
1154 fromDate = new Date;
1155 }
1156
1157 if (typeof period !== 'string') {
1158 // period must be a string
1159 return fromDate;
1160 }
1161
1162 let instructions = period.match(/^[\+\-]?([0-9]+)(d|w|m|y)$/i);
1163
1164 if (!instructions || instructions.length != 3) {
1165 // period not matched
1166 return fromDate;
1167 }
1168
1169 // period number of days/weeks/months/year
1170 let num = parseInt(instructions[1]);
1171
1172 if (num === 0) {
1173 // same day period ("0d")
1174 return fromDate;
1175 }
1176
1177 // period modifier
1178 if (period.substring(0, 1) == '-') {
1179 // turn number into negative
1180 num = num - (num * 2);
1181 }
1182
1183 switch ((instructions[2] + '').toLowerCase()) {
1184 case 'd':
1185 fromDate.setDate(fromDate.getDate() + num);
1186 break;
1187 case 'w':
1188 fromDate.setDate(fromDate.getDate() + (7 * num));
1189 break;
1190 case 'm':
1191 fromDate.setMonth(fromDate.getMonth() + num);
1192 break;
1193 case 'y':
1194 fromDate.setFullYear(fromDate.getFullYear() + num);
1195 break;
1196 }
1197
1198 return fromDate;
1199 }
1200
1201 /**
1202 * Compares two date objects against each other with year, month, day.
1203 *
1204 * @param string|Date date1 The first date.
1205 * @param string|Date date2 The second date.
1206 *
1207 * @return int Greater than 0 means date1 is after date2.
1208 * Equal to 0 means date1 is the same as date2.
1209 * Less than 0 means date1 is before date2.
1210 */
1211 const compareDates = (date1, date2) => {
1212 if (!date1) {
1213 // no empty dates allowed, default to today
1214 date1 = new Date;
1215 }
1216
1217 if (!date2) {
1218 // no empty dates allowed, default to today
1219 date2 = new Date;
1220 }
1221
1222 // get the DRP configuration
1223 let config = drpConfig();
1224
1225 if (typeof date1 === 'string') {
1226 try {
1227 // convert date string to date object
1228 date1 = $.datepicker.parseDate(config.dateFormat, date1);
1229 } catch (e) {
1230 // attempt to convert a period string into a date object
1231 date1 = convertPeriod(date1);
1232 }
1233 }
1234
1235 if (typeof date2 === 'string') {
1236 try {
1237 // convert date string to date object
1238 date2 = $.datepicker.parseDate(config.dateFormat, date2);
1239 } catch (e) {
1240 // attempt to convert a period string into a date object
1241 date2 = convertPeriod(date2);
1242 }
1243 }
1244
1245 // year check
1246 let diff = date1.getFullYear() - date2.getFullYear();
1247
1248 if (!diff) {
1249 // month check
1250 diff = date1.getMonth() - date2.getMonth();
1251
1252 if (!diff) {
1253 // day check
1254 diff = date1.getDate() - date2.getDate();
1255 }
1256 }
1257
1258 return diff;
1259 }
1260
1261 /**
1262 * Sets new dates in the DRP calendar.
1263 *
1264 * @param array dates List of dates to set.
1265 *
1266 * @return self
1267 */
1268 const setDates = (dates) => {
1269 if (!Array.isArray(dates)) {
1270 throw new Error('Invalid dates argument');
1271 }
1272
1273 // filter out empty dates
1274 dates = dates.filter(d => d);
1275
1276 // get the DRP configuration
1277 let config = drpConfig();
1278
1279 // map first the given dates to date strings in the desired format
1280 dates = dates.map((dt) => {
1281 if (typeof dt === 'object') {
1282 // convert date object into date string
1283 dt = $.datepicker.formatDate(config.dateFormat, dt);
1284 }
1285
1286 return dt;
1287 });
1288
1289 // remove all dates from the current selection
1290 let pickedDates = $(this).vboMultiDatesPicker('getDates');
1291 $(this).vboMultiDatesPicker('removeDates', pickedDates);
1292
1293 if (dates.length) {
1294 // set new date(s)
1295 $(this).vboMultiDatesPicker('addDates', dates);
1296 } else {
1297 // remove any active cell state
1298 $('.ui-state-active').removeClass('ui-state-active');
1299 $('.ui-datepicker-current-day').removeClass('ui-datepicker-current-day');
1300 }
1301
1302 // get the input fields for check-in and check-out
1303 let inputCheckin = config?.altFields?.checkin ? $(config.altFields.checkin) : $(this);
1304 let inputCheckout = config?.altFields?.checkout ? $(config.altFields.checkout) : $(config.checkout);
1305
1306 // populate checkin and checkout fields
1307 if (dates.length == 1) {
1308 inputCheckin.val(dates[0]);
1309 inputCheckout.val('');
1310 } else if (dates.length == 2) {
1311 inputCheckin.val(dates[0]);
1312 inputCheckout.val(dates[1]);
1313 } else {
1314 inputCheckin.val('');
1315 inputCheckout.val('');
1316 }
1317
1318 return this;
1319 }
1320
1321 /**
1322 * Handles the registration of the check-out update upon choosing the check-in.
1323 * Expected actions to perform are: minDate, maxDate, setCheckoutDate.
1324 *
1325 * @param string action The action to perform.
1326 * @param any value The value for the action to perform.
1327 *
1328 * @return self
1329 */
1330 const handleCheckoutConstraints = (action, value) => {
1331 if (typeof action !== 'string') {
1332 throw new Error('Invalid arguments for handleCheckoutConstraints');
1333 }
1334
1335 // access DRP configuration
1336 let config = drpConfig();
1337
1338 if (action.match(/^setcheckoutdate$/i)) {
1339 // do NOT update the check-out date unless running in legacy mode
1340 // with two native datepicker calendars, of if unsetting the date.
1341 if (config?.legacy || !value) {
1342 return $(this).vboDatesRangePicker('setCheckoutDate', value);
1343 }
1344
1345 // abort
1346 return this;
1347 }
1348
1349 if (config?.legacy) {
1350 // apply the requested option to the check-out field
1351 return $(config.checkout).datepicker('option', action, value);
1352 }
1353
1354 // build new settings
1355 let newConfig = Object.assign({}, config);
1356
1357 // inject action value
1358 newConfig.checkoutConstraints = newConfig.checkoutConstraints || {};
1359 newConfig.checkoutConstraints[action] = value;
1360
1361 // update DRP configuration
1362 drpConfig(newConfig)
1363
1364 return this;
1365 }
1366
1367 /**
1368 * Validates the current check-out constraints against the given date.
1369 *
1370 * @param Date date The date object to validate.
1371 * @param Array validation The default date validation array of values for "beforeShowDay".
1372 * @param string|Date checkin Optional check-in date.
1373 *
1374 * @return Array The new validation array of values (selectable, class-name, tooltip-text).
1375 */
1376 const validateCheckoutConstraints = (date, validation, checkin) => {
1377 // access DRP configuration
1378 let config = drpConfig();
1379
1380 if (!Array.isArray(validation) || !validation.length) {
1381 // set default date validation array
1382 validation = [true, '', null];
1383 }
1384
1385 if (config?.legacy || !(date instanceof Date)) {
1386 return validation;
1387 }
1388
1389 if (typeof config?.checkoutConstraints !== 'object') {
1390 return validation;
1391 }
1392
1393 if ((validation[1] + '').match(/checkin\-checkout\-inner/i)) {
1394 // no need to validate a date between the selected range of dates
1395 return validation;
1396 }
1397
1398 // check if the current date is actually the check-in date
1399 let isCheckin = false;
1400 if (checkin) {
1401 isCheckin = !compareDates(date, checkin);
1402 }
1403
1404 // get a list of both action and value constraints
1405 let actions = Object.keys(config.checkoutConstraints);
1406 let values = Object.values(config.checkoutConstraints);
1407
1408 // scan all actions and values
1409 actions.forEach((action, index) => {
1410 if (typeof action !== 'string') {
1411 return;
1412 }
1413
1414 // validate minDate
1415 if (action.match(/^mindate$/i) && (values[index] instanceof Date)) {
1416 let isPast = compareDates(date, values[index]) < 0;
1417 if (isPast && !isCheckin) {
1418 // date should not be selectable
1419 validation[0] = false;
1420 }
1421 }
1422
1423 // validate maxDate
1424 if (action.match(/^maxdate$/i) && (values[index] instanceof Date)) {
1425 let isBeyond = compareDates(date, values[index]) > 0;
1426 if (isBeyond && !isCheckin) {
1427 // date should not be selectable
1428 validation[0] = false;
1429 }
1430 }
1431 });
1432
1433 return validation;
1434 }
1435
1436 /**
1437 * Unsets any previously set check-out constraint.
1438 *
1439 * @param Date date The date object being validated.
1440 *
1441 * @return void
1442 */
1443 const unsetCheckoutConstraints = (date) => {
1444 // access DRP configuration
1445 let config = drpConfig();
1446
1447 if (config.hasOwnProperty('checkoutConstraints')) {
1448 // delete configuration object property
1449 delete config.checkoutConstraints;
1450
1451 // update DRP configuration
1452 drpConfig(config);
1453 }
1454 }
1455
1456 /**
1457 * Initializes the DRP calendar.
1458 */
1459 if (typeof method === 'object') {
1460 return init(method);
1461 }
1462
1463 /**
1464 * Compares two dates.
1465 */
1466 if (typeof method === 'string' && method.match(/^comparedates$/i)) {
1467 return compareDates(options, setvalue);
1468 }
1469
1470 /**
1471 * Converts a period string into a date object.
1472 */
1473 if (typeof method === 'string' && method.match(/^convertperiod$/i)) {
1474 return convertPeriod(options, setvalue);
1475 }
1476
1477 /**
1478 * Aliasing dates retrieval.
1479 */
1480 if (typeof method === 'string' && method.match(/^getdates$/i)) {
1481 return $(this).vboMultiDatesPicker('getDates');
1482 }
1483
1484 /**
1485 * Aliasing check-in date retrieval.
1486 */
1487 if (typeof method === 'string' && method.match(/^getcheckindate$/i)) {
1488 let pickedDates = $(this).vboMultiDatesPicker('getDates', 'object');
1489 return pickedDates[0] || null;
1490 }
1491
1492 /**
1493 * Aliasing check-out date retrieval.
1494 */
1495 if (typeof method === 'string' && method.match(/^getcheckoutdate$/i)) {
1496 let pickedDates = $(this).vboMultiDatesPicker('getDates', 'object');
1497 return pickedDates[1] || null;
1498 }
1499
1500 /**
1501 * Sets new dates in the DRP.
1502 */
1503 if (typeof method === 'string' && method.match(/^setdates$/i)) {
1504 return setDates(options);
1505 }
1506
1507 /**
1508 * Sets the check-in date in the DRP.
1509 */
1510 if (typeof method === 'string' && method.match(/^setcheckindate$/i)) {
1511 // get current dates
1512 let pickedDates = $(this).vboMultiDatesPicker('getDates');
1513
1514 // build new dates
1515 let newDates = Array.isArray(options) ? options : [options];
1516
1517 if (!newDates[0]) {
1518 // we are actually unsetting the check-in date
1519 newDates = [];
1520 pickedDates = [];
1521 }
1522
1523 if (pickedDates[1]) {
1524 // push existing check-out date
1525 newDates.push(pickedDates[1]);
1526 }
1527
1528 return setDates(newDates);
1529 }
1530
1531 /**
1532 * Sets the check-out date in the DRP.
1533 */
1534 if (typeof method === 'string' && method.match(/^setcheckoutdate$/i)) {
1535 // get current dates
1536 let pickedDates = $(this).vboMultiDatesPicker('getDates');
1537 if (!pickedDates.length) {
1538 // abort if no dates currently selected, or if multiple dates given
1539 return this;
1540 }
1541
1542 // build new dates
1543 let newDates = [pickedDates[0], Array.isArray(options) ? options[0] : options];
1544
1545 return setDates(newDates);
1546 }
1547
1548 /**
1549 * Hides the DRP calendar, as long as it is not rendered inline.
1550 */
1551 if (typeof method === 'string' && method.match(/^hide$/i)) {
1552 return $(this).datepicker('hide');
1553 }
1554
1555 /**
1556 * Setter or getter for the native datepicker calendar.
1557 */
1558 if (method === 'option') {
1559 // native datepicker option getter or setter
1560 if (typeof setvalue === 'undefined') {
1561 // native datepicker option getter
1562 return $(this).datepicker('option', options);
1563 }
1564
1565 // native datepicker option setter
1566 return $(this).datepicker('option', options, setvalue);
1567 }
1568
1569 /**
1570 * Gets one or all DRP options, or invokes a DRP function.
1571 */
1572 if (method === 'drpoption') {
1573 // access DRP configuration
1574 let config = drpConfig();
1575
1576 if (!options) {
1577 // return the whole DRP configuration object
1578 return config;
1579 }
1580
1581 if (typeof options === 'string' && options.match(/^.+\..+$/i)) {
1582 // check for nested configuration object properties (i.e. "beforeShowDay.checkin")
1583 let macroOption = options;
1584 macroOption.split('.').forEach((prop) => {
1585 if (typeof config === 'object' && config.hasOwnProperty(prop) && typeof config[prop] === 'object') {
1586 config = config[prop];
1587 } else {
1588 options = prop;
1589 }
1590 });
1591 }
1592
1593 if (typeof setvalue === 'undefined') {
1594 // DRP option getter
1595 return config[options] || null;
1596 }
1597
1598 if (typeof config[options] !== 'function') {
1599 throw new Error('Invalid DRP function invoked (' + options + ')');
1600 }
1601
1602 // call the requested method
1603 return Array.isArray(setvalue) ? config[options].apply(this, setvalue) : config[options].call(this, setvalue);
1604 }
1605
1606 /**
1607 * Handles check-out update operations upon selecting check-in.
1608 */
1609 if (method === 'checkout') {
1610 return handleCheckoutConstraints(options, setvalue);
1611 }
1612 }
1613 }));