361 lines
8.6 KiB
JavaScript
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);
|