@coreui/coreui-pro
Version:
The most popular front-end framework for developing responsive, mobile-first projects on the web rewritten by the CoreUI Team
1,130 lines (912 loc) • 34 kB
JavaScript
/**
* --------------------------------------------------------------------------
* CoreUI PRO date-range-picker.js
* License (https://coreui.io/pro/license/)
* --------------------------------------------------------------------------
*/
import * as Popper from '@popperjs/core'
import BaseComponent from './base-component.js'
import Calendar from './calendar.js'
import TimePicker from './time-picker.js'
import EventHandler from './dom/event-handler.js'
import Manipulator from './dom/manipulator.js'
import SelectorEngine from './dom/selector-engine.js'
import { defineJQueryPlugin, getElement, isRTL } from './util/index.js'
import {
convertToDateObject, getDateBySelectionType, getLocalDateFromString, isDateDisabled
} from './util/calendar.js'
import FocusTrap from './util/focustrap.js'
/**
* Constants
*/
const NAME = 'date-range-picker'
const DATA_KEY = 'coreui.date-range-picker'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const ENTER_KEY = 'Enter'
const ESCAPE_KEY = 'Escape'
const TAB_KEY = 'Tab'
const RIGHT_MOUSE_BUTTON = 2
const EVENT_CLICK = `click${EVENT_KEY}`
const EVENT_END_DATE_CHANGE = `endDateChange${EVENT_KEY}`
const EVENT_HIDE = `hide${EVENT_KEY}`
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
const EVENT_INPUT = `input${EVENT_KEY}`
const EVENT_KEYDOWN = `keydown${EVENT_KEY}`
const EVENT_RESIZE = 'resize'
const EVENT_SHOW = `show${EVENT_KEY}`
const EVENT_SHOWN = `shown${EVENT_KEY}`
const EVENT_SUBMIT = 'submit'
const EVENT_START_DATE_CHANGE = `startDateChange${EVENT_KEY}`
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
const CLASS_NAME_BODY = 'date-picker-body'
const CLASS_NAME_CALENDAR = 'date-picker-calendar'
const CLASS_NAME_CALENDARS = 'date-picker-calendars'
const CLASS_NAME_CLEANER = 'date-picker-cleaner'
const CLASS_NAME_DATE_PICKER = 'date-picker'
const CLASS_NAME_DATE_RANGE_PICKER = 'date-range-picker'
const CLASS_NAME_DISABLED = 'disabled'
const CLASS_NAME_DROPDOWN = 'date-picker-dropdown'
const CLASS_NAME_INDICATOR = 'date-picker-indicator'
const CLASS_NAME_INPUT = 'date-picker-input'
const CLASS_NAME_INPUT_GROUP = 'date-picker-input-group'
const CLASS_NAME_INPUT_PREVIEW = 'date-picker-input-preview'
const CLASS_NAME_INPUT_WRAPPER = 'date-picker-input-wrapper'
const CLASS_NAME_IS_INVALID = 'is-invalid'
const CLASS_NAME_IS_VALID = 'is-valid'
const CLASS_NAME_FOOTER = 'date-picker-footer'
const CLASS_NAME_RANGES = 'date-picker-ranges'
const CLASS_NAME_SEPARATOR = 'date-picker-separator'
const CLASS_NAME_SHOW = 'show'
const CLASS_NAME_TIME_PICKER = 'time-picker'
const CLASS_NAME_TIME_PICKERS = 'date-picker-timepickers'
const CLASS_NAME_WAS_VALIDATED = 'was-validated'
const SELECTOR_CALENDAR = '.calendars'
const SELECTOR_DATA_TOGGLE = '[data-coreui-toggle="date-range-picker"]:not(.disabled):not(:disabled)'
const SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE}.${CLASS_NAME_SHOW}`
const SELECTOR_INPUT = '.date-picker-input'
const SELECTOR_WAS_VALIDATED = 'form.was-validated'
const Default = {
ariaNavNextMonthLabel: 'Next month',
ariaNavNextYearLabel: 'Next year',
ariaNavPrevMonthLabel: 'Previous month',
ariaNavPrevYearLabel: 'Previous year',
calendarDate: null,
calendars: 2,
cancelButton: 'Cancel',
cancelButtonClasses: ['btn', 'btn-sm', 'btn-ghost-primary'],
confirmButton: 'OK',
confirmButtonClasses: ['btn', 'btn-sm', 'btn-primary'],
cleaner: true,
container: false,
date: null,
disabled: false,
disabledDates: null,
endDate: null,
endName: null,
firstDayOfWeek: 1,
footer: false,
inputDateFormat: null,
inputDateParse: null,
inputOnChangeDelay: 750,
inputReadOnly: false,
invalid: false,
indicator: true,
locale: 'default',
maxDate: null,
minDate: null,
name: null,
placeholder: ['Start date', 'End date'],
previewDateOnHover: true,
range: true,
ranges: {},
rangesButtonsClasses: ['btn', 'btn-ghost-secondary'],
required: true,
separator: true,
size: null,
startDate: null,
startName: null,
selectAdjacementDays: false,
selectEndDate: false,
selectionType: 'day',
showAdjacementDays: true,
showWeekNumber: false,
timepicker: false,
todayButton: 'Today',
todayButtonClasses: ['btn', 'btn-sm', 'btn-primary', 'me-auto'],
valid: false,
weekdayFormat: 2,
weekNumbersLabel: null
}
const DefaultType = {
ariaNavNextMonthLabel: 'string',
ariaNavNextYearLabel: 'string',
ariaNavPrevMonthLabel: 'string',
ariaNavPrevYearLabel: 'string',
calendarDate: '(date|number|string|null)',
calendars: 'number',
cancelButton: '(boolean|string)',
cancelButtonClasses: '(array|string)',
cleaner: 'boolean',
confirmButton: '(boolean|string)',
confirmButtonClasses: '(array|string)',
container: '(string|element|boolean)',
date: '(date|number|string|null)',
disabledDates: '(array|date|function|null)',
disabled: 'boolean',
endDate: '(date|number|string|null)',
endName: '(string|null)',
firstDayOfWeek: 'number',
footer: 'boolean',
indicator: 'boolean',
inputDateFormat: '(function|null)',
inputDateParse: '(function|null)',
inputOnChangeDelay: 'number',
inputReadOnly: 'boolean',
invalid: 'boolean',
locale: 'string',
maxDate: '(date|number|string|null)',
minDate: '(date|number|string|null)',
name: '(string|null)',
placeholder: '(array|string)',
previewDateOnHover: 'boolean',
range: 'boolean',
ranges: 'object',
rangesButtonsClasses: '(array|string)',
required: 'boolean',
separator: 'boolean',
size: '(string|null)',
startDate: '(date|number|string|null)',
startName: '(string|null)',
selectAdjacementDays: 'boolean',
selectEndDate: 'boolean',
selectionType: 'string',
showAdjacementDays: 'boolean',
showWeekNumber: 'boolean',
timepicker: 'boolean',
todayButton: '(boolean|string)',
todayButtonClasses: '(array|string)',
valid: 'boolean',
weekdayFormat: '(number|string)',
weekNumbersLabel: '(string|null)'
}
/**
* Class definition
*/
class DateRangePicker extends BaseComponent {
constructor(element, config) {
super(element)
this._config = this._getConfig(config)
this._calendarDate = this._config.calendarDate
this._startDate = this._config.date || this._config.startDate
this._endDate = this._config.endDate
this._initialStartDate = null
this._initialEndDate = null
this._mobile = window.innerWidth < 768
this._popper = null
this._selectEndDate = this._config.selectEndDate
this._calendar = null
this._calendars = null
this._endInput = null
this._endInputTimeout = null
this._endPreviewInput = null
this._indicatorElement = null
this._menu = null
this._startInput = null
this._startInputTimeout = null
this._startPreviewInput = null
this._timepickers = null
this._timePickerEnd = null
this._timePickerStart = null
this._togglerElement = null
this._createDateRangePicker()
this._createDateRangePickerCalendars()
this._addEventListeners()
this._addCalendarEventListeners()
this._focustrap = this._initializeFocusTrap()
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
toggle() {
return this._isShown() ? this.hide() : this.show()
}
show() {
if (this._config.disabled || this._isShown()) {
return
}
this._initialStartDate = this._startDate ? new Date(this._startDate) : null
this._initialEndDate = this._endDate ? new Date(this._endDate) : null
EventHandler.trigger(this._element, EVENT_SHOW)
this._element.classList.add(CLASS_NAME_SHOW)
this._element.setAttribute('aria-expanded', true)
if (this._config.container) {
this._menu.classList.add(CLASS_NAME_SHOW)
}
this._focustrap.activate()
EventHandler.trigger(this._element, EVENT_SHOWN)
this._createPopper()
}
hide() {
EventHandler.trigger(this._element, EVENT_HIDE)
if (this._popper) {
this._popper.destroy()
}
this._element.classList.remove(CLASS_NAME_SHOW)
this._element.setAttribute('aria-expanded', 'false')
if (this._config.container) {
this._menu.classList.remove(CLASS_NAME_SHOW)
}
this._focustrap.deactivate()
EventHandler.trigger(this._element, EVENT_HIDDEN)
}
dispose() {
if (this._popper) {
this._popper.destroy()
}
if (this._startInputTimeout) {
clearTimeout(this._startInputTimeout)
}
if (this._endInputTimeout) {
clearTimeout(this._endInputTimeout)
}
this._focustrap.deactivate()
super.dispose()
}
cancel() {
this.hide()
if (this._initialStartDate) {
this._changeStartDate(this._initialStartDate)
}
if (this._config.range && this._initialEndDate) {
this._changeEndDate(this._initialEndDate)
}
if (this._initialStartDate || this._initialEndDate) {
this._calendar.update(this._getCalendarConfig)
}
}
clear() {
this._changeStartDate(null)
this._changeEndDate(null)
this._calendar.update(this._getCalendarConfig())
}
reset() {
this._changeStartDate(this._config.startDate)
this._changeEndDate(this._config.endDate)
this._calendar.update(this._getCalendarConfig())
}
update(config) {
this._config = this._getConfig(config)
this._calendarDate = this._config.calendarDate
this._startDate = this._config.date || this._config.startDate
this._endDate = this._config.endDate
this._selectEndDate = this._config.selectEndDate
this._element.innerHTML = ''
this._createDateRangePicker()
this._createDateRangePickerCalendars()
this._addEventListeners()
this._addCalendarEventListeners()
}
// Private
_initializeFocusTrap() {
return new FocusTrap({
additionalElement: this._config.container ? this._menu : null,
trapElement: this._element
})
}
_addEventListeners() {
EventHandler.on(this._indicatorElement, EVENT_CLICK, () => {
if (!this._config.disabled) {
this.toggle()
}
})
EventHandler.on(this._indicatorElement, EVENT_KEYDOWN, event => {
if (!this._config.disabled && event.key === ENTER_KEY) {
this.toggle()
}
})
EventHandler.on(this._togglerElement, EVENT_CLICK, event => {
if (!this._config.disabled && event.target !== this._indicatorElement) {
this.show()
}
})
EventHandler.on(this._element, EVENT_KEYDOWN, event => {
if (event.key === ESCAPE_KEY) {
this.hide()
this._startInput.focus()
}
})
EventHandler.on(this._startInput, EVENT_CLICK, () => {
this._selectEndDate = false
this._calendar.update(this._getCalendarConfig())
})
EventHandler.on(this._startInput, EVENT_INPUT, event => {
if (this._startInputTimeout) {
clearTimeout(this._startInputTimeout)
}
this._startInputTimeout = setTimeout(() => {
const date = this._parseDate(event.target.value)
let formatedDate = date
if (date instanceof Date && date.getTime()) {
if (isDateDisabled(date, this._config.minDate, this._config.maxDate, this._config.disabledDates)) {
return // Don't update if date is disabled
}
if (this._config.selectionType !== 'day') {
formatedDate = getDateBySelectionType(date, this._config.selectionType)
}
this._calendarDate = formatedDate
this._startInput.value = this._setInputValue(formatedDate)
}
this._startDate = formatedDate
this._calendar.update(this._getCalendarConfig())
if (this._timePickerStart) {
this._timePickerStart.update(this._getTimePickerConfig(true))
}
EventHandler.trigger(this._element, EVENT_START_DATE_CHANGE, {
date: formatedDate
})
}, this._config.inputOnChangeDelay)
})
EventHandler.on(this._startInput.form, EVENT_SUBMIT, () => {
if (this._startInput.form.classList.contains(CLASS_NAME_WAS_VALIDATED)) {
if (this._config.range && (Number.isNaN(Date.parse(this._startInput.value)) || Number.isNaN(Date.parse(this._endInput.value)))) {
return this._element.classList.add(CLASS_NAME_IS_INVALID)
}
if (this._config.range && this._startDate instanceof Date && this._endDate instanceof Date) {
return this._element.classList.add(CLASS_NAME_IS_VALID)
}
if (!this._config.range && Number.isNaN(Date.parse(this._startInput.value))) {
return this._element.classList.add(CLASS_NAME_IS_INVALID)
}
if (!this._config.range && this._startDate instanceof Date) {
return this._element.classList.add(CLASS_NAME_IS_VALID)
}
this._element.classList.add(CLASS_NAME_IS_INVALID)
}
})
EventHandler.on(this._endInput, EVENT_CLICK, () => {
this._selectEndDate = true
this._calendar.update(this._getCalendarConfig())
})
EventHandler.on(this._endInput, EVENT_INPUT, event => {
if (this._endInputTimeout) {
clearTimeout(this._endInputTimeout)
}
this._endInputTimeout = setTimeout(() => {
const date = this._parseDate(event.target.value)
let formatedDate = date
if (date instanceof Date && date.getTime()) {
if (date && isDateDisabled(date, this._config.minDate, this._config.maxDate, this._config.disabledDates)) {
return // Don't update if date is disabled
}
if (this._config.selectionType !== 'day') {
formatedDate = getDateBySelectionType(date, this._config.selectionType)
}
this._calendarDate = formatedDate
this._endInput.value = this._setInputValue(formatedDate)
}
this._endDate = formatedDate
this._calendar.update(this._getCalendarConfig())
if (this._timePickerEnd) {
this._timePickerEnd.update(this._getTimePickerConfig(false))
}
EventHandler.trigger(this._element, EVENT_END_DATE_CHANGE, {
date: formatedDate
})
}, this._config.inputOnChangeDelay)
})
EventHandler.on(window, EVENT_RESIZE, () => {
this._mobile = window.innerWidth < 768
})
}
_addCalendarEventListeners() {
for (const calendar of SelectorEngine.find(SELECTOR_CALENDAR, this._menu)) {
EventHandler.on(calendar, 'startDateChange.coreui.calendar', event => {
this._changeStartDate(event.date)
if (!this._config.range && (!this._config.footer && !this._config.timepicker)) {
this.hide()
}
})
EventHandler.on(calendar, 'endDateChange.coreui.calendar', event => {
this._changeEndDate(event.date)
if (this._startDate && (!this._config.footer && !this._config.timepicker)) {
this.hide()
}
})
if (this._config.previewDateOnHover && !this._config.disabled) {
EventHandler.on(calendar, 'cellHover.coreui.calendar', event => {
if (this._selectEndDate) {
const previewValue = event.date ? this._setInputValue(event.date) : this._setInputValue(this._endDate)
this._updatePreviewInputVisibility(this._endPreviewInput, event.date ? previewValue : '')
return
}
const previewValue = event.date ? this._setInputValue(event.date) : this._setInputValue(this._startDate)
this._updatePreviewInputVisibility(this._startPreviewInput, event.date ? previewValue : '')
})
}
EventHandler.on(calendar, 'selectEndChange.coreui.calendar', event => {
this._selectEndDate = event.value
})
}
}
_changeStartDate(value, skipTimePickerUpdate = false) {
this._startDate = value
this._startInput.value = this._setInputValue(value)
this._startInput.dispatchEvent(new Event('change'))
EventHandler.trigger(this._element, EVENT_START_DATE_CHANGE, {
date: value
})
if (this._timePickerStart && !skipTimePickerUpdate) {
this._timePickerStart.update(this._getTimePickerConfig(true))
}
}
_changeEndDate(value, skipTimePickerUpdate = false) {
this._endDate = value
this._endInput.value = this._setInputValue(value)
this._endInput.dispatchEvent(new Event('change'))
EventHandler.trigger(this._element, EVENT_END_DATE_CHANGE, {
date: value
})
if (this._timePickerEnd && !skipTimePickerUpdate) {
this._timePickerEnd.update(this._getTimePickerConfig(false))
}
}
_getCalendarConfig() {
return {
ariaNavNextMonthLabel: this._config.ariaNavNextMonthLabel,
ariaNavNextYearLabel: this._config.ariaNavNextYearLabel,
ariaNavPrevMonthLabel: this._config.ariaNavPrevMonthLabel,
ariaNavPrevYearLabel: this._config.ariaNavPrevYearLabel,
calendarDate: this._calendarDate,
calendars: this._mobile ? 1 : this._config.calendars,
disabledDates: this._config.disabledDates,
endDate: this._endDate,
firstDayOfWeek: this._config.firstDayOfWeek,
locale: this._config.locale,
maxDate: this._config.maxDate,
minDate: this._config.minDate,
range: this._config.range,
selectAdjacementDays: this._config.selectAdjacementDays,
selectEndDate: this._selectEndDate,
selectionType: this._config.selectionType,
showAdjacementDays: this._config.showAdjacementDays,
showWeekNumber: this._config.showWeekNumber,
startDate: this._startDate,
weekdayFormat: this._config.weekdayFormat,
weekNumbersLabel: this._config.weekNumbersLabel
}
}
_getTimePickerConfig(start) {
return {
disabled: start ? !this._startDate : !this._endDate,
locale: this._config.locale,
time: start ? this._startDate && new Date(this._startDate) : this._endDate && new Date(this._endDate),
type: 'inline',
variant: 'select'
}
}
_createDateRangePicker() {
this._element.classList.add(CLASS_NAME_DATE_PICKER)
Manipulator.setDataAttribute(this._element, 'toggle', this._config.range ? CLASS_NAME_DATE_RANGE_PICKER : CLASS_NAME_DATE_PICKER)
if (this._config.size) {
this._element.classList.add(`date-picker-${this._config.size}`)
}
if (this._config.disabled) {
this._element.classList.add(CLASS_NAME_DISABLED)
}
this._element.classList.toggle(CLASS_NAME_IS_INVALID, this._config.invalid)
this._element.classList.toggle(CLASS_NAME_IS_VALID, this._config.valid)
this._element.append(this._createDateRangePickerInputGroup())
const dropdownEl = document.createElement('div')
dropdownEl.classList.add(CLASS_NAME_DROPDOWN)
dropdownEl.append(this._createDateRangePickerBody())
if (this._config.footer || this._config.timepicker) {
dropdownEl.append(this._createDateRangeFooter())
}
const { container } = this._config
if (container) {
container.append(dropdownEl)
} else {
this._element.append(dropdownEl)
}
this._menu = dropdownEl
}
_updatePreviewInputVisibility(previewInput, value) {
if (!previewInput) {
return
}
if (value && value.trim() !== '') {
previewInput.style.display = 'block'
previewInput.value = value
} else {
previewInput.style.display = 'none'
previewInput.value = ''
}
}
_createInputWrapper(inputEl, isStart = true) {
if (!this._config.previewDateOnHover || this._config.disabled) {
return inputEl
}
const wrapperEl = document.createElement('div')
wrapperEl.classList.add(CLASS_NAME_INPUT_WRAPPER)
wrapperEl.append(inputEl)
const previewInputEl = document.createElement('input')
previewInputEl.classList.add(CLASS_NAME_INPUT, CLASS_NAME_INPUT_PREVIEW)
previewInputEl.type = 'text'
previewInputEl.readOnly = true
previewInputEl.tabIndex = -1
previewInputEl.style.display = 'none'
if (isStart) {
this._startPreviewInput = previewInputEl
} else {
this._endPreviewInput = previewInputEl
}
wrapperEl.append(previewInputEl)
return wrapperEl
}
_createDateRangePickerInputGroup() {
const inputGroupEl = document.createElement('div')
inputGroupEl.classList.add(CLASS_NAME_INPUT_GROUP)
let startInputName = null
if (this._config.name || this._config.startName || this._element.id) {
startInputName = this._config.name || this._config.startName || (this._config.range ? `date-range-picker-start-date-${this._element.id}` : `date-picker-${this._element.id}`)
}
const startInputEl = this._createInput(startInputName, this._getPlaceholder()[0], this._setInputValue(this._startDate))
let endInputName = null
if (this._config.endName || this._element.id) {
endInputName = this._config.endName || `date-range-picker-end-date-${this._element.id}`
}
const endInputEl = this._createInput(endInputName, this._getPlaceholder()[1], this._setInputValue(this._endDate))
const inputGroupTextSeparatorEl = document.createElement('div')
inputGroupTextSeparatorEl.classList.add(CLASS_NAME_SEPARATOR)
this._startInput = startInputEl
this._endInput = endInputEl
const startInputWrapper = this._createInputWrapper(startInputEl, true)
inputGroupEl.append(startInputWrapper)
if (this._config.separator) {
inputGroupEl.append(inputGroupTextSeparatorEl)
}
if (this._config.range) {
const endInputWrapper = this._createInputWrapper(endInputEl, false)
inputGroupEl.append(endInputWrapper)
}
if (this._config.indicator) {
const inputGroupIndicatorEl = document.createElement('div')
inputGroupIndicatorEl.classList.add(CLASS_NAME_INDICATOR)
if (!this._config.disabled) {
inputGroupIndicatorEl.tabIndex = 0
}
inputGroupEl.append(inputGroupIndicatorEl)
this._indicatorElement = inputGroupIndicatorEl
}
if (this._config.cleaner) {
const inputGroupCleanerEl = document.createElement('div')
inputGroupCleanerEl.classList.add(CLASS_NAME_CLEANER)
inputGroupCleanerEl.addEventListener('click', event => {
event.stopPropagation()
this.clear()
})
inputGroupEl.append(inputGroupCleanerEl)
}
this._togglerElement = inputGroupEl
return inputGroupEl
}
_createDateRangePickerBody() {
const dateRangePickerBodyEl = document.createElement('div')
dateRangePickerBodyEl.classList.add(CLASS_NAME_BODY)
if (Object.keys(this._config.ranges).length) {
const dateRangePickerRangesEl = document.createElement('div')
dateRangePickerRangesEl.classList.add(CLASS_NAME_RANGES)
for (const key of Object.keys(this._config.ranges)) {
const buttonEl = document.createElement('button')
buttonEl.classList.add(...this._getButtonClasses(this._config.rangesButtonsClasses))
buttonEl.role = 'button'
buttonEl.addEventListener('click', () => {
this._changeStartDate(this._config.ranges[key][0])
this._changeEndDate(this._config.ranges[key][1])
this._calendar.update(this._getCalendarConfig())
})
buttonEl.innerHTML = key
dateRangePickerRangesEl.append(buttonEl)
}
dateRangePickerBodyEl.append(dateRangePickerRangesEl)
}
const calendarsEl = document.createElement('div')
calendarsEl.classList.add(CLASS_NAME_CALENDARS)
this._calendars = calendarsEl
dateRangePickerBodyEl.append(calendarsEl)
if (this._config.timepicker) {
const timepickersEl = document.createElement('div')
timepickersEl.classList.add(CLASS_NAME_TIME_PICKERS)
this._timepickers = timepickersEl
dateRangePickerBodyEl.append(timepickersEl)
}
return dateRangePickerBodyEl
}
_createDateRangePickerCalendars() {
const calendarEl = document.createElement('div')
calendarEl.classList.add(CLASS_NAME_CALENDAR)
this._calendars.append(calendarEl)
this._calendar = new Calendar(calendarEl, this._getCalendarConfig())
EventHandler.on(calendarEl, 'calendarDateChange.coreui.calendar', event => {
this._calendarDate = event.date
})
EventHandler.on(calendarEl, 'calendarMouseleave.coreui.calendar', () => {
this._updatePreviewInputVisibility(this._startPreviewInput, '')
this._updatePreviewInputVisibility(this._endPreviewInput, '')
})
if (this._config.timepicker) {
if ((this._mobile && this._config.range) || (this._config.range && this._config.calendars === 1)) {
const timePickerStartEl = document.createElement('div')
timePickerStartEl.classList.add(CLASS_NAME_TIME_PICKER)
this._timePickerStart = new TimePicker(timePickerStartEl, this._getTimePickerConfig(true))
this._timepickers.append(timePickerStartEl)
EventHandler.on(timePickerStartEl, 'timeChange.coreui.time-picker', event => {
this._changeStartDate(event.date, true)
this._calendar.update(this._getCalendarConfig())
})
const timePickerEndEl = document.createElement('div')
timePickerEndEl.classList.add(CLASS_NAME_TIME_PICKER)
this._timePickerEnd = new TimePicker(timePickerEndEl, this._getTimePickerConfig(false))
this._timepickers.append(timePickerEndEl)
EventHandler.on(timePickerEndEl, 'timeChange.coreui.time-picker', event => {
this._changeEndDate(event.date, true)
this._calendar.update(this._getCalendarConfig())
})
} else {
for (const [index, _] of Array.from({ length: this._config.calendars }).entries()) {
const timePickerEl = document.createElement('div')
timePickerEl.classList.add(CLASS_NAME_TIME_PICKER)
const _timepicker = new TimePicker(timePickerEl, this._getTimePickerConfig(index === 0))
if (index === 0) {
this._timePickerStart = _timepicker
} else {
this._timePickerEnd = _timepicker
}
this._timepickers.append(timePickerEl)
EventHandler.on(timePickerEl, 'timeChange.coreui.time-picker', event => {
if (index === 0) {
this._changeStartDate(event.date, true)
} else {
this._changeEndDate(event.date, true)
}
this._calendar.update(this._getCalendarConfig())
})
}
}
}
}
_createDateRangeFooter() {
const footerEl = document.createElement('div')
footerEl.classList.add(CLASS_NAME_FOOTER)
if (this._config.todayButton) {
const todayButtonEl = document.createElement('button')
todayButtonEl.classList.add(...this._getButtonClasses(this._config.todayButtonClasses))
todayButtonEl.type = 'button'
todayButtonEl.innerHTML = this._config.todayButton
todayButtonEl.addEventListener('click', () => {
const date = new Date()
this._calendarDate = date
this._changeStartDate(date)
if (this._config.range) {
this._changeEndDate(date)
}
this._calendar.update(this._getCalendarConfig())
})
footerEl.append(todayButtonEl)
}
if (this._config.cancelButton) {
const cancelButtonEl = document.createElement('button')
cancelButtonEl.classList.add(...this._getButtonClasses(this._config.cancelButtonClasses))
cancelButtonEl.type = 'button'
cancelButtonEl.innerHTML = this._config.cancelButton
cancelButtonEl.addEventListener('click', () => {
this.cancel()
})
footerEl.append(cancelButtonEl)
}
if (this._config.confirmButton) {
const confirmButtonEl = document.createElement('button')
confirmButtonEl.classList.add(...this._getButtonClasses(this._config.confirmButtonClasses))
confirmButtonEl.type = 'button'
confirmButtonEl.innerHTML = this._config.confirmButton
confirmButtonEl.addEventListener('click', () => {
this.hide()
})
footerEl.append(confirmButtonEl)
}
return footerEl
}
_createInput(name, placeholder, value) {
const inputEl = document.createElement('input')
inputEl.classList.add(CLASS_NAME_INPUT)
inputEl.autocomplete = 'off'
inputEl.disabled = this._config.disabled
inputEl.placeholder = placeholder
inputEl.readOnly = this._config.inputReadOnly
inputEl.required = this._config.required
inputEl.type = 'text'
inputEl.value = value
if (name) {
inputEl.name = name
}
const events = ['change', 'keyup', 'paste']
for (const event of events) {
inputEl.addEventListener(event, ({ target }) => {
if (target.closest(SELECTOR_WAS_VALIDATED)) {
const inputs = SelectorEngine.find(SELECTOR_INPUT, this._element)
for (const input of inputs) {
if (Number.isNaN(Date.parse(input.value))) {
this._element.classList.add(CLASS_NAME_IS_INVALID)
this._element.classList.remove(CLASS_NAME_IS_VALID)
return
}
}
if (this._config.range && this._startDate instanceof Date && this._endDate instanceof Date) {
this._element.classList.add(CLASS_NAME_IS_VALID)
this._element.classList.remove(CLASS_NAME_IS_INVALID)
return
}
if (!this._config.range && this._startDate instanceof Date) {
this._element.classList.add(CLASS_NAME_IS_VALID)
this._element.classList.remove(CLASS_NAME_IS_INVALID)
return
}
this._element.classList.add(CLASS_NAME_IS_INVALID)
this._element.classList.remove(CLASS_NAME_IS_VALID)
}
})
}
return inputEl
}
_createPopper() {
if (typeof Popper === 'undefined') {
throw new TypeError('CoreUI\'s date picker require Popper (https://popper.js.org)')
}
const popperConfig = {
modifiers: [{
name: 'preventOverflow',
options: {
boundary: 'clippingParents'
}
},
{
name: 'offset',
options: {
offset: [0, 2]
}
}],
placement: isRTL() ? 'bottom-end' : 'bottom-start'
}
this._popper = Popper.createPopper(this._togglerElement, this._menu, popperConfig)
}
_parseDate(str) {
if (!str) {
return null
}
if (this._config.inputDateParse) {
return this._config.inputDateParse(str)
}
if (this._config.selectionType === 'day') {
return getLocalDateFromString(str, this._config.locale, this._config.timepicker)
}
return convertToDateObject(str, this._config.selectionType)
}
_formatDate(date) {
if (!date) {
return ''
}
if (this._config.inputDateFormat) {
return this._config.inputDateFormat(
date instanceof Date ? new Date(date) : convertToDateObject(date, this._config.selectionType)
)
}
if (this._config.selectionType !== 'day') {
return date
}
const _date = new Date(date)
return this._config.timepicker ? _date.toLocaleString(this._config.locale) : _date.toLocaleDateString(this._config.locale)
}
_getButtonClasses(classes) {
if (typeof classes === 'string') {
return classes.split(' ')
}
return classes
}
_getPlaceholder() {
const { placeholder } = this._config
if (typeof placeholder === 'string') {
return placeholder.split(',')
}
return placeholder
}
_isShown() {
return this._element.classList.contains(CLASS_NAME_SHOW)
}
_setInputValue(date) {
if (date) {
return this._formatDate(date)
}
return ''
}
_configAfterMerge(config) {
if (config.container === true) {
config.container = document.body
}
if (typeof config.container === 'object' || typeof config.container === 'string') {
config.container = getElement(config.container)
}
return config
}
// Static
static dateRangePickerInterface(element, config) {
const data = DateRangePicker.getOrCreateInstance(element, config)
if (typeof config === 'string') {
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
}
}
static jQueryInterface(config) {
return this.each(function () {
const data = DateRangePicker.getOrCreateInstance(this, config)
if (typeof config !== 'string') {
return
}
if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {
throw new TypeError(`No method named "${config}"`)
}
data[config](this)
})
}
static clearMenus(event) {
if (event.button === RIGHT_MOUSE_BUTTON || (event.type === 'keyup' && event.key !== TAB_KEY)) {
return
}
const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN)
for (const toggle of openToggles) {
const context = DateRangePicker.getInstance(toggle)
if (!context) {
continue
}
const composedPath = event.composedPath()
if (
composedPath.includes(context._element) || composedPath.includes(context._menu)
) {
continue
}
const relatedTarget = { relatedTarget: context._element }
if (event.type === 'click') {
relatedTarget.clickEvent = event
}
context.hide()
}
}
}
/**
* Data API implementation
*/
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
const dateRangePickers = SelectorEngine.find(SELECTOR_DATA_TOGGLE)
for (let i = 0, len = dateRangePickers.length; i < len; i++) {
DateRangePicker.dateRangePickerInterface(dateRangePickers[i])
}
})
EventHandler.on(document, EVENT_CLICK_DATA_API, DateRangePicker.clearMenus)
EventHandler.on(document, EVENT_KEYUP_DATA_API, DateRangePicker.clearMenus)
/**
* jQuery
*/
defineJQueryPlugin(DateRangePicker)
export default DateRangePicker