403Webshell
Server IP : 80.87.202.40  /  Your IP : 216.73.216.169
Web Server : Apache
System : Linux rospirotorg.ru 5.14.0-539.el9.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Dec 5 22:26:13 UTC 2024 x86_64
User : bitrix ( 600)
PHP Version : 8.2.27
Disable Function : NONE
MySQL : OFF |  cURL : ON |  WGET : ON |  Perl : ON |  Python : OFF |  Sudo : ON |  Pkexec : ON
Directory :  /home/bitrix/ext_www/rospirotorg.ru/bitrix/js/ui/date-picker/src/

Upload File :
current_dir [ Writeable] document_root [ Writeable]

 

Command :


[ Back ]     

Current File : /home/bitrix/ext_www/rospirotorg.ru/bitrix/js/ui/date-picker/src/date-picker.js
import { Dom, Extension, Tag, Type, Event } from 'main.core';
import { type BaseCache, MemoryCache } from 'main.core.cache';
import { BaseEvent, EventEmitter } from 'main.core.events';
import { DateTimeFormat } from 'main.date';
import { Popup, type PopupOptions } from 'main.popup';

import { type BasePicker } from './base-picker';

import {
	type DatePickerSelectionMode,
	type DayColorOptions,
	type DateLike,
	type DatePickerOptions,
	type DatePickerType,
	type DayColor,
	type DayMark,
	type DayMarkOptions,
	type DateLikeMatcher,
	type DateMatcher,
} from './date-picker-options';

import { DayPicker } from './day-picker';
import { DatePickerEvent } from './date-picker-event';
import { addDate } from './helpers/add-date';
import { addToRange } from './helpers/add-to-range';
import { ceilDate } from './helpers/ceil-date';
import { cloneDate } from './helpers/clone-date';

import { createDate } from './helpers/create-date';
import { createUtcDate } from './helpers/create-utc-date';
import { floorDate } from './helpers/floor-date';
import { getDate, type DateComponents } from './helpers/get-date';
import { getFocusableBoundaryElements } from './helpers/get-focusable-boundary-elements';
import { isDateLike } from './helpers/is-date-like';
import { isDatesEqual } from './helpers/is-dates-equal';
import { setTime } from './helpers/set-time';
import { isDateMatch } from './helpers/is-date-match';
import { KeyboardNavigation } from './keyboard-navigation';
import { MonthPicker } from './month-picker';
import { TimePickerWheel } from './time-picker-wheel';
import { TimePickerGrid } from './time-picker-grid';
import { YearPicker } from './year-picker';

import './css/date-picker.css';

let singleOpenDatePicker: DatePicker = null;

/**
 * @namespace BX.UI.DatePicker
 */
export class DatePicker extends EventEmitter
{
	#viewDate: Date = null;
	#startDate: Date = null;
	#selectedDates: Date[] = [];
	#focusDate: Date = null;

	#type: DatePickerType = 'date';
	#currentView: 'day' | 'year' | 'month' | 'time' = null;
	#selectionMode: DatePickerSelectionMode = 'single';
	#views: Map = new Map();

	#firstWeekDay: number = 1;
	#showWeekDays: boolean = true;
	#showWeekNumbers: boolean = false;
	#showOutsideDays: boolean = true;
	#numberOfMonths: number = 1;

	#maxDays: number = Infinity;
	#minDays: number = 0;
	#fullYear: boolean = false;

	#weekends: number[] = [0, 6];
	#holidays: Array<[number, number]> = [];
	#workdays: Array<[number, number]> = [];
	#enableTime: boolean = false;
	#allowSeconds: boolean = false;
	#amPmMode: boolean = false;
	#minuteStep: number = 5;
	#defaultTime: string = '00:00:00';
	#defaultTimeSpan: number = 60;
	#timePickerStyle: 'wheel' | 'grid' = 'grid';
	#cutZeroTime: boolean = true;

	#targetNode: HTMLElement = null;
	#inputField: HTMLInputElement | HTMLTextAreaElement = null;
	#rangeStartInput: HTMLInputElement | HTMLTextAreaElement = null;
	#rangeEndInput: HTMLInputElement | HTMLTextAreaElement = null;
	#useInputEvents: boolean = true;
	#dateSeparator: string = ', ';

	#popup: Popup = null;
	#popupOptions: PopupOptions = {};
	#hideByEsc: boolean = true;
	#autoHide: boolean = true;
	#cacheable: boolean = true;
	#singleOpening: boolean = true;

	#refs: BaseCache<HTMLElement | Function> = new MemoryCache();
	#rendered: boolean = false;
	#inline: boolean = false;
	#autoFocus: boolean = true;

	#dateFormat: string = null;
	#timeFormat: string = null;

	#toggleSelected: boolean = null;
	#hideOnSelect: boolean = true;
	#locale: boolean = null;
	#hideHeader: boolean = false;

	#dayColors: DayColor[] = [];
	#dayMarks: DayMark[] = [];

	#keyboardNavigation: KeyboardNavigation = null;
	#destroying: boolean = false;

	constructor(pickerOptions: DatePickerOptions)
	{
		super();
		this.setEventNamespace('BX.UI.DatePicker');

		const settings = Extension.getSettings('ui.date-picker');
		const options: DatePickerOptions = Type.isPlainObject(pickerOptions) ? pickerOptions : {};

		this.#setType(options.type);
		this.#setSelectionMode(options.selectionMode);

		this.#locale = Type.isStringFilled(options.locale) ? options.locale : settings.get('locale', 'en');

		this.#enableTime = Type.isBoolean(options.enableTime) ? options.enableTime : this.#enableTime;
		if (this.isMultipleMode())
		{
			this.#enableTime = false;
		}

		this.#allowSeconds = Type.isBoolean(options.allowSeconds) ? options.allowSeconds : this.#allowSeconds;
		this.#amPmMode = Type.isBoolean(options.amPmMode) ? options.amPmMode : DateTimeFormat.isAmPmMode();
		this.#cutZeroTime = Type.isBoolean(options.cutZeroTime) ? options.cutZeroTime : this.#cutZeroTime;
		this.#dateFormat = Type.isStringFilled(options.dateFormat) ? options.dateFormat : this.#getDefaultDateFormat();

		this.setDefaultTime(options.defaultTime);
		this.setDefaultTimeSpan(options.defaultTimeSpan);

		this.#timeFormat = (
			Type.isStringFilled(options.timeFormat)
				? options.timeFormat
				: DateTimeFormat.getFormat(this.#allowSeconds ? 'LONG_TIME_FORMAT' : 'SHORT_TIME_FORMAT')
		);

		this.#minuteStep = (
			Type.isNumber(options.minuteStep) && [1, 5, 10, 15, 30].includes(options.minuteStep)
				? options.minuteStep
				: this.#minuteStep
		);

		this.#timePickerStyle = options.timePickerStyle === 'wheel' ? 'wheel' : this.#timePickerStyle;

		this.#viewDate = this.getToday();

		this.#useInputEvents = Type.isBoolean(options.useInputEvents) ? options.useInputEvents : this.#useInputEvents;
		this.setAutoFocus(options.autoFocus);
		this.setInputField(options.inputField);
		this.setRangeStartInput(options.rangeStartInput);
		this.setRangeEndInput(options.rangeEndInput);
		this.setDateSeparator(options.dateSeparator);

		this.selectDates(options.selectedDates, { emitEvents: false });

		this.#startDate = isDateLike(options.startDate) ? this.createDate(options.startDate) : null;
		const viewDate = this.getDefaultViewDate();
		this.setViewDate(viewDate);

		this.#inline = options.inline === true;

