/** @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`
this.previousMonth()}>
${this.monthNames[month]}
this.nextMonth()}>
this.previousYear()}>
${year}
this.nextYear()}>
this.selectDate(e)}> ${dayNames.map(label => html`
${label}
`)} ${curMonth.map(d => { const matchesValue = this.equalDate(d, value); const isToday = this.equalDate(d, today); return html`
${d.getDate()}
` })}
${years.map(y => html`
${y}
`)}
`; } 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` ` } static get iconPrev() { return svg` ` } 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);