File "datesrangepicker.js"
Full Path: /home/romayxjt/public_html/wp-content/plugins/vikbooking/site/resources/datesrangepicker.js
File size: 48.61 KB
MIME-type: text/x-Algol68
Charset: utf-8
/**
* VikBooking - DatesRangePicker v1.2.3.
* Copyright (C) 2025 E4J s.r.l. All Rights Reserved.
* http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
* https://vikwp.com | https://e4j.com | https://e4jconnect.com
*
* Forked from MultiDatesPicker v1.6.6
* https://dubrox.github.io/Multiple-Dates-Picker-for-jQuery-UI
*/
(function(factory) {
if (typeof define === 'function' && define.amd) {
define(['jquery', 'jquery-ui-dist'], factory);
} else {
factory(jQuery);
}
}(function($) {
$.extend($.ui, { vboMultiDatesPicker: { version: '1.6.6' } });
$.fn.vboMultiDatesPicker = function(method) {
var mdp_arguments = arguments;
var ret = this;
var today_date = new Date;
var day_zero = new Date(0);
var mdp_events = {};
function removeDate(date, type) {
if (!type) {
type = 'picked';
}
date = dateConvert.call(this, date);
for (var i = 0; i < this.vboMultiDatesPicker.dates[type].length; i++)
if (!methods.compareDates(this.vboMultiDatesPicker.dates[type][i], date)) {
return this.vboMultiDatesPicker.dates[type].splice(i, 1).pop();
}
}
function removeIndex(index, type) {
if (!type) {
type = 'picked';
}
return this.vboMultiDatesPicker.dates[type].splice(index, 1).pop();
}
function addDate(date, type, no_sort) {
if (!type) {
type = 'picked';
}
date = dateConvert.call(this, date);
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
if (methods.gotDate.call(this, date, type) === false) {
this.vboMultiDatesPicker.dates[type].push(date);
if (!no_sort) {
this.vboMultiDatesPicker.dates[type].sort(methods.compareDates);
}
}
}
function sortDates(type) {
if (!type) {
type = 'picked';
}
this.vboMultiDatesPicker.dates[type].sort(methods.compareDates);
}
function dateConvert(date, desired_type, date_format) {
if (!desired_type) {
desired_type = 'object';
}
return methods.dateConvert.call(this, date, desired_type, date_format);
}
var methods = {
init: function(options) {
var $this = $(this);
this.vboMultiDatesPicker.changed = false;
var mdp_events = {
beforeShow: function(input, inst) {
this.vboMultiDatesPicker.changed = false;
if (this.vboMultiDatesPicker.originalBeforeShow) {
this.vboMultiDatesPicker.originalBeforeShow.call(this, input, inst);
}
},
onSelect: function(dateText, inst) {
var $this = $(this);
this.vboMultiDatesPicker.changed = true;
if (dateText) {
$this.vboMultiDatesPicker('toggleDate', dateText);
this.vboMultiDatesPicker.changed = true;
}
if (this.vboMultiDatesPicker.mode == 'normal' && this.vboMultiDatesPicker.pickableRange) {
if (this.vboMultiDatesPicker.dates.picked.length > 0) {
var min_date = this.vboMultiDatesPicker.dates.picked[0],
max_date = new Date(min_date.getTime());
methods.sumDays(max_date, this.vboMultiDatesPicker.pickableRange-1);
// counts the number of disabled dates in the range
if (this.vboMultiDatesPicker.adjustRangeToDisabled) {
var c_disabled,
disabled = this.vboMultiDatesPicker.dates.disabled.slice(0);
do {
c_disabled = 0;
for (var i = 0; i < disabled.length; i++) {
if (disabled[i].getTime() <= max_date.getTime()) {
if ((min_date.getTime() <= disabled[i].getTime()) && (disabled[i].getTime() <= max_date.getTime()) ) {
c_disabled++;
}
disabled.splice(i, 1);
i--;
}
}
max_date.setDate(max_date.getDate() + c_disabled);
} while(c_disabled != 0);
}
if (this.vboMultiDatesPicker.maxDate && (max_date > this.vboMultiDatesPicker.maxDate)) {
max_date = this.vboMultiDatesPicker.maxDate;
}
$this.datepicker('option', 'minDate', min_date).datepicker('option', 'maxDate', max_date);
} else {
$this.datepicker('option', 'minDate', this.vboMultiDatesPicker.minDate).datepicker('option', 'maxDate', this.vboMultiDatesPicker.maxDate);
}
}
if (this.vboMultiDatesPicker.originalOnSelect && dateText) {
this.vboMultiDatesPicker.originalOnSelect.call(this, dateText, inst);
}
},
beforeShowDay: function(date) {
var $this = $(this),
gotThisDate = $this.vboMultiDatesPicker('gotDate', date) !== false,
isDisabledCalendar = $this.datepicker('option', 'disabled'),
isDisabledDate = $this.vboMultiDatesPicker('gotDate', date, 'disabled') !== false,
areAllSelected = this.vboMultiDatesPicker.maxPicks <= this.vboMultiDatesPicker.dates.picked.length;
var bsdReturn = [true, '', null];
if (this.vboMultiDatesPicker.originalBeforeShowDay) {
bsdReturn = this.vboMultiDatesPicker.originalBeforeShowDay.call(this, date);
}
bsdReturn[1] = gotThisDate ? 'ui-state-highlight ' + bsdReturn[1] : bsdReturn[1];
bsdReturn[0] = bsdReturn[0] && !(isDisabledCalendar || isDisabledDate || (areAllSelected && !bsdReturn[1]));
return bsdReturn;
}
};
// value needs to be extracted before datepicker is initiated
if ($this.val()) {
var inputDates = $this.val();
}
if (options) {
// value needs to be extracted before datepicker is initiated
if (options.separator) {
this.vboMultiDatesPicker.separator = options.separator;
}
if (!this.vboMultiDatesPicker.separator) {
this.vboMultiDatesPicker.separator = ', ';
}
this.vboMultiDatesPicker.originalBeforeShow = options.beforeShow;
this.vboMultiDatesPicker.originalOnSelect = options.onSelect;
this.vboMultiDatesPicker.originalBeforeShowDay = options.beforeShowDay;
this.vboMultiDatesPicker.originalOnClose = options.onClose;
// datepicker init
$this.datepicker(options);
this.vboMultiDatesPicker.minDate = $.datepicker._determineDate(this, options.minDate, null);
this.vboMultiDatesPicker.maxDate = $.datepicker._determineDate(this, options.maxDate, null);
if (options.addDates) {
methods.addDates.call(this, options.addDates);
}
if (options.addDisabledDates) {
methods.addDates.call(this, options.addDisabledDates, 'disabled');
}
methods.setMode.call(this, options);
} else {
$this.datepicker();
}
$this.datepicker('option', mdp_events);
// adds any dates found in the input or alt field
if (inputDates) $this.vboMultiDatesPicker('value', inputDates);
// generates the new string of added dates
var inputs_values = $this.vboMultiDatesPicker('value');
// fills the input field back with all the dates in the calendar
$this.val(inputs_values);
// Fixes the altField filled with defaultDate by default
var altFieldOption = $this.datepicker('option', 'altField');
if (altFieldOption) $(altFieldOption).val(inputs_values);
// Updates the calendar view
$this.datepicker('refresh');
},
compareDates: function(date1, date2) {
date1 = dateConvert.call(this, date1);
date2 = dateConvert.call(this, date2);
// return > 0 means date1 is later than date2
// return == 0 means date1 is the same day as date2
// return < 0 means date1 is earlier than date2
var diff = date1.getFullYear() - date2.getFullYear();
if (!diff) {
diff = date1.getMonth() - date2.getMonth();
if (!diff) {
diff = date1.getDate() - date2.getDate();
}
}
return diff;
},
sumDays: function(date, n_days) {
var origDateType = typeof date;
obj_date = dateConvert.call(this, date);
obj_date.setDate(obj_date.getDate() + n_days);
return dateConvert.call(this, obj_date, origDateType);
},
dateConvert: function(date, desired_format, dateFormat) {
var from_format = typeof date;
var $this = $(this);
if (from_format == desired_format) {
if (from_format == 'object') {
try {
date.getTime();
} catch (e) {
$.error('Received date is in a non supported format!');
return false;
}
}
return date;
}
if (typeof date == 'undefined') {
date = new Date(0);
}
if (desired_format != 'string' && desired_format != 'object' && desired_format != 'number')
$.error('Date format "'+ desired_format +'" not supported!');
if (!dateFormat) {
var dp_dateFormat = $this.datepicker('option', 'dateFormat');
if (dp_dateFormat) {
dateFormat = dp_dateFormat;
} else {
dateFormat = $.datepicker._defaults.dateFormat;
}
}
// converts to object as a neutral format
switch (from_format) {
case 'object': break;
case 'string': date = $.datepicker.parseDate(dateFormat, date); break;
case 'number': date = new Date(date); break;
default: $.error('Conversion from "'+ from_format +'" format not allowed on jQuery.vboMultiDatesPicker');
}
// then converts to the desired format
switch (desired_format) {
case 'object': return date;
case 'string': return $.datepicker.formatDate(dateFormat, date);
case 'number': return date.getTime();
default: $.error('Conversion to "'+ desired_format +'" format not allowed on jQuery.vboMultiDatesPicker');
}
return false;
},
gotDate: function(date, type) {
if (!type) {
type = 'picked';
}
for (var i = 0; i < this.vboMultiDatesPicker.dates[type].length; i++) {
if (methods.compareDates.call(this, this.vboMultiDatesPicker.dates[type][i], date) === 0) {
return i;
}
}
return false;
},
value: function(value) {
if (value && typeof value == 'string') {
methods.addDates.call(this, value.split(this.vboMultiDatesPicker.separator));
} else {
var dates = methods.getDates.call(this, 'string');
return dates.length
? dates.join(this.vboMultiDatesPicker.separator)
: '';
}
},
getDates: function(format, type) {
if (!format) {
format = 'string';
}
if (!type) {
type = 'picked';
}
switch (format) {
case 'object':
return this.vboMultiDatesPicker.dates[type];
case 'string':
case 'number':
var o_dates = [];
for (var i = 0; i < this.vboMultiDatesPicker.dates[type].length; i++)
o_dates.push(
dateConvert.call(
this,
this.vboMultiDatesPicker.dates[type][i],
format
)
);
return o_dates;
default: $.error('Format "'+format+'" not supported!');
}
},
addDates: function(dates, type) {
if (dates.length > 0) {
if (!type) {
type = 'picked';
}
switch (typeof dates) {
case 'object':
case 'array':
if (dates.length) {
for (var i = 0; i < dates.length; i++)
addDate.call(this, dates[i], type, true);
sortDates.call(this, type);
break;
} // else does the same as 'string'
case 'string':
case 'number':
addDate.call(this, dates, type);
break;
default:
$.error('Date format "'+ typeof dates +'" not allowed on jQuery.vboMultiDatesPicker');
}
} else {
$.error('Empty array of dates received.');
}
},
removeDates: function(dates, type) {
if (!type) {
type = 'picked';
}
var removed = [];
if (Object.prototype.toString.call(dates) === '[object Array]') {
dates.sort(function(a,b) {return b-a});
for (var i = 0; i < dates.length; i++) {
removed.push(removeDate.call(this, dates[i], type));
}
} else {
removed.push(removeDate.call(this, dates, type));
}
return removed;
},
removeIndexes: function(indexes, type) {
if (!type) {
type = 'picked';
}
var removed = [];
if (Object.prototype.toString.call(indexes) === '[object Array]') {
indexes.sort(function(a,b) {return b-a});
for (var i = 0; i < indexes.length; i++) {
removed.push(removeIndex.call(this, indexes[i], type));
}
} else {
removed.push(removeIndex.call(this, indexes, type));
}
return removed;
},
resetDates: function (type) {
if (!type) {
type = 'picked';
}
this.vboMultiDatesPicker.dates[type] = [];
},
toggleDate: function(date, type) {
if (!type) {
type = 'picked';
}
switch (this.vboMultiDatesPicker.mode) {
case 'daysRange':
this.vboMultiDatesPicker.dates[type] = []; // deletes all picked/disabled dates
var end = this.vboMultiDatesPicker.autoselectRange[1];
var begin = this.vboMultiDatesPicker.autoselectRange[0];
if (end < begin) {
end = this.vboMultiDatesPicker.autoselectRange[0];
begin = this.vboMultiDatesPicker.autoselectRange[1];
}
for (var i = begin; i < end; i++) {
methods.addDates.call(this, methods.sumDays.call(this,date, i), type);
}
break;
default:
if (methods.gotDate.call(this, date) === false) {
methods.addDates.call(this, date, type);
} else {
methods.removeDates.call(this, date, type);
}
break;
}
},
setMode: function(options) {
var $this = $(this);
if (options.mode) {
this.vboMultiDatesPicker.mode = options.mode;
}
switch (this.vboMultiDatesPicker.mode) {
case 'normal':
for (var option in options) {
switch (option) {
case 'maxPicks':
case 'minPicks':
case 'pickableRange':
case 'adjustRangeToDisabled':
this.vboMultiDatesPicker[option] = options[option];
break;
// default: $.error('Option ' + option + ' ignored for mode "'.options.mode.'".');
}
}
break;
case 'daysRange':
case 'weeksRange':
var mandatory = 1;
for (option in options) {
switch (option) {
case 'autoselectRange':
mandatory--;
case 'pickableRange':
case 'adjustRangeToDisabled':
this.vboMultiDatesPicker[option] = options[option];
break;
// default: $.error('Option ' + option + ' does not exist for setMode on jQuery.vboMultiDatesPicker');
}
}
if (mandatory > 0) {
$.error('Some mandatory options not specified!');
}
break;
}
if (mdp_events.onSelect) {
mdp_events.onSelect();
}
},
destroy: function() {
this.vboMultiDatesPicker = null;
$(this).datepicker('destroy');
}
};
this.each(function() {
var $this = $(this);
if (!this.vboMultiDatesPicker) {
this.vboMultiDatesPicker = {
dates: {
picked: [],
disabled: []
},
mode: 'normal',
adjustRangeToDisabled: true
};
}
if (methods[method]) {
var exec_result = methods[method].apply(this, Array.prototype.slice.call(mdp_arguments, 1));
switch (method) {
case 'removeDates':
case 'removeIndexes':
case 'resetDates':
case 'toggleDate':
case 'addDates':
var altField = $this.datepicker('option', 'altField');
var dates_string = methods.value.call(this);
if (altField !== undefined && altField != '') {
$(altField).val(dates_string);
}
$this.val(dates_string);
$.datepicker._refreshDatepicker(this);
}
switch (method) {
case 'removeDates':
case 'getDates':
case 'gotDate':
case 'sumDays':
case 'compareDates':
case 'dateConvert':
case 'value':
ret = exec_result;
}
return exec_result;
} else if (typeof method === 'object' || !method) {
return methods.init.apply(this, mdp_arguments);
} else {
$.error('Method ' + method + ' does not exist on jQuery.vboMultiDatesPicker');
}
return false;
});
return ret;
};
$.vboMultiDatesPicker = {version: false};
$.vboMultiDatesPicker.initialized = false;
$.vboMultiDatesPicker.uuid = new Date().getTime();
$.vboMultiDatesPicker.version = $.ui.vboMultiDatesPicker.version;
/**
* Allows MDP not to hide everytime a date is picked.
*/
$(function() {
/**
* Use a constant instead of a property object to break the loop caused
* my modal (AJAX) duplicate rendering.
*/
// $.vboMultiDatesPicker._hideDatepicker = $.datepicker._hideDatepicker;
const vboMultiDatesPicker_hideDatepicker = $.datepicker._hideDatepicker;
$.datepicker._hideDatepicker = function() {
/**
* Prevent errors with inline datepickers when _curInst is null.
*
* @see modified from original source code
*/
if (!this._curInst) {
return;
}
var target = this._curInst.input[0];
var mdp = target.vboMultiDatesPicker;
if (!mdp || (this._curInst.inline === false && !mdp.changed)) {
return vboMultiDatesPicker_hideDatepicker.apply(this, arguments);
} else {
mdp.changed = false;
$.datepicker._refreshDatepicker(target);
return;
}
};
});
/**
* VikBooking - DatesRangePicker declaration.
*
* @param string|object checkin Check-in input field selector or element.
* @param string|object checkout Check-out input field selector or element.
* @param object options Datepicker options.
*/
$.vboDatesRangePicker = function(checkin, checkout, options) {
if (typeof checkin === 'string') {
checkin = $(checkin);
}
if (typeof checkout === 'string') {
checkout = $(checkout);
}
if (!checkin || !checkout || !checkin.length || !checkout.length) {
throw new Error('Invalid vboDatesRangePicker check-in/check-out selectors.');
}
// ensure the current jQuery version is supported (v1.x is NOT supported)
let jq_version = $.fn.jquery || '3';
if (typeof jq_version === 'string' && jq_version.substring(0, 1) === '1') {
// fallback to regular datepicker
$(checkin).datepicker(options);
$(checkout).datepicker(options);
// abort
throw new Error('Unsupported jQuery version');
}
if (typeof options !== 'object') {
options = {};
}
// get the input fields for check-in and check-out
const inputCheckin = options?.altFields?.checkin ? $(options.altFields.checkin) : checkin;
const inputCheckout = options?.altFields?.checkout ? $(options.altFields.checkout) : checkout;
// get currently populated dates, if any
let prevCheckinDate = inputCheckin.val();
let prevCheckoutDate = inputCheckout.val();
const dates = [prevCheckinDate, prevCheckoutDate].filter(d => d);
// date range picker configuration
// 3 picks needed to rectify the completed selection
options.maxPicks = 3;
options.addDates = dates.length ? dates : null;
if (typeof options.altFormat !== 'undefined') {
// option not supported
delete options.altFormat;
}
// default configuration arguments
if (typeof options.numberOfMonths === 'undefined') {
options.numberOfMonths = 2;
}
if (typeof options.minDate === 'undefined') {
options.minDate = new Date;
} else {
// ensure minDate is an object
options.minDate = $(this).vboDatesRangePicker('convertPeriod', options.minDate);
}
const _onSelect = options.onSelect ?? null;
options.onSelect = function(date, instance) {
let pickedDates = $(this).vboMultiDatesPicker('getDates');
setTimeout(() => {
// clear the active status to prevent conflicts in case of deselection
let dateObj = $.datepicker.parseDate($(this).datepicker('option', 'dateFormat'), date);
const dayCell = $('.ui-datepicker').find('td.date-' + dateObj.getFullYear() + '-' + dateObj.getMonth() + '-' + dateObj.getDate());
dayCell.find('.ui-state-active').removeClass('ui-state-active');
dayCell.removeClass('ui-datepicker-current-day');
// resolve possible conflicts with tooltip
dayCell.trigger('mouseenter');
}, 10);
if (pickedDates.length > 2) {
// remove all the dates from the selection, except for the last picked one
$(this).vboMultiDatesPicker('removeDates', pickedDates.filter(d => d != date));
// adjust the picked dates
pickedDates = [date];
}
// populate checkin and checkout fields
if (pickedDates.length == 1) {
inputCheckin.val(pickedDates[0]);
inputCheckout.val('');
} else if (pickedDates.length == 2) {
inputCheckin.val(pickedDates[0]);
inputCheckout.val(pickedDates[1]);
} else {
inputCheckin.val('');
inputCheckout.val('');
}
/**
* @see Natively inline datepickers not rendered on input fields cannot be hid!
*/
// prevent the datepicker auto-hiding after selecting a date
instance.inline = true;
setTimeout(() => {
// remove the inline state after completing the selection process,
// this aims to re-allow the datepicker closure when clicking outside
instance.inline = false;
});
if (_onSelect) {
// propagate selection behavior
_onSelect(date, instance);
}
if (pickedDates.length == 2) {
setTimeout(() => {
// prepare the very next click outside to hide the datepicker rather than 2 next clicks
checkin.datepicker('hide');
});
if (options?.environment?.autoHide) {
// do not allow to rectify the selection, but rather hide the DRP on selection completed
setTimeout(() => {
checkin.datepicker('hide');
checkin.trigger('blur');
}, (options?.environment?.autoHideDelay || 100));
}
}
};
const _beforeShowDay = options.beforeShowDay ?? null;
options.beforeShowDay = function(date) {
// get the DRP configuration
let config = $(this).vboDatesRangePicker('drpoption');
// current dates
const pickedDates = $(this).vboMultiDatesPicker('getDates');
const isPicked = pickedDates.some(dt => dt == date);
// in case the date has been selected, use it as min date
// in case the date has been deselected, use the first date in the array (config min date if empty selection)
let minDate = isPicked ? date : (pickedDates[0] || config.minDate);
// in case we have 0 or 2 selected dates, ignore the minimum date
// in case we have 1 or 3 selected dates, use the new date
minDate = pickedDates.length % 2 ? minDate : config.minDate;
// class identifier to easily select the table cell from a date object
const dateIdClass = 'date-' + date.getFullYear() + '-' + date.getMonth() + '-' + date.getDate();
// in case of past date, disable cell
if ($(this).vboDatesRangePicker('compareDates', date, minDate) < 0) {
return [false, 'date-past ' + dateIdClass, null];
}
// build date return values
let returnVal = [true, '', null];
// whether the date is known
let knownDate = false;
if (pickedDates.length > 0) {
// date format
let dateFormat = $(this).datepicker('option', 'dateFormat');
// get check-in date
let checkinDate = $.datepicker.parseDate(dateFormat, pickedDates[0]);
if ($(this).vboDatesRangePicker('compareDates', checkinDate, date) == 0) {
// parsing the check-in date
let checkinDayTitle = config?.labels?.checkin || null;
if (pickedDates.length == 1 && parseInt((config?.checkoutConstraints?.minStayNights || 0)) > 1) {
if (typeof config?.labels?.minStayNights === 'function') {
checkinDayTitle = config.labels.minStayNights.call(null, config.checkoutConstraints.minStayNights);
} else if (typeof config?.labels?.minStayNights === 'string') {
checkinDayTitle = config.labels.minStayNights;
}
}
returnVal = [true, 'checkin-date' + (pickedDates.length == 1 ? ' without-checkout-date' : ''), checkinDayTitle];
knownDate = true;
} else if (pickedDates.length == 2) {
// get check-out date
let checkoutDate = $.datepicker.parseDate(dateFormat, pickedDates[1]);
if ($(this).vboDatesRangePicker('compareDates', checkoutDate, date) == 0) {
// parsing the check-out date
let checkoutDayTitle = config?.labels?.checkout || null;
returnVal = [true, 'checkout-date', checkoutDayTitle];
knownDate = true;
} else if (checkinDate < date && date < checkoutDate) {
// parsing a date between the check-in and the check-out
returnVal = [true, 'checkin-checkout-inner', null];
knownDate = true;
}
}
}
if (_beforeShowDay && !knownDate) {
// call the registered methods for validating the current date
returnVal = _beforeShowDay(date);
}
if (returnVal[2]) {
// in case of tooltip title text, append a specific class
returnVal[1] += ' date-tooltip';
if (returnVal[2].length > 20) {
// this is a large tooltip text
returnVal[1] += ' date-tooltip-large';
}
// determine the tooltip position
let firstwday = $(this).vboDatesRangePicker('option', 'firstDay');
if (date.getDay() == firstwday) {
// first day of week
returnVal[1] += ' date-tooltip-firstwday';
} else {
// calculate last week-day index
let lastwday = firstwday - 1;
lastwday = lastwday < 0 ? 6 : lastwday;
if (date.getDay() == lastwday) {
// last day of week
returnVal[1] += ' date-tooltip-lastwday';
}
}
}
// append the class for this exact day and trim
returnVal[1] += ' ' + dateIdClass;
returnVal[1] = returnVal[1].trim();
return returnVal;
}
/**
* The beforeShow function will NOT be called for inline datepickers.
*/
const _beforeShow = options.beforeShow ?? null;
const _mouseEnter = (inlineElement) => {
// determine the proper datepicker container
let dpContainer = typeof inlineElement === 'undefined' ? $('.ui-datepicker').not('.ui-datepicker-inline') : $(inlineElement).find('.ui-datepicker');
dpContainer.on('mouseenter', 'td', function() {
let pickedDates = [];
try {
pickedDates = $(checkin).vboMultiDatesPicker('getDates');
} catch (e) {
// silently abort in case of elements not being controlled through DRP
return;
}
if ($(this).hasClass('date-tooltip')) {
let title = $(this).attr('title');
if (title) {
// replace native title attribute with data attribute for tooltip
$(this).attr('title', '');
$(this).attr('data-title', title);
}
}
if (pickedDates.length == 1) {
// get the proper date format in case it differs from regional values
let dateFormat = $(checkin).datepicker('option', 'dateFormat') || options.dateFormat;
const checkinDate = $.datepicker.parseDate(dateFormat, pickedDates[0]);
const checkoutDate = new Date;
// 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)
// without doing so, the Date object constructed would get an extra month, and so the date would not be the desired one
checkoutDate.setDate(1);
checkoutDate.setFullYear($(this).data('year'));
checkoutDate.setMonth($(this).data('month'));
checkoutDate.setDate($(this).text());
$('.checkout-date').removeClass('checkout-date date-will');
$('.checkin-checkout-inner').removeClass('checkin-checkout-inner');
if ($(checkin).vboDatesRangePicker('compareDates', checkoutDate, checkinDate) == 0) {
return;
}
$('.date-' + checkoutDate.getFullYear() + '-' + checkoutDate.getMonth() + '-' + checkoutDate.getDate()).addClass('checkout-date date-will');
checkoutDate.setDate(checkoutDate.getDate() - 1);
while ($(checkin).vboDatesRangePicker('compareDates', checkoutDate, checkinDate) > 0) {
$('.date-' + checkoutDate.getFullYear() + '-' + checkoutDate.getMonth() + '-' + checkoutDate.getDate()).addClass('checkin-checkout-inner');
checkoutDate.setDate(checkoutDate.getDate() - 1);
}
}
});
}
options.beforeShow = function(input, instance) {
// register mouseenter event on date cells
_mouseEnter();
if (_beforeShow) {
// propagate show behavior
_beforeShow(input, instance);
}
};
/**
* The onUpdateDatepicker function will NOT be called for inline datepickers.
*/
const _onUpdateDatepicker = options.onUpdateDatepicker ?? null;
options.onUpdateDatepicker = function(instance) {
// get the DRP configuration
let config = $(this).vboDatesRangePicker('drpoption');
if (typeof config?.bottomCommands === 'object') {
if ($('.vbo-drp-commands-bottom').length) {
return;
}
// build bottom commands
let btmCommands = $('<div></div>').addClass('vbo-drp-commands-bottom');
// clear dates
let clearCommand = $('<div></div>')
.addClass('vbo-drp-command vbo-drp-command-clear')
.append(
$('<a></a>')
.attr('href', 'JavaScript: void(0);')
.text((config.bottomCommands?.clear || 'Clear dates'))
.on('click', () => {
$(this).vboDatesRangePicker('setDates', []);
$('#ui-datepicker-div').find('.ui-state-active').removeClass('ui-state-active');
$('#ui-datepicker-div').find('.ui-datepicker-current-day').removeClass('ui-datepicker-current-day');
if (typeof config.bottomCommands?.onClear === 'function') {
// invoke the provided method in case something needs to be cleared from the UI
config.bottomCommands.onClear.call(this);
}
})
);
btmCommands.append(clearCommand);
// close DRP
let closeCommand = $('<div></div>')
.addClass('vbo-drp-command vbo-drp-command-close')
.append(
$('<button></button>')
.addClass('btn btn-small ' + (config?.environment?.section === 'admin' ? 'vbo-dark-btn' : 'vbo-pref-color-btn'))
.attr('type', 'button')
.text((config.bottomCommands?.close || 'Close'))
.on('click', () => {
$(this).datepicker('hide');
})
);
btmCommands.append(closeCommand);
// append bottom commands to DRP
$('#ui-datepicker-div').append(btmCommands);
}
if (_onUpdateDatepicker) {
// propagate onUpdateDatepicker behavior
_onUpdateDatepicker(instance);
}
};
/**
* Define the onClose behavior.
*/
const _onClose = options.onClose ?? null;
options.onClose = function(dateText, instance) {
// unregister mouseenter event from every non-inline datepicker
$('.ui-datepicker').not('.ui-datepicker-inline').off('mouseenter', 'td');
if (_onClose) {
// propagate close behavior
_onClose(dateText, instance);
}
};
// check for regional default settings
if (options?.environment?.section === 'admin') {
let regionalDefaults = $.datepicker.regional['vikbooking'];
if (typeof regionalDefaults === 'object') {
// merge regional settings with DRP options
options = Object.assign(regionalDefaults, options);
}
}
// determine whether the datepicker is inline
let isInline = !$(checkin).is('input[type="text"]');
// instantiate multi-dates picker for a single range of dates
$(checkin).vboMultiDatesPicker(options);
// trigger datepicker opening when focusing the check-out field too
$(checkout).on('focus', () => {
$(checkin).datepicker('show');
});
// restore the initial check-in value
inputCheckin.val(prevCheckinDate);
if (isInline) {
// manually register the mouseenter event since the beforeShow won't run
setTimeout(() => {
_mouseEnter(checkin);
}, 100);
} else {
// add class to the check-out field to identify the trigger
$(checkout).addClass('vbo-drp-inp-trigger');
}
}
/**
* VikBooking - DatesRangePicker jQuery plugin.
*
* @param any method Either string for getter/setter, or object to start the DRP.
* @param any options Initialization object options, or mixed value for setter.
* @param any setvalue The value to set within the datepicker in case of "option" setter.
*/
$.fn.vboDatesRangePicker = function(method, options, setvalue) {
if (!method) {
// initialize the DRP
method = {};
}
// immediately exit in case of no elements found
if ($(this).length == 0) {
return this;
}
/**
* Initializes the DRP calendar.
*/
const init = (options) => {
if (!options.dateFormat) {
options.dateFormat = 'yy-mm-dd';
}
// set DRP cloned configuration data
drpConfig(Object.assign({}, options));
// element selector
let that = $(this);
// handle native datepicker method beforeShowDay
if (options.beforeShowDay) {
// get registered callbacks
let beforeShowDayCheckin = options.beforeShowDay.checkin;
let beforeShowDayCheckout = options.beforeShowDay.checkout;
// delete native property
delete options.beforeShowDay;
// register native property
options.beforeShowDay = (date) => {
// get currently selected dates
let pickedDates = that.vboMultiDatesPicker('getDates');
// default date selectable state
let isSelectable = true;
let className = '';
let tooltipText = null;
// invoke callbacks by injecting the proper arguments
if ((!pickedDates.length || pickedDates.length == 2) && typeof beforeShowDayCheckin === 'function') {
// validate cell for check-in selection
let validation = beforeShowDayCheckin.call(that, date);
// update cell states
isSelectable = validation[0];
className = validation[1] || className;
tooltipText = validation[2] || tooltipText;
} else if (pickedDates.length == 1 && typeof beforeShowDayCheckout === 'function') {
// validate cell for check-out selection
let validation = beforeShowDayCheckout.call(that, date);
// update cell states
isSelectable = validation[0];
className = validation[1] || className;
tooltipText = validation[2] || tooltipText;
}
if (!pickedDates.length) {
// unset any previously set checkout constraints when no dates selected
unsetCheckoutConstraints(date);
} else if (pickedDates.length == 1) {
// before showing the check-out day, validate the constraints, if any
let constrainData = validateCheckoutConstraints(date, [isSelectable, className, tooltipText], pickedDates[0]);
// update cell states
isSelectable = constrainData[0];
className = constrainData[1];
tooltipText = constrainData[2];
}
return [
// whether it's selectable
(isSelectable ? true : false),
// CSS class name to add
className,
// tooltip text
tooltipText,
];
};
}
// handle native datepicker method onSelect
if (options.onSelect) {
// get registered callbacks
let onSelectCheckin = options.onSelect.checkin;
let onSelectCheckout = options.onSelect.checkout;
// delete native property
delete options.onSelect;
// register native property
options.onSelect = (selectedDate) => {
// get currently selected dates
let pickedDates = that.vboMultiDatesPicker('getDates');
// invoke callbacks by injecting the proper arguments
if (pickedDates.length == 1 && typeof onSelectCheckin === 'function') {
// call the registered check-in function
onSelectCheckin.call(that, selectedDate);
} else if (pickedDates.length == 2 && typeof onSelectCheckout === 'function') {
// call the registered check-out function
onSelectCheckout.call(that, selectedDate);
}
};
}
// handle datepicker alternate field
if (options?.altFields?.checkin) {
// set native alternate field
options.altField = options.altFields.checkin;
}
// render DRP
$.vboDatesRangePicker(that, $(options.checkout), options);
// threshold for responsiveness
let thresholdWidth = options?.responsiveNumMonths?.threshold || 860;
// handle responsive number of months
if (options?.responsiveNumMonths && (options?.numberOfMonths || 1) > 1) {
// configure responsive number of months
options._onResizeWindow = () => {
let windowWidth = window.innerWidth;
if (windowWidth && windowWidth <= thresholdWidth) {
// just one month
that.datepicker('option', 'numberOfMonths', 1);
} else {
// use the number of months configured
that.datepicker('option', 'numberOfMonths', options.numberOfMonths);
}
};
// remove duplicate event listeners
window.removeEventListener('resize', options._onResizeWindow);
// register event listener
window.addEventListener('resize', options._onResizeWindow);
// call method on init
options._onResizeWindow();
}
// handle input fields focus/blur on mobile devices
if (options?.environment?.section === 'admin' && that.is('input[type="text"]')) {
// disable input fields focus on small screen resolutions
let windowWidth = window.innerWidth;
if (windowWidth && windowWidth <= thresholdWidth) {
// disable focus on check-in input field
that.on('focus', function() {
$(this).blur();
});
if ($(options.checkout).is('input[type="text"]')) {
// disable focus on check-out input field
$(options.checkout).on('focus', function() {
$(this).blur();
});
}
}
}
return this;
}
/**
* Setter or getter for the DRP calendar.
*/
const drpConfig = (options) => {
if (typeof options === 'undefined') {
// GETTER: return DRP configuration.
// Clone the object in order to prevent manual edits to
// the configuration properties.
return Object.assign({}, $(this).data('vboDrpConfig'));
}
// SETTER: update DRP configuration
return $(this).data('vboDrpConfig', options);
}
/**
* Converts a period string like "+1d" into a Date object.
*
* @param string period The date period string.
* @param Date fromDate Optional Date object from.
*
* @return Date
*/
const convertPeriod = (period, fromDate) => {
if (!fromDate || !(fromDate instanceof Date)) {
fromDate = new Date;
}
if (typeof period !== 'string') {
// period must be a string
return fromDate;
}
let instructions = period.match(/^[\+\-]?([0-9]+)(d|w|m|y)$/i);
if (!instructions || instructions.length != 3) {
// period not matched
return fromDate;
}
// period number of days/weeks/months/year
let num = parseInt(instructions[1]);
if (num === 0) {
// same day period ("0d")
return fromDate;
}
// period modifier
if (period.substring(0, 1) == '-') {
// turn number into negative
num = num - (num * 2);
}
switch ((instructions[2] + '').toLowerCase()) {
case 'd':
fromDate.setDate(fromDate.getDate() + num);
break;
case 'w':
fromDate.setDate(fromDate.getDate() + (7 * num));
break;
case 'm':
fromDate.setMonth(fromDate.getMonth() + num);
break;
case 'y':
fromDate.setFullYear(fromDate.getFullYear() + num);
break;
}
return fromDate;
}
/**
* Compares two date objects against each other with year, month, day.
*
* @param string|Date date1 The first date.
* @param string|Date date2 The second date.
*
* @return int Greater than 0 means date1 is after date2.
* Equal to 0 means date1 is the same as date2.
* Less than 0 means date1 is before date2.
*/
const compareDates = (date1, date2) => {
if (!date1) {
// no empty dates allowed, default to today
date1 = new Date;
}
if (!date2) {
// no empty dates allowed, default to today
date2 = new Date;
}
// get the DRP configuration
let config = drpConfig();
if (typeof date1 === 'string') {
try {
// convert date string to date object
date1 = $.datepicker.parseDate(config.dateFormat, date1);
} catch (e) {
// attempt to convert a period string into a date object
date1 = convertPeriod(date1);
}
}
if (typeof date2 === 'string') {
try {
// convert date string to date object
date2 = $.datepicker.parseDate(config.dateFormat, date2);
} catch (e) {
// attempt to convert a period string into a date object
date2 = convertPeriod(date2);
}
}
// year check
let diff = date1.getFullYear() - date2.getFullYear();
if (!diff) {
// month check
diff = date1.getMonth() - date2.getMonth();
if (!diff) {
// day check
diff = date1.getDate() - date2.getDate();
}
}
return diff;
}
/**
* Sets new dates in the DRP calendar.
*
* @param array dates List of dates to set.
*
* @return self
*/
const setDates = (dates) => {
if (!Array.isArray(dates)) {
throw new Error('Invalid dates argument');
}
// filter out empty dates
dates = dates.filter(d => d);
// get the DRP configuration
let config = drpConfig();
// map first the given dates to date strings in the desired format
dates = dates.map((dt) => {
if (typeof dt === 'object') {
// convert date object into date string
dt = $.datepicker.formatDate(config.dateFormat, dt);
}
return dt;
});
// remove all dates from the current selection
let pickedDates = $(this).vboMultiDatesPicker('getDates');
$(this).vboMultiDatesPicker('removeDates', pickedDates);
if (dates.length) {
// set new date(s)
$(this).vboMultiDatesPicker('addDates', dates);
} else {
// remove any active cell state
$('.ui-state-active').removeClass('ui-state-active');
$('.ui-datepicker-current-day').removeClass('ui-datepicker-current-day');
}
// get the input fields for check-in and check-out
let inputCheckin = config?.altFields?.checkin ? $(config.altFields.checkin) : $(this);
let inputCheckout = config?.altFields?.checkout ? $(config.altFields.checkout) : $(config.checkout);
// populate checkin and checkout fields
if (dates.length == 1) {
inputCheckin.val(dates[0]);
inputCheckout.val('');
} else if (dates.length == 2) {
inputCheckin.val(dates[0]);
inputCheckout.val(dates[1]);
} else {
inputCheckin.val('');
inputCheckout.val('');
}
return this;
}
/**
* Handles the registration of the check-out update upon choosing the check-in.
* Expected actions to perform are: minDate, maxDate, setCheckoutDate.
*
* @param string action The action to perform.
* @param any value The value for the action to perform.
*
* @return self
*/
const handleCheckoutConstraints = (action, value) => {
if (typeof action !== 'string') {
throw new Error('Invalid arguments for handleCheckoutConstraints');
}
// access DRP configuration
let config = drpConfig();
if (action.match(/^setcheckoutdate$/i)) {
// do NOT update the check-out date unless running in legacy mode
// with two native datepicker calendars, of if unsetting the date.
if (config?.legacy || !value) {
return $(this).vboDatesRangePicker('setCheckoutDate', value);
}
// abort
return this;
}
if (config?.legacy) {
// apply the requested option to the check-out field
return $(config.checkout).datepicker('option', action, value);
}
// build new settings
let newConfig = Object.assign({}, config);
// inject action value
newConfig.checkoutConstraints = newConfig.checkoutConstraints || {};
newConfig.checkoutConstraints[action] = value;
// update DRP configuration
drpConfig(newConfig)
return this;
}
/**
* Validates the current check-out constraints against the given date.
*
* @param Date date The date object to validate.
* @param Array validation The default date validation array of values for "beforeShowDay".
* @param string|Date checkin Optional check-in date.
*
* @return Array The new validation array of values (selectable, class-name, tooltip-text).
*/
const validateCheckoutConstraints = (date, validation, checkin) => {
// access DRP configuration
let config = drpConfig();
if (!Array.isArray(validation) || !validation.length) {
// set default date validation array
validation = [true, '', null];
}
if (config?.legacy || !(date instanceof Date)) {
return validation;
}
if (typeof config?.checkoutConstraints !== 'object') {
return validation;
}
if ((validation[1] + '').match(/checkin\-checkout\-inner/i)) {
// no need to validate a date between the selected range of dates
return validation;
}
// check if the current date is actually the check-in date
let isCheckin = false;
if (checkin) {
isCheckin = !compareDates(date, checkin);
}
// get a list of both action and value constraints
let actions = Object.keys(config.checkoutConstraints);
let values = Object.values(config.checkoutConstraints);
// scan all actions and values
actions.forEach((action, index) => {
if (typeof action !== 'string') {
return;
}
// validate minDate
if (action.match(/^mindate$/i) && (values[index] instanceof Date)) {
let isPast = compareDates(date, values[index]) < 0;
if (isPast && !isCheckin) {
// date should not be selectable
validation[0] = false;
}
}
// validate maxDate
if (action.match(/^maxdate$/i) && (values[index] instanceof Date)) {
let isBeyond = compareDates(date, values[index]) > 0;
if (isBeyond && !isCheckin) {
// date should not be selectable
validation[0] = false;
}
}
});
return validation;
}
/**
* Unsets any previously set check-out constraint.
*
* @param Date date The date object being validated.
*
* @return void
*/
const unsetCheckoutConstraints = (date) => {
// access DRP configuration
let config = drpConfig();
if (config.hasOwnProperty('checkoutConstraints')) {
// delete configuration object property
delete config.checkoutConstraints;
// update DRP configuration
drpConfig(config);
}
}
/**
* Initializes the DRP calendar.
*/
if (typeof method === 'object') {
return init(method);
}
/**
* Compares two dates.
*/
if (typeof method === 'string' && method.match(/^comparedates$/i)) {
return compareDates(options, setvalue);
}
/**
* Converts a period string into a date object.
*/
if (typeof method === 'string' && method.match(/^convertperiod$/i)) {
return convertPeriod(options, setvalue);
}
/**
* Aliasing dates retrieval.
*/
if (typeof method === 'string' && method.match(/^getdates$/i)) {
return $(this).vboMultiDatesPicker('getDates');
}
/**
* Aliasing check-in date retrieval.
*/
if (typeof method === 'string' && method.match(/^getcheckindate$/i)) {
let pickedDates = $(this).vboMultiDatesPicker('getDates', 'object');
return pickedDates[0] || null;
}
/**
* Aliasing check-out date retrieval.
*/
if (typeof method === 'string' && method.match(/^getcheckoutdate$/i)) {
let pickedDates = $(this).vboMultiDatesPicker('getDates', 'object');
return pickedDates[1] || null;
}
/**
* Sets new dates in the DRP.
*/
if (typeof method === 'string' && method.match(/^setdates$/i)) {
return setDates(options);
}
/**
* Sets the check-in date in the DRP.
*/
if (typeof method === 'string' && method.match(/^setcheckindate$/i)) {
// get current dates
let pickedDates = $(this).vboMultiDatesPicker('getDates');
// build new dates
let newDates = Array.isArray(options) ? options : [options];
if (!newDates[0]) {
// we are actually unsetting the check-in date
newDates = [];
pickedDates = [];
}
if (pickedDates[1]) {
// push existing check-out date
newDates.push(pickedDates[1]);
}
return setDates(newDates);
}
/**
* Sets the check-out date in the DRP.
*/
if (typeof method === 'string' && method.match(/^setcheckoutdate$/i)) {
// get current dates
let pickedDates = $(this).vboMultiDatesPicker('getDates');
if (!pickedDates.length) {
// abort if no dates currently selected, or if multiple dates given
return this;
}
// build new dates
let newDates = [pickedDates[0], Array.isArray(options) ? options[0] : options];
return setDates(newDates);
}
/**
* Hides the DRP calendar, as long as it is not rendered inline.
*/
if (typeof method === 'string' && method.match(/^hide$/i)) {
return $(this).datepicker('hide');
}
/**
* Setter or getter for the native datepicker calendar.
*/
if (method === 'option') {
// native datepicker option getter or setter
if (typeof setvalue === 'undefined') {
// native datepicker option getter
return $(this).datepicker('option', options);
}
// native datepicker option setter
return $(this).datepicker('option', options, setvalue);
}
/**
* Gets one or all DRP options, or invokes a DRP function.
*/
if (method === 'drpoption') {
// access DRP configuration
let config = drpConfig();
if (!options) {
// return the whole DRP configuration object
return config;
}
if (typeof options === 'string' && options.match(/^.+\..+$/i)) {
// check for nested configuration object properties (i.e. "beforeShowDay.checkin")
let macroOption = options;
macroOption.split('.').forEach((prop) => {
if (typeof config === 'object' && config.hasOwnProperty(prop) && typeof config[prop] === 'object') {
config = config[prop];
} else {
options = prop;
}
});
}
if (typeof setvalue === 'undefined') {
// DRP option getter
return config[options] || null;
}
if (typeof config[options] !== 'function') {
throw new Error('Invalid DRP function invoked (' + options + ')');
}
// call the requested method
return Array.isArray(setvalue) ? config[options].apply(this, setvalue) : config[options].call(this, setvalue);
}
/**
* Handles check-out update operations upon selecting check-in.
*/
if (method === 'checkout') {
return handleCheckoutConstraints(options, setvalue);
}
}
}));