/** @license Copyright (c) 2025 trading_peter This program is available under Apache License Version 2.0 */ import { LitElement, html, css } from 'lit'; import { FormElement } from '@tp/helpers/form-element.js'; class TpTimeSelector extends FormElement(LitElement) { static get styles() { return [ css` :host { display: block; position: relative; width: 100%; font-family: inherit; font-size: inherit; box-sizing: border-box; overflow: hidden; user-select: none; } .columns-container { display: flex; width: 100%; height: 100%; align-items: stretch; } .column { flex: 1; overflow-y: auto; overflow-x: hidden; position: relative; scrollbar-width: none; /* Firefox */ -ms-overflow-style: none; /* IE and Edge */ border-right: 1px solid var(--tp-time-selector-column-border-color, #e0e0e0); } .column:last-child { border-right: none; } /* Hide scrollbar for Chrome, Safari and Opera */ .column::-webkit-scrollbar { display: none; } .item { padding: 8px; text-align: center; cursor: pointer; box-sizing: border-box; display: flex; align-items: center; justify-content: center; height: var(--tp-time-selector-item-height, 36px); } .item:hover { background-color: var(--tp-time-selector-item-hover-bg, #f5f5f5); } .item.selected { background-color: var(--tp-time-selector-item-selected-bg, #e6f7ff); color: var(--tp-time-selector-item-selected-color, #007bff); } :host([disabled]) { opacity: 0.6; pointer-events: none; } ` ]; } render() { const hourFormat = this.useAmPm ? 12 : 24; return html`
${this._generateHours(hourFormat).map(hour => html`
this._selectHour(hour)}> ${hour.toString().padStart(2, '0')}
`)}
${this._generateMinutes().map(minute => html`
this._selectMinute(minute)}> ${minute.toString().padStart(2, '0')}
`)}
${this.useAmPm ? html`
this._selectPeriod('AM')}> AM
this._selectPeriod('PM')}> PM
` : ''}
`; } static get properties() { return { value: { type: String, reflect: true }, minuteStep: { type: Number }, useAmPm: { type: Boolean }, selectedHour: { type: Number, state: true }, selectedMinute: { type: Number, state: true }, selectedPeriod: { type: String, state: true }, disabled: { type: Boolean, reflect: true } }; } constructor() { super(); this.value = ''; this.minuteStep = 1; this.useAmPm = false; this.selectedHour = 0; this.selectedMinute = 0; this.selectedPeriod = 'AM'; this.disabled = false; } firstUpdated() { // Ensure initial scroll positions after the component is in the DOM this.updateComplete.then(() => { this._scrollToSelectedItems(); }); } updated(changedProperties) { if (changedProperties.has('value')) { this._parseValue(); if (this.isConnected) { this.updateComplete.then(() => { this._scrollToSelectedItems(); }); } } } _parseValue() { if (!this.value) { this.selectedHour = 0; this.selectedMinute = 0; this.selectedPeriod = 'AM'; return; } const [hours, minutes] = this.value.split(':'); const hoursInt = parseInt(hours, 10); if (this.useAmPm) { // Convert 24h to 12h format if (hoursInt === 0) { this.selectedHour = 12; this.selectedPeriod = 'AM'; } else if (hoursInt === 12) { this.selectedHour = 12; this.selectedPeriod = 'PM'; } else if (hoursInt > 12) { this.selectedHour = hoursInt - 12; this.selectedPeriod = 'PM'; } else { this.selectedHour = hoursInt; this.selectedPeriod = 'AM'; } } else { this.selectedHour = hoursInt; } this.selectedMinute = parseInt(minutes, 10); // Round to nearest step this.selectedMinute = Math.round(this.selectedMinute / this.minuteStep) * this.minuteStep; } _generateHours(format) { const hours = []; const start = format === 12 ? 1 : 0; const end = format === 12 ? 12 : 23; for (let i = start; i <= end; i++) { hours.push(i); } return hours; } _generateMinutes() { const minutes = []; for (let i = 0; i < 60; i += this.minuteStep) { minutes.push(i); } return minutes; } _selectHour(hour) { this.selectedHour = hour; this._updateValue(); } _selectMinute(minute) { this.selectedMinute = minute; this._updateValue(); } _selectPeriod(period) { this.selectedPeriod = period; this._updateValue(); } _updateValue() { let hours = this.selectedHour; // Convert to 24-hour format if using AM/PM if (this.useAmPm) { if (hours === 12) { hours = this.selectedPeriod === 'AM' ? 0 : 12; } else if (this.selectedPeriod === 'PM') { hours += 12; } } const formattedHours = hours.toString().padStart(2, '0'); const formattedMinutes = this.selectedMinute.toString().padStart(2, '0'); const newValue = `${formattedHours}:${formattedMinutes}`; if (this.value !== newValue) { this.value = newValue; this.dispatchEvent(new CustomEvent('change', { detail: { value: newValue, hours: hours, minutes: this.selectedMinute, displayHours: this.selectedHour, period: this.useAmPm ? this.selectedPeriod : null }, bubbles: true, composed: true })); } } _scrollToSelectedItems() { // Get references to each column const hoursColumn = this.shadowRoot.querySelector('#hoursColumn'); const minutesColumn = this.shadowRoot.querySelector('#minutesColumn'); const periodColumn = this.useAmPm ? this.shadowRoot.querySelector('#periodColumn') : null; if (!hoursColumn || !minutesColumn) return; // Find the selected items const hourItem = hoursColumn.querySelector('.item.selected'); const minuteItem = minutesColumn.querySelector('.item.selected'); // Scroll to selected items if they exist if (hourItem) { hourItem.scrollIntoView({ block: 'center' }); } if (minuteItem) { minuteItem.scrollIntoView({ block: 'center' }); } if (this.useAmPm && periodColumn) { const periodItem = periodColumn.querySelector('.item.selected'); if (periodItem) { periodItem.scrollIntoView({ block: 'center' }); } } } /** * Implement the validate method required by FormElement */ validate() { const isEmpty = !this.value; // If it has a value, it's valid (since we control the format) // If it's required, it must have a value const valid = !this.required || !isEmpty; this.invalid = !valid; return valid; } /** * Reset to initial state */ reset() { this.value = ''; this.selectedHour = 0; this.selectedMinute = 0; this.selectedPeriod = 'AM'; } } customElements.define('tp-time-selector', TpTimeSelector);