tp-date-picker/tp-date-picker.js
2023-01-27 12:45:15 +01:00

361 lines
8.6 KiB
JavaScript

/**
@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 { zonedTimeToUtc } from 'date-fns-tz';
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;
}
`
];
}
render() {
let { month, year, dayNames, value, yearsForward, yearsBackwards } = this;
const today = new Date();
if ((!month && month !== 0) || (!year && year !== 0)) {
month = today.getMonth();
year = today.getFullYear();
}
const years = Array(yearsForward + yearsBackwards).fill().map((_, i) => today.getFullYear()-yearsBackwards+i).reverse();
const curMonth = this.getMonthDates(month, year);
return html`
<div class="header">
<div>
<div>
<tp-icon .icon=${TpDatePicker.iconPrev} @click=${() => this.previousMonth()}></tp-icon>
</div>
<div>${this.monthNames[month]}</div>
<div class="right">
<tp-icon .icon=${TpDatePicker.iconNext} @click=${() => this.nextMonth()}></tp-icon>
</div>
</div>
<div>
<div>
<tp-icon .icon=${TpDatePicker.iconPrev} @click=${() => this.previousYear()}></tp-icon>
</div>
<div class="pointer" @click=${this.showYearOverlay}>${year}</div>
<div class="right">
<tp-icon .icon=${TpDatePicker.iconNext} @click=${() => this.nextYear()}></tp-icon>
</div>
</div>
</div>
<div class="grid" @click=${e => this.selectDate(e)}>
${dayNames.map(label => html`
<div class="day" part="day">${label}</div>
`)}
${curMonth.map(d => {
const matchesValue = this.equalDate(d, value);
const isToday = this.equalDate(d, today);
return html`
<div .date=${d} part="date ${d.getMonth() !== month ? 'filler' : 'of-month'}"><div class="number" part="number ${matchesValue ? 'selected' : ''} ${isToday ? 'today' : ''} ${d.getMonth() === month ? 'of-month' : ''}">${d.getDate()}</div></div>
`
})}
</div>
<div class="year-overlay" ?visible=${this.showYearSelector} @click=${this.selectYear}>
${years.map(y => html`
<div .value=${y}>${y}</div>
`)}
</div>
`;
}
static get properties() {
return {
value: { type: Date },
today: { type: Date },
month: { type: Number },
year: { type: Number },
monthNames: { type: Array },
dayNames: { type: Array },
timeZone: { type: String },
yearsForward: { type: Number },
yearsBackwards: { type: Number },
showYearSelector: { type: Boolean },
};
}
static get iconNext() {
return svg`
<path fill="var(--tp-icon-color)" d="M8.59,16.58L13.17,12L8.59,7.41L10,6L16,12L10,18L8.59,16.58Z" />
`
}
static get iconPrev() {
return svg`
<path fill="var(--tp-icon-color)" d="M15.41,16.58L10.83,12L15.41,7.41L14,6L8,12L14,18L15.41,16.58Z" />
`
}
constructor() {
super();
this.today = new Date();
this.month = this.today.getMonth();
this.year = this.today.getFullYear();
this.yearsForward = 10;
this.yearsBackwards = 100;
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.month = this.value.getMonth();
this.year = this.value.getFullYear();
}
return true;
}
equalDate(d, value) {
if (!value) return false;
return d.getDate() === value.getDate() && d.getFullYear() === value.getFullYear() && d.getMonth() === value.getMonth();
}
selectDate(e) {
for (const el of e.composedPath()) {
if (el.date !== undefined) {
this.value = this.timeZone ? zonedTimeToUtc(el.date, this.timeZone) : 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;
}
nextMonth() {
let nextYear = this.year;
let nextMonth = this.month + 1;
if (nextMonth > 11) {
nextYear += 1;
nextMonth = 0;
}
this.month = nextMonth;
this.year = nextYear;
}
previousYear() {
this.year -= 1;
}
nextYear() {
this.year += 1;
}
getMonthDates(month = null, year = null) {
month = month !== null ? month : new Date().getMonth();
year = year || new Date().getFullYear();
const firstOfMonth = new Date();
firstOfMonth.setFullYear(year);
firstOfMonth.setMonth(month);
firstOfMonth.setDate(1);
firstOfMonth.setHours(0);
firstOfMonth.setMinutes(0);
firstOfMonth.setSeconds(0);
const lastOfMonth = new Date();
lastOfMonth.setDate(1);
lastOfMonth.setFullYear(year);
lastOfMonth.setMonth(month + 1);
lastOfMonth.setDate(lastOfMonth.getDate() - 1);
lastOfMonth.setHours(0);
lastOfMonth.setMinutes(0);
lastOfMonth.setSeconds(0);
const dates = [];
while (firstOfMonth.getDay() > 1) {
firstOfMonth.setDate(firstOfMonth.getDate() - 1);
dates.push(new Date(firstOfMonth));
}
dates.reverse();
for (let i = 1; i <= lastOfMonth.getDate(); ++i) {
const d = new Date();
d.setFullYear(year);
d.setMonth(month);
d.setDate(i);
d.setHours(0);
d.setMinutes(0);
d.setSeconds(0);
dates.push(d);
}
// Make sure we always fill 42 date get guarantee equal size of the date picker.
while (dates.length < 42) {
lastOfMonth.setDate(lastOfMonth.getDate() + 1);
dates.push(new Date(lastOfMonth));
}
return dates;
}
showYearOverlay() {
this.showYearSelector = true;
}
selectYear(e) {
const elList = e.composedPath();
elList.forEach(el => {
if (el.value) {
this.year = el.value;
this.showYearSelector = false;
}
});
}
}
window.customElements.define('tp-date-picker', TpDatePicker);