synthetic-datepicker
Version:
This is a fast, simple and beauty datepicker library
302 lines (263 loc) • 12.6 kB
text/typescript
import pickerUi from '../../infrastructure/view/components/picker/picker.ui'
import { DEFAULT_CONFIG } from "../settings/constants"
export default class SynDatePicker extends EventTarget {
constructor(config: any, $element: Element | null) {
super()
this.config = Object.assign({}, DEFAULT_CONFIG, config)
this.$element = $element
if (this.$element !== null) this.init()
else throw new Error('')
}
private initData() {
this.MONTHS_MAP.set(0, { monthAbbrev: 'jan', monthLabel: 'January' })
this.MONTHS_MAP.set(1, { monthAbbrev: 'feb', monthLabel: 'February' })
this.MONTHS_MAP.set(2, { monthAbbrev: 'mar', monthLabel: 'March' })
this.MONTHS_MAP.set(3, { monthAbbrev: 'apr', monthLabel: 'April' })
this.MONTHS_MAP.set(4, { monthAbbrev: 'may', monthLabel: 'May' })
this.MONTHS_MAP.set(5, { monthAbbrev: 'jun', monthLabel: 'Juny' })
this.MONTHS_MAP.set(6, { monthAbbrev: 'jul', monthLabel: 'July' })
this.MONTHS_MAP.set(7, { monthAbbrev: 'aug', monthLabel: 'August' })
this.MONTHS_MAP.set(8, { monthAbbrev: 'sep', monthLabel: 'September' })
this.MONTHS_MAP.set(9, { monthAbbrev: 'oct', monthLabel: 'October' })
this.MONTHS_MAP.set(10, { monthAbbrev: 'nov', monthLabel: 'November' })
this.MONTHS_MAP.set(11, { monthAbbrev: 'dec', monthLabel: 'December' })
this.setSelectedDate(this.config.selectedDate)
const currentDate = new Date()
this.maxYear = currentDate.getFullYear()
this.minYear = this.maxYear - 80
}
private init() {
this.clearElement()
this.createInitialDOMComponents()
this.attachHandlers()
this.initData()
}
private clearElement() {
if (this.$element !== null) this.$element.innerHTML = ''
}
private createInitialDOMComponents() {
this.$datepickerInput = document.createElement('input') as HTMLInputElement
this.$datepickerInput.type = 'text'
this.$datepickerInput.readOnly = true
this.$datepickerInput.className = 'synDatepicker__input'
if (this.$element !== null) this.$element.insertAdjacentElement('beforeend', this.$datepickerInput)
}
private attachHandlers() {
this.handlerFocusInput()
}
private hidePicker() {
this.setIsPickerVisible(false)
this.$picker.remove()
}
private showPicker() {
this.currentCalendarPosition = {} as any
this.setCurrentCalendarPosition(this.getSelectedDate().getMonth(), this.getSelectedDate().getFullYear())
this.setIsPickerVisible(true)
this.$picker = document.createElement('div')
this.$picker.className = 'synDatepicker__picker'
this.$picker.innerHTML = pickerUi
if (this.$element !== null) {
this.$element.insertAdjacentElement('beforeend', this.$picker)
this.renderMonthDates(this.getSelectedDate())
this.attachPickersControlls()
}
this.changeMonthSelectedOption(this.getCurrentCalendarPosition().month)
}
private handleChangeMonth(order: number) {
const { month, year } = this.getCurrentCalendarPosition()
const selectedMonthYear = new Date(year, month + (1 * order))
if(selectedMonthYear.getFullYear() >= this.minYear && selectedMonthYear.getFullYear() <= this.maxYear){
this.renderMonthDates(selectedMonthYear)
this.setCurrentCalendarPosition(selectedMonthYear.getMonth(), selectedMonthYear.getFullYear())
this.changeMonthSelectedOption(this.getCurrentCalendarPosition().month)
this.changeYearSelectedOption(this.getCurrentCalendarPosition().year)
}
}
private setCurrentCalendarPosition(month: number, year: number) {
this.getCurrentCalendarPosition().month = month
this.getCurrentCalendarPosition().year = year
}
private renderYearOptionsCombo() {
for (let index = this.minYear; index <= this.maxYear; index++) {
this.$datepickerYearCombo?.insertAdjacentHTML('beforeend', `<option value="${index}">${index}</option>`)
}
}
private attachPickersControlls() {
const $controls = this.$picker.querySelectorAll('.synDatepicker__control') ?? []
$controls.forEach(($control: Element) => {
$control.addEventListener('click', () => {
const isNext = $control.closest('.synDatepicker__control--next') !== null
this.handleChangeMonth(isNext ? 1 : -1)
})
})
setTimeout(() => {
this.$datepickerMonthCombo = this.$picker.querySelector<HTMLSelectElement>('.synDatepicker__combo--month') ?? document.createElement('select')
this.$datepickerYearCombo = this.$picker.querySelector<HTMLSelectElement>('.synDatepicker__combo--year') ?? document.createElement('select')
this.renderYearOptionsCombo()
this.changeMonthSelectedOption(this.getCurrentCalendarPosition().month)
this.changeYearSelectedOption(this.getCurrentCalendarPosition().year)
this.$datepickerMonthCombo.addEventListener('change', () => {
if (this.$datepickerMonthCombo !== null) {
const selectedMonth = parseInt(this.$datepickerMonthCombo.value) ?? 0
const year = new Date(this.getSelectedDate().getTime()).getFullYear()
this.setCurrentCalendarPosition(selectedMonth, year)
const monthYearSelected = new Date(year, selectedMonth)
this.renderMonthDates(monthYearSelected)
}
})
this.$datepickerYearCombo.addEventListener('change', () => {
if (this.$datepickerYearCombo !== null) {
const selectedYear = parseInt(this.$datepickerYearCombo.value) ?? 0
const month = new Date(this.getSelectedDate().getTime()).getMonth()
this.setCurrentCalendarPosition(month, selectedYear)
const monthYearSelected = new Date(selectedYear, month)
this.renderMonthDates(monthYearSelected)
}
})
})
}
private handlerFocusInput() {
this.$datepickerInput.addEventListener('focus', (e: FocusEvent) => {
if (e.target === e.currentTarget && !(this.isPickerVisible())) this.showPicker()
})
document.addEventListener('mousedown', (e: MouseEvent) => {
if (this.isPickerVisible() && this.$element !== null && e.target !== this.$element && !(this.$element.contains(e.target as Node))) this.hidePicker()
})
}
private handleClickDay($cellDay: Element) {
const timestamp = window.parseInt($cellDay.getAttribute('data-timestamp') ?? '')
this.setSelectedDate(new Date(timestamp))
if (this.getSelectedDate().getMonth() < this.getCurrentCalendarPosition().month) {
this.handleChangeMonth(-1)
} else if (this.getSelectedDate().getMonth() > this.getCurrentCalendarPosition().month) {
this.handleChangeMonth(1)
} else {
if (this.$selectedDayCell !== null) this.$selectedDayCell.classList.toggle('syncDatepickerCalendar__day--selected', false)
$cellDay.classList.toggle('syncDatepickerCalendar__day--selected')
this.$selectedDayCell = $cellDay
}
this.hidePicker()
}
private getFormattedDates(date: Date): string {
return date.toLocaleDateString()
}
private renderMonthDates(date: Date) {
const $tableBody = this.$picker.querySelector('.synDatepicker__body') ?? document.createElement('tbody')
const dateCalendarArray = this.getArrayMonthDates(date)
const selectedMonth = date.getMonth()
$tableBody.addEventListener('click', (e: Event) => {
if ((e.target as Element).closest('.syncDatepickerCalendar__day') !== null) this.handleClickDay((e.target as Element).closest('.syncDatepickerCalendar__day') as Element)
})
$tableBody.innerHTML = ''
dateCalendarArray.forEach((dateWeek: Date[]) => {
const $rowFragment = document.createDocumentFragment()
$rowFragment.append(...dateWeek.map((date: Date) => {
const $tableCell = document.createElement('td')
if (this.cellIsSelected(date)) this.$selectedDayCell = $tableCell
$tableCell.textContent = `${date.getDate()}`
$tableCell.setAttribute('data-timestamp', `${date.getTime()}`)
$tableCell.className = `syncDatepicker__cell syncDatepickerCalendar__day ${this.cellIsSelected(date) ? 'syncDatepickerCalendar__day--selected' : ('')} syncDatepicker__cell--body${selectedMonth !== date.getMonth() ? ' syncDatepickerCalendar__day--diff' : ''}`
return $tableCell
}))
const $tableRow = document.createElement('tr')
$tableRow.className = 'synDatepicker__row synDatepicker__row--body'
$tableRow.append($rowFragment)
$tableBody.append($tableRow)
})
}
private getArrayMonthDates(calendar: Date) {
const selectedMonthCalendar = new Date(calendar.getFullYear(), calendar.getMonth(), 1)
selectedMonthCalendar.setDate(1)
const month = selectedMonthCalendar.getMonth()
const year = selectedMonthCalendar.getFullYear()
const nextMonthCalendar = new Date(year, month + 1, 1)
nextMonthCalendar.setDate(nextMonthCalendar.getDate() - 1)
const lastDayOfMonth = nextMonthCalendar.getDate()
nextMonthCalendar.setDate(nextMonthCalendar.getDate() + 1)
let dateArray: Date[][] = [new Array(7)]
let firstMonthDayWeek = 0
let lastMonthDayWeek = 6
let week = 0
for (let index = 1; index <= lastDayOfMonth; index++) {
const monthDate = new Date(year, month, index)
const weekDay = monthDate.getDay()
if (index === 1) firstMonthDayWeek = weekDay
dateArray[week][weekDay] = monthDate
if (weekDay === 6 && index < lastDayOfMonth) {
week++
dateArray[week] = new Array(7)
}
if (index === lastDayOfMonth) lastMonthDayWeek = weekDay
}
const previousMonthCalendar = new Date(selectedMonthCalendar.setDate(selectedMonthCalendar.getDate() - firstMonthDayWeek))
for (let index = 0; index < firstMonthDayWeek; index++) {
dateArray[0][index] = new Date(previousMonthCalendar.getTime())
previousMonthCalendar.setDate(previousMonthCalendar.getDate() + 1)
}
for (let index = lastMonthDayWeek + 1; index < 7; index++) {
dateArray[week][index] = new Date(nextMonthCalendar.getTime())
nextMonthCalendar.setDate(nextMonthCalendar.getDate() + 1)
}
return dateArray
}
public onChange(handler: EventListenerOrEventListenerObject) {
this.addEventListener('datepicker-changing', handler)
}
private renderSelectedDates() {
this.$datepickerInput.value = this.getFormattedDates(this.getSelectedDate())
}
private isSelectedDate(date: Date) {
return this.selectedDate === undefined || (this.getSelectedDate().getDate() === date.getDate() &&
this.getSelectedDate().getMonth() === date.getMonth() &&
this.getSelectedDate().getFullYear() === date.getFullYear())
}
private cellIsSelected(date: Date) {
return this.isSelectedDate(date)
}
private dispatchDatepickerEvent() {
const datepickerChangingEvent = new CustomEvent('datepicker-changing')
this.dispatchEvent(datepickerChangingEvent)
}
private changeMonthSelectedOption(selectedMonth: number) {
if (this.$datepickerMonthCombo !== null) {
this.$datepickerMonthCombo.value = `${selectedMonth}`
}
}
private changeYearSelectedOption(selectedYear: number) {
if (this.$datepickerYearCombo !== null) {
this.$datepickerYearCombo.value = `${selectedYear}`
}
}
public setSelectedDate(selectedDate: Date) {
if (!this.isSelectedDate(selectedDate)) {
this.dispatchDatepickerEvent()
}
this.selectedDate = selectedDate
this.renderSelectedDates()
}
public getSelectedDate() {
return this.selectedDate
}
public isPickerVisible() {
return this.visible
}
private setIsPickerVisible(visible: boolean) {
this.visible = visible
}
private getCurrentCalendarPosition() {
return this.currentCalendarPosition
}
private MONTHS_MAP: Map<number, { monthLabel: string, monthAbbrev: string }> = new Map()
private visible: boolean = false
private config: any
private $picker!: Element
private minYear!: number
private maxYear!: number
private selectedDate!: Date
private $datepickerMonthCombo: HTMLSelectElement | null = null
private $datepickerYearCombo: HTMLSelectElement | null = null
private $selectedDayCell: Element | null = null
private currentCalendarPosition!: { month: number, year: number }
private $datepickerInput!: HTMLInputElement
private $element: Element | null
}