@nuralyui/timepicker
Version:
NuralyUI TimePicker - A comprehensive time selection component with clock interface, multiple formats, and validation
747 lines • 29.6 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { LitElement, html, nothing } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
// Import UI components
import '../input/input.component.js';
import '../button/button.component.js';
// Import base mixin and types
import { NuralyUIBaseMixin } from '../../shared/base-mixin.js';
import { SharedDropdownController } from '../../shared/controllers/dropdown.controller.js';
import { TimeFormat, TimePickerState, TimePickerPlacement, TimePeriod, TimeStep, EMPTY_TIME_VALUE, TIME_PICKER_EVENTS, } from './timepicker.types.js';
// Import controllers
import { TimePickerSelectionController } from './controllers/selection.controller.js';
import { TimePickerValidationController } from './controllers/validation.controller.js';
import { TimePickerFormattingController } from './controllers/formatting.controller.js';
// Import utilities
import { TimeUtils } from './utils/time.utils.js';
// Import styles
import { styles as timePickerStyles } from './timepicker.style.js';
/**
* A comprehensive time picker component that supports both 12-hour and 24-hour formats,
* with optional seconds display and extensive customization options.
*
* @example
* ```html
* <nr-timepicker
* value="14:30:00"
* format="24h"
* show-seconds
* placeholder="Select time">
* </nr-timepicker>
* ```
*/
let NrTimePickerElement = class NrTimePickerElement extends NuralyUIBaseMixin(LitElement) {
constructor() {
super();
// Properties
this.value = '';
this.name = '';
this.placeholder = 'Select time';
this.format = TimeFormat.TwentyFourHour;
this.showSeconds = false;
this.disabled = false;
this.readonly = false;
this.required = false;
this.helperText = '';
this.label = '';
this.size = 'medium';
this.variant = 'outlined';
this.placement = TimePickerPlacement.Bottom;
/** Scroll behavior for dropdown navigation - 'instant' for immediate, 'smooth' for animated, 'auto' for browser default */
this.scrollBehavior = 'instant';
// State
this.inputValue = '';
this.state = TimePickerState.Default;
this.validationMessage = '';
// Controllers
this.dropdownController = new SharedDropdownController(this);
this.selectionController = new TimePickerSelectionController(this);
this.validationController = new TimePickerValidationController(this);
this.formattingController = new TimePickerFormattingController(this);
}
connectedCallback() {
super.connectedCallback();
this.updateConstraints();
if (this.value) {
this.setTimeFromValue(this.value);
}
// Add global click listener to close dropdown when clicking outside
this.addEventListener('click', this.handleComponentClick.bind(this));
document.addEventListener('click', this.handleDocumentClick.bind(this));
}
disconnectedCallback() {
super.disconnectedCallback();
// Clean up global event listeners
document.removeEventListener('click', this.handleDocumentClick.bind(this));
}
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('value') && this.value !== this.inputValue) {
this.setTimeFromValue(this.value);
// Scroll to the selected time when value changes from outside
if (this.dropdownController.isOpen) {
setTimeout(() => {
this.scrollToSelectedTime();
}, 50);
}
}
if (this.shouldUpdateConstraints(changedProperties)) {
this.updateConstraints();
}
// Set up dropdown elements
this.setupDropdownElements();
}
setupDropdownElements() {
var _a, _b;
const dropdown = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.time-picker__dropdown');
const trigger = (_b = this.shadowRoot) === null || _b === void 0 ? void 0 : _b.querySelector('.time-picker__input-wrapper');
if (dropdown && trigger) {
this.dropdownController.setElements(dropdown, trigger);
}
}
render() {
const wrapperClasses = {
'time-picker': true,
'time-picker--open': this.dropdownController.isOpen,
'time-picker--disabled': this.disabled,
'time-picker--readonly': this.readonly,
'time-picker--error': this.state === TimePickerState.Error,
};
return html `
<div class="${classMap(wrapperClasses)}" data-theme="${this.currentTheme}" part="wrapper">
${this.renderLabel()}
${this.renderInput()}
${this.renderDropdown()}
${this.renderHelperText()}
</div>
`;
}
// Public API methods
open() {
this.dropdownController.open();
// Scroll to selected time when opening programmatically
setTimeout(() => {
this.scrollToSelectedTime();
}, 50);
}
close() { this.dropdownController.close(); }
clear() {
this.value = '';
this.inputValue = '';
this.selectionController.clearSelection();
}
setToNow() {
const now = TimeUtils.getCurrentTime();
this.selectionController.selectTime(now);
this.updateInputValue();
// Scroll to the newly selected time if dropdown is open
if (this.dropdownController.isOpen) {
setTimeout(() => {
this.scrollToSelectedTime();
}, 10);
}
}
validate() {
const selectedTime = this.selectionController.getSelectedTime();
if (!selectedTime)
return true;
return this.validationController.validateConstraints(selectedTime);
}
validateTime(time) {
return this.validationController.validateConstraints(time);
}
// Helper methods for checking if individual time components are valid
isHourValid(hour, selectedTime) {
const testTime = Object.assign(Object.assign({}, selectedTime), { hours: hour });
return this.validateTime(testTime);
}
isMinuteValid(minute, selectedTime) {
const testTime = Object.assign(Object.assign({}, selectedTime), { minutes: minute });
return this.validateTime(testTime);
}
isSecondValid(second, selectedTime) {
const testTime = Object.assign(Object.assign({}, selectedTime), { seconds: second });
return this.validateTime(testTime);
}
// Private methods
renderLabel() {
if (!this.label)
return nothing;
return html `
<label class="time-picker__label" part="label" for="time-input">
${this.label}
${this.required ? html `<span class="time-picker__required">*</span>` : nothing}
</label>
`;
}
renderInput() {
const formatPlaceholder = this.getFormatPlaceholder();
return html `
<div class="time-picker__input-wrapper" part="input-wrapper">
<nr-input
id="time-input"
part="input"
type="calendar"
.value="${this.inputValue}"
placeholder="${this.placeholder || formatPlaceholder}"
?disabled="${this.disabled}"
?readonly="${false}"
?required="${this.required}"
.state="${this.state === TimePickerState.Error ? "error" /* INPUT_STATE.Error */ : "default" /* INPUT_STATE.Default */}"
="${this.handleInputClick}"
-input="${this.handleInputChange}"
-blur="${this.handleInputBlur}"
>
</nr-input>
</div>
`;
}
renderDropdown() {
if (!this.dropdownController.isOpen)
return nothing;
return html `
<div
class="time-picker__dropdown time-picker__dropdown--open"
part="dropdown"
="${this.handleDropdownClick}"
>
${this.renderColumnPicker()}
${this.renderActions()}
</div>
`;
}
renderColumnPicker() {
const selectedTime = this.selectionController.getSelectedTime();
const config = this.getConfig();
return html `
<div class="time-picker__columns" part="columns">
${this.renderHourColumn(selectedTime, config)}
${this.renderMinuteColumn(selectedTime)}
${this.showSeconds ? this.renderSecondColumn(selectedTime) : nothing}
</div>
`;
}
renderHourColumn(selectedTime, config) {
const hours = config.format === TimeFormat.TwelveHour
? Array.from({ length: 12 }, (_, i) => i === 0 ? 12 : i)
: Array.from({ length: 24 }, (_, i) => i);
const displayHour = selectedTime && config.format === TimeFormat.TwelveHour
? this.formattingController.formatHours(selectedTime.hours)
: selectedTime === null || selectedTime === void 0 ? void 0 : selectedTime.hours;
return html `
<div class="time-picker__column" part="hour-column">
<div class="time-picker__column-list">
${hours.map(hour => {
// Convert display hour to actual hour for validation
let actualHour = hour;
if (config.format === TimeFormat.TwelveHour && selectedTime) {
const currentPeriod = this.formattingController.getPeriod(selectedTime.hours);
if (hour === 12) {
actualHour = currentPeriod === TimePeriod.AM ? 0 : 12;
}
else {
actualHour = currentPeriod === TimePeriod.AM ? hour : hour + 12;
}
}
// Use EMPTY_TIME_VALUE for validation when no time is selected
const timeForValidation = selectedTime || EMPTY_TIME_VALUE;
const isValid = this.isHourValid(actualHour, timeForValidation);
const isSelected = selectedTime ? hour === displayHour : false;
return html `
<div
class="time-picker__column-item ${isSelected ? 'time-picker__column-item--selected' : ''} ${!isValid ? 'time-picker__column-item--disabled' : ''}"
="${isValid ? () => this.handleHourSelect(hour, config.format) : null}"
>
${hour.toString().padStart(2, '0')}
</div>
`;
})}
</div>
</div>
`;
}
renderMinuteColumn(selectedTime) {
const minutes = Array.from({ length: 60 }, (_, i) => i);
return html `
<div class="time-picker__column" part="minute-column">
<div class="time-picker__column-list">
${minutes.map(minute => {
// Use EMPTY_TIME_VALUE for validation when no time is selected
const timeForValidation = selectedTime || EMPTY_TIME_VALUE;
const isValid = this.isMinuteValid(minute, timeForValidation);
const isSelected = selectedTime ? minute === selectedTime.minutes : false;
return html `
<div
class="time-picker__column-item ${isSelected ? 'time-picker__column-item--selected' : ''} ${!isValid ? 'time-picker__column-item--disabled' : ''}"
="${isValid ? () => this.handleMinuteSelect(minute) : null}"
>
${minute.toString().padStart(2, '0')}
</div>
`;
})}
</div>
</div>
`;
}
renderSecondColumn(selectedTime) {
const seconds = Array.from({ length: 60 }, (_, i) => i);
return html `
<div class="time-picker__column" part="second-column">
<div class="time-picker__column-list">
${seconds.map(second => {
// Use EMPTY_TIME_VALUE for validation when no time is selected
const timeForValidation = selectedTime || EMPTY_TIME_VALUE;
const isValid = this.isSecondValid(second, timeForValidation);
const isSelected = selectedTime ? second === selectedTime.seconds : false;
return html `
<div
class="time-picker__column-item ${isSelected ? 'time-picker__column-item--selected' : ''} ${!isValid ? 'time-picker__column-item--disabled' : ''}"
="${isValid ? () => this.handleSecondSelect(second) : null}"
>
${second.toString().padStart(2, '0')}
</div>
`;
})}
</div>
</div>
`;
}
renderActions() {
return html `
<div class="time-picker__actions">
<nr-button
type="ghost"
size="small"
="${() => this.setToNow()}"
>
Now
</nr-button>
<nr-button
type="primary"
size="small"
="${this.handleOkClick}"
>
OK
</nr-button>
</div>
`;
}
renderHelperText() {
const text = this.validationMessage || this.helperText;
if (!text)
return nothing;
const isError = this.state === TimePickerState.Error || !!this.validationMessage;
return html `
<div class="time-picker__helper-text ${isError ? 'time-picker__helper-text--error' : ''}" part="helper-text">
${text}
</div>
`;
}
scrollToSelectedTime() {
try {
const selectedTime = this.selectionController.getSelectedTime();
if (!selectedTime)
return;
// Scroll each column to the selected value
this.scrollToSelectedHour(selectedTime);
this.scrollToSelectedMinute(selectedTime);
if (this.showSeconds) {
this.scrollToSelectedSecond(selectedTime);
}
}
catch (error) {
console.warn('Failed to scroll to selected time:', error);
}
}
scrollToSelectedHour(selectedTime) {
var _a;
const config = this.getConfig();
const hourColumn = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.time-picker__column:first-child .time-picker__column-list');
if (!hourColumn)
return;
let displayHour;
if (config.format === TimeFormat.TwelveHour) {
// Convert 24-hour to 12-hour format
if (selectedTime.hours === 0 || selectedTime.hours === 12) {
displayHour = 12;
}
else {
displayHour = selectedTime.hours > 12 ? selectedTime.hours - 12 : selectedTime.hours;
}
}
else {
displayHour = selectedTime.hours;
}
// Find the selected hour element
const selectedHourElement = hourColumn.querySelector(`.time-picker__column-item:nth-child(${this.getHourIndex(displayHour, config.format) + 1})`);
if (selectedHourElement) {
selectedHourElement.scrollIntoView({
behavior: this.scrollBehavior,
block: 'center',
inline: 'nearest'
});
}
}
scrollToSelectedMinute(selectedTime) {
var _a;
const minuteColumn = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.time-picker__column:nth-child(2) .time-picker__column-list');
if (!minuteColumn)
return;
// Find the selected minute element (minute + 1 because nth-child is 1-indexed)
const selectedMinuteElement = minuteColumn.querySelector(`.time-picker__column-item:nth-child(${selectedTime.minutes + 1})`);
if (selectedMinuteElement) {
selectedMinuteElement.scrollIntoView({
behavior: this.scrollBehavior,
block: 'center',
inline: 'nearest'
});
}
}
scrollToSelectedSecond(selectedTime) {
var _a;
const secondColumn = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.time-picker__column:nth-child(3) .time-picker__column-list');
if (!secondColumn)
return;
// Find the selected second element (second + 1 because nth-child is 1-indexed)
const selectedSecondElement = secondColumn.querySelector(`.time-picker__column-item:nth-child(${selectedTime.seconds + 1})`);
if (selectedSecondElement) {
selectedSecondElement.scrollIntoView({
behavior: this.scrollBehavior,
block: 'center',
inline: 'nearest'
});
}
}
getHourIndex(displayHour, format) {
if (format === TimeFormat.TwelveHour) {
// For 12-hour format: 12, 1, 2, ..., 11
return displayHour === 12 ? 0 : displayHour;
}
else {
// For 24-hour format: 0, 1, 2, ..., 23
return displayHour;
}
}
// Event handlers
handleComponentClick(e) {
// Stop propagation to prevent document click handler from firing
e.stopPropagation();
}
handleDocumentClick(e) {
var _a;
// Close dropdown when clicking outside the component
if (this.dropdownController.isOpen) {
const target = e.target;
const isClickInsideComponent = this.contains(target) ||
((_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.contains(target));
if (!isClickInsideComponent) {
this.dropdownController.close();
this.dispatchEvent(new CustomEvent(TIME_PICKER_EVENTS.BLUR, {
bubbles: true,
composed: true,
}));
}
}
}
handleDropdownClick(e) {
// Prevent dropdown from closing when clicking inside
e.stopPropagation();
}
handleOkClick() {
// Close the dropdown and emit final change event
this.dropdownController.close();
const selectedTime = this.selectionController.getSelectedTime();
if (selectedTime) {
this.dispatchEvent(new CustomEvent(TIME_PICKER_EVENTS.TIME_CHANGE, {
bubbles: true,
composed: true,
detail: { value: this.value, time: selectedTime }
}));
}
this.dispatchEvent(new CustomEvent(TIME_PICKER_EVENTS.BLUR, {
bubbles: true,
composed: true,
}));
}
handleInputBlur() {
// Only close dropdown if clicking outside the component
setTimeout(() => {
var _a;
const activeElement = document.activeElement;
const isWithinComponent = this.contains(activeElement) ||
((_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.contains(activeElement));
if (!isWithinComponent) {
this.dispatchEvent(new CustomEvent(TIME_PICKER_EVENTS.BLUR, {
bubbles: true,
composed: true,
}));
}
}, 150);
}
handleInputClick(e) {
e.preventDefault();
e.stopPropagation();
if (!this.disabled) {
// Only open if closed - don't close when clicking input
if (!this.dropdownController.isOpen) {
this.dropdownController.open();
// Scroll to selected items when dropdown opens
setTimeout(() => {
this.scrollToSelectedTime();
}, 50);
this.dispatchEvent(new CustomEvent(TIME_PICKER_EVENTS.FOCUS, {
bubbles: true,
composed: true,
}));
}
}
}
handleInputChange(e) {
var _a;
if (this.disabled)
return;
const inputValue = ((_a = e.detail) === null || _a === void 0 ? void 0 : _a.value) || '';
this.inputValue = inputValue;
// Parse the input value and update the time selection
const parsedTime = this.formattingController.parseInputValue(inputValue);
if (parsedTime) {
// Validate the parsed time
if (this.validateTime(parsedTime)) {
this.selectionController.selectTime(parsedTime);
this.value = this.formattingController.formatForInput(parsedTime);
this.state = TimePickerState.Default;
// Scroll to the newly selected time if dropdown is open
if (this.dropdownController.isOpen) {
setTimeout(() => {
this.scrollToSelectedTime();
}, 10);
}
// Emit change event
this.dispatchEvent(new CustomEvent(TIME_PICKER_EVENTS.TIME_CHANGE, {
bubbles: true,
composed: true,
detail: { value: this.value, time: parsedTime }
}));
}
else {
// Invalid time - show error state but don't clear the input
this.state = TimePickerState.Error;
}
}
else if (inputValue === '') {
// Empty input - clear the selection
this.selectionController.clearSelection();
this.value = '';
this.state = TimePickerState.Default;
this.dispatchEvent(new CustomEvent(TIME_PICKER_EVENTS.TIME_CHANGE, {
bubbles: true,
composed: true,
detail: { value: '', time: null }
}));
}
else {
// Invalid format - show error state
this.state = TimePickerState.Error;
}
// Request update to re-render with new state
this.requestUpdate();
}
handleHourSelect(hour, format) {
const selectedTime = this.selectionController.getSelectedTime() || TimeUtils.getCurrentTime();
let adjustedHour = hour;
if (format === TimeFormat.TwelveHour) {
const currentPeriod = this.formattingController.getPeriod(selectedTime.hours);
if (hour === 12) {
adjustedHour = currentPeriod === TimePeriod.AM ? 0 : 12;
}
else {
adjustedHour = currentPeriod === TimePeriod.AM ? hour : hour + 12;
}
}
const updatedTime = Object.assign(Object.assign({}, selectedTime), { hours: adjustedHour });
if (this.validateTime(updatedTime)) {
this.selectionController.selectTime(updatedTime);
this.updateInputValue();
// No scrolling when clicking on individual items
}
}
handleMinuteSelect(minute) {
const selectedTime = this.selectionController.getSelectedTime() || TimeUtils.getCurrentTime();
const updatedTime = Object.assign(Object.assign({}, selectedTime), { minutes: minute });
if (this.validateTime(updatedTime)) {
this.selectionController.selectTime(updatedTime);
this.updateInputValue();
// No scrolling when clicking on individual items
}
}
handleSecondSelect(second) {
const selectedTime = this.selectionController.getSelectedTime() || TimeUtils.getCurrentTime();
const updatedTime = Object.assign(Object.assign({}, selectedTime), { seconds: second });
if (this.validateTime(updatedTime)) {
this.selectionController.selectTime(updatedTime);
this.updateInputValue();
// No scrolling when clicking on individual items
}
}
// Utility methods
shouldUpdateConstraints(changedProperties) {
return (changedProperties.has('minTime') ||
changedProperties.has('maxTime') ||
changedProperties.has('disabledTimes') ||
changedProperties.has('enabledTimes'));
}
updateConstraints() {
const constraints = {
minTime: this.minTime,
maxTime: this.maxTime,
disabledTimes: this.disabledTimes || [],
enabledTimes: this.enabledTimes,
};
this.validationController.setConstraints(constraints);
}
setTimeFromValue(value) {
if (this.selectionController.setTimeFromString(value)) {
this.inputValue = value;
this.requestUpdate();
// Scroll to the time when setting from value (if dropdown is open)
if (this.dropdownController.isOpen) {
setTimeout(() => {
this.scrollToSelectedTime();
}, 50);
}
}
}
updateInputValue() {
const selectedTime = this.selectionController.getSelectedTime();
if (selectedTime) {
const formattedValue = this.formattingController.formatForDisplay(selectedTime);
this.inputValue = formattedValue;
this.value = formattedValue;
this.dispatchEvent(new CustomEvent(TIME_PICKER_EVENTS.TIME_CHANGE, {
detail: { value: formattedValue, time: selectedTime },
bubbles: true,
composed: true,
}));
}
}
getConfig() {
return {
format: this.format,
showSeconds: this.showSeconds,
step: {
hours: TimeStep.One,
minutes: TimeStep.One,
seconds: TimeStep.One,
},
use12HourClock: this.format === TimeFormat.TwelveHour,
minuteInterval: 1,
secondInterval: 1,
};
}
// TimePickerHost interface implementation
getCurrentTime() {
return this.selectionController.getSelectedTime() || EMPTY_TIME_VALUE;
}
setTime(time) {
this.selectionController.selectTime(time);
this.updateInputValue();
// Scroll to the newly selected time if dropdown is open
if (this.dropdownController.isOpen) {
setTimeout(() => {
this.scrollToSelectedTime();
}, 10);
}
}
formatTime(time) {
return this.formattingController.formatForDisplay(time);
}
/**
* Get appropriate placeholder text based on format
*/
getFormatPlaceholder() {
if (this.format === TimeFormat.TwelveHour) {
return this.showSeconds ? 'HH:MM:SS AM/PM' : 'HH:MM AM/PM';
}
else {
return this.showSeconds ? 'HH:MM:SS' : 'HH:MM';
}
}
parseTime(timeString) {
return this.formattingController.parseInputValue(timeString);
}
};
NrTimePickerElement.styles = [timePickerStyles];
__decorate([
property({ type: String })
], NrTimePickerElement.prototype, "value", void 0);
__decorate([
property({ type: String })
], NrTimePickerElement.prototype, "name", void 0);
__decorate([
property({ type: String })
], NrTimePickerElement.prototype, "placeholder", void 0);
__decorate([
property({ type: String })
], NrTimePickerElement.prototype, "format", void 0);
__decorate([
property({ type: Boolean, attribute: 'show-seconds' })
], NrTimePickerElement.prototype, "showSeconds", void 0);
__decorate([
property({ type: Boolean })
], NrTimePickerElement.prototype, "disabled", void 0);
__decorate([
property({ type: Boolean })
], NrTimePickerElement.prototype, "readonly", void 0);
__decorate([
property({ type: Boolean })
], NrTimePickerElement.prototype, "required", void 0);
__decorate([
property({ type: String, attribute: 'min-time' })
], NrTimePickerElement.prototype, "minTime", void 0);
__decorate([
property({ type: String, attribute: 'max-time' })
], NrTimePickerElement.prototype, "maxTime", void 0);
__decorate([
property({ type: Array, attribute: 'disabled-times' })
], NrTimePickerElement.prototype, "disabledTimes", void 0);
__decorate([
property({ type: Array, attribute: 'enabled-times' })
], NrTimePickerElement.prototype, "enabledTimes", void 0);
__decorate([
property({ type: String, attribute: 'helper-text' })
], NrTimePickerElement.prototype, "helperText", void 0);
__decorate([
property({ type: String })
], NrTimePickerElement.prototype, "label", void 0);
__decorate([
property({ type: String })
], NrTimePickerElement.prototype, "size", void 0);
__decorate([
property({ type: String })
], NrTimePickerElement.prototype, "variant", void 0);
__decorate([
property({ type: String })
], NrTimePickerElement.prototype, "placement", void 0);
__decorate([
property({ type: String, attribute: 'scroll-behavior' })
], NrTimePickerElement.prototype, "scrollBehavior", void 0);
__decorate([
state()
], NrTimePickerElement.prototype, "inputValue", void 0);
__decorate([
state()
], NrTimePickerElement.prototype, "state", void 0);
__decorate([
state()
], NrTimePickerElement.prototype, "validationMessage", void 0);
NrTimePickerElement = __decorate([
customElement('nr-timepicker')
], NrTimePickerElement);
export { NrTimePickerElement };
//# sourceMappingURL=timepicker.component.js.map