UNPKG

daterangepicker-4.x

Version:

Date range picker with time component and pre-defined ranges

822 lines (740 loc) 130 kB
// 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