/**
@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);