@coreui/coreui-pro
Version:
The most popular front-end framework for developing responsive, mobile-first projects on the web rewritten by the CoreUI Team
979 lines (827 loc) • 31.5 kB
JavaScript
/* eslint-disable complexity, indent, multiline-ternary, @stylistic/multiline-ternary */
/**
* --------------------------------------------------------------------------
* CoreUI PRO calendar.js
* License (https://coreui.io/pro/license/)
* --------------------------------------------------------------------------
*/
import BaseComponent from './base-component.js'
import EventHandler from './dom/event-handler.js'
import Manipulator from './dom/manipulator.js'
import SelectorEngine from './dom/selector-engine.js'
import { defineJQueryPlugin } from './util/index.js'
import {
convertToDateObject,
createGroupsInArray,
getCalendarDate,
getDateBySelectionType,
getMonthDetails,
getMonthsNames,
getYears,
isDateDisabled,
isDateInRange,
isDateSelected,
isDisableDateInRange,
isMonthDisabled,
isMonthInRange,
isMonthSelected,
isToday,
isYearDisabled,
isYearInRange,
isYearSelected,
setTimeFromDate
} from './util/calendar.js'
/**
* Constants
*/
const NAME = 'calendar'
const DATA_KEY = 'coreui.calendar'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const ARROW_UP_KEY = 'ArrowUp'
const ARROW_RIGHT_KEY = 'ArrowRight'
const ARROW_DOWN_KEY = 'ArrowDown'
const ARROW_LEFT_KEY = 'ArrowLeft'
const ENTER_KEY = 'Enter'
const SPACE_KEY = 'Space'
const EVENT_BLUR = `blur${EVENT_KEY}`
const EVENT_CALENDAR_DATE_CHANGE = `calendarDateChange${EVENT_KEY}`
const EVENT_CALENDAR_MOUSE_LEAVE = `calendarMouseleave${EVENT_KEY}`
const EVENT_CELL_HOVER = `cellHover${EVENT_KEY}`
const EVENT_END_DATE_CHANGE = `endDateChange${EVENT_KEY}`
const EVENT_FOCUS = `focus${EVENT_KEY}`
const EVENT_KEYDOWN = `keydown${EVENT_KEY}`
const EVENT_SELECT_END_CHANGE = `selectEndChange${EVENT_KEY}`
const EVENT_START_DATE_CHANGE = `startDateChange${EVENT_KEY}`
const EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`
const EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
const CLASS_NAME_CALENDAR_CELL = 'calendar-cell'
const CLASS_NAME_CALENDAR_CELL_INNER = 'calendar-cell-inner'
const CLASS_NAME_CALENDAR_ROW = 'calendar-row'
const CLASS_NAME_CALENDARS = 'calendars'
const CLASS_NAME_SHOW_WEEK_NUMBERS = 'show-week-numbers'
const SELECTOR_BTN_DOUBLE_NEXT = '.btn-double-next'
const SELECTOR_BTN_DOUBLE_PREV = '.btn-double-prev'
const SELECTOR_BTN_MONTH = '.btn-month'
const SELECTOR_BTN_NEXT = '.btn-next'
const SELECTOR_BTN_PREV = '.btn-prev'
const SELECTOR_BTN_YEAR = '.btn-year'
const SELECTOR_CALENDAR = '.calendar'
const SELECTOR_CALENDAR_CELL = '.calendar-cell'
const SELECTOR_CALENDAR_CELL_CLICKABLE = `${SELECTOR_CALENDAR_CELL}[tabindex="0"]`
const SELECTOR_CALENDAR_ROW = '.calendar-row'
const SELECTOR_CALENDAR_ROW_CLICKABLE = `${SELECTOR_CALENDAR_ROW}[tabindex="0"]`
const SELECTOR_DATA_TOGGLE = '[data-coreui-toggle="calendar"]'
const Default = {
ariaNavNextMonthLabel: 'Next month',
ariaNavNextYearLabel: 'Next year',
ariaNavPrevMonthLabel: 'Previous month',
ariaNavPrevYearLabel: 'Previous year',
calendarDate: null,
calendars: 1,
disabledDates: null,
endDate: null,
firstDayOfWeek: 1,
locale: 'default',
maxDate: null,
minDate: null,
range: false,
selectAdjacementDays: false,
selectEndDate: false,
selectionType: 'day',
showAdjacementDays: true,
showWeekNumber: false,
startDate: null,
weekdayFormat: 2,
weekNumbersLabel: null
}
const DefaultType = {
ariaNavNextMonthLabel: 'string',
ariaNavNextYearLabel: 'string',
ariaNavPrevMonthLabel: 'string',
ariaNavPrevYearLabel: 'string',
calendarDate: '(date|number|string|null)',
calendars: 'number',
disabledDates: '(array|date|function|null)',
endDate: '(date|number|string|null)',
firstDayOfWeek: 'number',
locale: 'string',
maxDate: '(date|number|string|null)',
minDate: '(date|number|string|null)',
range: 'boolean',
selectAdjacementDays: 'boolean',
selectEndDate: 'boolean',
selectionType: 'string',
showAdjacementDays: 'boolean',
showWeekNumber: 'boolean',
startDate: '(date|number|string|null)',
weekdayFormat: '(number|string)',
weekNumbersLabel: '(string|null)'
}
/**
* Class definition
*/
class Calendar extends BaseComponent {
constructor(element, config) {
super(element)
this._config = this._getConfig(config)
this._initializeDates()
this._initializeView()
this._createCalendar()
this._addEventListeners()
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
update(config) {
this._config = this._getConfig(config)
this._initializeDates()
this._initializeView()
// Clear the current calendar content
this._element.innerHTML = ''
this._createCalendar()
}
// Private
_focusOnFirstAvailableCell() {
const cell = SelectorEngine.findOne(SELECTOR_CALENDAR_CELL_CLICKABLE, this._element)
if (cell) {
cell.focus()
}
}
_getDate(target) {
if (this._config.selectionType === 'week') {
const firstCell = SelectorEngine.findOne(SELECTOR_CALENDAR_CELL, target.closest(SELECTOR_CALENDAR_ROW))
return new Date(Manipulator.getDataAttribute(firstCell, 'date'))
}
return new Date(Manipulator.getDataAttribute(target, 'date'))
}
_handleCalendarClick(event) {
const target = event.target.classList.contains(CLASS_NAME_CALENDAR_CELL_INNER) ? event.target.parentElement : event.target
const date = this._getDate(target)
const cloneDate = new Date(date)
const index = Manipulator.getDataAttribute(target.closest(SELECTOR_CALENDAR), 'calendar-index')
if (this._view === 'days') {
this._setCalendarDate(index ? new Date(cloneDate.setMonth(cloneDate.getMonth() - index)) : date)
}
if (this._view === 'months' && this._config.selectionType !== 'month') {
this._setCalendarDate(index ? new Date(cloneDate.setMonth(cloneDate.getMonth() - index)) : date)
this._view = 'days'
this._updateCalendar(this._focusOnFirstAvailableCell.bind(this))
return
}
if (this._view === 'years' && this._config.selectionType !== 'year') {
this._setCalendarDate(index ? new Date(cloneDate.setFullYear(cloneDate.getFullYear() - index)) : date)
this._view = 'months'
this._updateCalendar(this._focusOnFirstAvailableCell.bind(this))
return
}
// Allow to change the calendarDate but not startDate or endDate
if (isDateDisabled(date, this._minDate, this._maxDate, this._config.disabledDates)) {
return
}
this._hoverDate = null
this._selectDate(date)
this._updateClassNamesAndAriaLabels()
}
_handleCalendarKeydown(event) {
const date = this._getDate(event.target)
if (event.code === SPACE_KEY || event.key === ENTER_KEY) {
event.preventDefault()
this._handleCalendarClick(event)
}
if (
event.key === ARROW_RIGHT_KEY ||
event.key === ARROW_LEFT_KEY ||
event.key === ARROW_UP_KEY ||
event.key === ARROW_DOWN_KEY
) {
event.preventDefault()
if (
this._maxDate &&
date >= convertToDateObject(this._maxDate, this._config.selectionType) &&
(event.key === ARROW_RIGHT_KEY || event.key === ARROW_DOWN_KEY)
) {
return
}
if (
this._minDate &&
date <= convertToDateObject(this._minDate, this._config.selectionType) &&
(event.key === ARROW_LEFT_KEY || event.key === ARROW_UP_KEY)
) {
return
}
let element = event.target
if (this._config.selectionType === 'week' && element.tabIndex === -1) {
element = element.closest(SELECTOR_CALENDAR_ROW_CLICKABLE)
}
const list = SelectorEngine.find(
this._config.selectionType === 'week' ? SELECTOR_CALENDAR_ROW_CLICKABLE : SELECTOR_CALENDAR_CELL_CLICKABLE,
this._element
)
const index = list.indexOf(element)
const first = index === 0
const last = index === list.length - 1
const toBoundary = {
start: index,
end: list.length - (index + 1)
}
const gap = {
ArrowRight: 1,
ArrowLeft: -1,
ArrowUp: this._config.selectionType === 'week' && this._view === 'days' ? -1 : (this._view === 'days' ? -7 : -3),
ArrowDown: this._config.selectionType === 'week' && this._view === 'days' ? 1 : (this._view === 'days' ? 7 : 3)
}
if (
(event.key === ARROW_RIGHT_KEY && last) ||
(event.key === ARROW_DOWN_KEY && toBoundary.end < gap.ArrowDown) ||
(event.key === ARROW_LEFT_KEY && first) ||
(event.key === ARROW_UP_KEY && toBoundary.start < Math.abs(gap.ArrowUp))
) {
const callback = key => {
const _list = SelectorEngine.find(`${SELECTOR_CALENDAR_CELL_CLICKABLE}, ${SELECTOR_CALENDAR_ROW_CLICKABLE}`, this._element)
if (_list.length && key === ARROW_RIGHT_KEY) {
_list[0].focus()
}
if (_list.length && key === ARROW_LEFT_KEY) {
_list[_list.length - 1].focus()
}
if (_list.length && key === ARROW_DOWN_KEY) {
_list[gap.ArrowDown - (list.length - index)].focus()
}
if (_list.length && key === ARROW_UP_KEY) {
_list[_list.length - (Math.abs(gap.ArrowUp) + 1 - (index + 1))].focus()
}
}
if (this._view === 'days') {
this._modifyCalendarDate(0, event.key === ARROW_RIGHT_KEY || event.key === ARROW_DOWN_KEY ? 1 : -1, callback.bind(this, event.key))
}
if (this._view === 'months') {
this._modifyCalendarDate(event.key === ARROW_RIGHT_KEY || event.key === ARROW_DOWN_KEY ? 1 : -1, 0, callback.bind(this, event.key))
}
if (this._view === 'years') {
this._modifyCalendarDate(event.key === ARROW_RIGHT_KEY || event.key === ARROW_DOWN_KEY ? 10 : -10, 0, callback.bind(this, event.key))
}
return
}
if (list[index + gap[event.key]].tabIndex === 0) {
list[index + gap[event.key]].focus()
return
}
for (let i = index; i < list.length; event.key === ARROW_RIGHT_KEY || event.key === ARROW_DOWN_KEY ? i++ : i--) {
if (list[i + gap[event.key]].tabIndex === 0) {
list[i + gap[event.key]].focus()
break
}
}
}
}
_handleCalendarMouseEnter(event) {
const target = event.target.classList.contains(CLASS_NAME_CALENDAR_CELL_INNER) ? event.target.parentElement : event.target
const date = this._getDate(target)
if (isDateDisabled(date, this._minDate, this._maxDate, this._config.disabledDates)) {
return
}
this._hoverDate = setTimeFromDate(date, this._selectEndDate ? this._endDate : this._startDate)
EventHandler.trigger(this._element, EVENT_CELL_HOVER, {
date: getDateBySelectionType(this._hoverDate, this._config.selectionType)
})
this._updateClassNamesAndAriaLabels()
}
_handleCalendarMouseLeave() {
this._hoverDate = null
EventHandler.trigger(this._element, EVENT_CELL_HOVER, {
date: null
})
this._updateClassNamesAndAriaLabels()
}
_addEventListeners() {
EventHandler.on(this._element, EVENT_CLICK_DATA_API, SELECTOR_CALENDAR_CELL_CLICKABLE, event => {
this._handleCalendarClick(event)
})
EventHandler.on(this._element, EVENT_KEYDOWN, SELECTOR_CALENDAR_CELL_CLICKABLE, event => {
this._handleCalendarKeydown(event)
})
EventHandler.on(this._element, EVENT_MOUSEENTER, SELECTOR_CALENDAR_CELL_CLICKABLE, event => {
this._handleCalendarMouseEnter(event)
})
EventHandler.on(this._element, EVENT_MOUSELEAVE, SELECTOR_CALENDAR_CELL_CLICKABLE, () => {
this._handleCalendarMouseLeave()
})
EventHandler.on(this._element, EVENT_FOCUS, SELECTOR_CALENDAR_CELL_CLICKABLE, event => {
this._handleCalendarMouseEnter(event)
})
EventHandler.on(this._element, EVENT_BLUR, SELECTOR_CALENDAR_CELL_CLICKABLE, () => {
this._handleCalendarMouseLeave()
})
EventHandler.on(this._element, EVENT_CLICK_DATA_API, SELECTOR_CALENDAR_ROW_CLICKABLE, event => {
this._handleCalendarClick(event)
})
EventHandler.on(this._element, EVENT_KEYDOWN, SELECTOR_CALENDAR_ROW_CLICKABLE, event => {
this._handleCalendarKeydown(event)
})
EventHandler.on(this._element, EVENT_MOUSEENTER, SELECTOR_CALENDAR_ROW_CLICKABLE, event => {
this._handleCalendarMouseEnter(event)
})
EventHandler.on(this._element, EVENT_MOUSELEAVE, SELECTOR_CALENDAR_ROW_CLICKABLE, () => {
this._handleCalendarMouseLeave()
})
EventHandler.on(this._element, EVENT_FOCUS, SELECTOR_CALENDAR_ROW_CLICKABLE, event => {
this._handleCalendarMouseEnter(event)
})
EventHandler.on(this._element, EVENT_BLUR, SELECTOR_CALENDAR_ROW_CLICKABLE, () => {
this._handleCalendarMouseLeave()
})
// Navigation
this._addNavigationEventListeners()
EventHandler.on(this._element, EVENT_MOUSELEAVE, 'table', () => {
EventHandler.trigger(this._element, EVENT_CALENDAR_MOUSE_LEAVE)
})
}
_addNavigationEventListeners() {
const navigationSelectors = {
[SELECTOR_BTN_PREV]: () => this._modifyCalendarDate(0, -1),
[SELECTOR_BTN_DOUBLE_PREV]: () => this._modifyCalendarDate(this._view === 'years' ? -10 : -1),
[SELECTOR_BTN_NEXT]: () => this._modifyCalendarDate(0, 1),
[SELECTOR_BTN_DOUBLE_NEXT]: () => this._modifyCalendarDate(this._view === 'years' ? 10 : 1),
[SELECTOR_BTN_MONTH]: () => {
this._view = 'months'
this._updateCalendar()
},
[SELECTOR_BTN_YEAR]: () => {
this._view = 'years'
this._updateCalendar()
}
}
for (const [selector, handler] of Object.entries(navigationSelectors)) {
EventHandler.on(this._element, EVENT_CLICK_DATA_API, selector, event => {
event.preventDefault()
const selectors = SelectorEngine.find(selector, this._element)
const selectorIndex = selectors.indexOf(event.target.closest(selector))
handler()
// Retrieve focus to the navigation element
const _selectors = SelectorEngine.find(selector, this._element)
if (_selectors && _selectors[selectorIndex]) {
_selectors[selectorIndex].focus()
}
})
}
}
_setCalendarDate(date) {
this._calendarDate = date
EventHandler.trigger(this._element, EVENT_CALENDAR_DATE_CHANGE, {
date
})
}
_modifyCalendarDate(years, months = 0, callback) {
const year = this._calendarDate.getFullYear()
const month = this._calendarDate.getMonth()
const d = new Date(year, month, 1)
if (years) {
d.setFullYear(d.getFullYear() + years)
}
if (months) {
d.setMonth(d.getMonth() + months)
}
this._calendarDate = d
if (this._view === 'days') {
EventHandler.trigger(this._element, EVENT_CALENDAR_DATE_CHANGE, {
date: d
})
}
this._updateCalendar(callback)
}
_setEndDate(date) {
this._endDate = setTimeFromDate(date, this._endDate)
EventHandler.trigger(this._element, EVENT_END_DATE_CHANGE, {
date: getDateBySelectionType(this._endDate, this._config.selectionType)
})
}
_setStartDate(date) {
this._startDate = setTimeFromDate(date, this._startDate)
EventHandler.trigger(this._element, EVENT_START_DATE_CHANGE, {
date: getDateBySelectionType(this._startDate, this._config.selectionType)
})
}
_setSelectEndDate(value) {
this._selectEndDate = value
EventHandler.trigger(this._element, EVENT_SELECT_END_CHANGE, {
value
})
}
_selectDate(date) {
if (isDateDisabled(date, this._minDate, this._maxDate, this._config.disabledDates)) {
return
}
if (this._config.range) {
if (this._selectEndDate) {
this._setSelectEndDate(false)
if (this._startDate && this._startDate > date) {
this._setStartDate(null)
this._setEndDate(null)
return
}
if (isDisableDateInRange(this._startDate, date, this._config.disabledDates)) {
this._setStartDate(null)
this._setEndDate(null)
return
}
this._setEndDate(date)
return
}
if (this._endDate && this._endDate < date) {
this._setStartDate(null)
this._setEndDate(null)
return
}
if (isDisableDateInRange(date, this._endDate, this._config.disabledDates)) {
this._setStartDate(null)
this._setEndDate(null)
return
}
this._setSelectEndDate(true)
this._setStartDate(date)
return
}
this._setStartDate(date)
}
_createCalendarPanel(order) {
const calendarDate = getCalendarDate(this._calendarDate, order, this._view)
const year = calendarDate.getFullYear()
const month = calendarDate.getMonth()
const calendarPanelEl = document.createElement('div')
calendarPanelEl.classList.add('calendar')
Manipulator.setDataAttribute(calendarPanelEl, 'calendar-index', order)
// Create navigation
const navigationElement = document.createElement('div')
navigationElement.classList.add('calendar-nav')
navigationElement.innerHTML = `
<div class="calendar-nav-prev">
<button type="button" class="calendar-nav-btn btn-double-prev" aria-label="${this._config.ariaNavPrevYearLabel}">
<span class="calendar-nav-icon calendar-nav-icon-double-prev"></span>
</button>
${this._view === 'days' ? `<button type="button" class="calendar-nav-btn btn-prev" aria-label="${this._config.ariaNavPrevMonthLabel}">
<span class="calendar-nav-icon calendar-nav-icon-prev"></span>
</button>` : ''}
</div>
<div class="calendar-nav-date" aria-live="polite">
${this._view === 'days' ? `<button type="button" class="calendar-nav-btn btn-sm btn-month">
${calendarDate.toLocaleDateString(this._config.locale, { month: 'long' })}
</button>` : ''}
<button type="button" class="calendar-nav-btn btn-year">
${calendarDate.toLocaleDateString(this._config.locale, { year: 'numeric' })}
</button>
</div>
<div class="calendar-nav-next">
${this._view === 'days' ? `<button type="button" class="calendar-nav-btn btn-next" aria-label="${this._config.ariaNavNextMonthLabel}">
<span class="calendar-nav-icon calendar-nav-icon-next"></span>
</button>` : ''}
<button type="button" class="calendar-nav-btn btn-double-next" aria-label="${this._config.ariaNavNextYearLabel}">
<span class="calendar-nav-icon calendar-nav-icon-double-next"></span>
</button>
</div>
`
const monthDetails = getMonthDetails(year, month, this._config.firstDayOfWeek)
const listOfMonths = createGroupsInArray(getMonthsNames(this._config.locale), 4)
const listOfYears = createGroupsInArray(getYears(calendarDate.getFullYear()), 4)
const weekDays = monthDetails[0].days
const calendarTable = document.createElement('table')
calendarTable.innerHTML = `
${this._view === 'days' ? `
<thead>
<tr>
${this._config.showWeekNumber ?
`<th class="${CLASS_NAME_CALENDAR_CELL}">
<div class="calendar-header-cell-inner">
${this._config.weekNumbersLabel ?? ''}
</div>
</th>` : ''
}
${weekDays.map(({ date }) => (
`<th class="${CLASS_NAME_CALENDAR_CELL}" abbr="${date.toLocaleDateString(this._config.locale, { weekday: 'long' })}">
<div class="calendar-header-cell-inner">
${typeof this._config.weekdayFormat === 'string' ?
date.toLocaleDateString(this._config.locale, { weekday: this._config.weekdayFormat }) :
date
.toLocaleDateString(this._config.locale, { weekday: 'long' })
.slice(0, this._config.weekdayFormat)}
</div>
</th>`
)).join('')}
</tr>
</thead>` : ''}
<tbody>
${this._view === 'days' ? monthDetails.map(({ week, days }) => {
const { date } = days[0]
const rowAttributes = this._rowWeekAttributes(date)
return (
`<tr
class="${rowAttributes.className}"
tabindex="${rowAttributes.tabIndex}"
${rowAttributes.ariaSelected ? 'aria-selected="true"' : ''}
>
${this._config.showWeekNumber ?
`<th class="calendar-cell-week-number">${week.number}</td>` : ''
}
${days.map(({ date, month }) => {
const cellAttributes = this._cellDayAttributes(date, month)
return month === 'current' || this._config.showAdjacementDays ?
`<td
class="${cellAttributes.className}"
tabindex="${cellAttributes.tabIndex}"
${cellAttributes.ariaSelected ? 'aria-selected="true"' : ''}
data-coreui-date="${date}"
>
<div class="calendar-cell-inner day">
${date.toLocaleDateString(this._config.locale, { day: 'numeric' })}
</div>
</td>` :
'<td></td>'
}
).join('')}</tr>`
)
}).join('') : ''}
${this._view === 'months' ? listOfMonths.map((row, index) => (
`<tr>
${row.map((month, idx) => {
const date = new Date(calendarDate.getFullYear(), (index * 3) + idx, 1)
const cellAttributes = this._cellMonthAttributes(date)
return (
`<td
class="${cellAttributes.className}"
tabindex="${cellAttributes.tabIndex}"
${cellAttributes.ariaSelected ? 'aria-selected="true"' : ''}
data-coreui-date="${date.toDateString()}"
>
<div class="calendar-cell-inner month">
${month}
</div>
</td>`
)
}).join('')}
</tr>`
)).join('') : ''}
${this._view === 'years' ? listOfYears.map(row => (
`<tr>
${row.map(year => {
const date = new Date(year, 0, 1)
const cellAttributes = this._cellYearAttributes(date)
return (
`<td
class="${cellAttributes.className}"
tabindex="${cellAttributes.tabIndex}"
${cellAttributes.ariaSelected ? 'aria-selected="true"' : ''}
data-coreui-date="${date.toDateString()}"
>
<div class="calendar-cell-inner year">
${year}
</div>
</td>`
)
}).join('')}
</tr>`
)).join('') : ''}
</tbody>
`
calendarPanelEl.append(navigationElement, calendarTable)
return calendarPanelEl
}
_createCalendar() {
if (this._config.selectionType && this._view === 'days') {
this._element.classList.add(`select-${this._config.selectionType}`)
}
if (this._config.showWeekNumber) {
this._element.classList.add(CLASS_NAME_SHOW_WEEK_NUMBERS)
}
for (const [index, _] of Array.from({ length: this._config.calendars }).entries()) {
this._element.append(this._createCalendarPanel(index))
}
this._element.classList.add(CLASS_NAME_CALENDARS)
}
_initializeDates() {
// Convert dates to date objects based on the selection type
this._calendarDate = convertToDateObject(
this._config.calendarDate || this._config.startDate || this._config.endDate, this._config.selectionType
) || new Date()
this._startDate = convertToDateObject(this._config.startDate, this._config.selectionType)
this._endDate = convertToDateObject(this._config.endDate, this._config.selectionType)
this._minDate = convertToDateObject(this._config.minDate, this._config.selectionType)
this._maxDate = convertToDateObject(this._config.maxDate, this._config.selectionType)
this._hoverDate = null
this._selectEndDate = this._config.selectEndDate
}
_initializeView() {
const viewMap = {
day: 'days',
week: 'days',
month: 'months',
year: 'years'
}
this._view = viewMap[this._config.selectionType] || 'days'
}
_updateCalendar(callback) {
this._element.innerHTML = ''
this._createCalendar()
if (callback) {
setTimeout(callback, 1)
}
}
_updateClassNamesAndAriaLabels() {
if (this._config.selectionType === 'week') {
const rows = SelectorEngine.find(SELECTOR_CALENDAR_ROW, this._element)
for (const row of rows) {
const firstCell = SelectorEngine.findOne(SELECTOR_CALENDAR_CELL, row)
const date = new Date(Manipulator.getDataAttribute(firstCell, 'date'))
const rowAttributes = this._rowWeekAttributes(date)
row.className = rowAttributes.className
row.tabIndex = rowAttributes.tabIndex
if (rowAttributes.ariaSelected) {
row.setAttribute('aria-selected', true)
} else {
row.removeAttribute('aria-selected')
}
}
return
}
const cells = SelectorEngine.find(SELECTOR_CALENDAR_CELL_CLICKABLE, this._element)
for (const cell of cells) {
const date = new Date(Manipulator.getDataAttribute(cell, 'date'))
let cellAttributes
if (this._view === 'days') {
cellAttributes = this._cellDayAttributes(date, 'current')
} else if (this._view === 'months') {
cellAttributes = this._cellMonthAttributes(date)
} else {
cellAttributes = this._cellYearAttributes(date)
}
cell.className = cellAttributes.className
cell.tabIndex = cellAttributes.tabIndex
if (cellAttributes.ariaSelected) {
cell.setAttribute('aria-selected', true)
} else {
cell.removeAttribute('aria-selected')
}
}
}
_classNames(classNames) {
return Object.entries(classNames)
.filter(([_, value]) => Boolean(value))
.map(([key]) => key)
.join(' ')
}
_cellDayAttributes(date, month) {
const isCurrentMonth = month === 'current'
const isDisabled = isDateDisabled(date, this._minDate, this._maxDate, this._config.disabledDates)
const isSelected = isDateSelected(date, this._startDate, this._endDate)
const isTodayDate = isToday(date)
if (this._config.selectionType !== 'day' || this._view !== 'days') {
return {
className: this._classNames({
[CLASS_NAME_CALENDAR_CELL]: true,
today: isTodayDate,
[month]: true
}),
tabIndex: -1,
ariaSelected: false
}
}
const isInRange = isCurrentMonth && isDateInRange(date, this._startDate, this._endDate)
const isRangeHover = isCurrentMonth && this._hoverDate && (
this._selectEndDate ?
isDateInRange(date, this._startDate, this._hoverDate) :
isDateInRange(date, this._hoverDate, this._endDate)
)
const classNames = this._classNames({
[CLASS_NAME_CALENDAR_CELL]: true,
clickable: !isCurrentMonth && this._config.selectAdjacementDays,
disabled: isDisabled,
range: isInRange,
'range-hover': isRangeHover,
selected: isSelected,
today: isTodayDate,
[month]: true
})
return {
className: classNames,
tabIndex: (isCurrentMonth || this._config.selectAdjacementDays) && !isDisabled ? 0 : -1,
ariaSelected: isSelected
}
}
_cellMonthAttributes(date) {
const isDisabled = isMonthDisabled(date, this._minDate, this._maxDate, this._config.disabledDates)
const isSelected = isMonthSelected(date, this._startDate, this._endDate)
const isInRange = isMonthInRange(date, this._startDate, this._endDate)
const isRangeHover = this._config.selectionType === 'month' && this._hoverDate && (
this._selectEndDate ?
isMonthInRange(date, this._startDate, this._hoverDate) :
isMonthInRange(date, this._hoverDate, this._endDate)
)
const classNames = this._classNames({
[CLASS_NAME_CALENDAR_CELL]: true,
disabled: isDisabled,
'range-hover': isRangeHover,
range: isInRange,
selected: isSelected
})
return {
className: classNames,
tabIndex: isDisabled ? -1 : 0,
ariaSelected: isSelected
}
}
_cellYearAttributes(date) {
const isDisabled = isYearDisabled(date, this._minDate, this._maxDate, this._config.disabledDates)
const isSelected = isYearSelected(date, this._startDate, this._endDate)
const isInRange = isYearInRange(date, this._startDate, this._endDate)
const isRangeHover = this._config.selectionType === 'year' && this._hoverDate && (
this._selectEndDate ?
isYearInRange(date, this._startDate, this._hoverDate) :
isYearInRange(date, this._hoverDate, this._endDate)
)
const classNames = this._classNames({
[CLASS_NAME_CALENDAR_CELL]: true,
disabled: isDisabled,
'range-hover': isRangeHover,
range: isInRange,
selected: isSelected
})
return {
className: classNames,
tabIndex: isDisabled ? -1 : 0,
ariaSelected: isSelected
}
}
_rowWeekAttributes(date) {
if (this._config.selectionType !== 'week') {
return {
className: this._classNames({ [CLASS_NAME_CALENDAR_ROW]: true }),
tabIndex: -1,
ariaSelected: false
}
}
const isDisabled = isDateDisabled(date, this._minDate, this._maxDate, this._config.disabledDates)
const isSelected = isDateSelected(date, this._startDate, this._endDate)
const isInRange = isDateInRange(date, this._startDate, this._endDate)
const isRangeHover = this._hoverDate && (
this._selectEndDate ?
isYearInRange(date, this._startDate, this._hoverDate) :
isYearInRange(date, this._hoverDate, this._endDate)
)
const classNames = this._classNames({
[CLASS_NAME_CALENDAR_ROW]: true,
disabled: isDisabled,
range: isInRange,
'range-hover': isRangeHover,
selected: isSelected
})
return {
className: classNames,
tabIndex: isDisabled ? -1 : 0,
ariaSelected: isSelected
}
}
// Static
static calendarInterface(element, config) {
const data = Calendar.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 = Calendar.getOrCreateInstance(this, config)
if (typeof config !== 'string') {
return
}
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
})
}
}
/**
* Data API implementation
*/
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
for (const element of Array.from(document.querySelectorAll(SELECTOR_DATA_TOGGLE))) {
Calendar.calendarInterface(element)
}
})
/**
* jQuery
*/
defineJQueryPlugin(Calendar)
export default Calendar