@qeydar/datepicker
Version:
A comprehensive Date and Time Picker for Angular with Jalali calendar support
1,033 lines • 159 kB
JavaScript
import { Component, forwardRef, Input, ViewChild, Output, EventEmitter, Inject, ViewChildren, ChangeDetectionStrategy, ContentChildren, Optional } from '@angular/core';
import { NG_VALUE_ACCESSOR, ReactiveFormsModule, FormsModule } from '@angular/forms';
import { slideMotion } from './utils/animation/slide';
import { DATE_ADAPTER } from './date-adapter';
import { DatePickerPopupComponent } from './date-picker-popup/date-picker-popup.component';
import { CdkOverlayOrigin, OverlayModule } from '@angular/cdk/overlay';
import { DATE_PICKER_POSITION_MAP, DEFAULT_DATE_PICKER_POSITIONS, NzConnectedOverlayDirective } from './utils/overlay/overlay';
import { DOCUMENT, NgIf, NgTemplateOutlet } from '@angular/common';
import { DestroyService, QeydarDatePickerService } from './date-picker.service';
import { fromEvent, takeUntil } from 'rxjs';
import { DateMaskDirective } from './utils/input-mask.directive';
import { CustomTemplate } from './utils/template.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 DatePickerComponent {
constructor(fb, elementRef, renderer, cdref, dpService, destroy$, ngZone, jalali, gregorian, doc, injectedDateAdapter) {
this.fb = fb;
this.elementRef = elementRef;
this.renderer = renderer;
this.cdref = cdref;
this.dpService = dpService;
this.destroy$ = destroy$;
this.ngZone = ngZone;
this.jalali = jalali;
this.gregorian = gregorian;
this.injectedDateAdapter = injectedDateAdapter;
// ========== Input Properties ==========
this.rtl = false;
this.mode = 'day';
this.isRange = false;
this.calendarType = 'gregorian';
this.cssClass = '';
this.footerDescription = '';
this.placement = 'bottomRight';
this.disabled = false;
this.isInline = false;
this.showSidebar = true;
this.showToday = false;
this.valueFormat = 'gregorian';
this.disableInputMask = false;
this.disabledDates = [];
this.allowEmpty = false;
this.readOnly = false;
this.readOnlyInput = false;
this.dateAdapter = null;
// ========== Output Properties ==========
this.onFocus = new EventEmitter();
this.onBlur = new EventEmitter();
this.onChangeValue = new EventEmitter();
this.onOpenChange = new EventEmitter();
this.overlayPositions = [...DEFAULT_DATE_PICKER_POSITIONS];
this.currentPositionX = 'start';
this.currentPositionY = 'bottom';
this.isOpen = false;
this.selectedDate = null;
this.selectedStartDate = null;
this.selectedEndDate = null;
this.activeInput = '';
this.hideStateHelper = false;
this.isInternalChange = false;
this.lastEmittedValue = null;
this.showTimePicker = false;
this.timeDisplayFormat = 'HH:mm';
this._format = 'yyyy/MM/dd';
// ========== ControlValueAccessor Implementation ==========
this.onChange = () => { };
this.onTouch = () => { };
this.initializeComponent(doc);
}
set minDate(date) {
if (date) {
this._minDate = date;
}
}
;
get minDate() {
return this._minDate;
}
set maxDate(date) {
if (date) {
this._maxDate = date;
}
}
;
get maxDate() {
return this._maxDate;
}
set format(value) {
this._format = value;
this.showTimePicker = this.hasTimeComponent(value);
this.timeDisplayFormat = this.extractTimeFormat(value);
}
get format() {
return this._format;
}
get valueAdapter() {
return this.valueFormat == 'jalali' ? this.jalali : this.gregorian;
}
// ========== Lifecycle Hooks ==========
ngOnInit() {
this.initialize();
document.addEventListener('click', this.documentClickListener);
}
ngOnChanges(changes) {
this.handleChanges(changes);
}
ngAfterViewInit() {
this.setupAfterViewInit();
this._minDate = this.valueAdapter?.parse(this._minDate, this.extractDateFormat(this.format));
this._maxDate = this.valueAdapter?.parse(this._maxDate, this.extractDateFormat(this.format));
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
document.removeEventListener('click', this.documentClickListener);
}
// ========== Initialization Methods ==========
initializeComponent(doc) {
this.origin = new CdkOverlayOrigin(this.elementRef);
this.document = doc;
this.form = this.fb.group({
dateInput: [''],
startDateInput: [''],
endDateInput: ['']
});
this.documentClickListener = this.handleDocumentClick.bind(this);
this.lang = this.calendarType === 'jalali' ? this.dpService.locale_fa : this.dpService.locale_en;
this.dpService.locale = this.lang;
}
initialize() {
this.setDateAdapter();
this.setupFormControls();
}
setupAfterViewInit() {
this.setupActiveInputSubscription();
this.setupMouseDownEventHandler();
}
// ========== Date Adapter Methods ==========
setDateAdapter() {
// Update the injected adapter to match the selected adapter
if (this.injectedDateAdapter && !this.currentDateAdapter) {
this.currentDateAdapter = this.injectedDateAdapter;
}
// اولویت اول: اگر کاربر از طریق @Input یک dateAdapter شخصی پاس داده باشد
if (this.dateAdapter) {
this.currentDateAdapter = this.dateAdapter;
return;
}
// اولویت دوم: اگر از طریق provider یک dateAdapter شخصی تزریق شده باشد
if (this.injectedDateAdapter) {
this.currentDateAdapter = this.injectedDateAdapter;
return;
}
// اولویت سوم: انتخاب بر اساس calendarType
this.currentDateAdapter =
this.calendarType === 'jalali'
? this.jalali
: this.gregorian;
}
// ========== Form Control Methods ==========
setupFormControls() {
if (this.isRange) {
this.form.get('startDateInput')?.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => this.onInputChange(value, 'start'));
this.form.get('endDateInput')?.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => this.onInputChange(value, 'end'));
}
else {
this.form.get('dateInput')?.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => this.onInputChange(value));
}
}
// ========== Event Handlers ==========
handleChanges(changes) {
if (changes['calendarType']) {
this.setDateAdapter();
this.updateInputValue();
this.lang = this.calendarType === 'jalali' ? this.dpService.locale_fa : this.dpService.locale_en;
this.dpService.locale = this.lang;
}
if (changes['dateAdapter']) {
this.setDateAdapter();
this.updateInputValue();
}
if (changes['minDate'] || changes['maxDate']) {
this._minDate = this.valueAdapter?.parse(this._minDate, this.extractDateFormat(this.format));
this._maxDate = this.valueAdapter?.parse(this._maxDate, this.extractDateFormat(this.format));
this.form.updateValueAndValidity();
}
if (changes['mode'] || changes['isRange']) {
this.setupFormControls();
}
if (changes['placement']) {
this.setPlacement(this.placement);
}
if (changes['lang']) {
this.lang = changes['lang'].currentValue;
this.dpService.locale = this.lang;
}
if (changes['mode'] && !changes['format']) {
this.format = this.getFormatForMode();
}
if (changes['isRange'] && this.isRange == false) {
this.origin = new CdkOverlayOrigin(this.elementRef);
}
if (changes['valueFormat']) {
this.emitValueIfChanged();
}
}
handleDocumentClick(event) {
if (!this.elementRef.nativeElement.contains(event.target)) {
if (this.isOpen) {
this.close();
this.onInputBlur(this.activeInput, event);
}
this.hideStateHelper = false;
}
}
// ========== Input Handling Methods ==========
onInputChange(value, inputType) {
if (!this.isInternalChange) {
if (this.isRange) {
this.handleRangeInputChange(value, inputType);
}
else {
this.handleSingleInputChange(value);
}
this.updateDatePickerPopup();
}
}
handleRangeInputChange(value, inputType) {
const date = this.currentDateAdapter.parse(value, this.format);
if (date) {
if (inputType === 'start') {
this.selectedStartDate = this.clampDate(date);
}
else if (inputType === 'end') {
this.selectedEndDate = this.clampDate(date);
}
this.emitValueIfChanged();
}
}
handleSingleInputChange(value) {
const date = this.currentDateAdapter.parse(value, this.format);
if (date) {
this.selectedDate = this.clampDate(date);
this.emitValueIfChanged();
}
}
// ========== Value Emission Methods ==========
emitValueIfChanged() {
const newValue = this.prepareValueForEmission();
if (newValue && JSON.stringify(newValue) !== JSON.stringify(this.lastEmittedValue)) {
this.lastEmittedValue = newValue;
this.onChange(newValue);
this.onChangeValue.emit(newValue);
}
}
prepareValueForEmission() {
if (this.isRange) {
if (this.selectedStartDate && this.selectedEndDate) {
return {
start: this.convertDateToFormat(this.selectedStartDate, this.calendarType),
end: this.convertDateToFormat(this.selectedEndDate, this.calendarType)
};
}
}
else if (this.selectedDate) {
return this.convertDateToFormat(this.selectedDate, this.calendarType);
}
return null;
}
// ========== Date Selection Methods ==========
onDateSelected(date) {
const clampedDate = this.clampDate(date);
if (this.isRange) {
this.handleRangeDateSelection(clampedDate);
}
else {
this.handleSingleDateSelection(clampedDate);
}
this.hideStateHelper = true;
this.updateDatePickerPopup();
this.focus();
}
handleRangeDateSelection(date) {
if (!this.selectedStartDate || (this.selectedStartDate && this.selectedEndDate) ||
this.currentDateAdapter.isBefore(date, this.selectedStartDate)) {
this.selectedStartDate = date;
this.selectedEndDate = null;
this.form.get('startDateInput')?.setValue(this.currentDateAdapter.format(date, this.format), { emitEvent: false });
this.form.get('endDateInput')?.setValue('', { emitEvent: false });
}
else {
this.selectedEndDate = date;
this.form.get('endDateInput')?.setValue(this.currentDateAdapter.format(date, this.format), { emitEvent: false });
this.emitValueIfChanged();
this.close();
}
}
handleSingleDateSelection(date) {
this.selectedDate = date;
if (date) {
const formattedDate = this.currentDateAdapter.format(date, this.format);
this.form.get('dateInput')?.setValue(formattedDate, { emitEvent: false });
this.emitValueIfChanged();
}
this.close();
}
// ========== Date Range Methods ==========
onDateRangeSelected(dateRange) {
this.hideStateHelper = true;
this.selectedStartDate = this.clampDate(dateRange.start);
const startFormatted = this.currentDateAdapter.format(this.selectedStartDate, this.format);
this.form.get('startDateInput')?.setValue(startFormatted, { emitEvent: false });
if (dateRange.end) {
this.selectedEndDate = this.clampDate(dateRange.end);
const endFormatted = this.currentDateAdapter.format(this.selectedEndDate, this.format);
this.form.get('endDateInput')?.setValue(endFormatted, { emitEvent: false });
this.emitValueIfChanged();
if (!this.hasTimeComponent(this.format))
this.close();
this.updateDatePickerPopup();
this.focus();
}
}
// ========== UI State Methods ==========
close() {
if (this.isInline) {
return;
}
if (this.isOpen) {
this.isOpen = false;
this.onOpenChange.emit(false);
this.cdref.markForCheck();
}
}
open() {
if (this.isInline || this.isOpen || this.disabled || this.readOnly) {
return;
}
this.isOpen = true;
this.onOpenChange.emit(true);
this.focus();
this.cdref.markForCheck();
}
focus() {
const activeInputElement = this.getInput(this.activeInput);
if (this.document.activeElement !== activeInputElement) {
activeInputElement?.focus();
const length = activeInputElement?.value?.length;
activeInputElement?.setSelectionRange(length, length);
}
}
// ========== UI Helper Methods ==========
getInput(partType) {
if (this.isInline) {
return undefined;
}
return this.isRange
? partType === 'start'
? this.rangePickerInputs?.first.nativeElement
: this.rangePickerInputs?.last.nativeElement
: this.datePickerInput?.nativeElement;
}
getPlaceholder(inputType = null) {
if (inputType === 'start')
return this.lang.startDate;
if (inputType === 'end')
return this.lang.endDate;
switch (this.mode) {
case 'month': return this.lang.selectMonth;
case 'year': return this.lang.selectYear;
default: return this.lang.selectDate;
}
}
// ========== Date Validation Methods ==========
clampDate(date) {
if (!date)
return date;
let adjustedDate = this.currentDateAdapter.clone(date);
if (this.minDate && this.currentDateAdapter.isBefore(adjustedDate, this.minDate)) {
return this.minDate;
}
if (this.maxDate && this.currentDateAdapter.isAfter(adjustedDate, this.maxDate)) {
return this.maxDate;
}
if (this.isDateDisabled(adjustedDate)) {
// Find the nearest enabled date
adjustedDate = this.findNearestValidDate(adjustedDate);
}
// Preserve the original time if format includes time
adjustedDate = this.clampDateTime(adjustedDate, date);
return adjustedDate;
}
clampDateTime(adjustedDate, date) {
if (this.hasTimeComponent(this.format)) {
adjustedDate.setHours(date.getHours());
adjustedDate.setMinutes(date.getMinutes());
adjustedDate.setSeconds(date.getSeconds());
let { normalizedDate } = this.validateAndNormalizeTime(adjustedDate);
adjustedDate = normalizedDate;
}
return adjustedDate;
}
findNearestValidDate(date) {
let nextDate = this.currentDateAdapter.addDays(date, 1);
let prevDate = this.currentDateAdapter.addDays(date, -1);
while (this.isDateDisabled(nextDate) && this.isDateDisabled(prevDate)) {
nextDate = this.currentDateAdapter.addDays(nextDate, 1);
prevDate = this.currentDateAdapter.addDays(prevDate, -1);
}
// Return the first non-disabled date found
if (!this.isDateDisabled(nextDate)) {
date = nextDate;
}
else if (!this.isDateDisabled(prevDate)) {
date = prevDate;
}
return date;
}
validateAndNormalizeTime(date) {
if (!this.currentDateAdapter) {
return { isValid: false, normalizedDate: null };
}
let isValid = true;
let normalizedDate = this.currentDateAdapter.clone(date);
if (this.isTimeDisabled(normalizedDate)) {
isValid = false;
// Get start and end of the current day
const startOfDay = this.currentDateAdapter.clone(date);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = this.currentDateAdapter.clone(date);
endOfDay.setHours(23, 59, 59, 999);
// Try to find nearest valid time within the same day
const currentMinutes = date.getHours() * 60 + date.getMinutes();
const maxForwardMinutes = (24 * 60) - currentMinutes;
let validTimeFound = false;
// Check forward
for (let i = 1; i <= maxForwardMinutes; i++) {
const nextTime = this.currentDateAdapter.clone(date);
nextTime.setHours(Math.floor((currentMinutes + i) / 60), (currentMinutes + i) % 60, 0);
if (nextTime.getTime() <= endOfDay.getTime() && !this.isTimeDisabled(nextTime)) {
normalizedDate = nextTime;
validTimeFound = true;
break;
}
}
// Check backward
if (!validTimeFound)
for (let i = 1; i < currentMinutes; i++) {
const prevTime = this.currentDateAdapter.clone(date);
prevTime.setHours(Math.floor((currentMinutes - i) / 60), (currentMinutes - i) % 60, 0);
if (prevTime.getTime() >= startOfDay.getTime() && !this.isTimeDisabled(prevTime)) {
normalizedDate = prevTime;
break;
}
}
// If no valid time found in the current day, set to start of day
if (this.isTimeDisabled(normalizedDate)) {
normalizedDate = startOfDay;
}
}
return { isValid: isValid, normalizedDate };
}
parseDisabledDates() {
return this.disabledDates.map(date => {
if (date instanceof Date) {
return this.currentDateAdapter.startOfDay(date);
}
const parsedDate = this.currentDateAdapter.parse(date, this.extractDateFormat(this.format));
return parsedDate || null;
}).filter(date => date !== null);
}
isDateDisabled(date) {
if (!date)
return false;
const dateToCheck = this.currentDateAdapter.startOfDay(date);
// Check if date is in disabled dates array
const parsedDisabledDates = this.parseDisabledDates();
const isDisabledDate = parsedDisabledDates.some(disabledDate => this.currentDateAdapter.isSameDay(dateToCheck, disabledDate));
// Check custom filter function if provided
const isFilterDisabled = this.disabledDatesFilter ?
this.disabledDatesFilter(dateToCheck) :
false;
return isDisabledDate || isFilterDisabled;
}
isTimeDisabled(date) {
return this.disabledTimesFilter ? this.disabledTimesFilter(date) : false;
}
// ========== Date Validation Methods (continued) ==========
dateFormatValidator(control) {
const value = control.value;
if (!value)
return null;
const format = this.getFormatForMode();
if (!this.currentDateAdapter.isValidFormat(value, format)) {
return { invalidFormat: true };
}
return null;
}
getFormatForMode() {
switch (this.mode) {
case 'year':
return 'yyyy';
case 'month':
return 'yyyy/MM';
default:
return this.format;
}
}
// ========== Overlay Position Methods ==========
setPlacement(placement) {
const position = DATE_PICKER_POSITION_MAP[placement];
this.overlayPositions = [position, ...DEFAULT_DATE_PICKER_POSITIONS];
this.currentPositionX = position.originX;
this.currentPositionY = position.originY;
}
onPositionChange(position) {
if (this.currentPositionX !== position.connectionPair.originX ||
this.currentPositionY !== position.connectionPair.originY) {
this.currentPositionX = position.connectionPair.originX;
this.currentPositionY = position.connectionPair.originY;
this.cdref.markForCheck();
}
}
// ========== Input Event Handlers ==========
onFocusout(event) {
event.preventDefault();
this.onTouch();
if (!this.elementRef.nativeElement.contains(event.relatedTarget) &&
!this.datePickerPopup?.el.nativeElement.contains(event.relatedTarget)) {
this.close();
}
}
onInputBlur(inputType, event) {
const inputValue = this.getInputValue(inputType);
if (typeof inputValue === 'string' && !this.isOpen) {
const correctedValue = this.validateAndCorrectInput(inputValue);
if (correctedValue !== inputValue) {
if (inputValue) {
this.handleCorrectedValue(inputType, correctedValue);
}
else if (!this.allowEmpty) {
this.handleCorrectedValue(inputType, correctedValue);
}
else {
this.selectedDate = null;
this.onChange(inputValue);
}
}
this.onBlur.emit({
input: inputType,
event,
value: correctedValue
});
}
}
getInputValue(inputType) {
if (this.isRange) {
return inputType === 'start'
? this.form.get('startDateInput')?.value
: this.form.get('endDateInput')?.value;
}
return this.form.get('dateInput')?.value;
}
validateAndCorrectInput(value) {
let date = this.currentDateAdapter.parse(value, this.format);
if (!date) {
const today = this.currentDateAdapter.today();
date = this.clampDate(today);
}
else {
date = this.clampDate(date);
}
return this.currentDateAdapter.format(date, this.format);
}
handleCorrectedValue(inputType, correctedValue) {
this.isInternalChange = true;
if (this.isRange) {
this.handleRangeCorrectedValue(inputType, correctedValue);
}
else {
this.handleSingleCorrectedValue(correctedValue);
}
this.isInternalChange = false;
}
handleRangeCorrectedValue(inputType, correctedValue) {
if (inputType === 'start') {
this.form.get('startDateInput')?.setValue(correctedValue);
this.selectedStartDate = this.currentDateAdapter.parse(correctedValue, this.format);
}
else {
this.form.get('endDateInput')?.setValue(correctedValue);
this.selectedEndDate = this.currentDateAdapter.parse(correctedValue, this.format);
}
if (this.selectedStartDate && this.selectedEndDate) {
this.onChange({
start: this.currentDateAdapter.format(this.selectedStartDate, this.format),
end: this.currentDateAdapter.format(this.selectedEndDate, this.format)
});
}
if (this.datePickerPopup) {
this.datePickerPopup.selectedStartDate = this.selectedStartDate;
this.datePickerPopup.selectedEndDate = this.selectedEndDate;
this.datePickerPopup.generateCalendar();
}
}
handleSingleCorrectedValue(correctedValue) {
this.form.get('dateInput')?.setValue(correctedValue);
this.selectedDate = this.currentDateAdapter.parse(correctedValue, this.format);
this.onChange(this.selectedDate);
if (this.datePickerPopup) {
this.datePickerPopup.selectedDate = this.selectedDate;
}
}
onFocusInput(inputType, event) {
if (this.hideStateHelper == false) {
this.toggleDatePicker(inputType, event);
this.hideStateHelper = true;
}
else {
this.hideStateHelper = false;
}
}
toggleDatePicker(inputType, event) {
this.onFocus.emit({
input: inputType,
event
});
this.activeInput = inputType;
this.dpService.activeInput$.next(this.activeInput);
this.open();
this.cdref.detectChanges();
}
onInputKeydown(event) {
if ((!event.shiftKey && event.key === 'Tab') || (!event.shiftKey && event.key === 'Enter')) {
if (this.isRange) {
return;
}
this.close();
}
}
// ========== Update Methods ==========
updateInputValue() {
if (this.isRange) {
if (this.selectedStartDate) {
this.form.get('startDateInput')?.setValue(this.currentDateAdapter.format(this.selectedStartDate, this.format));
}
if (this.selectedEndDate) {
this.form.get('endDateInput')?.setValue(this.currentDateAdapter.format(this.selectedEndDate, this.format));
}
}
else if (this.selectedDate) {
this.form.get('dateInput')?.setValue(this.currentDateAdapter.format(this.selectedDate, this.format));
}
}
updateDatePickerPopup() {
if (this.datePickerPopup) {
if (this.isRange) {
this.datePickerPopup.selectedStartDate = this.selectedStartDate;
this.datePickerPopup.selectedEndDate = this.selectedEndDate;
if (this.showTimePicker) {
this.datePickerPopup.timePicker.updateFromDate(this.activeInput == 'start' ?
this.selectedStartDate :
this.selectedEndDate);
this.datePickerPopup.timePicker.scrollToTime();
}
}
else {
this.datePickerPopup.selectedDate = this.selectedDate;
if (this.showTimePicker) {
this.datePickerPopup.timePicker.updateFromDate(this.selectedDate);
this.datePickerPopup.timePicker.scrollToTime();
}
}
this.datePickerPopup.generateCalendar();
this.cdref.detectChanges();
}
}
convertDateToFormat(date, fromType) {
if (!date)
return null;
switch (this.valueFormat) {
case 'date':
return date;
case 'jalali':
return this.jalali.format(date, this.format);
case 'gregorian':
return this.gregorian.format(date, this.format);
default:
return this.currentDateAdapter.format(date, this.format);
}
}
writeValue(value) {
if (value) {
this.isInternalChange = true;
if (this.isRange && typeof value === 'object') {
const startDate = this.parseIncomingValue(value.start);
const endDate = this.parseIncomingValue(value.end);
if (startDate) {
this.selectedStartDate = startDate;
this.form.get('startDateInput')?.setValue(this.currentDateAdapter.format(startDate, this.format), { emitEvent: false });
}
if (endDate) {
this.selectedEndDate = endDate;
this.form.get('endDateInput')?.setValue(this.currentDateAdapter.format(endDate, this.format), { emitEvent: false });
}
}
else {
const parsedDate = this.parseIncomingValue(value);
if (parsedDate) {
this.selectedDate = parsedDate;
this.form.get('dateInput')?.setValue(this.currentDateAdapter.format(parsedDate, this.format), { emitEvent: false });
}
}
this.lastEmittedValue = value;
this.isInternalChange = false;
this.updateDatePickerPopup();
this.cdref.markForCheck();
}
else {
this.resetValues();
}
}
resetValues() {
this.isInternalChange = true;
this.selectedDate = null;
this.selectedStartDate = null;
this.selectedEndDate = null;
this.form.get('dateInput')?.setValue('', { emitEvent: false });
this.form.get('startDateInput')?.setValue('', { emitEvent: false });
this.form.get('endDateInput')?.setValue('', { emitEvent: false });
this.lastEmittedValue = null;
this.isInternalChange = false;
this.updateDatePickerPopup();
}
registerOnChange(fn) {
this.onChange = fn;
}
registerOnTouched(fn) {
this.onTouch = fn;
}
// ========== Setup Methods ==========
setupActiveInputSubscription() {
this.dpService.activeInput$
.pipe(takeUntil(this.destroy$))
.subscribe((active) => {
this.activeInput = active;
if (active) {
if (!this.isOpen)
this.origin = new CdkOverlayOrigin(this.activeInput == 'start' ? this.rangePickerInputs?.first : this.rangePickerInputs.last);
this.focus();
}
});
}
setupMouseDownEventHandler() {
this.ngZone.runOutsideAngular(() => fromEvent(this.elementRef.nativeElement, 'mousedown')
.pipe(takeUntil(this.destroy$))
.subscribe((event) => {
if (event.target.tagName.toLowerCase() !== 'input') {
event.preventDefault();
}
}));
}
parseDateValue(value) {
if (value instanceof Date) {
return value;
}
return this.currentDateAdapter.parse(value, this.format);
}
parseValueFromFormat(value, targetAdapter) {
if (!value)
return null;
if (value instanceof Date)
return value;
return targetAdapter.parse(value, this.format);
}
parseIncomingValue(value) {
if (!value)
return null;
if (value instanceof Date)
return value;
let parsedDate = null;
// try with value adapter
parsedDate = this.valueAdapter.parse(value, this.format);
if (parsedDate)
return parsedDate;
return null;
}
// ========== Time Methods ==========
hasTimeComponent(format) {
return /[Hh]|[m]|[s]|[a]/g.test(format);
}
extractTimeFormat(format) {
const timeMatch = format.match(/[Hh]{1,2}:mm(?::ss)?(?:\s*[aA])?/);
return timeMatch ? timeMatch[0] : 'HH:mm';
}
extractDateFormat(format) {
const dateFormatMatch = format.match(/[yMd\/.-]+/);
return dateFormatMatch ? dateFormatMatch[0] : '';
}
}
DatePickerComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: DatePickerComponent, deps: [{ token: i1.FormBuilder }, { token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.ChangeDetectorRef }, { token: i2.QeydarDatePickerService }, { token: i2.DestroyService }, { token: i0.NgZone }, { token: i3.JalaliDateAdapter }, { token: i3.GregorianDateAdapter }, { token: DOCUMENT }, { token: DATE_ADAPTER, optional: true }], target: i0.ɵɵFactoryTarget.Component });
DatePickerComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: DatePickerComponent, isStandalone: true, selector: "qeydar-date-picker", inputs: { rtl: "rtl", mode: "mode", isRange: "isRange", customLabels: "customLabels", calendarType: "calendarType", lang: "lang", cssClass: "cssClass", footerDescription: "footerDescription", rangeInputLabels: "rangeInputLabels", inputLabel: "inputLabel", placement: "placement", disabled: "disabled", isInline: "isInline", showSidebar: "showSidebar", showToday: "showToday", valueFormat: "valueFormat", disableInputMask: "disableInputMask", disabledDates: "disabledDates", disabledDatesFilter: "disabledDatesFilter", disabledTimesFilter: "disabledTimesFilter", allowEmpty: "allowEmpty", readOnly: "readOnly", readOnlyInput: "readOnlyInput", dateAdapter: "dateAdapter", minDate: "minDate", maxDate: "maxDate", format: "format" }, outputs: { onFocus: "onFocus", onBlur: "onBlur", onChangeValue: "onChangeValue", onOpenChange: "onOpenChange" }, host: { properties: { "class.qeydar-datepicker": "true", "class.qeydar-datepicker-rtl": "rtl" } }, providers: [
DestroyService,
QeydarDatePickerService,
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DatePickerComponent),
multi: true
}
], queries: [{ propertyName: "templates", predicate: CustomTemplate }], viewQueries: [{ propertyName: "datePickerInput", first: true, predicate: ["datePickerInput"], descendants: true }, { propertyName: "datePickerPopup", first: true, predicate: DatePickerPopupComponent, descendants: true }, { propertyName: "rangePickerInputs", predicate: ["rangePickerInput"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
<div qeydarDatepickerStyles class="date-picker-wrapper" [class.date-picker-rtl]="rtl" [class.disabled]="disabled" [formGroup]="form">
<ng-container *ngIf="!isInline; else inlineMode">
<ng-container *ngIf="!isRange; else rangeMode">
<div class="input-container" [class.rtl]>
<label for="dateInput" *ngIf="inputLabel">{{ inputLabel }}</label>
<input
#datePickerInput
type="text"
formControlName="dateInput"
[qeydar-dateMask]="format"
[disableInputMask]="disableInputMask"
(click)="toggleDatePicker(null,$event)"
(focus)="onFocusInput(null,$event)"
(blur)="onInputBlur(null,$event)"
(keydown)="onInputKeydown($event)"
[class.focus]="isOpen"
[placeholder]="getPlaceholder()"
[readonly]="readOnly || readOnlyInput"
[attr.disabled]="disabled? 'disabled':null"
>
<ng-container *ngTemplateOutlet="icon"></ng-container>
</div>
</ng-container>
<ng-template #rangeMode>
<div *ngIf="rangeInputLabels" class="range-input-labels">
<div class="start-label">
<label for="startDateInput">{{ rangeInputLabels.start }}</label>
</div>
<div class="end-label">
<label for="endDateInput">{{ rangeInputLabels.end }}</label>
</div>
</div>
<div class="range-input-container">
<input
#rangePickerInput
type="text"
formControlName="startDateInput"
[qeydar-dateMask]="format"
[disableInputMask]="disableInputMask"
(click)="toggleDatePicker('start',$event)"
(focus)="onFocusInput('start',$event)"
(focusout)="onFocusout($event)"
(blur)="onInputBlur('start',$event)"
(keydown)="onInputKeydown($event)"
[class.focus]="isOpen && activeInput === 'start'"
[placeholder]="getPlaceholder('start')"
[readonly]="readOnly || readOnlyInput"
[attr.disabled]="disabled? 'disabled':null"
>
<span class="range-separator">→</span>
<input
#rangePickerInput
type="text"
formControlName="endDateInput"
[qeydar-dateMask]="format"
[disableInputMask]="disableInputMask"
(click)="toggleDatePicker('end',$event)"
(focus)="onFocusInput('end',$event)"
(focusout)="onFocusout($event)"
(blur)="onInputBlur('end',$event)"
(keydown)="onInputKeydown($event)"
[class.focus]="isOpen && activeInput === 'end'"
[placeholder]="getPlaceholder('end')"
[readonly]="readOnly || readOnlyInput"
[attr.disabled]="disabled? 'disabled':null"
>
<ng-container *ngTemplateOutlet="icon"></ng-container>
</div>
</ng-template>
<ng-template #icon>
<button class="calendar-button" (click)="toggleDatePicker(null, $event)" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" width="18px" height="18px" viewBox="0 0 24 24" fill="#999">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 2C6 1.44772 6.44772 1 7 1C7.55228 1 8 1.44772 8 2V3H16V2C16 1.44772 16.4477 1 17 1C17.5523 1 18 1.44772 18 2V3H19C20.6569 3 22 4.34315 22 6V20C22 21.6569 20.6569 23 19 23H5C3.34315 23 2 21.6569 2 20V6C2 4.34315 3.34315 3 5 3H6V2ZM16 5V6C16 6.55228 16.4477 7 17 7C17.5523 7 18 6.55228 18 6V5H19C19.5523 5 20 5.44772 20 6V9H4V6C4 5.44772 4.44772 5 5 5H6V6C6 6.55228 6.44772 7 7 7C7.55228 7 8 6.55228 8 6V5H16ZM4 11V20C4 20.5523 4.44772 21 5 21H19C19.5523 21 20 20.5523 20 20V11H4Z" fill="#999"/>
</svg>
</button>
</ng-template>
</ng-container>
<ng-template #inlineMode>
<div
class="dp-dropdown"
[class.qeydar-picker-dropdown-rtl]="rtl"
[class.qeydar-picker-dropdown-placement-bottomLeft]="currentPositionY === 'bottom' && currentPositionX === 'start'"
[class.qeydar-picker-dropdown-placement-topLeft]="currentPositionY === 'top' && currentPositionX === 'start'"
[class.qeydar-picker-dropdown-placement-bottomRight]="currentPositionY === 'bottom' && currentPositionX === 'end'"
[class.qeydar-picker-dropdown-placement-topRight]="currentPositionY === 'top' && currentPositionX === 'end'"
[class.qeydar-picker-dropdown-range]="isRange"
>
<qeydar-date-picker-popup
[rtl]="rtl"
[@slideMotion]="'enter'"
[selectedDate]="selectedDate"
[selectedStartDate]="selectedStartDate"
[selectedEndDate]="selectedEndDate"
[mode]="mode"
[isRange]="isRange"
[customLabels]="customLabels"
[dateAdapter]="currentDateAdapter"
[minDate]="minDate"
[maxDate]="maxDate"
[cssClass]="cssClass"
[footerDescription]="footerDescription"
[activeInput]="activeInput"
[showSidebar]="showSidebar"
[showToday]="showToday"
[showTimePicker]="showTimePicker"
[timeDisplayFormat]="timeDisplayFormat"
[dateFormat]="extractDateFormat(format)"
[disabledDates]="disabledDates"
[disabledDatesFilter]="disabledDatesFilter"
[disabledTimesFilter]="disabledTimesFilter"
[templates]="templates"
[readOnly]="readOnly"
(dateSelected)="onDateSelected($event)"
(dateRangeSelected)="onDateRangeSelected($event)"
(closePicker)="close()"
(clickInside)="focus()"
tabindex="-1"
[attr.disabled]="disabled? 'disabled':null"
></qeydar-date-picker-popup>
</div>
</ng-template>
<ng-template
cdkConnectedOverlay
nzConnectedOverlay
[cdkConnectedOverlayOrigin]="origin"
[cdkConnectedOverlayOpen]="isOpen"
[cdkConnectedOverlayPositions]="overlayPositions"
[cdkConnectedOverlayTransformOriginOn]="'.qeydar-picker-wrapper'"
[cdkConnectedOverlayHasBackdrop]="false"
(positionChange)="onPositionChange($event)"
(detach)="close()"
>
<div
class="qeydar-picker-wrapper"
[class.disabled]="disabled"
[@slideMotion]="'enter'"
style="position: relative;"
(click)="$event.stopPropagation()"
>
<ng-container *ngTemplateOutlet="inlineMode"></ng-container>
</div>
</ng-template>
</div>
`, isInline: true, styles: [":host.qeydar-datepicker ::ng-deep{display:block;max-width:fit-content}.date-picker-wrapper{position:relative;max-width:fit-content}input{font-family:inherit;max-width:300px;padding:6px 10px;border:1px solid #d9d9d9;border-radius:4px;font-size:14px;outline:none;transition:all .3s}input:hover{border-color:#40a9ff}input.focus{border-color:#40a9ff;box-shadow:0 0 0 2px #1890ff33;outline:none}.range-input-container{display:flex;align-items:center;border:1px solid #d9d9d9;border-radius:4px;overflow:hidden}.range-input-container input{border:none;flex:1;width:50%;padding:6px 10px;border-radius:0}.range-input-container input.focus{border-bottom:1px solid;border-color:#40a9ff;box-shadow:none!important}.range-separator{padding:0 8px;color:#999}.calendar-button{background:none;border:none;padding:4px 4px 0;cursor:pointer;font-size:16px}.range-input-labels{display:flex;justify-content:space-between;gap:10px;color:#444;padding:0 5px 5px}.end-label{width:49%}.disabled{opacity:.8;pointer-events:none}.disabled .range-input-container{background:#f3f3f3}.input-container .calendar-button{position:absolute;right:0;bottom:5px}.date-picker-rtl .input-container .calendar-button{right:auto;left:0}.input-container{display:flex;flex-direction:column;gap:3px;color:#444}.input-container.rtl{direction:rtl}// rtl :dir(rtl) .range-separator{rotate:180deg}\n"], dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { 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: "ngmodule", type: ReactiveFormsModule }, { 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: "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: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: NzConnectedOverlayDirective, selector: "[cdkConnectedOverlay][nzConnectedOverlay]", inputs: ["nzArrowPointAtCenter"], exportAs: ["nzConnectedOverlay"] }, { kind: "directive", type: DateMaskDirective, selector: "[qeydar-dateMask]", inputs: ["qeydar-dateMask", "disableInputMask"] }, { kind: "component", type: DatePickerPopupComponent, selector: "qeydar-date-picker-popup", inputs: ["rtl", "selectedDate", "selectedStartDate", "selectedEndDate", "mode", "isRange", "customLabels", "minDate", "maxDate", "cssClass", "footerDescription", "activeInput", "showSidebar", "showToday", "showTimePicker", "timeDisplayFormat", "dateFormat", "readOnly", "disabledDates", "disabledDatesFilter", "disabledTimesFilter", "templates", "dateAdapter"], outputs: ["dateSelected", "dateRangeSelected", "closePicker", "clickInside"] }], animations: [slideMotion], changeDetection: i0.ChangeDetectionStrategy.OnPush });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: DatePickerComponent, decorators: [{
type: Component,
args: [{ selector: 'qeydar-date-picker', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: `
<div qeydarDatepickerStyles class="date-picker-wrapper" [class.date-picker-rtl]="rtl" [class.disabled]="disabled" [formGroup]="form">
<ng-container *ngIf="!isInline; else inlineMode">
<ng-container *ngIf="!isRange; else rangeMode">
<div class="input-container" [class.rtl]>
<label for="dateInput" *ngIf="inputLabel">{{ inputLabel }}</label>
<input
#datePickerInput
type="text"
formControlName="dateInput"
[qeydar-dateMask]="format"
[disableInputMask]="disableInputMask"
(click)="toggleDatePicker(null,$event)"
(focus)="onFocusInput(null,$event)"
(blur)="onInputBlur(null,$event)"
(keydown)="onInputKeydown($event)"
[class.focus]="isOpen"
[placeholder]="getPlaceholder()"
[readonly]="readOnly || readOnlyInput"
[attr.disabled]="disabled? 'disabled':null"
>
<ng-container *ngTemplateOutlet="icon"></ng-container>
</div>
</ng-container>
<ng-template #rangeMode>
<div *ngIf="rangeInputLabels" class="range-input-labels">
<div class="start-label">
<label for="startDateInput">{{ rangeInputLabels.start }}</label>
</div>
<div class="end-label">
<label for="endDateInput">{{ rangeInputLabels.end }}</label>
</div>
</div>
<div class="range-input-container">
<input
#rangePickerInput
type="text"
formControlName="startDateInput"
[qeydar-dateMask]="format"
[disableInputMask]="disableInputMask"
(click)="toggleDatePicker('start',$event)"
(focus)="onFocusInput('start',$event)"
(focusout)="onFocusout($event)"
(blur)="onInputBlur('start',$event)"
(keydown)="onInputKeydown($event)"
[class.focus]="isOpen && activeInput === 'start'"
[placeholder]="getPlaceholder('start')"
[readonly]="readOnly || readOnlyInput"
[attr.disabled]="disabled? 'disabled':null"
>
<span class="range-separator">→</span>
<input
#rangePickerInput
type="text"
formControlName="endDateInput"
[qeydar-dateMask]="format"
[disableInputMask]="disableInputMask"
(click)="toggleDatePicker('end',$event)"
(focus)="onFocusInput('end',$event)"
(focusout)="onFocusout($event)"
(blur)="onInputBlur('end',$event)"
(keydown)="onInputKeydown($event)"
[class.focus]="isOpen && activeInput === 'end'"
[placeholder]="getPlaceholder('end')"
[readonly]="readOnly || readOnlyInput"
[attr.disabled]="disabled? 'disabled':null"
>
<ng-container *ngTemplateOutlet="icon"></ng-container>
</div>
</ng-template>
<ng-template #icon>
<button class="calendar-button" (click)="toggleDatePicker(null, $event)" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" width="18px" height="18px" viewBox="0 0 24 24" fill="#999">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 2C6 1.44772 6.44772 1 7 1C7.