		let firstWeekDay = settings.get('firstWeekDay', this.#firstWeekDay);
		firstWeekDay = Type.isNumber(options.firstWeekDay) ? options.firstWeekDay : firstWeekDay;
		this.#firstWeekDay = Math.min(Math.max(0, firstWeekDay), 6);

		this.#numberOfMonths = Type.isNumber(options.numberOfMonths) ? options.numberOfMonths : this.#numberOfMonths;
		this.#fullYear = options.fullYear === true;
		if (this.#fullYear)
		{
			this.#enableTime = false;
			this.#numberOfMonths = 12;
			this.setViewDate(createUtcDate(viewDate.getUTCFullYear(), 0, 1));
		}

		this.#showWeekDays = Type.isBoolean(options.showWeekDays) ? options.showWeekDays : this.#showWeekDays;
		this.#showWeekNumbers = Type.isBoolean(options.showWeekNumbers) ? options.showWeekNumbers : this.#showWeekNumbers;

		const defaultWeekends = settings.get('weekends', []);
		this.#weekends = (
			Type.isArray(options.weekends)
				? options.weekends
				: (Type.isArrayFilled(defaultWeekends) ? defaultWeekends : this.#weekends)
		);

		const defaultHolidays = settings.get('holidays', []);
		this.#holidays = Type.isArray(options.holidays) ? options.holidays : defaultHolidays;

		const defaultWorkdays = settings.get('workdays', []);
		this.#workdays = Type.isArray(options.workdays) ? options.workdays : defaultWorkdays;

		this.#showOutsideDays = this.#numberOfMonths > 1 ? false : this.#showOutsideDays;
		this.#showOutsideDays = Type.isBoolean(options.showOutsideDays) ? options.showOutsideDays : this.#showOutsideDays;

		this.#popupOptions = Type.isPlainObject(options.popupOptions) ? options.popupOptions : this.#popupOptions;

		this.setMinDays(options.minDays);
		this.setMaxDays(options.maxDays);
		this.setHideOnSelect(options.hideOnSelect);
		this.setTargetNode(options.targetNode);
		this.setToggleSelected(options.toggleSelected);
		this.setAutoHide(options.autoHide);
		this.setHideByEsc(options.hideByEsc);
		this.setCacheable(options.cacheable);
		this.setSingleOpening(options.singleOpening);
		this.setDayColors(options.dayColors);
		this.setDayMarks(options.dayMarks);
		this.setHideHeader(options.hideHeader);

		this.subscribeFromOptions(options.events);
		this.#keyboardNavigation = new KeyboardNavigation(this);
	}

	setViewDate(date: DateLike)
	{
		let newDate = this.createDate(date);
		if (newDate === null)
		{
			return;
		}

		newDate = setTime(newDate, 0, 0, 0);

		this.#viewDate = newDate;

		if (this.isDateOutOfView(this.getFocusDate()))
		{
			this.setFocusDate(null, { adjustViewDate: false, render: false });
		}

		if (this.isRendered())
		{
			this.getPicker().render();
		}
	}

	getViewDate(): Date
	{
		return this.#viewDate;
	}

	getDefaultViewDate(): Date
	{
		return this.getSelectedDate() || this.#startDate || this.getToday();
	}

	adjustViewDate(date: Date): void
	{
		if (this.isSingleMode())
		{
			if (this.getNumberOfMonths() === 1)
			{
				if (!isDatesEqual(date, this.getViewDate(), 'month'))
				{
					this.setViewDate(createUtcDate(date.getUTCFullYear(), date.getUTCMonth()));
				}
			}
			else
			{
				const { year, month } = this.getViewDateParts();
				const firstMonth = createUtcDate(year, month);
				const lastMonth = ceilDate(createUtcDate(year, month + this.getNumberOfMonths() - 1), 'month');
				if (date < firstMonth || date >= lastMonth)
				{
					this.setViewDate(createUtcDate(date.getUTCFullYear(), date.getUTCMonth()));
				}
			}
		}
		else
		{
			const dayPicker: DayPicker = this.getPicker('day');
			const months = dayPicker.getMonths();
			const firstDay = months[0].weeks[0][0].date;
			const lastDay = months.at(-1).weeks.at(-1).at(-1).date;
			if (date < firstDay || date > lastDay)
			{
				this.setViewDate(createUtcDate(date.getUTCFullYear(), date.getUTCMonth()));
			}
		}
	}

	getViewDateParts(): DateComponents
	{
		return getDate(this.#viewDate);
	}

	selectDate(date: DateLike, options = {}): boolean
	{
		if (this.isRangeMode())
		{
			throw new Error('DatePicker: to select a range use selectRange method.');
		}

		if (!isDateLike(date))
		{
			return false;
		}

		const selectedDate = this.createDate(date);
		if (this.isDateSelected(selectedDate, 'datetime'))
		{
			return false;
		}

		const updateTime = this.isDateSelected(selectedDate, 'day');
		if (!updateTime && this.isMultipleMode() && this.#selectedDates.length >= this.getMaxDays())
		{
			return false;
		}

		const { emitEvents, render, updateInputs } = {
			emitEvents: true,
			render: true,
			updateInputs: true,
			...options,
		};

		if (emitEvents && !this.#canSelectDate(selectedDate))
		{
			return false;
		}

		if (this.isMultipleMode())
		{
			if (updateTime)
			{
				const index = this.#selectedDates.findIndex((currentDate: Date) => {
					return isDatesEqual(currentDate, selectedDate, 'day');
				});

				// replace existing date
				if (index !== -1)
				{
					this.#selectedDates.splice(index, 1, selectedDate);
				}
			}
			else
			{
				const index = this.#selectedDates.findIndex((currentDate: Date) => {
					return currentDate > selectedDate;
				});

				if (index === -1)
				{
					this.#selectedDates.push(selectedDate);
				}
				else if (index === 0)
				{
					this.#selectedDates.unshift(selectedDate);
				}
				else
				{
					this.#selectedDates.splice(index, 0, selectedDate);
				}
			}
		}
		else
		{
			const currentDate = this.#selectedDates[0] || null;
			if (emitEvents && currentDate !== null)
			{
				if (!this.#canDeselectDate(currentDate))
				{
					return false;
				}

				this.deselectDate(currentDate, { emitEvents: false, render: false });
				this.emit(DatePickerEvent.DESELECT, { date: currentDate });
			}

			this.#selectedDates = [selectedDate];
		}

		this.adjustViewDate(selectedDate);
		if (this.isRendered() && render)
		{
			this.getPicker().render();
		}

		if (updateInputs)
		{
			this.updateInputFields();
		}

		if (emitEvents)
		{
			this.emit(DatePickerEvent.SELECT, { date: selectedDate });
			this.emit(DatePickerEvent.SELECT_CHANGE);
		}

		return true;
	}

	selectDates(dates: DateLike[], options = {}): void
	{
		if (!Type.isArrayFilled(dates))
		{
			return;
		}

		if (this.isRangeMode())
		{
			const [start, end] = dates;
			this.selectRange(start, end, options);
		}
		else
		{
			dates.forEach((date: DateLike): void => {
				this.selectDate(date, options);
			});
		}
	}

	selectRange(start: DateLike, end: DateLike = null, options = {}): boolean
	{
		if (!this.isRangeMode())
		{
			throw new Error('DatePicker: to select a date use selectDate method.');
		}

		if (!isDateLike(start) || (end !== null && !isDateLike(end)))
		{
			return false;
		}

		let newStart = this.createDate(start);
		let newEnd = end === null ? null : this.createDate(end);
		if (newStart === null && newEnd === null)
		{
			return false;
		}

		if (newStart !== null && newEnd !== null && newStart > newEnd)
		{
			[newStart, newEnd] = [newEnd, newStart];
		}

		const currentStart = this.#selectedDates[0] || null;
		const currentEnd = this.#selectedDates[1] || null;

		if (
			isDatesEqual(newStart, currentStart, 'datetime')
			&& (
				(newEnd === null && currentEnd === null) || isDatesEqual(newEnd, currentEnd, 'datetime')
			)
		)
		{
			return false;
		}

		const { emitEvents, updateInputs } = { emitEvents: true, updateInputs: true, ...options };
		const deselectStart = (
			currentStart !== null
			&& emitEvents
			&& !isDatesEqual(newStart, currentStart, 'datetime')
			&& !isDatesEqual(newEnd, currentStart, 'datetime')
		);

		const deselectEnd = (
			currentEnd !== null
			&& emitEvents
			&& !isDatesEqual(newStart, currentEnd, 'datetime')
			&& !isDatesEqual(newEnd, currentEnd, 'datetime')
		);

		const selectStart = !this.isDateSelected(newStart, 'datetime');
		const selectEnd = (
			newEnd !== null
			&& (
				!this.isDateSelected(newEnd, 'datetime')
				|| (currentEnd === null && isDatesEqual(newEnd, newStart, 'datetime'))
			)
		);

		if (deselectStart && !this.#canDeselectDate(currentStart))
		{
			return false;
		}

		if (deselectEnd && !this.#canDeselectDate(currentEnd))
		{
			return false;
		}

		if (selectStart && !this.#canSelectDate(newStart))
		{
			return false;
		}

		if (selectEnd && !this.#canSelectDate(newEnd))
		{
			return false;
		}

		if (deselectStart)
		{
			this.deselectDate(currentStart, { emitEvents: false, render: false });
			this.emit(DatePickerEvent.DESELECT, { date: currentStart });
		}

		if (deselectEnd)
		{
			this.deselectDate(currentEnd, { emitEvents: false, render: false });
			this.emit(DatePickerEvent.DESELECT, { date: currentEnd });
		}

		this.#selectedDates = newEnd === null ? [newStart] : [newStart, newEnd];

		this.adjustViewDate(newStart);
		if (this.isRendered())
		{
			this.getPicker().render();
		}

		if (updateInputs)
		{
			this.updateInputFields();
		}

		if (emitEvents)
		{
			if (selectStart)
			{
				this.emit(DatePickerEvent.SELECT, { date: newStart });
			}

			if (selectEnd)
			{
				this.emit(DatePickerEvent.SELECT, { date: newEnd });
			}

			this.emit(DatePickerEvent.SELECT_CHANGE);
		}

		return true;
	}

	deselectDate(date: DateLike, options = {}): boolean
	{
		if (!isDateLike(date))
		{
			return false;
		}

		const dateToDeselect = this.createDate(date);
		const { emitEvents, render, updateInputs } = {
			emitEvents: true,
			render: true,
			updateInputs: true,
			...options,
		};

		if (emitEvents && !this.#canDeselectDate(dateToDeselect))
		{
			return false;
		}

		if (this.isMultipleMode() && this.#selectedDates.length <= this.getMinDays())
		{
			return false;
		}

		const index = this.#selectedDates.findIndex((selectedDate) => {
			return isDatesEqual(dateToDeselect, selectedDate);
		});

		if (index === -1)
		{
			return false;
		}

		this.#selectedDates.splice(index, 1);

		if (emitEvents)
		{
			this.emit(DatePickerEvent.DESELECT, { date: dateToDeselect });
			this.emit(DatePickerEvent.SELECT_CHANGE);
		}

		if (this.isRendered() && render)
		{
			this.getPicker().render();
		}

		if (updateInputs)
		{
			this.updateInputFields();
		}

		return true;
	}

	deselectAll(options = {}): boolean
	{
		const dates = [...this.#selectedDates];
		dates.forEach((date: Date) => {
			this.deselectDate(date, options);
		});

		return this.#selectedDates.length === 0;
	}

	#canSelectDate(date: Date): boolean
	{
		const event = new BaseEvent({ data: { date } });
		this.emit(DatePickerEvent.BEFORE_SELECT, event);

		return !event.isDefaultPrevented();
	}

	#canDeselectDate(date: Date): boolean
	{
		const event = new BaseEvent({ data: { date } });
		this.emit(DatePickerEvent.BEFORE_DESELECT, event);

		return !event.isDefaultPrevented();
	}

	getSelectedDates(): Date[]
	{
		return this.#selectedDates;
	}

	getSelectedDate(): Date | null
	{
		return this.#selectedDates[0] || null;
	}

	getRangeStart(): Date | null
	{
		return this.#selectedDates[0] || null;
	}

	getRangeEnd(): Date | null
	{
		return this.#selectedDates[1] || null;
	}

	isDateSelected(date: Date, precision: 'day' | 'datetime' | 'month' | 'year' = 'day'): boolean
	{
		return this.#selectedDates.some((selectedDate: Date): boolean => {
			return isDatesEqual(date, selectedDate, precision);
		});
	}

	setFocusDate(date: DateLike, options = {}): void
	{
		if (!isDateLike(date) && date !== null)
		{
			return;
		}

		this.#focusDate = date === null ? null : this.createDate(date);

		const { render, adjustViewDate } = { render: true, adjustViewDate: true, ...options };

		if (adjustViewDate && this.isDateOutOfView(this.#focusDate))
		{
			this.setViewDate(createUtcDate(this.#focusDate.getUTCFullYear(), this.#focusDate.getUTCMonth()));
		}

		if (this.isRendered() && render)
		{
			this.getPicker().render();
		}
	}

	getFocusDate(): Date | null
	{
		return this.#focusDate;
	}

	getInitialFocusDate(mode: 'datetime' | 'range-start' | 'range-end' = 'datetime'): Date
	{
		const focusDate = this.getFocusDate();
		if (focusDate !== null)
		{
			return focusDate;
		}

		if (mode === 'range-start')
		{
			const { year, month, day } = this.getViewDateParts();

			return this.getRangeStart() || createUtcDate(year, month, day);
		}

		if (mode === 'range-end')
		{
			const { year, month, day } = this.getViewDateParts();

			return this.getRangeEnd() || createUtcDate(year, month, day);
		}

		const selectedDates = this.getSelectedDates();
		if (Type.isArrayFilled(selectedDates))
		{
			const date = selectedDates.find((selectedDate: Date) => {
				return !this.isDateOutOfView(selectedDate);
			});

			if (Type.isDate(date))
			{
				return date;
			}
		}

		return this.getViewDate();
	}

	isDateOutOfView(date: Date | null): boolean
	{
		if (date === null)
		{
			return false;
		}

		let isOutOfView = false;
		const { year: currentViewYear } = this.getViewDateParts();
		const { year: focusYear } = getDate(date);
		if (this.getCurrentView() === 'day')
		{
			const dayPicker: DayPicker = this.getPicker('day');
			const firstDay = dayPicker.getFirstDay();
			const lastDay = dayPicker.getLastDay();

			const focusDate = createUtcDate(
				date.getUTCFullYear(),
				date.getUTCMonth(),
				date.getUTCDate(),
			);

			isOutOfView = focusDate < firstDay || focusDate >= lastDay;
		}
		else if (this.getCurrentView() === 'month')
		{
			isOutOfView = currentViewYear !== focusYear;
		}
		else if (this.getCurrentView() === 'year')
		{
			const yearPicker: YearPicker = this.getPicker('year');
			const firstYear = yearPicker.getFirstYear();
			const lastYear = yearPicker.getLastYear();

			isOutOfView = focusYear < firstYear || focusYear > lastYear;
		}

		return isOutOfView;
	}

	setCurrentView(view: string): void
	{
		if (this.#currentView === view)
		{
			return;
		}

		const picker = this.getPicker(view);
		if (picker === null)
		{
			return;
		}

		Dom.style(this.getPicker()?.getContainer(), 'display', 'none');
		Dom.attr(this.getPicker()?.getContainer(), 'inert', true);
		this.getPicker()?.onHide();

		this.#currentView = view;
		this.setFocusDate(null, { render: false });

		if (!picker.isRendered())
		{
			picker.renderTo(this.getViewsContainer());
		}

		this.focus();

		Dom.style(picker.getContainer(), 'display', null);
		Dom.attr(picker.getContainer(), 'inert', null);

		picker.onShow();
		picker.render();
	}

	getCurrentView(): 'day' | 'year' | 'month' | 'time'
	{
		return this.#currentView;
	}

	getPicker(pickerId?: string): BasePicker | null
	{
		const currentPickerId = Type.isStringFilled(pickerId) ? pickerId : this.#currentView;
		let view = this.#views.get(currentPickerId) || null;
		if (view === null)
		{
			view = this.#createPicker(currentPickerId);
			if (view !== null)
			{
				this.#views.set(currentPickerId, view);
			}
		}

		return view;
	}

	#setType(type: DatePickerType)
	{
		if (['date', 'year', 'month', 'time'].includes(type))
		{
			this.#type = type;
		}
	}

	getType(): DatePickerType
	{
		return this.#type;
	}

	getFirstWeekDay(): number
	{
		return this.#firstWeekDay;
	}

	getNumberOfMonths(): number
	{
		return this.#numberOfMonths;
	}

	shouldShowWeekDays(): boolean
	{
		return this.#showWeekDays;
	}

	shouldShowWeekNumbers(): boolean
	{
		return this.#showWeekNumbers;
	}

	shouldShowOutsideDays(): boolean
	{
		return this.#showOutsideDays;
	}

	getWeekends(): number[]
	{
		return this.#weekends;
	}

	isWeekend(date: Date): boolean
	{
		return this.#weekends.includes(date.getUTCDay());
	}

	isHoliday(date: Date): boolean
	{
		return this.#holidays.some(([day, month]) => {
			return date.getUTCDate() === day && date.getUTCMonth() === month;
		});
	}

	isWorkday(date: Date): boolean
	{
		return this.#workdays.some(([day, month]) => {
			return date.getUTCDate() === day && date.getUTCMonth() === month;
		});
	}

	isDayOff(date: Date): boolean
	{
		return !this.isWorkday(date) && (this.isWeekend(date) || this.isHoliday(date));
	}

	isTimeEnabled(): boolean
	{
		return this.#enableTime;
	}

	setDefaultTime(time: string): void
	{
		if (Type.isStringFilled(time) && /([01]{1,2}\d|2[0-3]):[0-5]\d(:[0-5]\d)?/.test(time))
		{
			this.#defaultTime = time;
		}
	}

	getDefaultTime(): string
	{
		return this.#defaultTime;
	}

	setDefaultTimeSpan(minutes: number): void
	{
		if (Type.isNumber(minutes) && minutes >= 0)
		{
			this.#defaultTimeSpan = minutes;
		}
	}

	getDefaultTimeSpan(): string
	{
		return this.#defaultTimeSpan;
	}

	getDefaultTimeParts(): { hours: number, minutes: number, seconds: number }
	{
		const parts = this.getDefaultTime().split(':');

		return {
			hours: Number(parts[0] || 0),
			minutes: Number(parts[1] || 0),
			seconds: Number(parts[2] || 0),
		};
	}

	getTimePickerStyle(): 'wheel' | 'grid'
	{
		return this.#timePickerStyle;
	}

	shouldCutZeroTime(): boolean
	{
		return this.#cutZeroTime;
	}

	shouldAllowSeconds(): boolean
	{
		return this.#allowSeconds;
	}

	setToggleSelected(flag: boolean | null): void
	{
		if (Type.isBoolean(flag) || Type.isNull(flag))
		{
			this.#toggleSelected = flag;
		}
	}

	shouldToggleSelected(): boolean
	{
		if (this.#toggleSelected !== null)
		{
			return this.#toggleSelected;
		}

		return this.isMultipleMode();
	}

	setMaxDays(days: number): void
	{
		if (Type.isNumber(days) && days > 0)
		{
			this.#maxDays = days;
		}
	}

	getMaxDays(): number
	{
		return this.#maxDays;
	}

	setMinDays(days: number)
	{
		if (Type.isNumber(days) && days > 0)
		{
			this.#minDays = days;
		}
	}

	getMinDays(): number
	{
		return this.#minDays;
	}

	isFullYear(): boolean
	{
		return this.#fullYear;
	}

	isAmPmMode(): boolean
	{
		return this.#amPmMode;
	}

	getMinuteStep(): number
	{
		return this.#minuteStep;
	}

	getMinuteStepByDate(date: Date): number
	{
		let step = this.getMinuteStep();
		if (!Type.isDate(date))
		{
			return step;
		}

		const selectedMinute = date.getUTCMinutes();
		if (selectedMinute > 0 && (selectedMinute % step) !== 0)
		{
			// Reduce a step to show a selected minute
			const availableSteps = [30, 15, 10, 5, 1];
			const index = availableSteps.indexOf(selectedMinute);
			const steps = index === -1 ? [1] : availableSteps.slice(index);
			for (const newStep of steps)
			{
				if (selectedMinute % newStep === 0)
				{
					step = newStep;
					break;
				}
			}
		}

		return step;
	}

	getToday(): Date
	{
		return this.createDate(new Date());
	}

	show(): void
	{
		this.updateFromInputFields();

		if (this.isInline())
		{
			if (!this.isRendered())
			{
				this.#render();
			}

			// Dom.removeClass(this.getContainer(), '--hidden');
		}
		else
		{
			this.getPopup().show();
		}
	}

	hide(): void
	{
		if (!this.isRendered() || this.isInline())
		{
			return;
		}

		// if (this.isInline())
		// {
		// Dom.addClass(this.getContainer(), '--hidden');
		// }

		this.getPopup().close();
	}

	isOpen(): boolean
	{
		return this.#popup !== null && this.#popup.isShown();
	}

	adjustPosition(): void
	{
		if (this.isRendered() && this.isOpen())
		{
			this.getPopup().adjustPosition();
		}
	}

	toggle(): void
	{
		if (this.isOpen())
		{
			this.hide();
		}
		else
		{
			this.show();
		}
	}

	focus(): void
	{
		if (this.isRendered())
		{
			this.getContainer().tabIndex = 0;
			this.getContainer().focus({ preventScroll: true });
			this.getContainer().tabIndex = -1;
		}
	}

	setSingleOpening(flag: boolean): void
	{
		if (Type.isBoolean(flag))
		{
			this.#singleOpening = flag;
		}
	}

	isSingleOpening(): boolean
	{
		return this.#singleOpening;
	}

	setDayColors(options: DayColorOptions[]): void
	{
		if (!Type.isArray(options))
		{
			return;
		}

		const dayColors = [];
		for (const option of options)
		{
			if (!Type.isStringFilled(option.bgColor) && !Type.isStringFilled(option.textColor))
			{
				continue;
			}

			const matchers = this.#createDateMatchers(option.matcher);
			if (Type.isArrayFilled(matchers))
			{
				dayColors.push({
					bgColor: Type.isStringFilled(option.bgColor) ? option.bgColor : null,
					textColor: Type.isStringFilled(option.textColor) ? option.textColor : null,
					matchers,
				});
			}
		}

		this.#dayColors = dayColors;

		if (this.isRendered())
		{
			this.getPicker().render();
		}
	}

	getDayColor(day: Date): DayColor | null
	{
		return this.#dayColors.find((dayColor: DayColor): boolean => isDateMatch(day, dayColor.matchers)) || null;
	}

	setDayMarks(options: DayMarkOptions[]): void
	{
		if (!Type.isArray(options))
		{
			return;
		}

		const dayMarks = [];
		for (const option of options)
		{
			if (!Type.isStringFilled(option.bgColor))
			{
				continue;
			}

			const matchers = this.#createDateMatchers(option.matcher);
			if (Type.isArrayFilled(matchers))
			{
				dayMarks.push({
					bgColor: option.bgColor,
					matchers,
				});
			}
		}

		this.#dayMarks = dayMarks;

		if (this.isRendered())
		{
			this.getPicker().render();
		}
	}

	getDayMarks(day: Date): DayMark[]
	{
		return this.#dayMarks.filter((dayMark: DayMark): boolean => isDateMatch(day, dayMark.matchers));
	}

	#createDateMatchers(matcher: DateLikeMatcher | DateLikeMatcher[]): DateMatcher[]
	{
		if (Type.isUndefined(matcher))
		{
			return [];
		}

		const result = [];
		const matchers = Type.isArray(matcher) ? [...matcher] : [matcher];
		matchers.forEach((matcherValue: DateLikeMatcher): void => {
			if (Type.isArray(matcherValue))
			{
				const dates = [];
				matcherValue.forEach((dateLike: DateLike): void => {
					if (!isDateLike(dateLike))
					{
						return;
					}

					const date = this.createDate(matcherValue);
					if (date !== null)
					{
						dates.push(date);
					}
				});

				result.push(dates);
			}
			else if (isDateLike(matcherValue))
			{
				const date = this.createDate(matcherValue);
				if (date !== null)
				{
					result.push(date);
				}
			}
			else if (Type.isBoolean(matcherValue) || Type.isFunction(matcherValue))
			{
				result.push(matcherValue);
			}
		});

		return result;
	}

	getPopup(): Popup
	{
		if (this.#popup !== null)
		{
			return this.#popup;
		}

		const popupOptions = { ...this.#popupOptions };
		const userEvents = popupOptions.events;
		delete popupOptions.events;

		this.#popup = new Popup({
			contentPadding: 0,
			padding: 0,
			offsetTop: 5,
			bindElement: this.getTargetNode(),
			bindOptions: {
				forceBindPosition: true,
			},
			autoHide: this.isAutoHide(),
			closeByEsc: this.shouldHideByEsc(),
			cacheable: this.isCacheable(),
			content: this.getContainer(),
			autoHideHandler: this.#handleAutoHide.bind(this),
			events: {
				onFirstShow: this.#handlePopupFirstShow.bind(this),
				onShow: this.#handlePopupShow.bind(this),
				onClose: this.#handlePopupClose.bind(this),
				onDestroy: this.#handlePopupDestroy.bind(this),
			},
			...popupOptions,
		});

		this.#popup.subscribeFromOptions(userEvents);

		return this.#popup;
	}

	#setSelectionMode(mode: DatePickerSelectionMode): void
	{
		if (this.getType() !== 'date')
		{
			this.#selectionMode = 'single';
		}
		else if (['single', 'multiple', 'range', 'none'].includes(mode))
		{
			this.#selectionMode = mode;
		}
	}

	setHideOnSelect(flag: boolean): void
	{
		if (Type.isBoolean(flag))
		{
			this.#hideOnSelect = flag;
		}
	}

	shouldHideOnSelect(): boolean
	{
		if (this.isInline())
		{
			return false;
		}

		return this.#hideOnSelect;
	}

	setDateSeparator(separator: string): void
	{
		if (Type.isStringFilled(separator))
		{
			this.#dateSeparator = separator;
		}
	}

	getDateSeparator(): string
	{
		return this.#dateSeparator;
	}

	setInputField(field: string | HTMLElement): void
	{
		const input = this.#getInputField(field);
		if (input !== null)
		{
			this.#inputField = input;
			this.#bindInputEvents(input);
		}
	}

	setRangeStartInput(field: string | HTMLElement): void
	{
		const input = this.#getInputField(field);
		if (input !== null)
		{
			this.#rangeStartInput = input;
			this.#bindInputEvents(input);
		}
	}

	setRangeEndInput(field: string | HTMLElement): void
	{
		const input = this.#getInputField(field);
		if (input !== null)
		{
			this.#rangeEndInput = input;
			this.#bindInputEvents(input);
		}
	}

	#getInputField(field: string | HTMLElement): HTMLElement | null
	{
		if (Type.isStringFilled(field))
		{
			const element = document.querySelector(field);
			if (Type.isElementNode(element) || (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA'))
			{
				return element;
			}

			console.error(`Date Picker: a form element was not found (${field}).`);
		}
		else if (Type.isElementNode(field) && (field.nodeName === 'INPUT' || field.nodeName === 'TEXTAREA'))
		{
			return field;
		}

		return null;
	}

	#bindInputEvents(input: HTMLElement): void
	{
		if (!this.shouldUseInputEvents())
		{
			return;
		}

		Event.bind(input, 'click', this.#refs.remember('click-handler', () => {
			return this.#handleInputClick.bind(this);
		}));

		Event.bind(input, 'focusout', this.#refs.remember('focusout-handler', () => {
			return this.#handleInputFocusOut.bind(this);
		}));

		Event.bind(input, 'keydown', this.#refs.remember('keydown-handler', () => {
			return this.#handleInputKeyDown.bind(this);
		}));

		Event.bind(input, 'input', this.#refs.remember('change-handler', () => {
			return this.#handleInputChange.bind(this);
		}));
	}

