/**
@license
Copyright (c) 2022 trading_peter
This program is available under Apache License Version 2.0
*/
import '@tp/tp-icon/tp-icon.js';
import { FormElement } from '@tp/helpers/form-element';
import { LitElement, html, css, svg } from 'lit';
import { DateTime } from 'luxon';
class TpDatePicker extends FormElement(LitElement) {
static get styles() {
return [
css`
:host {
display: block;
}
.header {
display: grid;
grid-template-columns: 1fr 1fr;
align-items: center;
}
.header > div {
display: flex;
align-items: center;
justify-content: space-around;
padding: 5px 10px;
}
.header > div > div {
display: flex;
align-items: center;
}
.right {
display: flex;
justify-content: end;
}
.grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-template-rows: repeat(7, 1fr);
}
.grid > div {
display: flex;
align-items: center;
justify-content: center;
min-width: 42px;
height: 42px;
padding: 2px;
}
.day {
font-weight: bold;
}
div[part~="filler"] {
opacity: 0.5;
}
.year-overlay {
display: grid;
grid-template-columns: 1fr;
grid-row-gap: 5px;
position: absolute;
inset: 0;
overflow: auto;
background: white;
opacity: 0;
transition: 300ms opacity ease-in-out;
pointer-events: none;
}
.year-overlay[visible] {
opacity: 1;
pointer-events: all;
}
.year-overlay > div {
text-align: center;
cursor: pointer;
}
.year-overlay > div:hover {
background: var(--date-picker-hover-color);
}
.number {
width: 40px;
height: 40px;
border-radius: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.number:hover,
.number[part~="selected"] {
background: var(--tp-datepicker-day-hover-bg, #fff);
color: var(--tp-datepicker-day-hover-color, #000);
cursor: pointer;
}
.number[part~="today"] {
background: var(--tp-datepicker-today-bg, #fff);
color: var(--tp-datepicker-today-color, #000);
}
.pointer {
cursor: pointer;
}
.event-dots {
display: flex;
gap: 4px;
justify-content: center;
position: absolute;
bottom: 8px;
left: 0;
right: 0;
}
.event-dot {
width: 4px;
height: 4px;
border-radius: 50%;
background-color: var(--tp-datepicker-event-dot-color, #666);
}
div[part~="date"] {
position: relative;
}
`
];
}
render() {
let { month, year, dayNames, value, yearsForward, yearsBackwards } = this;
const today = DateTime.utc();
if ((!month && month !== 0) || (!year && year !== 0)) {
month = today.month - 1; // Luxon uses 1-based months, convert to 0-based for compatibility
year = today.year;
}
const years = Array(yearsForward + yearsBackwards)
.fill()
.map((_, i) => today.year - yearsBackwards + i)
.reverse();
const curMonth = this.getMonthDates(month, year);
return html`
this.selectDate(e)}>
${dayNames.map(label => html`
${label}
`)}
${curMonth.map(d => {
const matchesValue = this.equalDate(d, value);
const isToday = this.equalDate(d, today);
const eventCount = this.countEventsForDate(d);
return html`
${d.day}
${eventCount > 0 ? html`
${eventCount > 1 ? html`
` : ''}
` : ''}
`
})}
${years.map(y => html`
${y}
`)}
`;
}
static get properties() {
return {
value: { type: Object }, // Changed from Date to Luxon DateTime
today: { type: Object }, // Changed from Date to Luxon DateTime
month: { type: Number },
year: { type: Number },
monthNames: { type: Array },
dayNames: { type: Array },
yearsForward: { type: Number },
yearsBackwards: { type: Number },
showYearSelector: { type: Boolean },
events: { type: Array },
};
}
static get iconNext() {
return svg`
`
}
static get iconPrev() {
return svg`
`
}
constructor() {
super();
this.today = DateTime.utc();
this.month = this.today.month - 1; // Convert 1-based to 0-based month
this.year = this.today.year;
this.yearsForward = 10;
this.yearsBackwards = 100;
this.events = [];
this.dayNames = [
'Mo',
'Tue',
'Wed',
'Th',
'Fr',
'Sat',
'Sun',
];
this.monthNames = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
]
}
shouldUpdate(changes) {
if (changes.has('value') && this.value) {
this.month = this.value.month - 1; // Convert 1-based to 0-based month
this.year = this.value.year;
}
return true;
}
equalDate(d1, d2) {
if (!d1 || !d2) return false;
return d1.day === d2.day && d1.year === d2.year && d1.month === d2.month;
}
selectDate(e) {
for (const el of e.composedPath()) {
if (el.date !== undefined) {
this.value = el.date;
this.dispatchEvent(new CustomEvent('value-changed', { detail: this.value, bubbles: true, composed: true }));
return;
}
}
}
previousMonth() {
let prevYear = this.year;
let prevMonth = this.month - 1;
if (prevMonth < 0) {
prevYear -= 1;
prevMonth = 11;
}
this.month = prevMonth;
this.year = prevYear;
this.dispatchRangeChangeEvent();
}
nextMonth() {
let nextYear = this.year;
let nextMonth = this.month + 1;
if (nextMonth > 11) {
nextYear += 1;
nextMonth = 0;
}
this.month = nextMonth;
this.year = nextYear;
this.dispatchRangeChangeEvent();
}
previousYear() {
this.year -= 1;
this.dispatchRangeChangeEvent();
}
nextYear() {
this.year += 1;
this.dispatchRangeChangeEvent();
}
getMonthDates(month = null, year = null) {
month = month !== null ? month : DateTime.utc().month - 1; // Convert 1-based to 0-based
year = year || DateTime.utc().year;
// Create DateTime for the first of the month (convert 0-based month back to 1-based)
const firstOfMonth = DateTime.utc(year, month + 1, 1, 0, 0, 0);
// Create DateTime for the last of the month
const lastOfMonth = firstOfMonth.endOf('month').startOf('day');
const dates = [];
// Calculate dates before the first of month to fill the calendar
let firstDayOfCalendar = firstOfMonth;
while (firstDayOfCalendar.weekday > 1) { // 1 is Monday in Luxon
firstDayOfCalendar = firstDayOfCalendar.minus({ days: 1 });
dates.push(firstDayOfCalendar);
}
dates.reverse();
// Add all days of the current month
for (let i = 1; i <= lastOfMonth.day; i++) {
dates.push(DateTime.utc(year, month + 1, i, 0, 0, 0));
}
// Make sure we always fill 42 dates to guarantee equal size of the date picker
let nextDate = lastOfMonth;
while (dates.length < 42) {
nextDate = nextDate.plus({ days: 1 });
dates.push(nextDate);
}
return dates;
}
showYearOverlay() {
this.showYearSelector = true;
}
selectYear(e) {
const elList = e.composedPath();
for (const el of elList) {
if (el.value) {
this.year = el.value;
this.showYearSelector = false;
this.dispatchRangeChangeEvent();
break;
}
}
}
countEventsForDate(date) {
if (!Array.isArray(this.events)) return 0;
return this.events.filter(event => this.equalDate(event, date)).length;
}
dispatchRangeChangeEvent() {
const currentMonthDates = this.getMonthDates(this.month, this.year);
const rangeStart = currentMonthDates[0];
const rangeEnd = currentMonthDates[currentMonthDates.length - 1];
this.dispatchEvent(new CustomEvent('range-changed', {
detail: {
month: this.month,
year: this.year,
rangeStart,
rangeEnd,
visibleDates: currentMonthDates
},
bubbles: true,
composed: true
}));
}
}
window.customElements.define('tp-date-picker', TpDatePicker);