|
|
|
/*
|
|
|
|
* https://github.com/morethanwords/tweb
|
|
|
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
|
|
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
|
|
|
*/
|
|
|
|
|
|
|
|
import PopupElement, { PopupOptions } from ".";
|
|
|
|
import mediaSizes from "../../helpers/mediaSizes";
|
|
|
|
import I18n, { i18n, LangPackKey } from "../../lib/langPack";
|
|
|
|
import InputField from "../inputField";
|
|
|
|
|
|
|
|
export default class PopupDatePicker extends PopupElement {
|
|
|
|
protected controlsDiv: HTMLElement;
|
|
|
|
protected monthTitle: HTMLElement;
|
|
|
|
protected prevBtn: HTMLElement;
|
|
|
|
protected nextBtn: HTMLElement;
|
|
|
|
|
|
|
|
protected monthsContainer: HTMLElement;
|
|
|
|
protected month: HTMLElement;
|
|
|
|
|
|
|
|
protected minMonth: Date;
|
|
|
|
protected maxMonth: Date;
|
|
|
|
protected minDate: Date;
|
|
|
|
protected maxDate: Date;
|
|
|
|
protected selectedDate: Date;
|
|
|
|
protected selectedMonth: Date;
|
|
|
|
protected selectedEl: HTMLElement;
|
|
|
|
|
|
|
|
protected timeDiv: HTMLDivElement;
|
|
|
|
protected hoursInputField: InputField;
|
|
|
|
protected minutesInputField: InputField;
|
|
|
|
|
|
|
|
constructor(initDate: Date, public onPick: (timestamp: number) => void, protected options: Partial<{
|
|
|
|
noButtons: true,
|
|
|
|
noTitle: true,
|
|
|
|
minDate: Date,
|
|
|
|
maxDate: Date
|
|
|
|
withTime: true,
|
|
|
|
showOverflowMonths: true
|
|
|
|
}> & PopupOptions = {}) {
|
|
|
|
super('popup-date-picker', options.noButtons ? [] : [{
|
|
|
|
langKey: 'JumpToDate',
|
|
|
|
callback: () => {
|
|
|
|
if(this.onPick) {
|
|
|
|
this.onPick(this.selectedDate.getTime() / 1000 | 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, {
|
|
|
|
langKey: 'Cancel',
|
|
|
|
isCancel: true
|
|
|
|
}], {body: true, overlayClosable: true, ...options});
|
|
|
|
|
|
|
|
this.minDate = options.minDate || new Date('2013-08-01T00:00:00');
|
|
|
|
|
|
|
|
if(initDate < this.minDate) {
|
|
|
|
initDate.setFullYear(this.minDate.getFullYear(), this.minDate.getMonth(), this.minDate.getDate());
|
|
|
|
}
|
|
|
|
|
|
|
|
// Controls
|
|
|
|
this.controlsDiv = document.createElement('div');
|
|
|
|
this.controlsDiv.classList.add('date-picker-controls');
|
|
|
|
|
|
|
|
this.prevBtn = document.createElement('button');
|
|
|
|
this.prevBtn.classList.add('btn-icon', 'tgico-down', 'date-picker-prev');
|
|
|
|
this.prevBtn.addEventListener('click', this.onPrevClick);
|
|
|
|
|
|
|
|
this.nextBtn = document.createElement('button');
|
|
|
|
this.nextBtn.classList.add('btn-icon', 'tgico-down', 'date-picker-next');
|
|
|
|
this.nextBtn.addEventListener('click', this.onNextClick);
|
|
|
|
|
|
|
|
this.monthTitle = document.createElement('div');
|
|
|
|
this.monthTitle.classList.add('date-picker-month-title');
|
|
|
|
|
|
|
|
this.controlsDiv.append(this.prevBtn, this.monthTitle, this.nextBtn);
|
|
|
|
|
|
|
|
// Month
|
|
|
|
this.monthsContainer = document.createElement('div');
|
|
|
|
this.monthsContainer.classList.add('date-picker-months');
|
|
|
|
this.monthsContainer.addEventListener('click', this.onDateClick);
|
|
|
|
|
|
|
|
this.body.append(this.controlsDiv, this.monthsContainer);
|
|
|
|
|
|
|
|
// Time inputs
|
|
|
|
if(options.withTime) {
|
|
|
|
this.timeDiv = document.createElement('div');
|
|
|
|
this.timeDiv.classList.add('date-picker-time');
|
|
|
|
|
|
|
|
const delimiter = document.createElement('div');
|
|
|
|
delimiter.classList.add('date-picker-time-delimiter');
|
|
|
|
delimiter.append(':');
|
|
|
|
|
|
|
|
const handleTimeInput = (max: number, inputField: InputField, onInput: (length: number) => void, onOverflow?: (number: number) => void) => {
|
|
|
|
const maxString = '' + max;
|
|
|
|
inputField.input.addEventListener('input', (e) => {
|
|
|
|
let value = inputField.value.replace(/\D/g, '');
|
|
|
|
if(value.length > 2) {
|
|
|
|
value = value.slice(0, 2);
|
|
|
|
} else {
|
|
|
|
if((value.length === 1 && +value[0] > +maxString[0]) || (value.length === 2 && +value > max)) {
|
|
|
|
if(value.length === 2 && onOverflow) {
|
|
|
|
onOverflow(+value[1]);
|
|
|
|
}
|
|
|
|
|
|
|
|
value = '0' + value[0];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
inputField.setValueSilently(value);
|
|
|
|
onInput(value.length);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
this.hoursInputField = new InputField({plainText: true});
|
|
|
|
this.minutesInputField = new InputField({plainText: true});
|
|
|
|
|
|
|
|
handleTimeInput(23, this.hoursInputField, (length) => {
|
|
|
|
if(length === 2) {
|
|
|
|
this.minutesInputField.input.focus();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setTimeTitle();
|
|
|
|
}, (number) => {
|
|
|
|
this.minutesInputField.value = (number + this.minutesInputField.value).slice(0, 2);
|
|
|
|
});
|
|
|
|
handleTimeInput(59, this.minutesInputField, (length) => {
|
|
|
|
if(!length) {
|
|
|
|
this.hoursInputField.input.focus();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setTimeTitle();
|
|
|
|
});
|
|
|
|
|
|
|
|
this.selectedDate = initDate;
|
|
|
|
|
|
|
|
initDate.setMinutes(initDate.getMinutes() + 10);
|
|
|
|
|
|
|
|
this.hoursInputField.setValueSilently(('0' + initDate.getHours()).slice(-2));
|
|
|
|
this.minutesInputField.setValueSilently(('0' + initDate.getMinutes()).slice(-2));
|
|
|
|
|
|
|
|
initDate.setHours(0, 0, 0, 0);
|
|
|
|
|
|
|
|
this.timeDiv.append(this.hoursInputField.container, delimiter, this.minutesInputField.container);
|
|
|
|
|
|
|
|
this.btnConfirm.addEventListener('click', () => {
|
|
|
|
if(this.onPick) {
|
|
|
|
this.selectedDate.setHours(+this.hoursInputField.value || 0, +this.minutesInputField.value || 0, 0, 0);
|
|
|
|
this.onPick(this.selectedDate.getTime() / 1000 | 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.hide();
|
|
|
|
}, {once: true});
|
|
|
|
|
|
|
|
this.body.append(this.timeDiv);
|
|
|
|
|
|
|
|
this.prevBtn.classList.add('primary');
|
|
|
|
this.nextBtn.classList.add('primary');
|
|
|
|
}
|
|
|
|
|
|
|
|
const popupCenterer = document.createElement('div');
|
|
|
|
popupCenterer.classList.add('popup-centerer');
|
|
|
|
popupCenterer.append(this.container);
|
|
|
|
this.element.append(popupCenterer);
|
|
|
|
|
|
|
|
//const passed = (initDate.getTime() - (initDate.getTimezoneOffset() * 60000)) % 86400000;
|
|
|
|
//this.selectedDate = this.maxDate = new Date(initDate.getTime() - passed);
|
|
|
|
initDate.setHours(0, 0, 0, 0);
|
|
|
|
this.selectedDate = initDate;
|
|
|
|
|
|
|
|
this.maxDate = options.maxDate || new Date();
|
|
|
|
this.maxDate.setHours(0, 0, 0, 0);
|
|
|
|
|
|
|
|
this.selectedMonth = new Date(this.selectedDate);
|
|
|
|
this.selectedMonth.setDate(1);
|
|
|
|
|
|
|
|
this.maxMonth = new Date(this.maxDate);
|
|
|
|
this.maxMonth.setDate(1);
|
|
|
|
|
|
|
|
this.minMonth = new Date(this.minDate);
|
|
|
|
this.minMonth.setHours(0, 0, 0, 0);
|
|
|
|
this.minMonth.setDate(1);
|
|
|
|
|
|
|
|
if(this.selectedMonth.getTime() === this.minMonth.getTime()) {
|
|
|
|
this.prevBtn.setAttribute('disabled', 'true');
|
|
|
|
}
|
|
|
|
|
|
|
|
if(this.selectedMonth.getTime() === this.maxMonth.getTime()) {
|
|
|
|
this.nextBtn.setAttribute('disabled', 'true');
|
|
|
|
}
|
|
|
|
|
|
|
|
if(options.noTitle) {
|
|
|
|
this.setTitle = () => {};
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setTimeTitle();
|
|
|
|
this.setTitle();
|
|
|
|
this.setMonth();
|
|
|
|
}
|
|
|
|
|
|
|
|
onPrevClick = (e: MouseEvent) => {
|
|
|
|
this.selectedMonth.setMonth(this.selectedMonth.getMonth() - 1);
|
|
|
|
this.setMonth();
|
|
|
|
|
|
|
|
if(this.selectedMonth.getTime() === this.minMonth.getTime()) {
|
|
|
|
this.prevBtn.setAttribute('disabled', 'true');
|
|
|
|
}
|
|
|
|
|
|
|
|
this.nextBtn.removeAttribute('disabled');
|
|
|
|
};
|
|
|
|
|
|
|
|
onNextClick = (e: MouseEvent) => {
|
|
|
|
this.selectedMonth.setMonth(this.selectedMonth.getMonth() + 1);
|
|
|
|
this.setMonth();
|
|
|
|
|
|
|
|
if(this.selectedMonth.getTime() === this.maxMonth.getTime()) {
|
|
|
|
this.nextBtn.setAttribute('disabled', 'true');
|
|
|
|
}
|
|
|
|
|
|
|
|
this.prevBtn.removeAttribute('disabled');
|
|
|
|
};
|
|
|
|
|
|
|
|
onDateClick = (e: MouseEvent) => {
|
|
|
|
//cancelEvent(e);
|
|
|
|
const target = e.target as HTMLElement;
|
|
|
|
|
|
|
|
if(!target.dataset.timestamp) return;
|
|
|
|
|
|
|
|
if(this.selectedEl) {
|
|
|
|
if(this.selectedEl === target) return;
|
|
|
|
this.selectedEl.classList.remove('active');
|
|
|
|
}
|
|
|
|
|
|
|
|
this.selectedEl = target;
|
|
|
|
|
|
|
|
target.classList.add('active');
|
|
|
|
const timestamp = +target.dataset.timestamp;
|
|
|
|
|
|
|
|
this.selectedDate = new Date(timestamp);
|
|
|
|
|
|
|
|
this.setTitle();
|
|
|
|
this.setTimeTitle();
|
|
|
|
};
|
|
|
|
|
|
|
|
public setTimeTitle() {
|
|
|
|
if(this.btnConfirm && this.selectedDate) {
|
|
|
|
let key: LangPackKey, args: any[] = [];
|
|
|
|
const date = new Date();
|
|
|
|
date.setHours(0, 0, 0, 0);
|
|
|
|
|
|
|
|
const timeOptions: Intl.DateTimeFormatOptions = {
|
|
|
|
minute: '2-digit',
|
|
|
|
hour: '2-digit'
|
|
|
|
};
|
|
|
|
|
|
|
|
const sendDate = new Date(this.selectedDate.getTime());
|
|
|
|
sendDate.setHours(+this.hoursInputField.value, +this.minutesInputField.value);
|
|
|
|
|
|
|
|
if(this.selectedDate.getTime() === date.getTime()) {
|
|
|
|
key = 'Schedule.SendToday';
|
|
|
|
}/* else if(this.selectedDate.getTime() === (date.getTime() + 86400e3)) {
|
|
|
|
dayStr = 'Tomorrow';
|
|
|
|
} */ else {
|
|
|
|
key = 'Schedule.SendDate';
|
|
|
|
|
|
|
|
const dateOptions: Intl.DateTimeFormatOptions = {
|
|
|
|
month: 'short',
|
|
|
|
day: 'numeric'
|
|
|
|
};
|
|
|
|
|
|
|
|
if(sendDate.getFullYear() !== date.getFullYear()) {
|
|
|
|
dateOptions.year = 'numeric';
|
|
|
|
}
|
|
|
|
|
|
|
|
args.push(new I18n.IntlDateElement({
|
|
|
|
date: sendDate,
|
|
|
|
options: dateOptions
|
|
|
|
}).element);
|
|
|
|
}
|
|
|
|
|
|
|
|
args.push(new I18n.IntlDateElement({
|
|
|
|
date: sendDate,
|
|
|
|
options: timeOptions
|
|
|
|
}).element);
|
|
|
|
|
|
|
|
this.btnConfirm.firstChild.replaceWith(i18n(key, args));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public setTitle() {
|
|
|
|
//const splitted = this.selectedDate.toString().split(' ', 3);
|
|
|
|
//this.title.innerText = splitted[0] + ', ' + splitted[1] + ' ' + splitted[2];
|
|
|
|
this.title.textContent = '';
|
|
|
|
this.title.append(new I18n.IntlDateElement({
|
|
|
|
date: this.selectedDate,
|
|
|
|
options: {
|
|
|
|
day: 'numeric',
|
|
|
|
month: 'long',
|
|
|
|
weekday: 'short'
|
|
|
|
}
|
|
|
|
}).element);
|
|
|
|
}
|
|
|
|
|
|
|
|
private renderElement(disabled: boolean, innerText: string | HTMLElement = '') {
|
|
|
|
const el = document.createElement('button');
|
|
|
|
el.classList.add('btn-icon', 'date-picker-month-date');
|
|
|
|
|
|
|
|
if(disabled) {
|
|
|
|
el.setAttribute('disabled', 'true');
|
|
|
|
}
|
|
|
|
|
|
|
|
if(innerText) {
|
|
|
|
el.append(innerText);
|
|
|
|
}
|
|
|
|
|
|
|
|
return el;
|
|
|
|
}
|
|
|
|
|
|
|
|
public setMonth() {
|
|
|
|
const firstDate = new Date(this.selectedMonth);
|
|
|
|
|
|
|
|
const options: Intl.DateTimeFormatOptions = {
|
|
|
|
year: 'numeric',
|
|
|
|
month: this.timeDiv && mediaSizes.isMobile ? 'short' : 'long'
|
|
|
|
};
|
|
|
|
|
|
|
|
this.monthTitle.textContent = '';
|
|
|
|
this.monthTitle.append(new I18n.IntlDateElement({date: firstDate, options}).element);
|
|
|
|
//this.monthTitle.innerText = (this.timeDiv && mediaSizes.isMobile ? monthName.slice(0, 3) : monthName) + ' ' + this.selectedMonth.getFullYear();
|
|
|
|
|
|
|
|
if(this.month) {
|
|
|
|
this.month.remove();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.month = document.createElement('div');
|
|
|
|
this.month.classList.add('date-picker-month');
|
|
|
|
|
|
|
|
const weekStartDate = new Date();
|
|
|
|
const day = weekStartDate.getDay();
|
|
|
|
if(day !== 1) {
|
|
|
|
weekStartDate.setHours(-24 * (day - 1));
|
|
|
|
}
|
|
|
|
|
|
|
|
for(let i = 0; i < 7; ++i) {
|
|
|
|
const el = this.renderElement(true, new I18n.IntlDateElement({date: weekStartDate, options: {weekday: 'narrow'}}).element);
|
|
|
|
el.classList.remove('date-picker-month-date');
|
|
|
|
el.classList.add('date-picker-month-day');
|
|
|
|
this.month.append(el);
|
|
|
|
weekStartDate.setDate(weekStartDate.getDate() + 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0 - sunday
|
|
|
|
let dayIndex = firstDate.getDay() - 1;
|
|
|
|
if(dayIndex === -1) dayIndex = 7 - 1;
|
|
|
|
|
|
|
|
const clonedDate = new Date(firstDate.getTime());
|
|
|
|
clonedDate.setDate(clonedDate.getDate() - dayIndex - 1);
|
|
|
|
|
|
|
|
// Padding first week
|
|
|
|
for(let i = 0; i < dayIndex; ++i) {
|
|
|
|
if(this.options.showOverflowMonths) {
|
|
|
|
clonedDate.setDate(clonedDate.getDate() + 1);
|
|
|
|
this.month.append(this.renderElement(true, '' + clonedDate.getDate()));
|
|
|
|
} else {
|
|
|
|
this.month.append(this.renderElement(true));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
do {
|
|
|
|
const date = firstDate.getDate();
|
|
|
|
const el = this.renderElement(firstDate > this.maxDate || firstDate < this.minDate, '' + date);
|
|
|
|
el.dataset.timestamp = '' + firstDate.getTime();
|
|
|
|
|
|
|
|
if(firstDate.getTime() === this.selectedDate.getTime()) {
|
|
|
|
this.selectedEl = el;
|
|
|
|
el.classList.add('active');
|
|
|
|
}
|
|
|
|
|
|
|
|
this.month.append(el);
|
|
|
|
|
|
|
|
firstDate.setDate(date + 1);
|
|
|
|
} while(firstDate.getDate() !== 1);
|
|
|
|
|
|
|
|
const remainder = this.month.childElementCount % 7;
|
|
|
|
if(this.options.showOverflowMonths && remainder) {
|
|
|
|
for(let i = remainder; i < 7; ++i) {
|
|
|
|
this.month.append(this.renderElement(true, '' + firstDate.getDate()));
|
|
|
|
firstDate.setDate(firstDate.getDate() + 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const lines = Math.ceil(this.month.childElementCount / 7);
|
|
|
|
this.container.dataset.lines = '' + lines;
|
|
|
|
|
|
|
|
this.monthsContainer.append(this.month);
|
|
|
|
}
|
|
|
|
}
|