	#unbindInputEvents(input: HTMLElement): void
	{
		Event.unbind(input, 'click', this.#refs.get('click-handler'));
		Event.unbind(input, 'focusout', this.#refs.get('focusout-handler'));
		Event.unbind(input, 'keydown', this.#refs.get('keydown-handler'));
		Event.unbind(input, 'input', this.#refs.get('change-handler'));
	}

	#handleInputClick(event: MouseEvent): void
	{
		if (this.isRangeMode())
		{
			this.setTargetNode(event.target);
			if (!this.isOpen())
			{
				this.show();
			}
		}
		else
		{
			this.show();
		}
	}

	#handleInputFocusOut(event: MouseEvent): void
	{
		if (!this.getContainer().contains(event.relatedTarget))
		{
			this.hide();
		}
	}

	#handleInputKeyDown(event: KeyboardEvent): void
	{
		if (event.key === 'Tab' && !event.shiftKey && this.isOpen())
		{
			event.preventDefault();

			const currentPickerContainer = this.getPicker().getContainer();
			const [, next] = getFocusableBoundaryElements(
				currentPickerContainer,
				(element: HTMLElement) => element.dataset.tabPriority === 'true',
			);

			if (next === null)
			{
				this.focus();
			}
			else
			{
				next.focus({ preventScroll: true, focusVisible: true });
				this.#keyboardNavigation.setLastFocusElement(next);
			}
		}
	}

	#handleInputChange(event: KeyboardEvent): void
	{
		if (this.isOpen())
		{
			this.updateFromInputFields();
		}
	}

	#handleAutoHide(event: MouseEvent): boolean
	{
		const target = event.target;
		const el = this.getPopup().getPopupContainer();
		if (target === el || el.contains(target))
		{
			return false;
		}

		if (this.isRangeMode())
		{
			const anotherInput = (
				(this.getRangeStartInput() === target || this.getRangeEndInput() === target)
				&& this.getTargetNode() !== target
			);

			return !anotherInput;
		}

		return true;
	}

	shouldUseInputEvents(): boolean
	{
		return this.#useInputEvents;
	}

	getInputField(): HTMLInputElement | HTMLTextAreaElement | null
	{
		return this.#inputField;
	}

	getRangeStartInput(): HTMLInputElement | HTMLTextAreaElement | null
	{
		return this.#rangeStartInput;
	}

	getRangeEndInput(): HTMLInputElement | HTMLTextAreaElement | null
	{
		return this.#rangeEndInput;
	}

	updateInputFields(): void
	{
		if (this.isSingleMode())
		{
			if (this.getType() === 'time')
			{
				this.#setInputDate(this.getInputField(), this.getSelectedDate(), this.getTimeFormat());
			}
			else
			{
				this.#setInputDate(this.getInputField(), this.getSelectedDate());
			}
		}
		else if (this.isMultipleMode())
		{
			this.#setInputDate(
				this.getInputField(),
				this.getSelectedDates()
					.map((date: Date) => this.formatDate(date))
					.join(this.getDateSeparator())
				,
			);
		}
		else if (this.isRangeMode())
		{
			this.#setInputDate(this.getRangeStartInput(), this.getRangeStart());
			this.#setInputDate(this.getRangeEndInput(), this.getRangeEnd());
		}
	}

	#focusInputField(): void
	{
		if (this.getInputField() !== null)
		{
			this.getInputField().focus({ preventScroll: true });
		}
		else if (this.getRangeStartInput() !== null)
		{
			this.getRangeStartInput().focus({ preventScroll: true });
		}
	}

	updateFromInputFields(): void
	{
		if (this.isSingleMode() && this.getInputField() !== null)
		{
			const inputDate = this.#getDateFromInput(this.getInputField());
			if (inputDate === null)
			{
				this.deselectAll({ updateInputs: false, emitEvents: false });
			}
			else
			{
				this.selectDate(inputDate, { updateInputs: false, emitEvents: false });
			}
		}
		else if (this.isMultipleMode() && this.getInputField() !== null)
		{
			const value = this.getInputField().value.trim();
			const inputDates: Date[] = value
				.split(this.getDateSeparator().trim())
				.map((part: string) => this.createDate(part.trim()))
				.filter((date: Date | null) => date !== null)
			;

			this.deselectAll({ updateInputs: false, emitEvents: false });
			this.selectDates(inputDates, { updateInputs: false, emitEvents: false });
		}
		else if (this.isRangeMode() && this.getRangeStartInput() !== null)
		{
			const rangeStart = this.#getDateFromInput(this.getRangeStartInput());
			const rangeEnd = this.#getDateFromInput(this.getRangeEndInput());

			if (rangeStart === null)
			{
				this.deselectAll({ updateInputs: false, emitEvents: false });
			}
			else
			{
				this.selectRange(rangeStart, rangeEnd, { updateInputs: false, emitEvents: false });
			}
		}
	}

	#getDateFromInput(input: HTMLInputElement | HTMLTextAreaElement | null): Date | null
	{
		if (input === null)
		{
			return null;
		}

		const value = input.value.trim();
		if (!Type.isStringFilled(value))
		{
			return null;
		}

		if (this.getType() === 'time')
		{
			return createDate(value, this.getTimeFormat());
		}

		return this.createDate(value);
	}

	#setInputDate(input: HTMLInputElement | HTMLTextAreaElement | null, date: Date | null, format: string = null): void
	{
		if (input !== null)
		{
			let value = '';
			if (date === null)
			{
				value = '';
			}
			else if (Type.isString(date))
			{
				value = date;
			}
			else
			{
				value = this.formatDate(date, format);
			}

			// eslint-disable-next-line no-param-reassign
			input.value = value;
		}
	}

	getLocale(): string
	{
		return this.#locale;
	}

	isRendered(): boolean
	{
		return this.#rendered;
	}

	getContainer(): HTMLElement
	{
		return this.#refs.remember('container', () => {
			const classes = ['ui-date-picker'];
			if (this.isInline())
			{
				classes.push('--inline');
			}

			if (this.shouldHideHeader())
			{
				classes.push('--hide-header');
			}

			classes.push(`--${this.getType()}-picker`);

			return Tag.render`
				<div tabindex="-1" onkeyup="${this.#handleContainerKeyUp.bind(this)}" class="${classes.join(' ')}">
					${this.getViewsContainer()}
				</div>
			`;
		});
	}

	getViewsContainer(): HTMLElement
	{
		return this.#refs.remember('views', () => {
			return Tag.render`<div class="ui-date-picker-views"></div>`;
		});
	}

	isMultipleMode(): boolean
	{
		return this.#selectionMode === 'multiple';
	}

	isSingleMode(): boolean
	{
		return this.#selectionMode === 'single';
	}

	isRangeMode(): boolean
	{
		return this.#selectionMode === 'range';
	}

	isInline(): boolean
	{
		return this.#inline;
	}

	isFocused(): boolean
	{
		const rootContainer = this.getContainer();
		const activeElement = rootContainer.ownerDocument.activeElement;

		return rootContainer.contains(activeElement) || rootContainer === activeElement;
	}

	setAutoFocus(flag: boolean): boolean
	{
		if (Type.isBoolean(flag))
		{
			this.#autoFocus = flag;
		}
	}

	isAutoFocus(): boolean
	{
		return this.#autoFocus;
	}

	setTargetNode(node: HTMLElement | { left: number, top: number } | null | MouseEvent): void
	{
		if (!Type.isDomNode(node) && !Type.isNull(node) && !Type.isObject(node))
		{
			return;
		}

		this.#targetNode = node;

		if (this.isRendered())
		{
			this.getPopup().setBindElement(this.#targetNode);
			this.getPopup().adjustPosition();
		}
	}

	getTargetNode(): HTMLElement | null
	{
		return this.#targetNode;
	}

	setAutoHide(enable: boolean): void
	{
		if (Type.isBoolean(enable))
		{
			this.#autoHide = enable;
			if (this.isRendered())
			{
				this.getPopup().setAutoHide(enable);
			}
		}
	}

	isAutoHide(): boolean
	{
		return this.#autoHide;
	}

	setHideByEsc(enable: boolean): void
	{
		if (Type.isBoolean(enable))
		{
			this.#hideByEsc = enable;
			if (this.isRendered())
			{
				this.getPopup().setClosingByEsc(enable);
			}
		}
	}

	shouldHideByEsc(): boolean
	{
		return this.#hideByEsc;
	}

	isCacheable(): boolean
	{
		return this.#cacheable;
	}

	setCacheable(cacheable: boolean): void
	{
		if (Type.isBoolean(cacheable))
		{
			this.#cacheable = cacheable;
			if (this.isRendered())
			{
				this.getPopup().setCacheable(cacheable);
			}
		}
	}

	setHideHeader(enable: boolean): void
	{
		if (Type.isBoolean(enable))
		{
			this.#hideHeader = enable;
			if (this.isRendered())
			{
				if (enable)
				{
					Dom.addClass(this.getContainer(), '--hide-header');
				}
				else
				{
					Dom.removeClass(this.getContainer(), '--hide-header');
				}
			}
		}
	}

	shouldHideHeader(): boolean
	{
		return this.#hideHeader;
	}

	createDate(date: DateLike): Date | null
	{
		return createDate(date, this.getDateFormat());
	}

	formatDate(date: Date, format: string = null): string
	{
		const midnight = date.getUTCHours() === 0 && date.getUTCMinutes() === 0 && date.getUTCSeconds() === 0;
		const dateFormat = format === null ? this.getDateFormat() : format;
		let result = DateTimeFormat.format(dateFormat, date, null, true);

		if (this.isTimeEnabled() && midnight && this.shouldCutZeroTime())
		{
			result = result
				.replaceAll(/\s*12:00:00 am\s*/gi, '')
				.replaceAll(/\s*12:00 am\s*/gi, '')
				.replaceAll(/\s*00:00:00\s*/g, '')
				.replaceAll(/\s*00:00\s*/g, '')
			;
		}

		return result;
	}

	formatTime(date: Date, format: string = null): string
	{
		return DateTimeFormat.format(
			format === null ? this.getTimeFormat() : format,
			date,
			null,
			true,
		);
	}

	getDateFormat(): string
	{
		return this.#dateFormat;
	}

	#getDefaultDateFormat(): string
	{
		if (this.getType() === 'year')
		{
			return 'Y';
		}

		if (this.getType() === 'month')
		{
			return 'f - Y';
		}

		if (this.isTimeEnabled())
		{
			if (this.shouldAllowSeconds())
			{
				return DateTimeFormat.getFormat('FORMAT_DATETIME');
			}

			return DateTimeFormat.getFormat('FORMAT_DATETIME').replace(/:s/i, '');
		}

		return DateTimeFormat.getFormat('FORMAT_DATE');
	}

	getTimeFormat(): string
	{
		return this.#timeFormat;
	}

	#render(): void
	{
		if (this.isRendered())
		{
			return;
		}

		if (this.isInline() && this.getTargetNode() !== null)
		{
			Dom.append(this.getContainer(), this.getTargetNode());
		}

		const views = ['day', 'month', 'year', 'time'];
		const index = views.indexOf(this.getType());
		const view = index === -1 ? 'day' : views[index];

		this.setCurrentView(view);
		this.#rendered = true;

		if (this.#keyboardNavigation !== null)
		{
			this.#keyboardNavigation.init();
		}
	}

	#createPicker(pickerId: string): BasePicker
	{
		if (pickerId === 'day')
		{
			const dayPicker = new DayPicker(this);
			dayPicker.subscribe('onSelect', this.#handleDaySelect.bind(this));
			dayPicker.subscribe('onFocus', this.#handleDayFocus.bind(this));
			dayPicker.subscribe('onBlur', this.#handleDayBlur.bind(this));

			dayPicker.subscribe('onPrevBtnClick', () => {
				const unit = this.isFullYear() ? 'year' : 'month';
				const viewDate = addDate(floorDate(this.getViewDate(), unit), unit, -1);
				this.setViewDate(viewDate);
			});

			dayPicker.subscribe('onNextBtnClick', () => {
				const unit = this.isFullYear() ? 'year' : 'month';
				const viewDate = ceilDate(this.getViewDate(), unit);
				this.setViewDate(viewDate);
			});

			dayPicker.subscribe('onMonthClick', () => this.setCurrentView('month'));
			dayPicker.subscribe('onYearClick', () => this.setCurrentView('year'));
			dayPicker.subscribe('onTimeClick', this.#handleTimeClick.bind(this, 'datetime'));
			dayPicker.subscribe('onRangeStartClick', this.#handleTimeClick.bind(this, 'range-start'));
			dayPicker.subscribe('onRangeEndClick', this.#handleTimeClick.bind(this, 'range-end'));

			return dayPicker;
		}

		if (pickerId === 'month')
		{
			const monthPicker = new MonthPicker(this);
			monthPicker.subscribe('onSelect', this.#handleMonthSelect.bind(this));
			monthPicker.subscribe('onFocus', this.#handleMonthFocus.bind(this));
			monthPicker.subscribe('onBlur', this.#handleMonthBlur.bind(this));

			monthPicker.subscribe('onPrevBtnClick', () => {
				const { year, month } = getDate(this.getViewDate());
				const viewDate = createUtcDate(year - 1, month, 1);
				this.setViewDate(viewDate);
			});
			monthPicker.subscribe('onNextBtnClick', () => {
				const { year, month } = getDate(this.getViewDate());
				const viewDate = createUtcDate(year + 1, month, 1);
				this.setViewDate(viewDate);
			});

			monthPicker.subscribe('onTitleClick', () => this.setCurrentView('year'));

			return monthPicker;
		}

		if (pickerId === 'year')
		{
			const yearPicker = new YearPicker(this);
			yearPicker.subscribe('onSelect', this.#handleYearSelect.bind(this));
			yearPicker.subscribe('onFocus', this.#handleYearFocus.bind(this));
			yearPicker.subscribe('onBlur', this.#handleYearBlur.bind(this));
			yearPicker.subscribe('onPrevBtnClick', () => {
				const { year } = getDate(this.getViewDate());
				const viewDate = createUtcDate(year - 12, 0, 1);
				this.setViewDate(viewDate);
			});
			yearPicker.subscribe('onNextBtnClick', () => {
				const { year } = getDate(this.getViewDate());
				const viewDate = createUtcDate(year + 12, 0, 1);
				this.setViewDate(viewDate);
			});

			return yearPicker;
		}

		if (pickerId === 'time')
		{
			const timePicker = this.getTimePickerStyle() === 'wheel' ? new TimePickerWheel(this) : new TimePickerGrid(this);
			if (this.isRangeMode())
			{
				timePicker.subscribe('onSelect', this.#handleTimeRangeSelect.bind(this));
			}
			else
			{
				timePicker.subscribe('onSelect', this.#handleTimeSelect.bind(this));
			}

			timePicker.subscribe('onFocus', this.#handleTimeFocus.bind(this));
			timePicker.subscribe('onBlur', this.#handleTimeBlur.bind(this));
			timePicker.subscribe('onPrevBtnClick', () => this.setCurrentView('day'));
			timePicker.subscribe('onTitleClick', () => this.setCurrentView('day'));

			return timePicker;
		}

		return null;
	}

	#handleContainerKeyUp(event: KeyboardEvent): void
	{
		if (this.isInline())
		{
			return;
		}

		if (event.key === 'Escape' && this.shouldHideByEsc())
		{
			this.hide();
		}
	}

	#handleTimeClick(mode)
	{
		const timePicker: TimePickerWheel = this.getPicker('time');
		const selectTime = (
			(mode === 'range-start' && this.getRangeStart() !== null)
			|| (mode === 'range-end' && this.getRangeEnd() !== null)
			|| (this.getSelectedDate() !== null)
		);

		if (selectTime)
		{
			timePicker.setMode(mode);
			this.setCurrentView('time');
		}
	}

	#handleDaySelect(event: BaseEvent): void
	{
		const { year, month, day } = event.getData();
		let selectedDate = createUtcDate(year, month, day);
		if (this.isRangeMode())
		{
			const currentRange = this.#selectedDates;
			if (currentRange.length === 0)
			{
				const { hours, minutes, seconds } = this.getDefaultTimeParts();
				selectedDate = setTime(selectedDate, hours, minutes, seconds);
			}
			else if (currentRange.length === 1)
			{
				let { hours, minutes, seconds } = this.getDefaultTimeParts();
				if (this.isDateSelected(selectedDate, 'day'))
				{
					({ hours, minutes, seconds } = getDate(this.getRangeStart()));
					minutes += this.getDefaultTimeSpan();
				}

				selectedDate = setTime(selectedDate, hours, minutes, seconds);
			}

			const range = addToRange(selectedDate, currentRange);
			const [start, end] = range;
			if (range.length === 0)
			{
				this.deselectAll();
			}
			else
			{
				this.selectRange(start, end);
			}
		}
		else if (this.isDateSelected(selectedDate))
		{
			if (this.shouldToggleSelected())
			{
				this.deselectDate(selectedDate);
			}
			else if (this.shouldHideOnSelect() && this.isSingleMode())
			{
				this.hide();
			}
		}
		else
		{
			let { hours, minutes, seconds } = this.getDefaultTimeParts();
			if (this.isSingleMode() && this.getSelectedDate() !== null)
			{
				// save previous time
				({ hours, minutes, seconds } = getDate(this.getSelectedDate()));
			}

			this.selectDate(createUtcDate(year, month, day, hours, minutes, seconds));

			if (this.shouldHideOnSelect() && this.isSingleMode() && !this.isTimeEnabled())
			{
				this.hide();
			}
		}
	}

	#handleDayFocus(event: BaseEvent): void
	{
		const { year, month, day } = event.getData();

		const focusDate = createUtcDate(year, month, day);
		if (!isDatesEqual(focusDate, this.getFocusDate()))
		{
			this.setFocusDate(focusDate);
		}
	}

	#handleDayBlur(event: BaseEvent): void
	{
		this.setFocusDate(null);
	}

	#handleMonthFocus(event: BaseEvent): void
	{
		const { year, month } = event.getData();

		const focusDate = createUtcDate(year, month);
		if (!isDatesEqual(focusDate, this.getFocusDate(), 'month'))
		{
			this.setFocusDate(focusDate);
		}
	}

	#handleMonthBlur(event: BaseEvent): void
	{
		this.setFocusDate(null);
	}

	#handleYearFocus(event: BaseEvent): void
	{
		const { year } = event.getData();

		const focusDate = createUtcDate(year);
		if (!isDatesEqual(focusDate, this.getFocusDate(), 'year'))
		{
			this.setFocusDate(focusDate);
		}
	}

	#handleYearBlur(event: BaseEvent): void
	{
		this.setFocusDate(null);
	}

	#handleTimeFocus(event: BaseEvent): void
	{
		const { hour, minute } = event.getData();
		let focusDate = cloneDate(this.getInitialFocusDate());
		if (Type.isNumber(hour))
		{
			focusDate = setTime(focusDate, hour, null, null);
			this.setFocusDate(focusDate);
		}
		else if (Type.isNumber(minute))
		{
			focusDate = setTime(focusDate, null, minute, null);
			this.setFocusDate(focusDate);
		}
	}

	#handleTimeBlur(event: BaseEvent): void
	{
		this.setFocusDate(null);
	}

	#handleMonthSelect(event: BaseEvent): void
	{
		const { year } = getDate(this.getViewDate());
		const month: number = event.getData().month;
		const date = createUtcDate(year, month);

		if (this.getType() === 'month')
		{
			this.selectDate(date);
			if (this.shouldHideOnSelect())
			{
				this.hide();
			}
		}
		else
		{
			this.setViewDate(date);
			this.setCurrentView('day');
		}
	}

	#handleYearSelect(event: BaseEvent): void
	{
		const { month } = getDate(this.getViewDate());
		const year: number = event.getData().year;
		const date = createUtcDate(year, month);

		if (this.getType() === 'year')
		{
			this.selectDate(createUtcDate(year));
			if (this.shouldHideOnSelect())
			{
				this.hide();
			}
		}
		else
		{
			this.setViewDate(date);
			this.setCurrentView('day');
		}
	}

	#handleTimeSelect(event: BaseEvent<{ hour: number, minute: number }>): void
	{
		let selectedDate = null;
		if (this.getType() === 'time')
		{
			selectedDate = (
				this.getSelectedDate() === null
					? ceilDate(this.getToday(), 'day')
					: cloneDate(this.getSelectedDate())
			);
		}
		else if (this.getSelectedDate() === null)
		{
			return;
		}
		else
		{
			selectedDate = cloneDate(this.getSelectedDate());
		}

		const hideOrSwitchToDayView = () => {
			if (this.shouldHideOnSelect())
			{
				this.hide();
			}
			else if (this.getType() === 'date')
			{
				this.setCurrentView('day');
			}
		};

		const { hour, minute } = event.getData();
		if (Type.isNumber(hour))
		{
			const currentHour = this.getSelectedDate() === null ? -1 : selectedDate.getUTCHours();
			if (currentHour === hour)
			{
				hideOrSwitchToDayView();
			}
			else
			{
				selectedDate.setUTCHours(hour);
				this.selectDate(selectedDate);
			}
		}
		else if (Type.isNumber(minute))
		{
			const currentMinute = this.getSelectedDate() === null ? -1 : selectedDate.getUTCMinutes();
			if (currentMinute !== minute)
			{
				selectedDate.setUTCMinutes(minute);
				this.selectDate(selectedDate);
			}

			if (this.getTimePickerStyle() === 'grid')
			{
				hideOrSwitchToDayView();
			}
		}
	}

	#handleTimeRangeSelect(event: BaseEvent<{ hour: number, minute: number }>): void
	{
		const timePicker: TimePickerWheel = event.getTarget();
		const rangeEndChange = timePicker.getMode() === 'range-end';

		let rangeStart = this.getRangeStart() === null ? null : cloneDate(this.getRangeStart());
		let rangeEnd = this.getRangeEnd() === null ? null : cloneDate(this.getRangeEnd());

		if (rangeStart === null || (rangeEnd === null && rangeEndChange))
		{
			return;
		}

		const switchToDayView = (): boolean => {
			if (this.getType() === 'date' && this.getTimePickerStyle() === 'grid')
			{
				this.setCurrentView('day');
			}
		};

		const { hour, minute } = event.getData();
		if (Type.isNumber(hour))
		{
			if (rangeEndChange)
			{
				const currentHour = rangeEnd.getUTCHours();
				if (currentHour === hour)
				{
					switchToDayView();

					return;
				}

				rangeEnd.setUTCHours(hour);
			}
			else
			{
				const currentHour = rangeStart.getUTCHours();
				if (currentHour === hour)
				{
					switchToDayView();

					return;
				}

				rangeStart.setUTCHours(hour);
			}
		}
		else if (Type.isNumber(minute))
		{
			if (rangeEndChange)
			{
				const currentMinute = rangeEnd.getUTCMinutes();
				if (currentMinute === minute)
				{
					switchToDayView();

					return;
				}

				rangeEnd.setUTCMinutes(minute);
			}
			else
			{
				const currentMinute = rangeStart.getUTCMinutes();
				if (currentMinute === minute)
				{
					switchToDayView();

					return;
				}

				rangeStart.setUTCMinutes(minute);
			}
		}

		if (rangeEnd !== null && rangeStart > rangeEnd)
		{
			if (rangeEndChange)
			{
				rangeStart = addDate(rangeEnd, 'minute', -this.getDefaultTimeSpan());
			}
			else
			{
				rangeEnd = addDate(rangeStart, 'minute', this.getDefaultTimeSpan());
			}
		}

		this.selectRange(rangeStart, rangeEnd);

		if (Type.isNumber(minute))
		{
			switchToDayView();
		}
	}

	#handlePopupShow(): void
	{
		if (!this.isFocused() && this.isAutoFocus())
		{
			this.focus();
		}

		if (this.isSingleOpening())
		{
			if (singleOpenDatePicker !== null)
			{
				singleOpenDatePicker.hide();
			}

			// eslint-disable-next-line unicorn/no-this-assignment
			singleOpenDatePicker = this;
		}

		this.emit('onShow');
	}

	#handlePopupFirstShow(): void
	{
		this.#render();

		this.emit('onFirstShow');
	}

	#handlePopupClose(): void
	{
		if (this.getType() === 'date')
		{
			this.setCurrentView('day');
		}

		this.setFocusDate(null);
		this.setViewDate(this.getDefaultViewDate());

		if (this.isSingleOpening())
		{
			singleOpenDatePicker = null;
		}

		if (this.isFocused())
		{
			this.#focusInputField();
		}

		this.emit('onHide');
	}

	#handlePopupDestroy(): void
	{
		this.destroy();
	}

	destroy(): void
	{
		if (this.#destroying)
		{
			return;
		}

		this.#destroying = true;
		this.emit(DatePickerEvent.DESTROY);

		if (this.isRendered())
		{
			Dom.remove(this.getContainer());
		}

		this.#unbindInputEvents(this.getInputField());
		this.#unbindInputEvents(this.getRangeStartInput());
		this.#unbindInputEvents(this.getRangeEndInput());

		if (this.#popup !== null)
		{
			this.#popup.destroy();
		}

		this.#refs = null;
		this.#views = null;
		this.#selectedDates = null;

		Object.setPrototypeOf(this, null);
	}
}

Youez - 2016 - github.com/yon3zu
LinuXploit