UNPKG

asa-date-picker

Version:

An Angular date picker component supporting Gregorian and Persian (Jalali/Shamsi) calendars, powered by Asa Co.

735 lines 130 kB
/** * Time Picker Component * A customizable time picker that supports 12/24 hour formats, seconds, and multiple locales. * * Features: * - 12/24 hour format * - Optional seconds * - Localization support * - String or Date value types * - Min/Max time validation * - Custom styling */ import { Component, forwardRef, Input, Output, EventEmitter, ViewChild, HostListener, ChangeDetectionStrategy } from '@angular/core'; import { NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; import { CdkOverlayOrigin, OverlayModule } from '@angular/cdk/overlay'; import { slideMotion } from '../utils/animation/slide'; import { AsaDatePickerService } from '../asa-date-picker.service'; import { DEFAULT_DATE_PICKER_POSITIONS, NzConnectedOverlayDirective } from "../utils/overlay/overlay"; import { NgClass, NgFor, NgIf, NgTemplateOutlet } from '@angular/common'; import { DateMaskDirective } from '../utils/input-mask.directive'; import * as i0 from "@angular/core"; import * as i1 from "@angular/forms"; import * as i2 from "../asa-date-picker.service"; import * as i3 from "../asa-date-adapter"; import * as i4 from "@angular/cdk/overlay"; export class TimePickerComponent { constructor(fb, elementRef, cdref, datePickerService, jalaliAdapter, gregorianAdapter) { this.fb = fb; this.elementRef = elementRef; this.cdref = cdref; this.datePickerService = datePickerService; this.jalaliAdapter = jalaliAdapter; this.gregorianAdapter = gregorianAdapter; this.rtl = false; this.placement = 'right'; this.valueType = 'string'; this.cssClass = ''; this.showIcon = true; this.inline = false; this.disableInputMask = false; this.disabled = false; this.allowEmpty = true; this.readOnly = false; this.readOnlyInput = false; this.timeChange = new EventEmitter(); this.openChange = new EventEmitter(); this.timeFormat = '12'; this._displayFormat = 'hh:mm a'; this._value = null; this._selectedDate = new Date(); this.onChange = () => { }; this.onTouched = () => { }; this.timeoutId = null; this.showSeconds = false; this.hours = []; this.minutes = Array.from({ length: 60 }, (_, i) => i); this.seconds = Array.from({ length: 60 }, (_, i) => i); this.periods = []; this.selectedTime = { hour: 12, minute: 0, second: 0, period: '' }; this.isOpen = false; this.overlayPositions = [...DEFAULT_DATE_PICKER_POSITIONS]; this.currentFocusedColumn = null; this.isScrolling = false; this.scrollTimeout = null; this.handleDocumentClick = (event) => { if (!this.elementRef.nativeElement.contains(event.target) && this.isOpen) { this.close(); this.handleTimeInput(); } }; this.dateAdapter = this.gregorianAdapter; this.initializeForm(); this.initializeLocale(); } set displayFormat(value) { this._displayFormat = value; this.showSeconds = value.toLowerCase().includes('s'); // Infer time format from display format this.timeFormat = this.getTimeFormatFromDisplayFormat(value); this.updateHourRange(); this.updateTimeDisplay(); } get displayFormat() { return this._displayFormat; } set selectedDate(date) { if (date) { this._selectedDate = date; } } get selectedDate() { return this._selectedDate; } // Lifecycle hooks async ngOnInit() { this.updateHourRange(); this.origin = new CdkOverlayOrigin(this.elementRef); this.setupInputSubscription(); this.value = this.selectedDate; // Only add document click listener for non-inline mode if (!this.inline) { document.addEventListener('click', this.handleDocumentClick); } // Auto-open for inline mode if (this.inline) { this.isOpen = true; } await this.scrollToTime(); this.isOpen = true; } ngOnDestroy() { this.cleanupTimeouts(); document.removeEventListener('click', this.handleDocumentClick); } ngOnChanges(changes) { if (changes['rtl'] || changes['lang']) { this.updateLocale(); } if (changes['rtl'] && !changes['dateAdapter']) { this.dateAdapter = this.rtl ? this.jalaliAdapter : this.gregorianAdapter; } } // Initialization methods initializeForm() { this.form = this.fb.group({ timeInput: [''] }); } initializeLocale() { this.lang = this.datePickerService.locale_en; this.selectedTime.period = this.lang.am; this.periods = [this.lang.am, this.lang.pm]; } updateLocale() { this.lang = this.rtl ? this.datePickerService.locale_fa : this.datePickerService.locale_en; this.selectedTime.period = this.lang.am; this.periods = [this.lang.am, this.lang.pm]; this.placeholder = this.lang.selectTime; } setupInputSubscription() { this.form.get('timeInput')?.valueChanges.subscribe(value => { if (!value) return; if (!this.isOpen) { this.validateAndUpdateTime(value); } else { this.parseTimeString(value); this.scrollToTime(); } }); } // Time management updateHourRange() { const format = this.getTimeFormatFromDisplayFormat(this._displayFormat); this.hours = format === '12' ? Array.from({ length: 12 }, (_, i) => i + 1) : Array.from({ length: 24 }, (_, i) => i); } formatTime(date) { if (!date && !this.dateAdapter) return ''; const currentDate = date || this.updateDateFromSelection(); return this.dateAdapter.format(currentDate, this._displayFormat); } parseTimeString(value) { if (!this.dateAdapter) return; const date = value instanceof Date ? value : this.dateAdapter.parse(value, this._displayFormat); if (!date) return; const hours = this.dateAdapter.getHours(date); const minutes = this.dateAdapter.getMinutes(date); const seconds = this.dateAdapter.getSeconds(date); if (hours === null || minutes === null || seconds === null) return; this.selectedTime = { hour: hours, minute: minutes, second: seconds, period: hours >= 12 ? this.lang.pm : this.lang.am }; this.cdref.markForCheck(); } // Value accessors and form control get value() { return this._value; } set value(val) { this._value = val; this.updateFromValue(val); } updateFromValue(value) { if (!value) { this.resetSelection(); return; } if (value instanceof Date) { this.updateFromDate(value); } else { this.parseTimeString(value); } } updateFromDate(date) { if (date && !isNaN(date.getTime()) && this.dateAdapter) { const hours = this.dateAdapter.getHours(date); if (hours === null) return; this.selectedTime = { hour: hours, minute: this.dateAdapter.getMinutes(date) ?? 0, second: this.dateAdapter.getSeconds(date) ?? 0, period: hours >= 12 ? this.lang.pm : this.lang.am }; } else { this.resetSelection(); } this.cdref.markForCheck(); } resetSelection() { this.selectedTime = { hour: 0, minute: 0, second: 0, period: this.lang.am }; this.cdref.markForCheck(); } writeValue(value) { if (!value) { this.value = null; return; } if (value instanceof Date) { this.value = value; } else if (value.trim()) { const date = this.selectedDate; this.value = !isNaN(date.getTime()) && this.valueType === 'date' ? date : value; this.parseTimeString(value); } this.updateTimeDisplay(); this.save(false); } registerOnChange(fn) { this.onChange = fn; } registerOnTouched(fn) { this.onTouched = fn; } // UI Event handlers handleKeydown(event) { if (event.key === 'Tab' || event.key === 'Enter') { this.handleTimeInput(); this.close(); } else if (event.key === 'Escape') { this.close(); } else if (this.isOpen && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) { event.preventDefault(); this.handleArrowNavigation(event.key === 'ArrowUp' ? -1 : 1); } } handleArrowNavigation(direction) { if (!this.currentFocusedColumn) return; if (this.currentFocusedColumn === 'hour') { const currentIndex = this.hours.findIndex(h => h === this.selectedTime.hour); const nextIndex = this.getNextValidIndex(this.hours, currentIndex, direction, this.isHourDisabled.bind(this)); if (nextIndex !== -1) { this.selectHour(this.hours[nextIndex]); this.focusTimeButton('hour', this.hours[nextIndex]); } } else if (this.currentFocusedColumn === 'minute') { const currentIndex = this.minutes.findIndex(m => m === this.selectedTime.minute); const nextIndex = this.getNextValidIndex(this.minutes, currentIndex, direction, this.isMinuteDisabled.bind(this)); if (nextIndex !== -1) { this.selectMinute(this.minutes[nextIndex]); this.focusTimeButton('minute', this.minutes[nextIndex]); } } else if (this.currentFocusedColumn === 'second' && this.showSeconds) { const currentIndex = this.seconds.findIndex(s => s === this.selectedTime.second); const nextIndex = this.getNextValidIndex(this.seconds, currentIndex, direction, this.isSecondDisabled.bind(this)); if (nextIndex !== -1) { this.selectSecond(this.seconds[nextIndex]); this.focusTimeButton('second', this.seconds[nextIndex]); } } else if (this.currentFocusedColumn === 'period' && this.timeFormat === '12') { const currentIndex = this.periods.findIndex(p => p === this.selectedTime.period); const nextIndex = (currentIndex + direction + this.periods.length) % this.periods.length; this.selectPeriod(this.periods[nextIndex]); this.focusTimeButton('period', this.periods[nextIndex]); } } getNextValidIndex(items, currentIndex, direction, isDisabledFn) { let nextIndex = currentIndex; const totalItems = items.length; // Search for the next valid item in the specified direction for (let i = 0; i < totalItems; i++) { nextIndex = (nextIndex + direction + totalItems) % totalItems; if (!isDisabledFn(items[nextIndex])) { return nextIndex; } } // If no valid item found, return -1 return -1; } focusTimeButton(type, value) { setTimeout(() => { const buttonId = type === 'period' ? `period_${value}` : `selector_${type.charAt(0)}${value}`; const button = this.popupWrapper?.nativeElement.querySelector(`#${buttonId}`); if (button) { button.focus(); } }, 0); } onTimeButtonFocus(column) { this.currentFocusedColumn = column; } handleTimeInput() { const currentValue = this.form.get('timeInput')?.value; if (currentValue || (!currentValue && !this.allowEmpty)) { this.validateAndUpdateTime(currentValue); } } onFocusInput() { if (!this.isOpen) { this.open(); } } toggleTimePicker(event) { event.stopPropagation(); this.isOpen ? this.close() : this.open(); } // Picker operations open() { if (this.inline || this.disabled || this.readOnly) return; const wasOpen = this.isOpen; this.isOpen = true; this.openChange.emit(true); this.scrollToTime(); if (!wasOpen) { this.cdref.markForCheck(); } } close() { if (this.inline) return; this.cleanupTimeouts(); if (this.isOpen) { this.isOpen = false; this.openChange.emit(false); this.cdref.markForCheck(); } } // Selection methods selectHour(hour) { if (!this.isHourDisabled(hour)) { this.selectedTime.hour = hour; this.updateTimeDisplay(); this.scrollToSelectedItem(`h${hour}`); if (this.inline) this.save(); } } selectMinute(minute) { if (!this.isMinuteDisabled(minute)) { this.selectedTime.minute = minute; this.updateTimeDisplay(); this.scrollToSelectedItem(`m${minute}`); if (this.inline) this.save(); } } selectSecond(second) { if (!this.isSecondDisabled(second)) { this.selectedTime.second = second; this.updateTimeDisplay(); this.scrollToSelectedItem(`s${second}`); if (this.inline) this.save(); } } selectPeriod(period) { this.selectedTime.period = period; this.updateTimeDisplay(); } selectNow() { const now = this.selectedDate; this.selectedTime = { hour: now.getHours(), minute: now.getMinutes(), second: now.getSeconds(), period: now.getHours() >= 12 ? this.lang.pm : this.lang.am }; this.updateTimeDisplay(); this.scrollToTime(); this.save(); } save(close = true) { const date = this.updateDateFromSelection(); const { isValid, normalizedDate } = this.validateAndNormalizeTime(date); if (!isValid || !normalizedDate) return; const outputValue = this.valueType === 'date' ? normalizedDate : this.formatTime(normalizedDate); const valueChanged = JSON.stringify(this._value) !== JSON.stringify(outputValue); if (valueChanged) { this._value = outputValue; this.form.get('timeInput')?.setValue(this.formatTime(normalizedDate), { emitEvent: false }); this.onChange(outputValue); this.timeChange.emit(outputValue); this.cdref.markForCheck(); } if (close && !this.inline) { this.close(); } } // Validation methods validateAndUpdateTime(value) { if (!value || !this.dateAdapter) { this.updateTimeDisplay(); return; } try { const parsedDate = this.dateAdapter.parse(value, this._displayFormat); if (!parsedDate) { this.updateTimeDisplay(); return; } const { isValid, normalizedDate } = this.validateAndNormalizeTime(parsedDate); // @ts-ignore const formattedTime = this.dateAdapter.format(normalizedDate, this._displayFormat); this.form.get('timeInput')?.setValue(formattedTime, { emitEvent: false }); // @ts-ignore this.parseTimeString(normalizedDate); const outputValue = this.valueType === 'date' ? normalizedDate : formattedTime; this._value = outputValue; this.onChange(outputValue); // @ts-ignore this.timeChange.emit(outputValue); } catch (error) { console.error('Error normalizing time:', error); this.updateTimeDisplay(); } } isHourDisabled(hour) { if (!this.dateAdapter) return false; return this.isFullHourDisabled(hour); } isMinuteDisabled(minute) { if (!this.dateAdapter) return false; return this.isFullMinuteDisabled(minute); } isSecondDisabled(second) { if (!this.dateAdapter) return false; const testConfig = { ...this.selectedTime, second }; const testDate = this.createDateWithTime(testConfig); return this.isTimeDisabled(testDate); } isTimeDisabled(testDate) { if (!this.dateAdapter) return false; if (this.minTime) { const minDate = this.dateAdapter.parse(this.minTime, this._displayFormat); if (minDate && this.dateAdapter.isBefore(testDate, minDate)) { return true; } } if (this.maxTime) { const maxDate = this.dateAdapter.parse(this.maxTime, this._displayFormat); if (maxDate && this.dateAdapter.isAfter(testDate, maxDate)) { return true; } } return this.disabledTimesFilter ? this.disabledTimesFilter(testDate) : false; } validateAndNormalizeTime(date) { if (!this.dateAdapter) { return { isValid: false, normalizedDate: null }; } let isValid = true; // Clone the date to avoid modifying the original let normalizedDate = this.dateAdapter.clone(date); if (this.isTimeDisabled(normalizedDate)) { isValid = false; // Try to find nearest valid time (check next and previous 48 intervals of 30 minutes) for (let i = 1; i <= 48; i++) { const nextTime = this.dateAdapter.addMinutes(date, i * 30); const prevTime = this.dateAdapter.addMinutes(date, -i * 30); if (!this.isTimeDisabled(nextTime)) { normalizedDate = nextTime; break; } if (!this.isTimeDisabled(prevTime)) { normalizedDate = prevTime; break; } } // If still disabled after trying to find valid time if (this.isTimeDisabled(normalizedDate)) { return { isValid: false, normalizedDate: null }; } } return { isValid: isValid, normalizedDate }; } isFullHourDisabled(hour) { for (let minute = 0; minute < 60; minute++) { const testConfig = { ...this.selectedTime, hour, minute, second: 0 }; const testDate = this.createDateWithTime(testConfig); if (!this.isTimeDisabled(testDate)) { return false; // If any minute is enabled, hour is not fully disabled } } return true; // All minutes in hour are disabled } isFullMinuteDisabled(minute) { if (!this.showSeconds) { const testConfig = { ...this.selectedTime, minute, second: 0 }; const testDate = this.createDateWithTime(testConfig); return this.isTimeDisabled(testDate); } // If showing seconds, check each second for (let second = 0; second < 60; second++) { const testConfig = { ...this.selectedTime, minute, second }; const testDate = this.createDateWithTime(testConfig); if (!this.isTimeDisabled(testDate)) { return false; // If any second is enabled, minute is not fully disabled } } return true; // All seconds in minute are disabled } // Helper methods createDateWithTime(config) { if (!this.dateAdapter) return this.selectedDate; let testHour = config.hour; if (this.timeFormat === '12') { if (config.period === this.lang.pm && testHour < 12) testHour += 12; if (config.period === this.lang.am && testHour === 12) testHour = 0; } let date = this.selectedDate; date = this.dateAdapter.setHours(date, testHour); date = this.dateAdapter.setMinutes(date, config.minute); date = this.dateAdapter.setSeconds(date, config.second); return date; } updateDateFromSelection() { if (!this.dateAdapter) return this.selectedDate; let hours = this.selectedTime.hour; if (this.timeFormat === '12') { if (this.selectedTime.period === this.lang.pm && hours < 12) hours += 12; if (this.selectedTime.period === this.lang.am && hours === 12) hours = 0; } let date = this._value instanceof Date ? this.dateAdapter.clone(this._value) : this.selectedDate; date = this.dateAdapter.setHours(date, hours); date = this.dateAdapter.setMinutes(date, this.selectedTime.minute); date = this.dateAdapter.setSeconds(date, this.selectedTime.second); return date; } updateTimeDisplay() { if (this.form) { this.form.get('timeInput')?.setValue(this.formatTime(), { emitEvent: false }); } } getTimeFormatFromDisplayFormat(format) { // Check for 24-hour format indicators const has24HourFormat = /\bH{1,2}\b/.test(format); return has24HourFormat ? '24' : '12'; } // UI Update methods async scrollToTime() { await this.scrollToSelectedItem(`h${this.selectedTime.hour}`, 'auto'), await this.scrollToSelectedItem(`m${this.selectedTime.minute}`, 'auto'), this.showSeconds ? await this.scrollToSelectedItem(`s${this.selectedTime.second}`, 'auto') : ''; } scrollToSelectedItem(id, behavior = 'smooth') { this.cleanupTimeouts(); return new Promise((resolve) => { if (!id) { resolve(false); return; } this.timeoutId = window.setTimeout(() => { const selectedElement = this.popupWrapper?.nativeElement.querySelector(`#selector_${id}`); if (selectedElement) { selectedElement.scrollIntoView({ behavior, block: 'center' }); } resolve(true); }, 0); }); } cleanupTimeouts() { if (this.timeoutId !== null) { window.clearTimeout(this.timeoutId); this.timeoutId = null; } } onPositionChange(position) { this.cdref.detectChanges(); } onTimeScroll(event, type) { event.preventDefault(); // If already scrolling, return to prevent rapid successive scrolls if (this.isScrolling) return; this.isScrolling = true; // Determine direction: negative delta means scroll down (next value) const direction = event.deltaY > 0 ? 1 : -1; // Perform the appropriate action based on the column type switch (type) { case 'hour': this.changeHour(direction); break; case 'minute': this.changeMinute(direction); break; case 'second': this.changeSecond(direction); break; } // Clear any existing timeout if (this.scrollTimeout) { clearTimeout(this.scrollTimeout); } // Set a timeout to allow next scroll action this.scrollTimeout = setTimeout(() => { this.isScrolling = false; }, 200); // Adjust timing as needed for comfort } changeHour(direction) { let currentIndex = this.hours.indexOf(this.selectedTime.hour); let newIndex = (currentIndex + direction + this.hours.length) % this.hours.length; // Find next enabled hour while (this.isHourDisabled(this.hours[newIndex]) && newIndex !== currentIndex) { newIndex = (newIndex + direction + this.hours.length) % this.hours.length; } if (!this.isHourDisabled(this.hours[newIndex])) { this.selectHour(this.hours[newIndex]); } } changeMinute(direction) { let currentIndex = this.minutes.indexOf(this.selectedTime.minute); let newIndex = (currentIndex + direction + this.minutes.length) % this.minutes.length; // Find next enabled minute while (this.isMinuteDisabled(this.minutes[newIndex]) && newIndex !== currentIndex) { newIndex = (newIndex + direction + this.minutes.length) % this.minutes.length; } if (!this.isMinuteDisabled(this.minutes[newIndex])) { this.selectMinute(this.minutes[newIndex]); } } changeSecond(direction) { let currentIndex = this.seconds.indexOf(this.selectedTime.second); let newIndex = (currentIndex + direction + this.seconds.length) % this.seconds.length; // Find next enabled second while (this.isSecondDisabled(this.seconds[newIndex]) && newIndex !== currentIndex) { newIndex = (newIndex + direction + this.seconds.length) % this.seconds.length; } if (!this.isSecondDisabled(this.seconds[newIndex])) { this.selectSecond(this.seconds[newIndex]); } } } TimePickerComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TimePickerComponent, deps: [{ token: i1.FormBuilder }, { token: i0.ElementRef }, { token: i0.ChangeDetectorRef }, { token: i2.AsaDatePickerService }, { token: i3.JalaliDateAdapter }, { token: i3.GregorianDateAdapter }], target: i0.ɵɵFactoryTarget.Component }); TimePickerComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: TimePickerComponent, isStandalone: true, selector: "asa-time-picker", inputs: { placeholder: "placeholder", rtl: "rtl", placement: "placement", minTime: "minTime", maxTime: "maxTime", lang: "lang", valueType: "valueType", cssClass: "cssClass", showIcon: "showIcon", dateAdapter: "dateAdapter", inline: "inline", disableInputMask: "disableInputMask", disabled: "disabled", disabledTimesFilter: "disabledTimesFilter", allowEmpty: "allowEmpty", readOnly: "readOnly", readOnlyInput: "readOnlyInput", isTimerVertical: "isTimerVertical", displayFormat: "displayFormat", selectedDate: "selectedDate" }, outputs: { timeChange: "timeChange", openChange: "openChange" }, host: { listeners: { "click": "open()", "keydown": "handleKeydown($event)" } }, providers: [ AsaDatePickerService, { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TimePickerComponent), multi: true } ], viewQueries: [{ propertyName: "timePickerInput", first: true, predicate: ["timePickerInput"], descendants: true }, { propertyName: "popupWrapper", first: true, predicate: ["popupWrapper"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"time-picker-wrapper\" [ngClass]=\"{\r\n 'time-picker-wrapper-column':isTimerVertical}\"\r\n [formGroup]=\"form\">\r\n <!-- Regular input mode -->\r\n <ng-container *ngIf=\"!inline\">\r\n <div class=\"input-wrapper\" [class.focus]=\"isOpen\" [class.disabled]=\"disabled\">\r\n <input\r\n #timePickerInput\r\n [asa-dateMask]=\"displayFormat\"\r\n [disableInputMask]=\"disableInputMask\"\r\n [class.disabled]=\"disabled\"\r\n type=\"text\"\r\n class=\"time-picker-input\"\r\n [class.focus]=\"isOpen\"\r\n formControlName=\"timeInput\"\r\n (focus)=\"onFocusInput()\"\r\n [placeholder]=\"placeholder\"\r\n [readonly]=\"readOnly || readOnlyInput\"\r\n [attr.disabled]=\"disabled? 'disabled':null\"\r\n >\r\n <button *ngIf=\"showIcon\" class=\"time-button\" (click)=\"toggleTimePicker($event)\" tabindex=\"-1\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#999\"\r\n stroke-width=\"2\">\r\n <circle cx=\"12\" cy=\"12\" r=\"10\"/>\r\n <path d=\"M12 6v6l4 2\"/>\r\n </svg>\r\n </button>\r\n </div>\r\n\r\n <ng-template\r\n cdkConnectedOverlay\r\n nzConnectedOverlay\r\n [cdkConnectedOverlayOrigin]=\"origin\"\r\n [cdkConnectedOverlayOpen]=\"isOpen\"\r\n [cdkConnectedOverlayPositions]=\"overlayPositions\"\r\n [cdkConnectedOverlayTransformOriginOn]=\"'.time-picker-popup'\"\r\n [cdkConnectedOverlayHasBackdrop]=\"false\"\r\n (positionChange)=\"onPositionChange($event)\"\r\n (detach)=\"close()\"\r\n >\r\n <ng-container *ngTemplateOutlet=\"timePickerContent\"></ng-container>\r\n </ng-template>\r\n </ng-container>\r\n\r\n <!-- Inline mode -->\r\n <ng-container *ngIf=\"inline\">\r\n <ng-container *ngTemplateOutlet=\"timePickerContent\"></ng-container>\r\n </ng-container>\r\n\r\n <!-- Time Picker Content Template -->\r\n <ng-template #timePickerContent>\r\n <div\r\n #popupWrapper\r\n [class]=\"'time-picker-popup ' + cssClass\"\r\n [ngClass]=\"{'time-picker-popup-column':isTimerVertical}\"\r\n [@slideMotion]=\"'enter'\"\r\n [class.inline]=\"inline\"\r\n [class.disabled]=\"disabled\"\r\n style=\"position: relative\"\r\n (click)=\"$event.stopPropagation()\"\r\n >\r\n <div class=\"time-picker-content\">\r\n <div class=\"time-columns\">\r\n <!-- Hours -->\r\n <div class=\"time-column\">\r\n <div class=\"time-scroller\" (wheel)=\"isTimerVertical?onTimeScroll($event, 'hour'):null\">\r\n <button\r\n *ngFor=\"let hour of hours\"\r\n [id]=\"'selector_h'+hour\"\r\n [class.selected]=\"selectedTime.hour === hour\"\r\n [class.disabled]=\"isHourDisabled(hour)\"\r\n (click)=\"selectHour(hour)\"\r\n (focus)=\"onTimeButtonFocus('hour')\"\r\n [tabindex]=\"selectedTime.hour === hour ? 0 : -1\"\r\n type=\"button\"\r\n >\r\n {{ hour.toString().padStart(2, '0') }}\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"time-separator\">:</div>\r\n\r\n <!-- Minutes -->\r\n <div class=\"time-column\">\r\n <div (wheel)=\"isTimerVertical?onTimeScroll($event, 'minute'):null\" class=\"time-scroller\">\r\n <button\r\n *ngFor=\"let minute of minutes\"\r\n [id]=\"'selector_m'+minute\"\r\n [class.selected]=\"selectedTime.minute === minute\"\r\n [class.disabled]=\"isMinuteDisabled(minute)\"\r\n (focus)=\"onTimeButtonFocus('minute')\"\r\n [tabindex]=\"selectedTime.minute === minute ? 0 : -1\"\r\n (click)=\"selectMinute(minute)\"\r\n type=\"button\"\r\n >\r\n {{ minute.toString().padStart(2, '0') }}\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Seconds (if format includes seconds) -->\r\n <ng-container *ngIf=\"showSeconds\">\r\n <div class=\"time-separator\">:</div>\r\n <div class=\"time-column\">\r\n <div (wheel)=\"isTimerVertical?onTimeScroll($event, 'second'):null\" class=\"time-scroller\">\r\n <button\r\n *ngFor=\"let second of seconds\"\r\n [id]=\"'selector_s'+second\"\r\n [class.selected]=\"selectedTime.second === second\"\r\n [class.disabled]=\"isSecondDisabled(second)\"\r\n (click)=\"selectSecond(second)\"\r\n (focus)=\"onTimeButtonFocus('second')\"\r\n [tabindex]=\"selectedTime.second === second ? 0 : -1\"\r\n type=\"button\"\r\n >\r\n {{ second.toString().padStart(2, '0') }}\r\n </button>\r\n </div>\r\n </div>\r\n </ng-container>\r\n\r\n <!-- AM/PM (only in 12-hour format) -->\r\n <ng-container *ngIf=\"timeFormat === '12'\">\r\n <div class=\"time-column period\">\r\n <button\r\n *ngFor=\"let period of periods\"\r\n [class.selected]=\"selectedTime.period === period\"\r\n (click)=\"selectPeriod(period)\"\r\n type=\"button\"\r\n >\r\n {{ period }}\r\n </button>\r\n </div>\r\n </ng-container>\r\n </div>\r\n </div>\r\n\r\n <div class=\"time-picker-footer\" *ngIf=\"!inline\">\r\n <div class=\"footer-buttons\">\r\n <button class=\"now-btn\" (click)=\"selectNow()\" type=\"button\">{{ lang.now }}</button>\r\n </div>\r\n <div class=\"footer-actions\">\r\n <button class=\"save-btn\" (click)=\"save()\" type=\"button\">{{ lang.ok }}</button>\r\n </div>\r\n </div>\r\n </div>\r\n </ng-template>\r\n</div>\r\n", styles: [":host *{font-family:inherit;font-weight:400;box-sizing:border-box;padding:0;margin:0}.time-picker-wrapper{display:inline-block}.time-picker-wrapper-column{width:100%;padding-top:0;padding-bottom:0}.input-wrapper{position:relative;display:inline-flex;align-items:center;border:1px solid #d9d9d9;border-radius:4px}.input-wrapper.focus{border-color:#40a9ff;box-shadow:0 0 0 2px #1890ff33;outline:none}.input-wrapper.disabled{background:#fafafa}input:focus{outline:none}input:hover{border-color:#40a9ff}input.time-picker-input{font-family:inherit;width:100%;padding:6px;border:none;border-radius:4px;font-size:14px;transition:all .3s}.time-button{background:none;border:none;cursor:pointer;padding:4px 4px 0}.time-picker-popup{background:white;border-radius:8px;box-shadow:0 6px 16px #00000014;width:fit-content;min-width:200px;overflow:hidden;margin-block:3px;height:40vh;max-height:280px}.time-picker-popup.inline{box-shadow:none;margin:0;border:1px solid #d9d9d9}.time-picker-popup.inline .time-picker-content{padding:4px;height:100%}.time-picker-popup.inline .time-column::-webkit-scrollbar{width:6px}.time-picker-popup.inline .time-column::-webkit-scrollbar-thumb{background:#d9d9d9;border-radius:3px}.time-picker-popup.inline .time-column::-webkit-scrollbar-track{background:#f5f5f5}.time-picker-header{padding:16px;font-size:16px;font-weight:500;border-bottom:1px solid #f0f0f0}.time-picker-content{padding:0 8px;height:calc(100% - 59px)}.time-columns{display:flex;align-items:center;justify-content:center;padding:0 8px;gap:8px;height:100%}.time-column{flex:1;height:100%;overflow-y:auto;scrollbar-width:none}.time-column::-webkit-scrollbar{display:none}.time-column button{width:100%;padding:6px 8px;background:none;border:none;cursor:pointer;color:#666;font-size:14px;border-radius:4px}.time-column button:hover:not(.disabled){background:#dcf7ff}.time-column button.selected{background:#c8eeff;color:#000}.time-column button.disabled{color:#d9d9d9;cursor:not-allowed}.time-scroller{display:flex;flex-direction:column;align-items:center;max-height:80px;padding-top:4px;scroll-behavior:smooth}.time-separator{padding:8px 0;color:#999;font-weight:700}.time-picker-footer{display:flex;justify-content:space-between;padding:8px;border-top:1px solid #f0f0f0}button{padding:4px 15px;border-radius:4px;border:1px solid #d9d9d9;background:white;cursor:pointer;font-size:14px;font-family:inherit}.save-btn{background:#3C44FE;border-color:#3c44fe;color:#fff}.save-btn:hover{background:#40a9ff;border-color:#40a9ff}.cancel-btn:hover{border-color:#40a9ff;color:#40a9ff}.footer-buttons,.footer-actions{display:flex;gap:8px}.now-btn{color:#3c44fe;border-color:transparent;background:transparent;box-shadow:none;padding-left:0}.now-btn:hover{color:#40a9ff}.embedded-time-picker.time-picker-popup .time-picker-content{height:100%}.embedded-time-picker.time-picker-popup-column{max-height:37px!important;width:100%}.embedded-time-picker.time-picker-popup.inline{border:none;border-radius:0;height:286px;max-height:290px;direction:ltr}\n"], dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: OverlayModule }, { kind: "directive", type: i4.CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: NzConnectedOverlayDirective, selector: "[cdkConnectedOverlay][nzConnectedOverlay]", inputs: ["nzArrowPointAtCenter"], exportAs: ["nzConnectedOverlay"] }, { kind: "directive", type: DateMaskDirective, selector: "[asa-dateMask]", inputs: ["asa-dateMask", "disableInputMask"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], animations: [slideMotion], changeDetection: i0.ChangeDetectionStrategy.OnPush }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TimePickerComponent, decorators: [{ type: Component, args: [{ selector: 'asa-time-picker', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ NgIf, NgFor, ReactiveFormsModule, NgTemplateOutlet, OverlayModule, NzConnectedOverlayDirective, DateMaskDirective, NgClass ], providers: [ AsaDatePickerService, { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TimePickerComponent), multi: true } ], host: { '(click)': 'open()' }, animations: [slideMotion], template: "<div class=\"time-picker-wrapper\" [ngClass]=\"{\r\n 'time-picker-wrapper-column':isTimerVertical}\"\r\n [formGroup]=\"form\">\r\n <!-- Regular input mode -->\r\n <ng-container *ngIf=\"!inline\">\r\n <div class=\"input-wrapper\" [class.focus]=\"isOpen\" [class.disabled]=\"disabled\">\r\n <input\r\n #timePickerInput\r\n [asa-dateMask]=\"displayFormat\"\r\n [disableInputMask]=\"disableInputMask\"\r\n [class.disabled]=\"disabled\"\r\n type=\"text\"\r\n class=\"time-picker-input\"\r\n [class.focus]=\"isOpen\"\r\n formControlName=\"timeInput\"\r\n (focus)=\"onFocusInput()\"\r\n [placeholder]=\"placeholder\"\r\n [readonly]=\"readOnly || readOnlyInput\"\r\n [attr.disabled]=\"disabled? 'disabled':null\"\r\n >\r\n <button *ngIf=\"showIcon\" class=\"time-button\" (click)=\"toggleTimePicker($event)\" tabindex=\"-1\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#999\"\r\n stroke-width=\"2\">\r\n <circle cx=\"12\" cy=\"12\" r=\"10\"/>\r\n <path d=\"M12 6v6l4 2\"/>\r\n </svg>\r\n </button>\r\n </div>\r\n\r\n <ng-template\r\n cdkConnectedOverlay\r\n nzConnectedOverlay\r\n [cdkConnectedOverlayOrigin]=\"origin\"\r\n [cdkConnectedOverlayOpen]=\"isOpen\"\r\n [cdkConnectedOverlayPositions]=\"overlayPositions\"\r\n [cdkConnectedOverlayTransformOriginOn]=\"'.time-picker-popup'\"\r\n [cdkConnectedOverlayHasBackdrop]=\"false\"\r\n (positionChange)=\"onPositionChange($event)\"\r\n (detach)=\"close()\"\r\n >\r\n <ng-container *ngTemplateOutlet=\"timePickerContent\"></ng-container>\r\n </ng-template>\r\n </ng-container>\r\n\r\n <!-- Inline mode -->\r\n <ng-container *ngIf=\"inline\">\r\n <ng-container *ngTemplateOutlet=\"timePickerContent\"></ng-container>\r\n </ng-container>\r\n\r\n <!-- Time Picker Content Template -->\r\n <ng-template #timePickerContent>\r\n <div\r\n #popupWrapper\r\n [class]=\"'time-picker-popup ' + cssClass\"\r\n [ngClass]=\"{'time-picker-popup-column':isTimerVertical}\"\r\n [@slideMotion]=\"'enter'\"\r\n [class.inline]=\"inline\"\r\n [class.disabled]=\"disabled\"\r\n style=\"position: relative\"\r\n (click)=\"$event.stopPropagation()\"\r\n >\r\n <div class=\"time-picker-content\">\r\n <div class=\"time-columns\">\r\n <!-- Hours -->\r\n <div class=\"time-column\">\r\n <div class=\"time-scroller\" (wheel)=\"isTimerVertical?onTimeScroll($event, 'hour'):null\">\r\n <button\r\n *ngFor=\"let hour of hours\"\r\n [id]=\"'selector_h'+hour\"\r\n [class.selected]=\"selectedTime.hour === hour\"\r\n [class.disabled]=\"isHourDisabled(hour)\"\r\n (click)=\"selectHour(hour)\"\r\n (focus)=\"onTimeButtonFocus('hour')\"\r\n [tabindex]=\"selectedTime.hour === hour ? 0 : -1\"\r\n type=\"button\"\r\n >\r\n {{ hour.toString().padStart(2, '0') }}\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"time-separator\">:</div>\r\n\r\n <!-- Minutes -->\r\n <div class=\"time-column\">\r\n <div (wheel)=\"isTimerVertical?onTimeScroll($event, 'minute'):null\" class=\"time-scroller\">\r\n <button\r\n *ngFor=\"let minute of minutes\"\r\n [id]=\"'selector_m'+minute\"\r\n [class.selected]=\"selectedTime.minute === minute\"\r\n [class.disabled]=\"isMinuteDisabled(minute)\"\r\n (focus)=\"onTimeButtonFocus('minute')\"\r\n [tabindex]=\"selectedTime.minute === minute ? 0 : -1\"\r\n (click)=\"selectMinute(minute)\"\r\n type=\"button\"\r\n >\r\n {{ minute.toString().padStart(2, '0') }}\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Seconds (if format includes seconds) -->\r\n <ng-container *ngIf=\"showSeconds\">\r\n <div class=\"time-separator\">:</div>\r\n <div class=\"time-column\">\r\n <div (wheel)=\"isTimerVertical?onTimeScroll($event, 'second'):null\" class=\"time-scroller\">\r\n <button\r\n *ngFor=\"let second of seconds\"\r\n [id]=\"'selector_s'+second\"\r\n [class.selected]=\"selectedTime.second === second\"\r\n [class.disabled]=\"isSecondDisabled(second)\"\r\n (click)=\"selectSecond(second)\"\r\n (focus)=\"onTimeButtonFocus('second')\"\r\n [tabindex]=\"selectedTime.second === second ? 0 : -1\"\r\n type=\"button\"\r\n >\r\n {{ second.toString().padStart(2, '0') }}\r\n </button>\r\n </div>\r\n </div>\r\n </ng-container>\r\n\r\n <!-- AM/PM (only in 12-hour format) -->\r\n <ng-container *ngIf=\"timeFormat === '12'\">\r\n <div class=\"time-column period\">\r\n <button\r\n *ngFor=\"let period of periods\"\r\n [class.selected]=\"selectedTime.period === period\"\r\n (click)=\"selectPeriod(period)\"\r\n type=\"button\"\r\n >\r\n {{ period }}\r\n </button>\r\n </div>\r\n </ng-container>\r\n </div>\r\n </div>\r\n\r\n <div class=\"time-picker-footer\" *ngIf=\"!inline\">\r\n <div class=\"footer-buttons\">\r\n <button class=\"now-btn\" (click)=\"selectNow()\" type=\"button\">{{ lang.now }}</button>\r\n </div>\r\n <div class=\"footer-actions\">\r\n <button class=\"save-btn\" (click)=\"save()\" type=\"button\">{{ lang.ok }}</button>\r\n </div>\r\n </div>\r\n </div>\r\n </ng-template>\r\n</div>\r\n", styles: [":host *{font-family:inherit;font-weight:400;box-sizing:border-box;padding:0;margin:0}.time-picker-wrapper{display:inline-block}.time-picker-wrapper-column{width:100%;padding-top:0;padding-bottom:0}.input-wrapper{position:relative;display:inline-flex;align-items:center;border:1px solid #d9d9d9;border-radius:4px}.input-wrapper.focus{border-color:#40a9ff;box-shadow:0 0 0 2px #1890ff33;outline:none}.input-wrapper.disabled{background:#fafafa}input:focus{outline:none}input:hover{border-color:#40a9ff}input.time-picker-input{font-family:inherit;width:100%;padding:6px;border:none;border-radius:4px;font-size:14px;transition:all .3s}.time-button{background:none;border:none;cursor:pointer;padding:4px 4px 0}.time-picker-popup{background:white;border-radius:8px;box-shadow:0 6px 16px #00000014;width:fit-content;min-width:200px;overflow:hidden;margin-block:3px;height:40vh;max-height:280px}.time-picker-popup.inline{box-shadow:none;margin:0;border:1px solid #d9d9d9}.time-picker-popup.inline .time-picker-content{padding:4px;height:100%}.time-picker-popup.inline .time-column::-webkit-scrollbar{width:6px}.time-picker-popup.inline .time-column::-webkit-scrollbar-thumb{background:#d9d9d9;border-radius:3px}.time-picker-popup.inline .time-column::-webkit-scrollbar-track{background:#f5f5f5}.time-picker-header{padding:16px;font-size:16px;font-weight:500;border-bottom:1px solid #f0f0f0}.time-picker-content{padding:0 8px;height:calc(100% - 59px)}.time-columns{display:flex;align-items:center;justify-content:center;padding:0 8px;gap:8px;height:100%}.time-column{flex:1;height:100%;overflow-y:auto;scrollbar-width:none}.time-column::-webkit-scrollbar{display:none}.time-column button{width:100%;padding:6px 8px;background:none;border:none;cursor:pointer;color:#666;font-size:14px;border-radius:4px}.time-column button:hover:not(.disabled){background:#dcf7ff}.time-column button.selected{background:#c8eeff;color:#000}.time-column button.disabled{color:#d9d9d9;cursor:not-allowed}.time-scroller{display:flex;flex-direction:column;align-items:center;max-height:80px;padding-top:4px;scroll-behavior:smooth}.time-separator{padding:8px 0;color:#999;font-weight:700}.time-picker-footer{display:flex;justify-content:space-between;padding:8px;border-top:1px solid #f0f0f0}button{padding:4px 15px;border-radius:4px;border:1px solid #d9d9d9;background:white;cursor:pointer;font-size:14px;font-family:inherit}.save-