341 lines
9.0 KiB
JavaScript
341 lines
9.0 KiB
JavaScript
/**
|
|
@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`
|
|
<div class="columns-container" part="columns-container">
|
|
<!-- Hours column -->
|
|
<div
|
|
class="column hours"
|
|
part="hours-column number-column"
|
|
id="hoursColumn">
|
|
${this._generateHours(hourFormat).map(hour => html`
|
|
<div
|
|
class="item ${this.selectedHour === hour ? 'selected' : ''}"
|
|
part="hour-item ${this.selectedHour === hour ? 'hour-item-selected' : ''}"
|
|
data-value="${hour}"
|
|
@click=${() => this._selectHour(hour)}>
|
|
${hour.toString().padStart(2, '0')}
|
|
</div>
|
|
`)}
|
|
</div>
|
|
|
|
<!-- Minutes column -->
|
|
<div
|
|
class="column minutes"
|
|
part="minutes-column number-column"
|
|
id="minutesColumn">
|
|
${this._generateMinutes().map(minute => html`
|
|
<div
|
|
class="item ${this.selectedMinute === minute ? 'selected' : ''}"
|
|
part="minute-item ${this.selectedMinute === minute ? 'minute-item-selected' : ''}"
|
|
data-value="${minute}"
|
|
@click=${() => this._selectMinute(minute)}>
|
|
${minute.toString().padStart(2, '0')}
|
|
</div>
|
|
`)}
|
|
</div>
|
|
|
|
<!-- Period column (AM/PM) -->
|
|
${this.useAmPm ? html`
|
|
<div
|
|
class="column period"
|
|
part="period-column"
|
|
id="periodColumn">
|
|
<div
|
|
class="item ${this.selectedPeriod === 'AM' ? 'selected' : ''}"
|
|
part="period-item ${this.selectedPeriod === 'AM' ? 'period-item-selected' : ''}"
|
|
data-value="AM"
|
|
@click=${() => this._selectPeriod('AM')}>
|
|
AM
|
|
</div>
|
|
<div
|
|
class="item ${this.selectedPeriod === 'PM' ? 'selected' : ''}"
|
|
part="period-item ${this.selectedPeriod === 'PM' ? 'period-item-selected' : ''}"
|
|
data-value="PM"
|
|
@click=${() => this._selectPeriod('PM')}>
|
|
PM
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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); |