UNPKG

@nuralyui/timepicker

Version:

NuralyUI TimePicker - A comprehensive time selection component with clock interface, multiple formats, and validation

747 lines 29.6 kB
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 */}" @click="${this.handleInputClick}" @nr-input="${this.handleInputChange}" @nr-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" @click="${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' : ''}" @click="${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' : ''}" @click="${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' : ''}" @click="${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" @click="${() => this.setToNow()}" > Now </nr-button> <nr-button type="primary" size="small" @click="${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