asa-date-picker
Version:
An Angular date picker component supporting Gregorian and Persian (Jalali/Shamsi) calendars, powered by Asa Co.
735 lines • 130 kB
JavaScript
/**
* 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-