UNPKG

@qeydar/datepicker

Version:

A comprehensive Date and Time Picker for Angular with Jalali calendar support

932 lines (916 loc) 108 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 { QeydarDatePickerService } from '../date-picker.service'; import { DEFAULT_DATE_PICKER_POSITIONS, NzConnectedOverlayDirective } from "../utils/overlay/overlay"; import { 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 "../date-picker.service"; import * as i3 from "../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.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 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; this.scrollToTime(); } } 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(); if (event.key === 'Tab') this.close(); } else if (event.key === 'Escape') { this.close(); } } 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); const formattedTime = this.dateAdapter.format(normalizedDate, this._displayFormat); this.form.get('timeInput')?.setValue(formattedTime, { emitEvent: false }); this.parseTimeString(normalizedDate); const outputValue = this.valueType === 'date' ? normalizedDate : formattedTime; this._value = outputValue; this.onChange(outputValue); 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(); } } 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.QeydarDatePickerService }, { 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: "qeydar-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", displayFormat: "displayFormat", selectedDate: "selectedDate" }, outputs: { timeChange: "timeChange", openChange: "openChange" }, host: { listeners: { "click": "open()", "keydown": "handleKeydown($event)" } }, providers: [ QeydarDatePickerService, { 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" [formGroup]="form"> <!-- Regular input mode --> <ng-container *ngIf="!inline"> <div class="input-wrapper" [class.focus]="isOpen" [class.disabled]="disabled"> <input #timePickerInput [qeydar-dateMask]="displayFormat" [disableInputMask]="disableInputMask" [class.disabled]="disabled" type="text" class="time-picker-input" [class.focus]="isOpen" formControlName="timeInput" (focus)="onFocusInput()" [placeholder]="placeholder" [readonly]="readOnly || readOnlyInput" [attr.disabled]="disabled? 'disabled':null" > <button *ngIf="showIcon" class="time-button" (click)="toggleTimePicker($event)" tabindex="-1"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2"> <circle cx="12" cy="12" r="10"/> <path d="M12 6v6l4 2"/> </svg> </button> </div> <ng-template cdkConnectedOverlay nzConnectedOverlay [cdkConnectedOverlayOrigin]="origin" [cdkConnectedOverlayOpen]="isOpen" [cdkConnectedOverlayPositions]="overlayPositions" [cdkConnectedOverlayTransformOriginOn]="'.time-picker-popup'" [cdkConnectedOverlayHasBackdrop]="false" (positionChange)="onPositionChange($event)" (detach)="close()" > <ng-container *ngTemplateOutlet="timePickerContent"></ng-container> </ng-template> </ng-container> <!-- Inline mode --> <ng-container *ngIf="inline"> <ng-container *ngTemplateOutlet="timePickerContent"></ng-container> </ng-container> <!-- Time Picker Content Template --> <ng-template #timePickerContent> <div #popupWrapper [class]="'time-picker-popup ' + cssClass" [@slideMotion]="'enter'" [class.inline]="inline" [class.disabled]="disabled" style="position: relative" (click)="$event.stopPropagation()" > <div class="time-picker-content"> <div class="time-columns"> <!-- Hours --> <div class="time-column"> <div class="time-scroller"> <button *ngFor="let hour of hours" [id]="'selector_h'+hour" [class.selected]="selectedTime.hour === hour" [class.disabled]="isHourDisabled(hour)" (click)="selectHour(hour)" type="button" > {{ hour.toString().padStart(2, '0') }} </button> </div> </div> <div class="time-separator">:</div> <!-- Minutes --> <div class="time-column"> <div class="time-scroller"> <button *ngFor="let minute of minutes" [id]="'selector_m'+minute" [class.selected]="selectedTime.minute === minute" [class.disabled]="isMinuteDisabled(minute)" (click)="selectMinute(minute)" type="button" > {{ minute.toString().padStart(2, '0') }} </button> </div> </div> <!-- Seconds (if format includes seconds) --> <ng-container *ngIf="showSeconds"> <div class="time-separator">:</div> <div class="time-column"> <div class="time-scroller"> <button *ngFor="let second of seconds" [id]="'selector_s'+second" [class.selected]="selectedTime.second === second" [class.disabled]="isSecondDisabled(second)" (click)="selectSecond(second)" type="button" > {{ second.toString().padStart(2, '0') }} </button> </div> </div> </ng-container> <!-- AM/PM (only in 12-hour format) --> <ng-container *ngIf="timeFormat === '12'"> <div class="time-column period"> <button *ngFor="let period of periods" [class.selected]="selectedTime.period === period" (click)="selectPeriod(period)" type="button" > {{ period }} </button> </div> </ng-container> </div> </div> <div class="time-picker-footer" *ngIf="!inline"> <div class="footer-buttons"> <button class="now-btn" (click)="selectNow()" type="button">{{ lang.now }}</button> </div> <div class="footer-actions"> <button class="save-btn" (click)="save()" type="button">{{ lang.ok }}</button> </div> </div> </div> </ng-template> </div> `, isInline: true, styles: [":host *{font-family:inherit;font-weight:400;box-sizing:border-box;padding:0;margin:0}.time-picker-wrapper{display:inline-block}.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-columns{padding:4px}.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:8px;height:calc(100% - 59px)}.time-columns{display:flex;align-items:center;justify-content:center;padding: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:#f5f5f5}.time-column button.selected{background:#e6f4ff;color:#1890ff}.time-column button.disabled{color:#d9d9d9;cursor:not-allowed}.time-scroller{display:flex;flex-direction:column;align-items:center}.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:#1890ff;border-color:#1890ff;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:#1890ff;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.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: "directive", type: DateMaskDirective, selector: "[qeydar-dateMask]", inputs: ["qeydar-dateMask", "disableInputMask"] }, { 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"] }], 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: 'qeydar-time-picker', changeDetection: ChangeDetectionStrategy.OnPush, template: ` <div class="time-picker-wrapper" [formGroup]="form"> <!-- Regular input mode --> <ng-container *ngIf="!inline"> <div class="input-wrapper" [class.focus]="isOpen" [class.disabled]="disabled"> <input #timePickerInput [qeydar-dateMask]="displayFormat" [disableInputMask]="disableInputMask" [class.disabled]="disabled" type="text" class="time-picker-input" [class.focus]="isOpen" formControlName="timeInput" (focus)="onFocusInput()" [placeholder]="placeholder" [readonly]="readOnly || readOnlyInput" [attr.disabled]="disabled? 'disabled':null" > <button *ngIf="showIcon" class="time-button" (click)="toggleTimePicker($event)" tabindex="-1"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2"> <circle cx="12" cy="12" r="10"/> <path d="M12 6v6l4 2"/> </svg> </button> </div> <ng-template cdkConnectedOverlay nzConnectedOverlay [cdkConnectedOverlayOrigin]="origin" [cdkConnectedOverlayOpen]="isOpen" [cdkConnectedOverlayPositions]="overlayPositions" [cdkConnectedOverlayTransformOriginOn]="'.time-picker-popup'" [cdkConnectedOverlayHasBackdrop]="false" (positionChange)="onPositionChange($event)" (detach)="close()" > <ng-container *ngTemplateOutlet="timePickerContent"></ng-container> </ng-template> </ng-container> <!-- Inline mode --> <ng-container *ngIf="inline"> <ng-container *ngTemplateOutlet="timePickerContent"></ng-container> </ng-container> <!-- Time Picker Content Template --> <ng-template #timePickerContent> <div #popupWrapper [class]="'time-picker-popup ' + cssClass" [@slideMotion]="'enter'" [class.inline]="inline" [class.disabled]="disabled" style="position: relative" (click)="$event.stopPropagation()" > <div class="time-picker-content"> <div class="time-columns"> <!-- Hours --> <div class="time-column"> <div class="time-scroller"> <button *ngFor="let hour of hours" [id]="'selector_h'+hour" [class.selected]="selectedTime.hour === hour" [class.disabled]="isHourDisabled(hour)" (click)="selectHour(hour)" type="button" > {{ hour.toString().padStart(2, '0') }} </button> </div> </div> <div class="time-separator">:</div> <!-- Minutes --> <div class="time-column"> <div class="time-scroller"> <button *ngFor="let minute of minutes" [id]="'selector_m'+minute" [class.selected]="selectedTime.minute === minute" [class.disabled]="isMinuteDisabled(minute)" (click)="selectMinute(minute)" type="button" > {{ minute.toString().padStart(2, '0') }} </button> </div> </div> <!-- Seconds (if format includes seconds) --> <ng-container *ngIf="showSeconds"> <div class="time-separator">:</div> <div class="time-column"> <div class="time-scroller"> <button *ngFor="let second of seconds" [id]="'selector_s'+second" [class.selected]="selectedTime.second === second" [class.disabled]="isSecondDisabled(second)" (click)="selectSecond(second)" type="button" > {{ second.toString().padStart(2, '0') }} </button> </div> </div> </ng-container> <!-- AM/PM (only in 12-hour format) --> <ng-container *ngIf="timeFormat === '12'"> <div class="time-column period"> <button *ngFor="let period of periods" [class.selected]="selectedTime.period === period" (click)="selectPeriod(period)" type="button" > {{ period }} </button> </div> </ng-container> </div> </div> <div class="time-picker-footer" *ngIf="!inline"> <div class="footer-buttons"> <button class="now-btn" (click)="selectNow()" type="button">{{ lang.now }}</button> </div> <div class="footer-actions"> <button class="save-btn" (click)="save()" type="button">{{ lang.ok }}</button> </div> </div> </div> </ng-template> </div> `, standalone: true, imports: [ NgIf, NgFor, ReactiveFormsModule, NgTemplateOutlet, DateMaskDirective, OverlayModule, NzConnectedOverlayDirective ], providers: [ QeydarDatePickerService, { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TimePickerComponent), multi: true } ], host: { '(click)': 'open()' }, animations: [slideMotion], styles: [":host *{font-family:inherit;font-weight:400;box-sizing:border-box;padding:0;margin:0}.time-picker-wrapper{display:inline-block}.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-columns{padding:4px}.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:8px;height:calc(100% - 59px)}.time-columns{display:flex;align-items:center;justify-content:center;padding: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:#f5f5f5}.time-column button.selected{background:#e6f4ff;color:#1890ff}.time-column button.disabled{color:#d9d9d9;cursor:not-allowed}.time-scroller{display:flex;flex-direction:column;align-items:center}.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:#1890ff;border-color:#1890ff;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:#1890ff;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.inline{border:none;border-radius:0;height:286px;max-height:290px;direction:ltr}\n"] }] }], ctorParameters: function () { return [{ type: i1.FormBuilder }, { type: i0.ElementRef }, { type: i0.ChangeDetectorRef }, { type: i2.QeydarDatePickerService }, { type: i3.JalaliDateAdapter }, { type: i3.GregorianDateAdapter }]; }, propDecorators: { placeholder: [{ type: Input }], rtl: [{ type: Input }], placement: [{ type: Input }], minTime: [{ type: Input }], maxTime: [{ type: Input }], lang: [{ type: Input }], valueType: [{ type: Input }], cssClass: [{ type: Input }], showIcon: [{ type: Input }], dateAdapter: [{ type: Input }], inline: [{ type: Input }], disableInputMask: [{ type: Input }], disabled: [{ type: Input }], disabledTimesFilter: [{ type: Input }], allowEmpty: [{ type: Input }], readOnly: [{ type: Input }], readOnlyInput: [{ type: Input }], displayFormat: [{ type: Input }], selectedDate: [{ type: Input }], timeChange: [{ type: Output }], openChange: [{ type: Output }], timePickerInput: [{ type: ViewChild, args: ['timePickerInput'] }], popupWrapper: [{ type: ViewChild, args: ['popupWrapper'] }], handleKeydown: [{ type: HostListener, args: ['keydown', ['$event']] }] } }); //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGltZS1waWNrZXIuY29tcG9uZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vcHJvamVjdHMvcWV5ZGFyLWRhdGVwaWNrZXIvc3JjL3RpbWUtcGlja2VyL3RpbWUtcGlja2VyLmNvbXBvbmVudC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7R0FXRztBQUNILE9BQU8sRUFBRSxTQUFTLEVBQWMsVUFBVSxFQUFFLEtBQUssRUFBVSxNQUFNLEVBQUUsWUFBWSxFQUFFLFNBQVMsRUFBYSxZQUFZLEVBQStDLHVCQUF1QixFQUFFLE1BQU0sZUFBZSxDQUFDO0FBQ2pOLE9BQU8sRUFBd0IsaUJBQWlCLEVBQTBCLG1CQUFtQixFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFDdEgsT0FBTyxFQUFFLGdCQUFnQixFQUFrQyxhQUFhLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUN2RyxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFFdkQsT0FBTyxFQUFFLHVCQUF1QixFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFHakUsT0FBTyxFQUFFLDZCQUE2QixFQUFFLDJCQUEyQixFQUFFLE1BQU0sMEJBQTBCLENBQUE7QUFDckcsT0FBTyxFQUFFLEtBQUssRUFBRSxJQUFJLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUNoRSxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSwrQkFBK0IsQ0FBQzs7Ozs7O0FBMEtsRSxNQUFNLE9BQU8sbUJBQW1CO0lBcUU5QixZQUNTLEVBQWUsRUFDZixVQUFzQixFQUN0QixLQUF3QixFQUN4QixpQkFBMEMsRUFDMUMsYUFBZ0MsRUFDaEMsZ0JBQXNDO1FBTHRDLE9BQUUsR0FBRixFQUFFLENBQWE7UUFDZixlQUFVLEdBQVYsVUFBVSxDQUFZO1FBQ3RCLFVBQUssR0FBTCxLQUFLLENBQW1CO1FBQ3hCLHNCQUFpQixHQUFqQixpQkFBaUIsQ0FBeUI7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