diff --git a/ts_web/elements/dees-form.ts b/ts_web/elements/dees-form.ts index 1a835b3..1e8acf1 100644 --- a/ts_web/elements/dees-form.ts +++ b/ts_web/elements/dees-form.ts @@ -9,6 +9,7 @@ import { import * as domtools from '@design.estate/dees-domtools'; import { DeesInputCheckbox } from './dees-input-checkbox.js'; +import { DeesInputDatepicker } from './dees-input-datepicker.js'; import { DeesInputText } from './dees-input-text.js'; import { DeesInputQuantitySelector } from './dees-input-quantityselector.js'; import { DeesInputRadiogroup } from './dees-input-radiogroup.js'; @@ -25,6 +26,7 @@ import { demoFunc } from './dees-form.demo.js'; // Unified set for form input types const FORM_INPUT_TYPES = [ DeesInputCheckbox, + DeesInputDatepicker, DeesInputDropdown, DeesInputFileupload, DeesInputIban, @@ -39,6 +41,7 @@ const FORM_INPUT_TYPES = [ export type TFormInputElement = | DeesInputCheckbox + | DeesInputDatepicker | DeesInputDropdown | DeesInputFileupload | DeesInputIban diff --git a/ts_web/elements/dees-input-datepicker.demo.ts b/ts_web/elements/dees-input-datepicker.demo.ts new file mode 100644 index 0000000..83853e5 --- /dev/null +++ b/ts_web/elements/dees-input-datepicker.demo.ts @@ -0,0 +1,146 @@ +import { html } from '@design.estate/dees-element'; +import './dees-input-datepicker.js'; + +export const demoFunc = () => html` + + +
+
+
Basic Date Picker
+
Simple date selection without time
+ +
+ +
+
Date and Time Picker
+
Date selection with time in 24-hour format
+ +
+ +
+
12-Hour Time Format
+
Date and time with AM/PM selector
+ +
+ +
+
Date Range Constraints
+
Limit selectable dates with min and max
+ +
+ +
+
Custom Date Format
+
Different date display format
+ +
+ +
+
Required Field
+
Date picker as a required form field
+ +
+ +
+
Disabled State
+
Date picker in disabled state
+ +
+ +
+
Week Starts on Sunday
+
Calendar with Sunday as first day of week
+ +
+ +
+
With Disabled Dates
+
Some dates are disabled and cannot be selected
+ +
+ +
+
Event Listeners
+
Check console for change events
+ +
+
+`; \ No newline at end of file diff --git a/ts_web/elements/dees-input-datepicker.ts b/ts_web/elements/dees-input-datepicker.ts new file mode 100644 index 0000000..059c76b --- /dev/null +++ b/ts_web/elements/dees-input-datepicker.ts @@ -0,0 +1,917 @@ +import { + customElement, + type TemplateResult, + property, + html, + css, + cssManager, + state, +} from '@design.estate/dees-element'; +import { DeesInputBase } from './dees-input-base.js'; +import { demoFunc } from './dees-input-datepicker.demo.js'; +import './dees-icon.js'; +import './dees-label.js'; + +declare global { + interface HTMLElementTagNameMap { + 'dees-input-datepicker': DeesInputDatepicker; + } +} + +@customElement('dees-input-datepicker') +export class DeesInputDatepicker extends DeesInputBase { + public static demo = demoFunc; + + @property({ type: String }) + public value: string = ''; + + @property({ type: Boolean }) + public enableTime: boolean = false; + + @property({ type: String }) + public timeFormat: '24h' | '12h' = '24h'; + + @property({ type: Number }) + public minuteIncrement: number = 1; + + @property({ type: String }) + public dateFormat: string = 'DD/MM/YYYY'; + + @property({ type: String }) + public minDate: string = ''; + + @property({ type: String }) + public maxDate: string = ''; + + @property({ type: Array }) + public disabledDates: string[] = []; + + @property({ type: Number }) + public weekStartsOn: 0 | 1 = 1; // Default to Monday + + @property({ type: String }) + public placeholder: string = 'Select date'; + + @state() + private isOpened: boolean = false; + + @state() + private opensToTop: boolean = false; + + @state() + private selectedDate: Date | null = null; + + @state() + private viewDate: Date = new Date(); + + @state() + private selectedHour: number = 0; + + @state() + private selectedMinute: number = 0; + + public static styles = [ + ...DeesInputBase.baseStyles, + cssManager.defaultStyles, + css` + :host { + display: block; + position: relative; + } + + .input-container { + position: relative; + width: 100%; + } + + .date-input { + width: 100%; + height: 40px; + padding: 0 12px; + background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')}; + border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; + border-radius: 6px; + font-size: 14px; + line-height: 1.5; + color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')}; + cursor: pointer; + transition: all 0.2s ease; + outline: none; + font-family: inherit; + } + + .date-input::placeholder { + color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; + } + + .date-input:hover:not(:disabled) { + border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; + background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')}; + } + + .date-input:focus, + .date-input.open { + border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')}; + outline: 2px solid transparent; + outline-offset: 2px; + box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')}, 0 0 0 4px ${cssManager.bdTheme('hsl(222.2 47.4% 11.2% / 0.1)', 'hsl(210 20% 98% / 0.1)')}; + } + + .date-input:disabled { + background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')}; + color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; + cursor: not-allowed; + opacity: 0.5; + } + + /* Icon container using flexbox for better positioning */ + .icon-container { + position: absolute; + right: 0; + top: 0; + bottom: 0; + display: flex; + align-items: center; + gap: 4px; + padding: 0 12px; + pointer-events: none; + } + + .icon-container > * { + pointer-events: auto; + } + + .calendar-icon { + color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; + pointer-events: none; + display: flex; + align-items: center; + justify-content: center; + } + + .clear-button { + width: 20px; + height: 20px; + border: none; + background: transparent; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; + transition: opacity 0.2s ease, background-color 0.2s ease; + padding: 0; + flex-shrink: 0; + } + + .clear-button:hover { + background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')}; + color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')}; + } + + .clear-button:disabled { + display: none; + } + + /* Calendar Popup Styles */ + .calendar-popup { + will-change: transform, opacity; + pointer-events: none; + transition: all 0.2s ease; + opacity: 0; + transform: translateY(-4px); + background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')}; + border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; + box-shadow: ${cssManager.bdTheme( + '0 10px 15px -3px hsl(0 0% 0% / 0.1), 0 4px 6px -4px hsl(0 0% 0% / 0.1)', + '0 10px 15px -3px hsl(0 0% 0% / 0.2), 0 4px 6px -4px hsl(0 0% 0% / 0.2)' + )}; + border-radius: 6px; + padding: 12px; + position: absolute; + user-select: none; + margin-top: 4px; + z-index: 50; + left: 0; + min-width: 280px; + } + + .calendar-popup.top { + bottom: calc(100% + 4px); + top: auto; + margin-top: 0; + margin-bottom: 4px; + transform: translateY(4px); + } + + .calendar-popup.bottom { + top: 100%; + } + + .calendar-popup.show { + pointer-events: all; + transform: translateY(0); + opacity: 1; + } + + /* Calendar Header */ + .calendar-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; + gap: 8px; + } + + .month-year-display { + font-weight: 500; + font-size: 14px; + color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')}; + flex: 1; + text-align: center; + } + + .nav-button { + width: 28px; + height: 28px; + border: none; + background: transparent; + cursor: pointer; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; + transition: all 0.2s ease; + } + + .nav-button:hover { + background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')}; + color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')}; + } + + .nav-button:active { + background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; + } + + /* Weekday headers */ + .weekdays { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 0; + margin-bottom: 4px; + } + + .weekday { + text-align: center; + font-size: 12px; + font-weight: 400; + color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; + padding: 0 0 8px 0; + } + + /* Days grid */ + .days-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 2px; + } + + .day { + aspect-ratio: 1; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border-radius: 6px; + font-size: 14px; + transition: all 0.2s ease; + color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')}; + border: none; + width: 36px; + height: 36px; + background: transparent; + } + + .day:hover:not(.disabled) { + background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')}; + } + + .day.other-month { + color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; + opacity: 0.5; + } + + .day.today { + background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')}; + font-weight: 500; + } + + .day.selected { + background: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')}; + color: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(222.2 47.4% 11.2%)')}; + font-weight: 500; + } + + .day.disabled { + color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; + cursor: not-allowed; + opacity: 0.3; + } + + /* Time selector */ + .time-selector { + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; + } + + .time-selector-title { + font-size: 12px; + font-weight: 500; + margin-bottom: 8px; + color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; + } + + .time-inputs { + display: flex; + gap: 8px; + align-items: center; + } + + .time-input { + width: 65px; + height: 36px; + border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; + border-radius: 6px; + padding: 0 12px; + font-size: 14px; + text-align: center; + background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')}; + color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')}; + transition: all 0.2s ease; + } + + .time-input:hover { + border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; + background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')}; + } + + .time-input:focus { + outline: none; + border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')}; + box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(222.2 47.4% 11.2% / 0.1)', 'hsl(210 20% 98% / 0.1)')}; + } + + .time-separator { + font-size: 14px; + font-weight: 500; + color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; + } + + .am-pm-selector { + display: flex; + gap: 4px; + margin-left: 8px; + } + + .am-pm-button { + padding: 6px 12px; + border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; + background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')}; + border-radius: 6px; + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; + } + + .am-pm-button.selected { + background: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')}; + color: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(222.2 47.4% 11.2%)')}; + border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')}; + } + + .am-pm-button:hover:not(.selected) { + background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')}; + border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; + } + + /* Action buttons */ + .calendar-actions { + display: flex; + gap: 8px; + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; + } + + .action-button { + flex: 1; + height: 36px; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + } + + .today-button { + background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')}; + border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; + color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')}; + } + + .today-button:hover { + background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')}; + border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; + } + + .today-button:active { + background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; + } + + .clear-button { + background: transparent; + border: 1px solid transparent; + color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; + } + + .clear-button:hover { + background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.1)', 'hsl(0 62.8% 30.6% / 0.1)')}; + color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')}; + } + + .clear-button:active { + background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.2)', 'hsl(0 62.8% 30.6% / 0.2)')}; + } + `, + ]; + + render(): TemplateResult { + const monthNames = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' + ]; + + const weekDays = this.weekStartsOn === 1 + ? ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'] + : ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']; + + const days = this.getDaysInMonth(); + const isAM = this.selectedHour < 12; + + return html` +
+ +
+ +
+ ${this.value && !this.disabled ? html` + + ` : ''} + +
+ + +
+ +
+ +
+ ${monthNames[this.viewDate.getMonth()]} ${this.viewDate.getFullYear()} +
+ +
+ + +
+ ${weekDays.map(day => html`
${day}
`)} +
+ + +
+ ${days.map(day => { + const isToday = this.isToday(day); + const isSelected = this.isSelected(day); + const isOtherMonth = day.getMonth() !== this.viewDate.getMonth(); + const isDisabled = this.isDisabled(day); + + return html` +
+ ${day.getDate()} +
+ `; + })} +
+ + + ${this.enableTime ? html` +
+
Time
+
+ + : + + ${this.timeFormat === '12h' ? html` +
+ + +
+ ` : ''} +
+
+ ` : ''} + + +
+ + +
+
+
+
+ `; + } + + async connectedCallback() { + super.connectedCallback(); + this.handleClickOutside = this.handleClickOutside.bind(this); + } + + async disconnectedCallback() { + await super.disconnectedCallback(); + document.removeEventListener('click', this.handleClickOutside); + } + + async firstUpdated() { + // Initialize with empty value if not set + if (!this.value) { + this.value = ''; + } + + // Initialize view date and selected time + if (this.value) { + try { + const date = new Date(this.value); + if (!isNaN(date.getTime())) { + this.selectedDate = date; + this.viewDate = new Date(date); + this.selectedHour = date.getHours(); + this.selectedMinute = date.getMinutes(); + } + } catch { + // Invalid date + } + } else { + const now = new Date(); + this.viewDate = new Date(now); + this.selectedHour = now.getHours(); + this.selectedMinute = 0; + } + } + + private formatDate(isoString: string): string { + if (!isoString) return ''; + + try { + const date = new Date(isoString); + if (isNaN(date.getTime())) return ''; + + let formatted = this.dateFormat; + + // Basic date formatting + const day = date.getDate().toString().padStart(2, '0'); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const year = date.getFullYear().toString(); + + formatted = formatted.replace('DD', day); + formatted = formatted.replace('MM', month); + formatted = formatted.replace('YYYY', year); + formatted = formatted.replace('YY', year.slice(-2)); + + // Time formatting if enabled + if (this.enableTime) { + const hours24 = date.getHours(); + const hours12 = hours24 === 0 ? 12 : hours24 > 12 ? hours24 - 12 : hours24; + const minutes = date.getMinutes().toString().padStart(2, '0'); + const ampm = hours24 >= 12 ? 'PM' : 'AM'; + + if (this.timeFormat === '12h') { + formatted += ` ${hours12}:${minutes} ${ampm}`; + } else { + formatted += ` ${hours24.toString().padStart(2, '0')}:${minutes}`; + } + } + + return formatted; + } catch { + return ''; + } + } + + private handleClickOutside = (event: MouseEvent) => { + const path = event.composedPath(); + if (!path.includes(this)) { + this.isOpened = false; + document.removeEventListener('click', this.handleClickOutside); + } + }; + + private async toggleCalendar(): Promise { + if (this.disabled) return; + + this.isOpened = !this.isOpened; + + if (this.isOpened) { + // Check available space and set position + const inputContainer = this.shadowRoot!.querySelector('.input-container') as HTMLElement; + const rect = inputContainer.getBoundingClientRect(); + const spaceBelow = window.innerHeight - rect.bottom; + const spaceAbove = rect.top; + + // Determine if we should open upwards (approximate height of 400px) + this.opensToTop = spaceBelow < 400 && spaceAbove > spaceBelow; + + // Add click outside listener + setTimeout(() => { + document.addEventListener('click', this.handleClickOutside); + }, 0); + } else { + document.removeEventListener('click', this.handleClickOutside); + } + } + + private getDaysInMonth(): Date[] { + const year = this.viewDate.getFullYear(); + const month = this.viewDate.getMonth(); + const firstDay = new Date(year, month, 1); + const lastDay = new Date(year, month + 1, 0); + const days: Date[] = []; + + // Adjust for week start + const startOffset = this.weekStartsOn === 1 + ? (firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1) + : firstDay.getDay(); + + // Add days from previous month + for (let i = startOffset; i > 0; i--) { + days.push(new Date(year, month, 1 - i)); + } + + // Add days of current month + for (let i = 1; i <= lastDay.getDate(); i++) { + days.push(new Date(year, month, i)); + } + + // Add days from next month to complete the grid (6 rows) + const remainingDays = 42 - days.length; + for (let i = 1; i <= remainingDays; i++) { + days.push(new Date(year, month + 1, i)); + } + + return days; + } + + private isToday(date: Date): boolean { + const today = new Date(); + return date.getDate() === today.getDate() && + date.getMonth() === today.getMonth() && + date.getFullYear() === today.getFullYear(); + } + + private isSelected(date: Date): boolean { + if (!this.selectedDate) return false; + return date.getDate() === this.selectedDate.getDate() && + date.getMonth() === this.selectedDate.getMonth() && + date.getFullYear() === this.selectedDate.getFullYear(); + } + + private isDisabled(date: Date): boolean { + // Check min date + if (this.minDate) { + const min = new Date(this.minDate); + if (date < min) return true; + } + + // Check max date + if (this.maxDate) { + const max = new Date(this.maxDate); + if (date > max) return true; + } + + // Check disabled dates + if (this.disabledDates && this.disabledDates.length > 0) { + return this.disabledDates.some(disabledStr => { + try { + const disabled = new Date(disabledStr); + return date.getDate() === disabled.getDate() && + date.getMonth() === disabled.getMonth() && + date.getFullYear() === disabled.getFullYear(); + } catch { + return false; + } + }); + } + + return false; + } + + private selectDate(date: Date): void { + this.selectedDate = new Date( + date.getFullYear(), + date.getMonth(), + date.getDate(), + this.selectedHour, + this.selectedMinute + ); + + this.value = this.selectedDate.toISOString(); + this.changeSubject.next(this); + + if (!this.enableTime) { + this.isOpened = false; + } + } + + private selectToday(): void { + const today = new Date(); + this.selectedDate = today; + this.viewDate = new Date(today); + this.selectedHour = today.getHours(); + this.selectedMinute = today.getMinutes(); + + this.value = this.selectedDate.toISOString(); + this.changeSubject.next(this); + + if (!this.enableTime) { + this.isOpened = false; + } + } + + private clear(): void { + this.value = ''; + this.selectedDate = null; + this.changeSubject.next(this); + this.isOpened = false; + } + + private previousMonth(): void { + this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() - 1, 1); + } + + private nextMonth(): void { + this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() + 1, 1); + } + + private handleHourInput(e: InputEvent): void { + const input = e.target as HTMLInputElement; + let value = parseInt(input.value) || 0; + + if (this.timeFormat === '12h') { + value = Math.max(1, Math.min(12, value)); + // Convert to 24h format + if (this.selectedHour >= 12 && value !== 12) { + this.selectedHour = value + 12; + } else if (this.selectedHour < 12 && value === 12) { + this.selectedHour = 0; + } else { + this.selectedHour = value; + } + } else { + this.selectedHour = Math.max(0, Math.min(23, value)); + } + + this.updateSelectedDateTime(); + } + + private handleMinuteInput(e: InputEvent): void { + const input = e.target as HTMLInputElement; + let value = parseInt(input.value) || 0; + value = Math.max(0, Math.min(59, value)); + + if (this.minuteIncrement && this.minuteIncrement > 1) { + value = Math.round(value / this.minuteIncrement) * this.minuteIncrement; + } + + this.selectedMinute = value; + this.updateSelectedDateTime(); + } + + private setAMPM(period: 'am' | 'pm'): void { + if (period === 'am' && this.selectedHour >= 12) { + this.selectedHour -= 12; + } else if (period === 'pm' && this.selectedHour < 12) { + this.selectedHour += 12; + } + this.updateSelectedDateTime(); + } + + private updateSelectedDateTime(): void { + if (this.selectedDate) { + this.selectedDate = new Date( + this.selectedDate.getFullYear(), + this.selectedDate.getMonth(), + this.selectedDate.getDate(), + this.selectedHour, + this.selectedMinute + ); + this.value = this.selectedDate.toISOString(); + this.changeSubject.next(this); + } + } + + private handleKeydown(e: KeyboardEvent): void { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + this.toggleCalendar(); + } else if (e.key === 'Escape' && this.isOpened) { + e.preventDefault(); + this.isOpened = false; + } + } + + private clearValue(e: Event): void { + e.stopPropagation(); + this.value = ''; + this.selectedDate = null; + this.changeSubject.next(this); + } + + public getValue(): string { + return this.value; + } + + public setValue(value: string): void { + this.value = value; + if (value) { + try { + const date = new Date(value); + if (!isNaN(date.getTime())) { + this.selectedDate = date; + this.viewDate = new Date(date); + this.selectedHour = date.getHours(); + this.selectedMinute = date.getMinutes(); + } + } catch { + // Invalid date + } + } + } +} \ No newline at end of file diff --git a/ts_web/elements/index.ts b/ts_web/elements/index.ts index cdf29e5..6357b96 100644 --- a/ts_web/elements/index.ts +++ b/ts_web/elements/index.ts @@ -28,6 +28,7 @@ export * from './dees-heading.js'; export * from './dees-hint.js'; export * from './dees-icon.js'; export * from './dees-input-checkbox.js'; +export * from './dees-input-datepicker.js'; export * from './dees-input-dropdown.js'; export * from './dees-input-fileupload.js'; export * from './dees-input-iban.js'; diff --git a/ts_web/pages/input-showcase.ts b/ts_web/pages/input-showcase.ts index f6387ed..85f0561 100644 --- a/ts_web/pages/input-showcase.ts +++ b/ts_web/pages/input-showcase.ts @@ -443,6 +443,32 @@ export const inputShowcase = () => html` Specialized input components for specific data types like phone numbers, IBAN, and file uploads.

+ +
+ + + + + +
+
+