daterangepicker-4.x
Version:
Date range picker with time component and pre-defined ranges
822 lines (740 loc) • 130 kB
JavaScript
// Following the UMD template https://github.com/umdjs/umd/blob/master/templates/returnExportsGlobal.js
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Make globaly available as well
define(['luxon', 'jquery'], function (luxon, jquery) {
if (!jquery.fn) jquery.fn = {}; // webpack server rendering
if (typeof luxon !== 'function' && luxon.hasOwnProperty('default')) luxon = luxon['default']
return factory(luxon, jquery);
});
} else if (typeof module === 'object' && module.exports) {
// Node / Browserify
//isomorphic issue
var jQuery = (typeof window != 'undefined') ? window.jQuery : undefined;
if (!jQuery) {
jQuery = require('jquery');
if (!jQuery.fn) jQuery.fn = {};
}
var luxon = (typeof window != 'undefined' && typeof window.luxon != 'undefined') ? window.luxon : require('luxon');
module.exports = factory(luxon, jQuery);
} else {
// Browser globals
root.daterangepicker = factory(root.luxon, root.jQuery);
}
}(typeof window !== 'undefined' ? window : this, function (luxon, $) {
const DateTime = luxon.DateTime;
const Duration = luxon.Duration;
const Info = luxon.Info;
const Settings = luxon.Settings;
/**
* @constructs DateRangePicker
* @param {external:jQuery} element - jQuery selector of the parent element that the date range picker will be added to
* @param {Options} options - Object to configure the DateRangePicker
* @param {function} cb - Callback function executed when
*/
var DateRangePicker = function (element, options, cb) {
/**
* Options for DateRangePicker
* @typedef Options
* @property {string} parentEl=body - {@link https://api.jquery.com/category/selectors/|jQuery selector} of the parent element that the date range picker will be added to
* @property {external:DateTime|external:Date|string} startDate - Default: `DateTime.now().startOf('day')`<br/>The beginning date of the initially selected date range.<br/>
* Must be a `luxon.DateTime` or `Date` or `string` according to {@link https://en.wikipedia.org/wiki/ISO_8601|ISO-8601} or a string matching `locale.format`.<br/>
* Date value is rounded to match option `timePickerStepSize`<br/>
* Option `isInvalidDate` and `isInvalidTime` are not evaluated, you may set date/time which is not selectable in calendar.<br/>
* If the date does not fall into `minDate` and `maxDate` then date is shifted and a warning is written to console.
* @property {external:DateTime|external:Date|string} endDate - Defautl: `DateTime.now().endOf('day')`<br/>The end date of the initially selected date range.<br/>
* Must be a `luxon.DateTime` or `Date` or `string` according to {@link https://en.wikipedia.org/wiki/ISO_8601|ISO-8601} or a string matching `locale.format`.<br/>
* Date value is rounded to match option `timePickerStepSize`<br/>
* Option `isInvalidDate`, `isInvalidTime` and `minSpan`, `maxSpan` are not evaluated, you may set date/time which is not selectable in calendar.<br/>
* If the date does not fall into `minDate` and `maxDate` then date is shifted and a warning is written to console.<br/>
* @property {external:DateTime|external:Date|string|null} minDate - The earliest date a user may select or `null` for no limit.<br/>
* Must be a `luxon.DateTime` or `Date` or `string` according to {@link https://en.wikipedia.org/wiki/ISO_8601|ISO-8601} or a string matching `locale.format`.
* @property {external:DateTime|external:Date|string|null} maxDate - The latest date a user may select or `null` for no limit.<br/>
* Must be a `luxon.DateTime` or `Date` or `string` according to {@link https://en.wikipedia.org/wiki/ISO_8601|ISO-8601} or a string matching `locale.format`.
* @property {external:Duration|string|number|null} minSpan - The maximum span between the selected start and end dates.<br/>
* Must be a `luxon.Duration` or number of seconds or a string according to {@link https://en.wikipedia.org/wiki/ISO_8601|ISO-8601} duration.<br/>
* Ignored when `singleDatePicker: true`
* @property {external:Duration|string|number|null} maxSpan - The minimum span between the selected start and end dates.<br/>
* Must be a `luxon.Duration` or number of seconds or a string according to {@link https://en.wikipedia.org/wiki/ISO_8601|ISO-8601} duration.<br/>
* Ignored when `singleDatePicker: true`
* @property {external:DateTime|external:Date|string|null} initalMonth - Default: `DateTime.now().startOf('month')`<br/>
* The inital month shown when `startDate: null`. Be aware, the attached `<input>` element must be also empty.`<br/>
* Must be a `luxon.DateTime` or `Date` or `string` according to {@link https://en.wikipedia.org/wiki/ISO_8601|ISO-8601} or a string matching `locale.format`.<br/>
* When `initalMonth` is used, then `endDate` is ignored and it works only with `timePicker: false`
* @property {boolean} autoApply=false - Hide the `Apply` and `Cancel` buttons, and automatically apply a new date range as soon as two dates are clicked.<br/>
* Only useful when `timePicker: false`
* @property {boolean} singleDatePicker=false - Show only a single calendar to choose one date, instead of a range picker with two calendars.<br/>
* The start and end dates provided to your callback will be the same single date chosen.
* @property {boolean} singleMonthView=false - Show only a single month calendar, useful when typically selected ranges are rather short.<br/>
* Ignored for `singleDatePicker: true`.
* @property {boolean} showDropdowns=false - Show year and month select boxes above calendars to jump to a specific month and year
* @property {number} minYear - Default: `DateTime.now().minus({year:100}).year`<br/>The minimum year shown in the dropdowns when `showDropdowns: true`
* @property {number} maxYear - Default: `DateTime.now().plus({year:100}).year`<br/>The maximum year shown in the dropdowns when `showDropdowns: true`
* @property {boolean} showWeekNumbers=false - Show **localized** week numbers at the start of each week on the calendars
* @property {boolean} showISOWeekNumbers=false - Show **ISO** week numbers at the start of each week on the calendars.<br/>
* Takes precedence over localized `showWeekNumbers`
* @property {boolean} timePicker=false - Adds select boxes to choose times in addition to dates
* @property {boolean} timePicker24Hour=true - Use 24-hour instead of 12-hour times, removing the AM/PM selection
* @property {external:Duration|string|number} timePickerStepSize - Default: `Duration.fromObject({minutes:1})`<br/>Set the time picker step size.<br/>
* Must be a `luxon.Duration` or the number of seconds or a string according to {@link https://en.wikipedia.org/wiki/ISO_8601|ISO-8601} duration.<br/>
* Valid values are 1,2,3,4,5,6,10,12,15,20,30 for `Duration.fromObject({seconds: ...})` and `Duration.fromObject({minutes: ...})`
* and 1,2,3,4,6,(8,12) for `Duration.fromObject({hours: ...})`.<br/>
* Duration must be greater than `minSpan` and smaller than `maxSpan`.<br/>
* For example `timePickerStepSize: 600` will disable time picker seconds and time picker minutes are set to step size of 10 Minutes.<br/>
* Overwrites `timePickerIncrement` and `timePickerSeconds`, ignored when `timePicker: false`
* @property {boolean} timePickerSeconds=boolean - **Deprecated**, use `timePickerStepSize`<br/>Show seconds in the timePicker
* @property {boolean} timePickerIncrement=1 - **Deprecated**, use `timePickerStepSize`<br/>Increment of the minutes selection list for times
* @property {boolean} autoUpdateInput=true - Indicates whether the date range picker should instantly update the value of the attached `<input>`
* element when the selected dates change.<br/>The `<input>` element will be always updated on `Apply` and reverted when user clicks on `Cancel`.
* @property {string} onOutsideClick=apply - Defines what picker shall do when user clicks outside the calendar.
* `'apply'` or `'cancel'`. Event {@link #event_outsideClick.daterangepicker|onOutsideClick.daterangepicker} is always emitted.
* @property {boolean} linkedCalendars=true - When enabled, the two calendars displayed will always be for two sequential months (i.e. January and February),
* and both will be advanced when clicking the left or right arrows above the calendars.<br/>
* When disabled, the two calendars can be individually advanced and display any month/year
* @property {function} isInvalidDate=false - A function that is passed each date in the two calendars before they are displayed,<br/>
* and may return `true` or `false` to indicate whether that date should be available for selection or not.<br/>
* Signature: `isInvalidDate(date)`<br/>
* Function has no effect on date values set by `startDate`, `endDate`, `ranges`, {@link #DateRangePicker+setStartDate|setStartDate}, {@link #DateRangePicker+setEndDate|setEndDate}.
* @property {function} isInvalidTime=false - A function that is passed each hour/minute/second/am-pm in the two calendars before they are displayed,<br/>
* and may return `true` or `false` to indicate whether that date should be available for selection or not.<br/>
* Signature: `isInvalidTime(time, side, unit)`<br/>
* `side` is `'start'` or `'end'` or `null` for `singleDatePicker: true`<br/>
* `unit` is `'hour'`, `'minute'`, `'second'` or `'ampm'`<br/>
* Hours are always given as 24-hour clock<br/>
* Function has no effect on time values set by `startDate`, `endDate`, `ranges`, {@link #DateRangePicker+setStartDate|setStartDate}, {@link #DateRangePicker+setEndDate|setEndDate}.<br/>
* Ensure that your function returns `false` for at least one item. Otherwise the calender is not rendered.<br/>
* @property {function} isCustomDate=false - A function that is passed each date in the two calendars before they are displayed,
* and may return a string or array of CSS class names to apply to that date's calendar cell.<br/>
* Signature: `isCustomDate(date)`
* @property {string|Array} altInput=null - A {@link https://api.jquery.com/category/selectors/|jQuery selector} string for an alternative
* ouput (typically hidden) `<input>` element. Requires `altFormat` to be set.<br/>
* Must be a single string for `singleDatePicker: true` or an array of two strings for `singleDatePicker: false`<br/>
* Example: `['#start', '#end']`
* @property {function|string}=null - The output format used for `altInput`.<br/>
* Either a string used with {@link https://moment.github.io/luxon/api-docs/index.html#datetimetoformat|toFormat()} or a function.<br/>
* Examples: `'yyyyMMddHHmm'`, `(date) => date.toUnixInteger()`
* @property {string} applyButtonClasses=btn-primary - CSS class names that will be added only to the apply button
* @property {string} cancelButtonClasses=btn-default - CSS class names that will be added only to the cancel button
* @property {string} buttonClasses - Default: `'btn btn-sm'`<br/>CSS class names that will be added to both the apply and cancel buttons.
* @property {string} weekendClasses=weekend - CSS class names that will be used to highlight weekend days.<br/>
* Use `null` or empty string if you don't like to highlight weekend days.
* @property {string} weekendDayClasses=weekend-day - CSS class names that will be used to highlight weekend day names.<br/>
* Weekend days are evaluated by [Info.getWeekendWeekdays](https://moment.github.io/luxon/api-docs/index.html#infogetweekendweekdays) and depend on current
* locale settings.
* Use `null` or empty string if you don't like to highlight weekend day names.
* @property {string} todayClasses=today - CSS class names that will be used to highlight the current day.<br/>
* Use `null` or empty string if you don't like to highlight the current day.
* @property {string} opens=right - Whether the picker appears aligned to the left, to the right, or centered under the HTML element it's attached to.<br/>
* `'left' \| 'right' \| 'center'`
* @property {string} drops=down - Whether the picker appears below or above the HTML element it's attached to.<br/>
* `'down' \| 'up' \| 'auto'`
* @property {object} ranges={} - Set predefined date {@link #Ranges|Ranges} the user can select from. Each key is the label for the range,
* and its value an array with two dates representing the bounds of the range.
* @property {boolean} showCustomRangeLabel=true - Displays "Custom Range" at the end of the list of predefined {@link #Ranges|Ranges},
* when the ranges option is used.<br>
* This option will be highlighted whenever the current date range selection does not match one of the predefined ranges.<br/>
* Clicking it will display the calendars to select a new range.
* @property {boolean} alwaysShowCalendars=false - Normally, if you use the ranges option to specify pre-defined date ranges,
* calendars for choosing a custom date range are not shown until the user clicks "Custom Range".<br/>
* When this option is set to true, the calendars for choosing a custom date range are always shown instead.
* @property {object} locale={} - Allows you to provide localized strings for buttons and labels, customize the date format,
* and change the first day of week for the calendars.
* @property {string} locale.direction=ltr - Direction of reading, `'ltr'` or `'rtl'`
* @property {object|string} locale.format - Default: `DateTime.DATE_SHORT` or `DateTime.DATETIME_SHORT` when `timePicker: true`<br/>Date formats.
* Either given as string, see [Format Tokens](https://moment.github.io/luxon/#/formatting?id=table-of-tokens) or an object according
* to [Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat)<br/>
* I recommend to use the luxon [Presets](https://moment.github.io/luxon/#/formatting?id=presets).
* @property {string} locale.separator - Defaut: `' - '`<br/>Separator for start and end time
* @property {string} locale.weekLabel=W - Label for week numbers
* @property {Array} locale.daysOfWeek - Default: `luxon.Info.weekdays('short')`<br/>Array with weekday names, from Monday to Sunday
* @property {Array} locale.monthNames - Default: `luxon.Info.months('long')`<br/>Array with month names
* @property {number} locale.firstDay - Default: `luxon.Info.getStartOfWeek()`<br/>First day of the week, 1 for Monday through 7 for Sunday
* @property {string} locale.applyLabel=Apply - Label of `Apply` Button
* @property {string} locale.cancelLabel=Cancel - Label of `Cancel` Button
* @property {string} locale.customRangeLabel=Custom Range - Title for custom ranges
* @property {object|string} locale.durationFormat={} - Format a custom label for selected duration, for example `'5 Days, 12 Hours'`.<br/>
* Define the format either as string, see [Duration.toFormat - Format Tokens](https://moment.github.io/luxon/api-docs/index.html#durationtoformat) or
* an object according to [Intl.NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#options),
* see [Duration.toHuamn](https://moment.github.io/luxon/api-docs/index.html#durationtohuman).
*/
/**
* A set of predefined ranges
* @typedef Ranges
* @type {Object}
* @property {string} name - The name of the range
* @property {external:DateTime|external:Date|string} range - Array of 2 elements with startDate and endDate
* @example {
* 'Today': [DateTime.now().startOf('day'), DateTime.now().endOf('day')],
* 'Yesterday': [DateTime.now().startOf('day').minus({days: 1}), DateTime.now().minus({days: 1}).endOf('day')],
* 'Last 7 Days': [DateTime.now().startOf('day').minus({days: 6}), DateTime.now()],
* 'Last 30 Days': [DateTime.now().startOf('day').minus({days: 29}), DateTime.now()],
* 'This Month': [DateTime.now().startOf('day').startOf('month'), DateTime.now().endOf('month')],
* 'Last Month': [DateTime.now().startOf('day').minus({months: 1}).startOf('month'), DateTime.now().minus({months: 1}).endOf('month')]
* }
*/
/**
* A single predefined range
* @typedef Range
* @type {Object}
* @property {string} name - The name of the range
* @property {external:DateTime|external:Date|string} range - Array of 2 elements with startDate and endDate
* @example { Today: [DateTime.now().startOf('day'), DateTime.now().endOf('day')] }
*/
//default settings for options
this.parentEl = 'body';
this.element = $(element);
this.startDate = DateTime.now().startOf('day');
this.endDate = DateTime.now().endOf('day');
this.minDate = null;
this.maxDate = null;
this.maxSpan = null;
this.minSpan = null;
this.initalMonth = DateTime.now().startOf('month');
this.autoApply = false;
this.singleDatePicker = false;
this.singleMonthView = false;
this.showDropdowns = false;
this.minYear = DateTime.now().minus({ year: 100 }).year;
this.maxYear = DateTime.now().plus({ year: 100 }).year;
this.showWeekNumbers = false;
this.showISOWeekNumbers = false;
this.showCustomRangeLabel = true;
this.timePicker = false;
this.timePicker24Hour = true;
this.timePickerStepSize = Duration.fromObject({ minutes: 1 });
this.linkedCalendars = true;
this.autoUpdateInput = true;
this.alwaysShowCalendars = false;
this.isInvalidDate = null;
this.isInvalidTime = null;
this.isCustomDate = null;
this.onOutsideClick = 'apply';
this.opens = this.element.hasClass('pull-right') ? 'left' : 'right';
this.drops = this.element.hasClass('dropup') ? 'up' : 'down';
this.buttonClasses = 'btn btn-sm';
this.applyButtonClasses = 'btn-primary';
this.cancelButtonClasses = 'btn-default';
this.weekendClasses = 'weekend';
this.weekendDayClasses = 'weekend-day';
this.todayClasses = 'today';
this.warnings = true;
this.altInput = null;
this.altFormat = null;
this.ranges = {};
this.locale = {
direction: 'ltr',
format: DateTime.DATE_SHORT, // or DateTime.DATETIME_SHORT when timePicker: true
separator: ' - ',
applyLabel: 'Apply',
cancelLabel: 'Cancel',
weekLabel: 'W',
customRangeLabel: 'Custom Range',
daysOfWeek: Info.weekdays('short'),
monthNames: Info.months('long'),
firstDay: Info.getStartOfWeek(),
durationFormat: null
};
this.callback = function () { };
//some state information
this.isShowing = false;
this.leftCalendar = {};
this.rightCalendar = {};
if (typeof options.singleDatePicker === 'boolean')
this.singleDatePicker = options.singleDatePicker;
if (!this.singleDatePicker && typeof options.singleMonthView === 'boolean') {
this.singleMonthView = options.singleMonthView;
} else {
this.singleMonthView = false;
}
//custom options from user
if (typeof options !== 'object' || options === null)
options = {};
//allow setting options with data attributes
//data-api options will be overwritten with custom javascript options
options = $.extend(this.element.data(), options);
//html template for the picker UI
if (typeof options.template !== 'string' && !(options.template instanceof $)) {
let template = [
'<div class="daterangepicker">',
'<div class="ranges"></div>',
'<div class="drp-calendar left">',
'<div class="calendar-table"></div>',
'<div class="calendar-time start-time"></div>'];
if (this.singleMonthView)
template.push('<div class="calendar-time end-time"></div>');
template.push(...[
'</div>',
'<div class="drp-calendar right">',
'<div class="calendar-table"></div>',
'<div class="calendar-time end-time"></div>',
'</div>',
'<div class="drp-buttons">',
'<span class="drp-duration-label"></span>',
'<span class="drp-selected"></span>',
'<button class="cancelBtn" type="button"></button>',
'<button class="applyBtn" disabled="disabled" type="button"></button> ',
'</div>',
'</div>']);
options.template = template.join('');
}
this.parentEl = (options.parentEl && $(options.parentEl).length) ? $(options.parentEl) : $(this.parentEl);
this.container = $(options.template).appendTo(this.parentEl);
//
// handle all the possible options overriding defaults
//
if (typeof options.timePicker === 'boolean')
this.timePicker = options.timePicker;
if (this.timePicker)
this.locale.format = DateTime.DATETIME_SHORT;
if (typeof options.locale === 'object') {
for (let key of ['separator', 'applyLabel', 'cancelLabel', 'weekLabel']) {
if (typeof options.locale[key] === 'string')
this.locale[key] = options.locale[key];
}
if (typeof options.locale.direction === 'string') {
if (['rtl', 'ltr'].includes(options.locale.direction))
this.locale.direction = options.locale.direction
else
console.error(`Option 'options.locale.direction' must be 'rtl' or 'ltr'`);
}
if (['string', 'object'].includes(typeof options.locale.format))
this.locale.format = options.locale.format;
if (Array.isArray(options.locale.daysOfWeek)) {
if (options.locale.daysOfWeek.some(x => typeof x !== 'string'))
console.error(`Option 'options.locale.daysOfWeek' must be an array of strings`)
else
this.locale.daysOfWeek = options.locale.daysOfWeek.slice();
}
if (Array.isArray(options.locale.monthNames)) {
if (options.locale.monthNames.some(x => typeof x !== 'string'))
console.error(`Option 'locale.monthNames' must be an array of strings`)
else
this.locale.monthNames = options.locale.monthNames.slice();
}
if (typeof options.locale.firstDay === 'number')
this.locale.firstDay = options.locale.firstDay;
if (typeof options.locale.customRangeLabel === 'string') {
//Support unicode chars in the custom range name.
var elem = document.createElement('textarea');
elem.innerHTML = options.locale.customRangeLabel;
var rangeHtml = elem.value;
this.locale.customRangeLabel = rangeHtml;
}
if (['string', 'object'].includes(typeof options.locale.durationFormat) && options.locale.durationFormat != null)
this.locale.durationFormat = options.locale.durationFormat;
}
this.container.addClass(this.locale.direction);
for (let key of ['timePicker24Hour', 'showWeekNumbers', 'showISOWeekNumbers',
'showDropdowns', 'linkedCalendars', 'showCustomRangeLabel',
'alwaysShowCalendars', 'autoApply', 'autoUpdateInput', 'warnings']) {
if (typeof options[key] === 'boolean')
this[key] = options[key];
}
for (let key of ['applyButtonClasses', 'cancelButtonClasses', 'weekendClasses', 'weekendDayClasses', 'todayClasses']) {
if (typeof options[key] === 'string') {
this[key] = options[key];
} else if (['weekendClasses', 'weekendDayClasses', 'todayClasses'].includes(key) && options[key] === null) {
this[key] = options[key];
}
}
for (let key of ['minYear', 'maxYear']) {
if (typeof options[key] === 'number')
this[key] = options[key];
}
for (let key of ['isInvalidDate', 'isInvalidTime', 'isCustomDate']) {
if (typeof options[key] === 'function')
this[key] = options[key]
else
this[key] = function () { return false };
}
if (!this.singleDatePicker) {
for (let opt of ['minSpan', 'maxSpan']) {
if (['string', 'number', 'object'].includes(typeof options[opt])) {
if (options[opt] instanceof Duration && options[opt].isValid) {
this[opt] = options[opt];
} else if (Duration.fromISO(options[opt]).isValid) {
this[opt] = Duration.fromISO(options[opt]);
} else if (typeof options[opt] === 'number' && Duration.fromObject({ seconds: options[opt] }).isValid) {
this[opt] = Duration.fromObject({ seconds: options[opt] });
} else if (options[opt] === null) {
this[opt] = null;
} else {
console.error(`Option '${key}' is not valid`);
};
}
}
if (this.minSpan && this.maxSpan && this.minSpan > this.maxSpan) {
this.minSpan = null;
this.maxSpan = null;
console.warn(`Ignore option 'minSpan' and 'maxSpan', because 'minSpan' must be smaller than 'maxSpan'`);
}
}
if (this.timePicker) {
if (typeof options.timePickerSeconds === 'boolean') // backward compatibility
this.timePickerStepSize = Duration.fromObject({ [options.timePickerSeconds ? 'seconds' : 'minutes']: 1 });
if (typeof options.timePickerIncrement === 'number') // backward compatibility
this.timePickerStepSize = Duration.fromObject({ minutes: options.timePickerIncrement });
if (['string', 'object', 'number'].includes(typeof options.timePickerStepSize)) {
let duration;
if (options.timePickerStepSize instanceof Duration && options.timePickerStepSize.isValid) {
duration = options.timePickerStepSize;
} else if (Duration.fromISO(options.timePickerStepSize).isValid) {
duration = Duration.fromISO(options.timePickerStepSize);
} else if (typeof options.timePickerStepSize === 'number' && Duration.fromObject({ seconds: options.timePickerStepSize }).isValid) {
duration = Duration.fromObject({ seconds: options.timePickerStepSize });
} else {
console.error(`Option 'timePickerStepSize' is not valid`);
duration = this.timePickerStepSize;
};
var valid = [];
for (let unit of ['minutes', 'seconds'])
valid.push(...[1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30].map(x => { return Duration.fromObject({ [unit]: x }) }));
valid.push(...[1, 2, 3, 4, 6].map(x => { return Duration.fromObject({ hours: x }) }));
if (this.timePicker24Hour)
valid.push(...[8, 12].map(x => { return Duration.fromObject({ hours: x }) }));
if (valid.some(x => duration.rescale().equals(x))) {
this.timePickerStepSize = duration.rescale();
} else {
console.error(`Option 'timePickerStepSize' ${JSON.stringify(duration.toObject())} is not valid`);
}
}
if (this.maxSpan && this.timePickerStepSize > this.maxSpan)
console.error(`Option 'timePickerStepSize' ${JSON.stringify(this.timePickerStepSize.toObject())} must be smaller than 'maxSpan'`);
this.timePickerOpts = {
showMinutes: this.timePickerStepSize < Duration.fromObject({ hours: 1 }),
showSeconds: this.timePickerStepSize < Duration.fromObject({ minutes: 1 }),
hourStep: this.timePickerStepSize >= Duration.fromObject({ hours: 1 }) ? this.timePickerStepSize.hours : 1,
minuteStep: this.timePickerStepSize >= Duration.fromObject({ minutes: 1 }) ? this.timePickerStepSize.minutes : 1,
secondStep: this.timePickerStepSize.seconds
};
}
for (let opt of ['startDate', 'endDate', 'minDate', 'maxDate', 'initalMonth']) {
if (opt == 'endDate' && this.singleDatePicker)
continue;
if (typeof options[opt] === 'object') {
if (options[opt] instanceof DateTime && options[opt].isValid) {
this[opt] = options[opt];
} else if (options[opt] instanceof Date) {
this[opt] = DateTime.fromJSDate(options[opt]);
} else if (options[opt] === null) {
this[opt] = null;
} else {
console.error(`Option '${opt}' must be a luxon.DateTime or Date or string`);
}
} else if (typeof options[opt] === 'string') {
const format = typeof this.locale.format === 'string' ? this.locale.format : DateTime.parseFormatForOpts(this.locale.format);
if (DateTime.fromISO(options[opt]).isValid) {
this[opt] = DateTime.fromISO(options[opt]);
} else if (DateTime.fromFormat(options[opt], format, { locale: DateTime.now().locale }).isValid) {
this[opt] = DateTime.fromFormat(options[opt], format, { locale: DateTime.now().locale });
} else {
const invalid = DateTime.fromFormat(options[opt], format, { locale: DateTime.now().locale }).invalidExplanation;
console.error(`Option '${opt}' is not a valid string: ${invalid}`);
}
}
}
if (!this.timePicker) {
if (this.minDate)
this.minDate = this.minDate.startOf('day');
if (this.maxDate)
this.maxDate = this.maxDate.endOf('day');
}
//if no start/end dates set, check if the input element contains initial values
if (typeof options.startDate === 'undefined' && typeof options.endDate === 'undefined') {
if ($(this.element).is(':text')) {
let start, end;
const val = $(this.element).val();
if (val != '') {
const split = val.split(this.locale.separator);
const format = typeof this.locale.format === 'string' ? this.locale.format : DateTime.parseFormatForOpts(this.locale.format);
if (split.length == 2) {
start = DateTime.fromFormat(split[0], format, { locale: DateTime.now().locale });
end = DateTime.fromFormat(split[1], format, { locale: DateTime.now().locale });
} else if (this.singleDatePicker) {
start = DateTime.fromFormat(val, format, { locale: DateTime.now().locale });
end = DateTime.fromFormat(val, format, { locale: DateTime.now().locale });
}
if (start.isValid && end.isValid) {
this.setStartDate(start, false);
this.setEndDate(end, false);
} else {
if (this.singleDatePicker)
console.error(`Value in <input> is not a valid string: ${start.invalidExplanation}`)
else
console.error(`Value in <input> is not a valid string: ${start.invalidExplanation} - ${end.invalidExplanation}`);
}
}
}
}
if (this.singleDatePicker) {
this.endDate = this.startDate;
} else if (this.endDate < this.startDate) {
this.endDate = this.startDate;
console.warn(`Set 'endDate' to ${this - this.logDate(endDate)} because it was earlier than 'startDate'`);
}
if (['function', 'string'].includes(typeof options.altFormat))
this.altFormat = options.altFormat;
if (['object', 'string'].includes(typeof options.altInput) && options.altInput != null) {
if (this.singleDatePicker && typeof options.altInput === 'string') {
this.altInput = options.altInput
} else if (!this.singleDatePicker && Array.isArray(options.altInput) && options.altInput.length == 2) {
this.altInput = options.altInput;
}
}
if (!this.startDate && this.initalMonth) {
// No initial date selected
this.endDate = null;
if (this.timePicker)
console.error(`Option 'initalMonth' works only with 'timePicker: false'`);
} else {
// Do some sanity checks on startDate and endDate for minDate, maxDate, minSpan, maxSpan, etc.
this.constrainDate();
}
if (typeof options.opens === 'string') {
if (['left', 'right', 'center'].includes(options.opens))
this.opens = options.opens
else
console.error(`Option 'options.opens' must be 'left', 'right' or 'center'`);
}
if (typeof options.drops === 'string') {
if (['drop', 'down', 'auto'].includes(options.drops))
this.drops = options.drops
else
console.error(`Option 'options.drops' must be 'drop', 'down' or 'auto'`);
}
if (Array.isArray(options.buttonClasses)) {
this.buttonClasses = options.buttonClasses.join(' ')
} else if (typeof options.buttonClasses === 'string') {
this.buttonClasses = options.buttonClasses;
}
if (typeof options.onOutsideClick === 'string') {
if (['cancel', 'apply'].includes(options.onOutsideClick))
this.onOutsideClick = options.onOutsideClick
else
console.error(`Option 'options.onOutsideClick' must be 'cancel' or 'apply'`);
}
// update day names order to firstDay
if (this.locale.firstDay != 1) {
let iterator = this.locale.firstDay;
while (iterator > 1) {
this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift());
iterator--;
}
}
if (!this.singleDatePicker && typeof options.ranges === 'object') {
// Process custom ranges
for (let range in options.ranges) {
let start, end;
if (['string', 'object'].includes(typeof options.ranges[range][0])) {
if (options.ranges[range][0] instanceof DateTime && options.ranges[range][0].isValid) {
start = options.ranges[range][0];
} else if (typeof options.ranges[range][0] === 'string' && DateTime.fromISO(options.ranges[range][0]).isValid) {
start = DateTime.fromISO(options.ranges[range][0]);
} else {
console.error(`Option 'ranges.${range}' is not a valid ISO-8601 string or DateTime`);
}
}
if (['string', 'object'].includes(typeof options.ranges[range][1])) {
if (options.ranges[range][1] instanceof DateTime && options.ranges[range][1].isValid) {
end = options.ranges[range][1];
} else if (typeof options.ranges[range][1] === 'string' && DateTime.fromISO(options.ranges[range][1]).isValid) {
end = DateTime.fromISO(options.ranges[range][1]);
} else {
console.error(`Option 'ranges.${range}' is not a valid ISO-8601 string or DateTime`);
}
}
if (start == null || end == null)
continue;
const validRange = this.constrainDate({ span: false }, [range, start, end]);
options.ranges[range] = [validRange[0], validRange[1]];
//Support unicode chars in the range names.
var elem = document.createElement('textarea');
elem.innerHTML = range;
var rangeHtml = elem.value;
this.ranges[rangeHtml] = [validRange[0], validRange[1]];
}
var list = '<ul>';
for (range in this.ranges) {
list += '<li data-range-key="' + range + '">' + range + '</li>';
}
if (this.showCustomRangeLabel) {
list += '<li data-range-key="' + this.locale.customRangeLabel + '">' + this.locale.customRangeLabel + '</li>';
}
list += '</ul>';
this.container.find('.ranges').prepend(list);
this.container.addClass('show-ranges');
}
if (typeof cb === 'function') {
this.callback = cb;
}
if (!this.timePicker) {
if (this.startDate)
this.startDate = this.startDate.startOf('day');
if (this.endDate)
this.endDate = this.endDate.endOf('day');
this.container.find('.calendar-time').hide();
}
//can't be used together for now
if (this.timePicker && this.autoApply)
this.autoApply = false;
if (this.autoApply)
this.container.addClass('auto-apply');
if (this.singleDatePicker || this.singleMonthView) {
this.container.addClass('single');
this.container.find('.drp-calendar.left').addClass('single');
this.container.find('.drp-calendar.left').show();
this.container.find('.drp-calendar.right').hide();
if (!this.timePicker && this.autoApply)
this.container.addClass('auto-apply');
}
if ((typeof options.ranges === 'undefined' && !this.singleDatePicker) || this.alwaysShowCalendars)
this.container.addClass('show-calendar');
this.container.addClass('opens' + this.opens);
//apply CSS classes and labels to buttons
this.container.find('.applyBtn, .cancelBtn').addClass(this.buttonClasses);
if (this.applyButtonClasses.length)
this.container.find('.applyBtn').addClass(this.applyButtonClasses);
if (this.cancelButtonClasses.length)
this.container.find('.cancelBtn').addClass(this.cancelButtonClasses);
this.container.find('.applyBtn').html(this.locale.applyLabel);
this.container.find('.cancelBtn').html(this.locale.cancelLabel);
//
// event listeners
//
this.container.find('.drp-calendar')
.on('click.daterangepicker', '.prev', $.proxy(this.clickPrev, this))
.on('click.daterangepicker', '.next', $.proxy(this.clickNext, this))
.on('mousedown.daterangepicker', 'td.available', $.proxy(this.clickDate, this))
.on('mouseenter.daterangepicker', 'td.available', $.proxy(this.hoverDate, this))
.on('change.daterangepicker', 'select.yearselect', $.proxy(this.monthOrYearChanged, this))
.on('change.daterangepicker', 'select.monthselect', $.proxy(this.monthOrYearChanged, this))
.on('change.daterangepicker', 'select.hourselect,select.minuteselect,select.secondselect,select.ampmselect', $.proxy(this.timeChanged, this));
this.container.find('.ranges')
.on('click.daterangepicker', 'li', $.proxy(this.clickRange, this))
.on('mouseenter.daterangepicker', 'li', $.proxy(this.hoverRange, this));
this.container.find('.drp-buttons')
.on('click.daterangepicker', 'button.applyBtn', $.proxy(this.clickApply, this))
.on('click.daterangepicker', 'button.cancelBtn', $.proxy(this.clickCancel, this));
if (this.element.is('input') || this.element.is('button')) {
this.element.on({
'click.daterangepicker': $.proxy(this.show, this),
'focus.daterangepicker': $.proxy(this.show, this),
'keyup.daterangepicker': $.proxy(this.elementChanged, this),
'keydown.daterangepicker': $.proxy(this.keydown, this) //IE 11 compatibility
});
} else {
this.element.on('click.daterangepicker', $.proxy(this.toggle, this));
this.element.on('keydown.daterangepicker', $.proxy(this.toggle, this));
}
//
// if attached to a text input, set the initial value
//
this.updateElement();
};
DateRangePicker.prototype = {
constructor: DateRangePicker,
/**
* Sets the date range picker's currently selected start date to the provided date.<br/>
* `startDate` must be a `luxon.DateTime` or `Date` or `string` according to {@link ISO-8601} or
* a string matching `locale.format`.
* The value of the attached `<input>` element is also updated.
* Date value is rounded to match option `timePickerStepSize`<br/>
* Functions `isInvalidDate` and `isInvalidTime` are not evaluated, you may set date/time which is not selectable in calendar.<br/>
* If the `startDate` does not fall into `minDate` and `maxDate` then `startDate` is shifted and a warning is written to console.
* @param {external:DateTime|external:Date|string} startDate - startDate to be set
* @param {boolean} isValid=false - If `true` then the `startDate` is not checked against `minDate` and `maxDate`<br/>
* Use this option only if you are really sure about the value you put in.
* @throws `RangeError` for invalid date values.
* @example const DateTime = luxon.DateTime;
* const drp = $('#picker').data('daterangepicker');
* drp.setStartDate(DateTime.now().startOf('hour'));
*/
setStartDate: function (startDate, isValid = false) {
// If isValid == true, then value is selected from calendar and stepSize, minDate, maxDate are already considered
if (isValid === undefined || !isValid) {
if (typeof startDate === 'object') {
if (startDate instanceof DateTime && startDate.isValid) {
this.startDate = startDate;
} else if (startDate instanceof Date) {
this.startDate = DateTime.fromJSDate(startDate);
} else {
throw RangeError(`The 'startDate' must be a luxon.DateTime or Date or string`);
}
} else if (typeof startDate === 'string') {
const format = typeof this.locale.format === 'string' ? this.locale.format : DateTime.parseFormatForOpts(this.locale.format);
if (DateTime.fromISO(startDate).isValid) {
this.startDate = DateTime.fromISO(startDate);
} else if (DateTime.fromFormat(startDate, format, { locale: DateTime.now().locale }).isValid) {
this.startDate = DateTime.fromFormat(startDate, format, { locale: DateTime.now().locale });
} else {
const invalid = DateTime.fromFormat(startDate, format, { locale: DateTime.now().locale }).invalidExplanation;
throw RangeError(`The 'startDate' is not a valid string: ${invalid}`);
}
}
} else {
this.startDate = startDate;
}
if (isValid === undefined || !isValid)
this.constrainDate();
if (!this.singleDatePicker && !this.endDate) {
if (this.locale.durationFormat)
this.container.find('.drp-duration-label').html('');
if (typeof this.locale.format === 'object') {
const empty = `<span style="color: rgb(0,0,0,0);">${this.startDate.toLocaleString(this.locale.format)}</span>`;
this.container.find('.drp-selected').html(this.startDate.toLocaleString(this.locale.format) + this.locale.separator + empty);
} else {
const empty = `<span style="color: rgb(0,0,0,0);">${this.startDate.toFormat(this.locale.format)}</span>`;
this.container.find('.drp-selected').html(this.startDate.toFormat(this.locale.format) + this.locale.separator + empty);
}
}
if (!this.isShowing)
this.updateElement();
this.updateMonthsInView();
},
/**
* Sets the date range picker's currently selected end date to the provided date.<br/>
* `endDate` must be a `luxon.DateTime` or `Date` or `string` according to {@link ISO-8601} or
* a string matching`locale.format`.
* The value of the attached `<input>` element is also updated.
* Date value is rounded to match option `timePickerStepSize`<br/>
* Functions `isInvalidDate` and `isInvalidTime` are not evaluated, you may set date/time which is not selectable in calendar.<br/>
* If the `endDate` does not fall into `minDate` and `maxDate` or into `minSpan` and `maxSpan`
* then `endDate` is shifted and a warning is written to console.
* @param {external:DateTime|external:Date|string} endDate - endDate to be set
* @param {boolean} isValid=false - If `true` then the `endDate` is not checked against `minDate`, `maxDate` and `minSpan`, `maxSpan`<br/>
* Use this option only if you are really sure about the value you put in.
* @throws `RangeError` for invalid date values.
* @example const drp = $('#picker').data('daterangepicker');
* drp.setEndDate('2025-03-28T18:30:00');
*/
setEndDate: function (endDate, isValid = false) {
// If isValid == true, then value is selected from calendar and stepSize, minDate, maxDate are already considered
if (isValid === undefined || !isValid) {
if (typeof endDate === 'object') {
if (endDate instanceof DateTime && endDate.isValid) {
this.endDate = endDate;
} else if (endDate instanceof Date) {
this.endDate = DateTime.fromJSDate(endDate);
} else {
throw RangeError(`The 'endDate' must be a luxon.DateTime or Date or string`);
}
} else if (typeof endDate === 'string') {
const format = typeof this.locale.format === 'string' ? this.locale.format : DateTime.parseFormatForOpts(this.locale.format);
if (DateTime.fromISO(endDate).isValid) {
this.endDate = DateTime.fromISO(endDate);
} else if (DateTime.fromFormat(endDate, format, { locale: DateTime.now().locale }).isValid) {
this.endDate = DateTime.fromFormat(endDate, format, { locale: DateTime.now().locale });
} else {
const invalid = DateTime.fromFormat(endDate, format, { locale: DateTime.now().locale }).invalidExplanation;
throw RangeError(`The 'endDate' is not a valid string: ${invalid}`);
}
}
} else {
this.endDate = endDate;
}
if (isValid === undefined || !isValid)
this.constrainDate();
this.previousRightTime = this.endDate;
if (!this.singleDatePicker) {
if (this.locale.durationFormat) {
const duration = this.endDate.diff(thi