First version
This commit is contained in:
360
tp-date-picker.js
Normal file
360
tp-date-picker.js
Normal file
@ -0,0 +1,360 @@
|
||||
/**
|
||||
@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);
|
Reference in New Issue
Block a user