Compare commits

...

11 Commits

Author SHA1 Message Date
pk 093ab1372d Bump version 2026-04-07 11:01:38 +02:00
pk c66db8cd86 Returned date should always in UTC. Timezone is only for display. 2026-04-07 11:01:25 +02:00
pk 1df7db4303 Update date 2025-11-20 15:18:27 +01:00
pk 9b793d47d8 Migrate to luxon. Fixes wrong weekday to date bug as well. 2025-06-23 10:33:37 +02:00
pk 7ddca43a01 Add range change event 2025-05-02 00:13:40 +02:00
pk 00a36e0b35 Add part definition for navigation icons 2025-02-06 22:29:02 +01:00
pk fd011d239f Add support for marking calendar events as dots on the day circles. 2025-01-13 00:37:09 +01:00
pk f4505cc916 Define yearly-overlay part for styling access 2025-01-07 23:32:48 +01:00
pk 05af1b6474 Bump version 2025-01-02 22:28:24 +01:00
pk bc54fabe96 Break the loop if a value was found when clicking an entry in the year selector.
Otherwise it can pick up the value of the date picker itself.
2025-01-02 22:28:12 +01:00
pk bb638c7e8a Make the datepicker UTC only and use the UTC helper package by date-fns 2025-01-02 22:15:04 +01:00
3 changed files with 212 additions and 65 deletions
+86
View File
@@ -0,0 +1,86 @@
{
"name": "@tp/tp-date-picker",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@tp/tp-date-picker",
"version": "1.0.0",
"license": "Apache-2.0",
"dependencies": {
"@date-fns/utc": "^2.1.0",
"date-fns": "^4.0.0",
"lit": "^2.2.0"
}
},
"node_modules/@date-fns/utc": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@date-fns/utc/-/utc-2.1.0.tgz",
"integrity": "sha512-176grgAgU2U303rD2/vcOmNg0kGPbhzckuH1TEP2al7n0AQipZIy9P15usd2TKQCG1g+E1jX/ZVQSzs4sUDwgA==",
"license": "MIT"
},
"node_modules/@lit-labs/ssr-dom-shim": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz",
"integrity": "sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ==",
"license": "BSD-3-Clause"
},
"node_modules/@lit/reactive-element": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz",
"integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.0.0"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT"
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/lit": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz",
"integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==",
"license": "BSD-3-Clause",
"dependencies": {
"@lit/reactive-element": "^1.6.0",
"lit-element": "^3.3.0",
"lit-html": "^2.8.0"
}
},
"node_modules/lit-element": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz",
"integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==",
"license": "BSD-3-Clause",
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.1.0",
"@lit/reactive-element": "^1.3.0",
"lit-html": "^2.8.0"
}
},
"node_modules/lit-html": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz",
"integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==",
"license": "BSD-3-Clause",
"dependencies": {
"@types/trusted-types": "^2.0.2"
}
}
}
}
+2 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@tp/tp-date-picker", "name": "@tp/tp-date-picker",
"version": "1.0.0", "version": "3.0.1",
"description": "", "description": "",
"main": "tp-date-picker.js", "main": "tp-date-picker.js",
"scripts": { "scripts": {
@@ -13,8 +13,7 @@
"author": "trading_peter", "author": "trading_peter",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"date-fns": "^2.28.0", "luxon": "^3.6.1",
"date-fns-tz": "^1.3.3",
"lit": "^2.2.0" "lit": "^2.2.0"
} }
} }
+124 -62
View File
@@ -1,13 +1,13 @@
/** /**
@license @license
Copyright (c) 2022 trading_peter Copyright (c) 2025 trading_peter
This program is available under Apache License Version 2.0 This program is available under Apache License Version 2.0
*/ */
import '@tp/tp-icon/tp-icon.js'; import '@tp/tp-icon/tp-icon.js';
import { FormElement } from '@tp/helpers/form-element'; import { FormElement } from '@tp/helpers/form-element';
import { LitElement, html, css, svg } from 'lit'; import { LitElement, html, css, svg } from 'lit';
import { zonedTimeToUtc } from 'date-fns-tz'; import { DateTime } from 'luxon';
class TpDatePicker extends FormElement(LitElement) { class TpDatePicker extends FormElement(LitElement) {
static get styles() { static get styles() {
@@ -114,6 +114,27 @@ class TpDatePicker extends FormElement(LitElement) {
.pointer { .pointer {
cursor: 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;
}
` `
]; ];
} }
@@ -121,34 +142,37 @@ class TpDatePicker extends FormElement(LitElement) {
render() { render() {
let { month, year, dayNames, value, yearsForward, yearsBackwards } = this; let { month, year, dayNames, value, yearsForward, yearsBackwards } = this;
const today = new Date(); const today = DateTime.utc();
if ((!month && month !== 0) || (!year && year !== 0)) { if ((!month && month !== 0) || (!year && year !== 0)) {
month = today.getMonth(); month = today.month - 1; // Luxon uses 1-based months, convert to 0-based for compatibility
year = today.getFullYear(); year = today.year;
} }
const years = Array(yearsForward + yearsBackwards).fill().map((_, i) => today.getFullYear()-yearsBackwards+i).reverse(); const years = Array(yearsForward + yearsBackwards)
.fill()
.map((_, i) => today.year - yearsBackwards + i)
.reverse();
const curMonth = this.getMonthDates(month, year); const curMonth = this.getMonthDates(month, year);
return html` return html`
<div class="header"> <div class="header">
<div> <div>
<div> <div>
<tp-icon .icon=${TpDatePicker.iconPrev} @click=${() => this.previousMonth()}></tp-icon> <tp-icon part="nav-icon" .icon=${TpDatePicker.iconPrev} @click=${() => this.previousMonth()}></tp-icon>
</div> </div>
<div>${this.monthNames[month]}</div> <div>${this.monthNames[month]}</div>
<div class="right"> <div class="right">
<tp-icon .icon=${TpDatePicker.iconNext} @click=${() => this.nextMonth()}></tp-icon> <tp-icon part="nav-icon" .icon=${TpDatePicker.iconNext} @click=${() => this.nextMonth()}></tp-icon>
</div> </div>
</div> </div>
<div> <div>
<div> <div>
<tp-icon .icon=${TpDatePicker.iconPrev} @click=${() => this.previousYear()}></tp-icon> <tp-icon part="nav-icon" .icon=${TpDatePicker.iconPrev} @click=${() => this.previousYear()}></tp-icon>
</div> </div>
<div class="pointer" @click=${this.showYearOverlay}>${year}</div> <div class="pointer" @click=${this.showYearOverlay}>${year}</div>
<div class="right"> <div class="right">
<tp-icon .icon=${TpDatePicker.iconNext} @click=${() => this.nextYear()}></tp-icon> <tp-icon part="nav-icon" .icon=${TpDatePicker.iconNext} @click=${() => this.nextYear()}></tp-icon>
</div> </div>
</div> </div>
</div> </div>
@@ -159,12 +183,24 @@ class TpDatePicker extends FormElement(LitElement) {
${curMonth.map(d => { ${curMonth.map(d => {
const matchesValue = this.equalDate(d, value); const matchesValue = this.equalDate(d, value);
const isToday = this.equalDate(d, today); const isToday = this.equalDate(d, today);
const eventCount = this.countEventsForDate(d);
return html` 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 .date=${d} part="date ${d.month - 1 !== month ? 'filler' : 'of-month'}">
<div class="number" part="number ${matchesValue ? 'selected' : ''} ${isToday ? 'today' : ''} ${d.month - 1 === month ? 'of-month' : ''}">
${d.day}
${eventCount > 0 ? html`
<div class="event-dots">
<div class="event-dot" part="event-dot"></div>
${eventCount > 1 ? html`<div class="event-dot" part="event-dot"></div>` : ''}
</div>
` : ''}
</div>
</div>
` `
})} })}
</div> </div>
<div class="year-overlay" ?visible=${this.showYearSelector} @click=${this.selectYear}> <div class="year-overlay" part="year-overlay" ?visible=${this.showYearSelector} @click=${this.selectYear}>
${years.map(y => html` ${years.map(y => html`
<div .value=${y}>${y}</div> <div .value=${y}>${y}</div>
`)} `)}
@@ -174,16 +210,17 @@ class TpDatePicker extends FormElement(LitElement) {
static get properties() { static get properties() {
return { return {
value: { type: Date }, value: { type: Object }, // Changed from Date to Luxon DateTime
today: { type: Date }, today: { type: Object }, // Changed from Date to Luxon DateTime
month: { type: Number }, month: { type: Number },
year: { type: Number }, year: { type: Number },
monthNames: { type: Array }, monthNames: { type: Array },
dayNames: { type: Array }, dayNames: { type: Array },
timeZone: { type: String },
yearsForward: { type: Number }, yearsForward: { type: Number },
yearsBackwards: { type: Number }, yearsBackwards: { type: Number },
showYearSelector: { type: Boolean }, showYearSelector: { type: Boolean },
events: { type: Array },
timeZone: { type: String }, // User's timezone for date interpretation
}; };
} }
@@ -202,11 +239,12 @@ class TpDatePicker extends FormElement(LitElement) {
constructor() { constructor() {
super(); super();
this.today = new Date(); this.today = DateTime.utc();
this.month = this.today.getMonth(); this.month = this.today.month - 1; // Convert 1-based to 0-based month
this.year = this.today.getFullYear(); this.year = this.today.year;
this.yearsForward = 10; this.yearsForward = 10;
this.yearsBackwards = 100; this.yearsBackwards = 100;
this.events = [];
this.dayNames = [ this.dayNames = [
'Mo', 'Mo',
@@ -235,24 +273,33 @@ class TpDatePicker extends FormElement(LitElement) {
} }
shouldUpdate(changes) { shouldUpdate(changes) {
if (changes.has('value')) { if (changes.has('value') && this.value) {
this.month = this.value.getMonth(); this.month = this.value.month - 1; // Convert 1-based to 0-based month
this.year = this.value.getFullYear(); this.year = this.value.year;
} }
return true; return true;
} }
equalDate(d, value) { equalDate(d1, d2) {
if (!value) return false; if (!d1 || !d2) return false;
return d.getDate() === value.getDate() && d.getFullYear() === value.getFullYear() && d.getMonth() === value.getMonth(); return d1.day === d2.day && d1.year === d2.year && d1.month === d2.month;
} }
selectDate(e) { selectDate(e) {
for (const el of e.composedPath()) { for (const el of e.composedPath()) {
if (el.date !== undefined) { if (el.date !== undefined) {
this.value = this.timeZone ? zonedTimeToUtc(el.date, this.timeZone) : el.date; this.value = el.date;
this.dispatchEvent(new CustomEvent('value-changed', { detail: this.value, bubbles: true, composed: true })); // Interpret the selected date as being in the user's timezone, output as UTC ISO string
const selectedDate = el.date;
let outputValue;
const zone = this.timeZone || 'local';
const localDate = DateTime.fromObject(
{ year: selectedDate.year, month: selectedDate.month, day: selectedDate.day },
{ zone }
);
outputValue = localDate.toUTC().toISO();
this.dispatchEvent(new CustomEvent('value-changed', { detail: outputValue, bubbles: true, composed: true }));
return; return;
} }
} }
@@ -269,6 +316,7 @@ class TpDatePicker extends FormElement(LitElement) {
this.month = prevMonth; this.month = prevMonth;
this.year = prevYear; this.year = prevYear;
this.dispatchRangeChangeEvent();
} }
nextMonth() { nextMonth() {
@@ -282,61 +330,50 @@ class TpDatePicker extends FormElement(LitElement) {
this.month = nextMonth; this.month = nextMonth;
this.year = nextYear; this.year = nextYear;
this.dispatchRangeChangeEvent();
} }
previousYear() { previousYear() {
this.year -= 1; this.year -= 1;
this.dispatchRangeChangeEvent();
} }
nextYear() { nextYear() {
this.year += 1; this.year += 1;
this.dispatchRangeChangeEvent();
} }
getMonthDates(month = null, year = null) { getMonthDates(month = null, year = null) {
month = month !== null ? month : new Date().getMonth(); month = month !== null ? month : DateTime.utc().month - 1; // Convert 1-based to 0-based
year = year || new Date().getFullYear(); year = year || DateTime.utc().year;
const firstOfMonth = new Date(); // Create DateTime for the first of the month (convert 0-based month back to 1-based)
firstOfMonth.setFullYear(year); const firstOfMonth = DateTime.utc(year, month + 1, 1, 0, 0, 0);
firstOfMonth.setMonth(month);
firstOfMonth.setDate(1); // Create DateTime for the last of the month
firstOfMonth.setHours(0); const lastOfMonth = firstOfMonth.endOf('month').startOf('day');
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 = []; const dates = [];
while (firstOfMonth.getDay() > 1) { // Calculate dates before the first of month to fill the calendar
firstOfMonth.setDate(firstOfMonth.getDate() - 1); let firstDayOfCalendar = firstOfMonth;
dates.push(new Date(firstOfMonth)); while (firstDayOfCalendar.weekday > 1) { // 1 is Monday in Luxon
firstDayOfCalendar = firstDayOfCalendar.minus({ days: 1 });
dates.push(firstDayOfCalendar);
} }
dates.reverse(); dates.reverse();
for (let i = 1; i <= lastOfMonth.getDate(); ++i) { // Add all days of the current month
const d = new Date(); for (let i = 1; i <= lastOfMonth.day; i++) {
d.setFullYear(year); dates.push(DateTime.utc(year, month + 1, i, 0, 0, 0));
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. // Make sure we always fill 42 dates to guarantee equal size of the date picker
let nextDate = lastOfMonth;
while (dates.length < 42) { while (dates.length < 42) {
lastOfMonth.setDate(lastOfMonth.getDate() + 1); nextDate = nextDate.plus({ days: 1 });
dates.push(new Date(lastOfMonth)); dates.push(nextDate);
} }
return dates; return dates;
@@ -348,12 +385,37 @@ class TpDatePicker extends FormElement(LitElement) {
selectYear(e) { selectYear(e) {
const elList = e.composedPath(); const elList = e.composedPath();
elList.forEach(el => { for (const el of elList) {
if (el.value) { if (el.value) {
this.year = el.value; this.year = el.value;
this.showYearSelector = false; 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
}));
} }
} }