UNPKG

ngxsmk-datepicker

Version:

<!-- SEO Keywords: Angular DatePicker, Angular Date Range Picker, Lightweight Calendar Component, Angular Signals DatePicker, SSR Ready DatePicker, Zoneless Angular, A11y DatePicker, Mobile-Friendly DatePicker, Ionic DatePicker Meta Description: The m

1,288 lines (1,281 loc) 941 kB
import * as i0 from '@angular/core'; import { EventEmitter, ViewChild, Output, Input, ChangeDetectionStrategy, Component, inject, ElementRef, PLATFORM_ID, HostListener, ViewEncapsulation, signal, computed, InjectionToken, Injector, runInInjectionContext, effect, isDevMode, Injectable, forwardRef, ApplicationRef, ChangeDetectorRef, HostBinding, NgModule } from '@angular/core'; import { NgClass, DOCUMENT, isPlatformBrowser, NgTemplateOutlet, DatePipe } from '@angular/common'; import { NgControl } from '@angular/forms'; import { Subject } from 'rxjs'; function getStartOfDay(d) { return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0); } function getEndOfDay(d) { return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999); } function addMonths(d, months) { const newDate = new Date(d); const originalDay = d.getDate(); newDate.setMonth(d.getMonth() + months); // Check for overflow (e.g., Jan 31 + 1 month -> March 3). // If the date changed, it means the target month didn't have enough days. if (newDate.getDate() !== originalDay) { // Set to the last day of the previous month (which is the target month) newDate.setDate(0); } return newDate; } function subtractDays(d, days) { const newDate = new Date(d); newDate.setDate(d.getDate() - days); return newDate; } function getStartOfMonth(d) { return new Date(d.getFullYear(), d.getMonth(), 1); } function getEndOfMonth(d) { const lastDay = new Date(d.getFullYear(), d.getMonth() + 1, 0); return getEndOfDay(lastDay); } function getStartOfWeek(d, firstDayOfWeek = 0) { const date = new Date(d); const day = date.getDay(); const diff = (day < firstDayOfWeek ? 7 : 0) + day - firstDayOfWeek; date.setDate(date.getDate() - diff); return getStartOfDay(date); } function getEndOfWeek(d, firstDayOfWeek = 0) { const startOfWeek = getStartOfWeek(d, firstDayOfWeek); const endOfWeek = new Date(startOfWeek); endOfWeek.setDate(endOfWeek.getDate() + 6); return getEndOfDay(endOfWeek); } function getStartOfQuarter(d) { const quarter = Math.floor(d.getMonth() / 3); return new Date(d.getFullYear(), quarter * 3, 1); } function getEndOfQuarter(d) { const quarter = Math.floor(d.getMonth() / 3); return new Date(d.getFullYear(), (quarter + 1) * 3, 0); } function getStartOfYear(d) { return new Date(d.getFullYear(), 0, 1); } function getEndOfYear(d) { return new Date(d.getFullYear(), 11, 31); } function isSameDay(d1, d2) { if (!d1 || !d2) return false; return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate(); } function normalizeDate(date) { if (date === null || date === undefined || date === '') return null; const d = date instanceof Date ? new Date(date.getTime()) : new Date(date.toDate ? date.toDate() : date); if (isNaN(d.getTime())) return null; return d; } /** * Format a date with timezone support * @param date The date to format * @param locale The locale for formatting * @param options Intl.DateTimeFormatOptions * @param timezone Optional timezone (IANA timezone name, e.g., 'America/New_York', 'UTC', 'Europe/London') * @returns Formatted date string */ function formatDateWithTimezone(date, locale, options, timezone) { if (timezone) { const formatter = new Intl.DateTimeFormat(locale, { ...options, timeZone: timezone, }); return formatter.format(date); } return date.toLocaleString(locale, options); } /** * Parse a date string with timezone awareness * @param dateString The date string to parse * @param timezone Optional timezone for parsing (IANA timezone name) * @returns Date object (always in UTC internally) */ function parseDateWithTimezone(dateString, timezone) { if (!dateString) return null; if (timezone) { try { const date = new Date(dateString); if (isNaN(date.getTime())) { return null; } return date; } catch { return null; } } const date = new Date(dateString); return isNaN(date.getTime()) ? null : date; } /** * Convert a date from one timezone to another * @param date The date to convert * @param fromTimezone Source timezone (IANA name) * @param _toTimezone Target timezone (IANA name) - currently unused in simplified implementation * @returns New Date object (still UTC internally, but represents the time in target timezone) */ function convertTimezone(date, fromTimezone, _toTimezone) { const fromFormatter = new Intl.DateTimeFormat('en-US', { timeZone: fromTimezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }); const parts = fromFormatter.formatToParts(date); const year = parseInt(parts.find((p) => p.type === 'year')?.value || '0'); const month = parseInt(parts.find((p) => p.type === 'month')?.value || '0') - 1; const day = parseInt(parts.find((p) => p.type === 'day')?.value || '0'); const hour = parseInt(parts.find((p) => p.type === 'hour')?.value || '0'); const minute = parseInt(parts.find((p) => p.type === 'minute')?.value || '0'); const second = parseInt(parts.find((p) => p.type === 'second')?.value || '0'); const dateString = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}T${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}:${String(second).padStart(2, '0')}`; const result = new Date(dateString); result.setMilliseconds(date.getMilliseconds()); return result; } /** * Get the current timezone offset in minutes for a given timezone * @param timezone IANA timezone name * @param date Optional date to check offset for (defaults to now) * @returns Offset in minutes from UTC */ function getTimezoneOffset(timezone, date = new Date()) { try { const formatter = new Intl.DateTimeFormat('en-US', { timeZone: timezone, timeZoneName: 'longOffset', }); const parts = formatter.formatToParts(date); const offsetPart = parts.find((p) => p.type === 'timeZoneName'); if (offsetPart) { const offsetStr = offsetPart.value.replace('GMT', '').trim(); const sign = offsetStr[0] === '-' ? -1 : 1; const [hours = 0, minutes = 0] = offsetStr.slice(1).split(':').map(Number); return sign * (hours * 60 + minutes); } } catch { } return date.getTimezoneOffset(); } /** * Check if a timezone string is valid * @param timezone IANA timezone name * @returns true if valid, false otherwise */ function isValidTimezone(timezone) { if (!timezone) return false; try { Intl.DateTimeFormat(undefined, { timeZone: timezone }); return true; } catch { return false; } } function generateRecurringDates(config) { const dates = []; const interval = config.interval || 1; const startDate = getStartOfDay(config.startDate); const endDate = config.endDate ? getEndOfDay(config.endDate) : null; const currentDate = new Date(startDate); let count = 0; const maxOccurrences = config.occurrences || (endDate ? 365 : 10); switch (config.pattern) { case 'daily': { while (count < maxOccurrences) { if (endDate && currentDate > endDate) break; dates.push(new Date(currentDate)); currentDate.setDate(currentDate.getDate() + interval); count++; } break; } case 'weekly': { if (config.dayOfWeek === undefined) { const targetDay = startDate.getDay(); while (count < maxOccurrences) { if (endDate && currentDate > endDate) break; const dayOfWeek = currentDate.getDay(); const daysUntilTarget = (targetDay - dayOfWeek + 7) % 7; if (daysUntilTarget === 0 && count === 0) { dates.push(new Date(currentDate)); count++; currentDate.setDate(currentDate.getDate() + 7 * interval); } else { currentDate.setDate(currentDate.getDate() + daysUntilTarget); dates.push(new Date(currentDate)); count++; currentDate.setDate(currentDate.getDate() + 7 * interval); } } } else { while (count < maxOccurrences) { if (endDate && currentDate > endDate) break; const dayOfWeek = currentDate.getDay(); const daysUntilTarget = (config.dayOfWeek - dayOfWeek + 7) % 7; if (daysUntilTarget === 0 && count === 0) { dates.push(new Date(currentDate)); count++; currentDate.setDate(currentDate.getDate() + 7 * interval); } else { currentDate.setDate(currentDate.getDate() + daysUntilTarget); dates.push(new Date(currentDate)); count++; currentDate.setDate(currentDate.getDate() + 7 * interval); } } } break; } case 'monthly': { const targetDay = config.dayOfMonth || startDate.getDate(); while (count < maxOccurrences) { if (endDate && currentDate > endDate) break; const lastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0).getDate(); const dayToSet = Math.min(targetDay, lastDayOfMonth); currentDate.setDate(dayToSet); dates.push(new Date(currentDate)); count++; currentDate.setMonth(currentDate.getMonth() + interval); } break; } case 'yearly': { const monthDay = config.monthAndDay || { month: startDate.getMonth(), day: startDate.getDate() }; while (count < maxOccurrences) { if (endDate && currentDate > endDate) break; currentDate.setMonth(monthDay.month); currentDate.setDate(monthDay.day); dates.push(new Date(currentDate)); count++; currentDate.setFullYear(currentDate.getFullYear() + interval); } break; } case 'weekdays': while (count < maxOccurrences) { if (endDate && currentDate > endDate) break; const dayOfWeek = currentDate.getDay(); if (dayOfWeek >= 1 && dayOfWeek <= 5) { dates.push(new Date(currentDate)); count++; } currentDate.setDate(currentDate.getDate() + 1); } break; case 'weekends': while (count < maxOccurrences) { if (endDate && currentDate > endDate) break; const dayOfWeek = currentDate.getDay(); if (dayOfWeek === 0 || dayOfWeek === 6) { dates.push(new Date(currentDate)); count++; } currentDate.setDate(currentDate.getDate() + 1); } break; default: break; } return dates; } function matchesRecurringPattern(date, config) { const generatedDates = generateRecurringDates(config); const dateTime = getStartOfDay(date).getTime(); return generatedDates.some((d) => getStartOfDay(d).getTime() === dateTime); } function generateMonthOptions(locale, year) { return Array.from({ length: 12 }).map((_, i) => ({ label: new Date(year, i, 1).toLocaleDateString(locale, { month: 'long' }), value: i, })); } /** * Format a number using locale-aware number formatting. * Uses Intl.NumberFormat for proper localization of numeric separators and decimals. * * @param value - The number to format * @param locale - The locale to use for formatting (e.g., 'en-US', 'de-DE', 'fr-FR') * @param options - Optional Intl.NumberFormatOptions for customization * @returns Formatted number string */ function formatLocaleNumber(value, locale, options) { try { const formatter = new Intl.NumberFormat(locale, { useGrouping: true, minimumIntegerDigits: 1, ...options, }); return formatter.format(value); } catch { // Fallback for invalid locale return String(value); } } function generateYearOptions(currentYear, range = 10) { const startYear = currentYear - range; const endYear = currentYear + range; const options = []; for (let i = startYear; i <= endYear; i++) { // Years should not use thousand separators (e.g., "2026" not "2,026") const label = `${i}`; options.push({ label, value: i }); } return options; } function generateTimeOptions(minuteInterval = 1, secondInterval = 1, includeSeconds = false, use24Hour = false) { const hourOptions = use24Hour ? Array.from({ length: 24 }).map((_, i) => ({ label: i.toString().padStart(2, '0'), value: i, })) : Array.from({ length: 12 }).map((_, i) => ({ label: (i + 1).toString().padStart(2, '0'), value: i + 1, })); const minuteOptions = []; for (let i = 0; i < 60; i += minuteInterval) { minuteOptions.push({ label: i.toString().padStart(2, '0'), value: i, }); } const result = { hourOptions, minuteOptions }; if (includeSeconds) { const secondOptions = []; for (let i = 0; i < 60; i += secondInterval) { secondOptions.push({ label: i.toString().padStart(2, '0'), value: i, }); } result.secondOptions = secondOptions; } return result; } function generateWeekDays(locale, firstDayOfWeek = 0) { const day = new Date(2024, 0, 7 + firstDayOfWeek); return Array.from({ length: 7 }).map(() => { const weekDay = new Date(day).toLocaleDateString(locale, { weekday: 'short', }); day.setDate(day.getDate() + 1); return weekDay; }); } function getFirstDayOfWeek(locale) { try { const intlExt = Intl; if (typeof intlExt !== 'undefined' && typeof intlExt.Locale !== 'undefined') { const localeObj = new intlExt.Locale(locale); if ('weekInfo' in localeObj && localeObj.weekInfo?.firstDay !== undefined) { return localeObj.weekInfo.firstDay % 7; } } const localeLower = locale.toLowerCase(); if (localeLower.startsWith('en-gb') || localeLower.startsWith('en-au') || localeLower.startsWith('en-nz') || localeLower.startsWith('de') || localeLower.startsWith('fr') || localeLower.startsWith('es') || localeLower.startsWith('it') || localeLower.startsWith('pt') || localeLower.startsWith('nl') || localeLower.startsWith('pl') || localeLower.startsWith('ru') || localeLower.startsWith('sv') || localeLower.startsWith('no') || localeLower.startsWith('da') || localeLower.startsWith('fi')) { return 1; // Monday } // Default to Sunday for en-US and other locales return 0; } catch { // If locale parsing fails, default based on locale string const localeLower = locale.toLowerCase(); if (localeLower.startsWith('en-gb') || localeLower.startsWith('en-au') || localeLower.startsWith('en-nz')) { return 1; // Monday } return 0; // Sunday (default for en-US and others) } } function get24Hour(displayHour, isPm) { if (isPm) { return displayHour === 12 ? 12 : displayHour + 12; } return displayHour === 12 ? 0 : displayHour; } function update12HourState(fullHour) { return { isPm: fullHour >= 12, displayHour: fullHour % 12 || 12, }; } function processDateRanges(ranges) { if (!ranges) return null; return Object.entries(ranges).reduce((acc, [key, dates]) => { const start = normalizeDate(dates[0]); const end = normalizeDate(dates[1]); if (start && end) acc[key] = [start, end]; return acc; }, {}); } function generateYearGrid(currentYear) { const startYear = Math.floor(currentYear / 10) * 10 - 1; const years = []; for (let i = 0; i < 12; i++) { years.push(startYear + i); } return years; } function generateDecadeGrid(currentDecade) { const decades = []; for (let i = 0; i < 12; i++) { decades.push(currentDecade + i * 10 - 10); } return decades; } /** * Generate a large year range for virtual scrolling (100 years centered on current) */ function generateLargeYearRange$1(centerYear, range = 100) { const startYear = centerYear - Math.floor(range / 2); const years = []; for (let i = 0; i < range; i++) { years.push(startYear + i); } return years; } /** * Generate a large decade range for virtual scrolling (50 decades centered on current) */ function generateLargeDecadeRange$1(centerDecade, range = 50) { const startDecade = centerDecade - Math.floor(range / 2) * 10; const decades = []; for (let i = 0; i < range; i++) { decades.push(startDecade + i * 10); } return decades; } class NgxsmkDatepickerInputComponent { constructor() { this.isNative = false; this.disabled = false; this.classes = undefined; this.nativeInputType = 'date'; this.formattedValue = ''; this.placeholder = ''; this.id = ''; this.name = ''; this.autocomplete = 'off'; this.required = false; this.minDateNative = null; this.maxDateNative = null; this.ariaLabel = ''; this.ariaDescribedBy = ''; this.errorState = false; this.clearAriaLabel = ''; this.clearLabel = ''; this.isCalendarOpen = false; this.allowTyping = false; this.typedInputValue = ''; this.displayValue = ''; this.showCalendarButton = false; this.calendarAriaLabel = ''; this.validationErrorMessage = null; this.nativeInputChange = new EventEmitter(); this.inputBlur = new EventEmitter(); this.clearValue = new EventEmitter(); this.toggleCalendar = new EventEmitter(); this.pointerDown = new EventEmitter(); this.pointerUp = new EventEmitter(); this.inputGroupFocus = new EventEmitter(); this.inputKeyDown = new EventEmitter(); this.inputChange = new EventEmitter(); this.inputFocus = new EventEmitter(); } focus() { if (this.isNative) { this.nativeInput?.nativeElement.focus(); } else { this.customInput?.nativeElement.focus(); } } onNativeInputChange(event) { this.nativeInputChange.emit(event); } onInputBlur(event) { this.inputBlur.emit(event); } onClearValue(event) { event.stopPropagation(); this.clearValue.emit(event); } onToggleCalendar(event) { this.toggleCalendar.emit(event); } onPointerDown(event) { this.pointerDown.emit(event); } onPointerUp(event) { this.pointerUp.emit(event); } onInputGroupFocus() { this.inputGroupFocus.emit(); } onInputKeyDown(event) { this.inputKeyDown.emit(event); } onInputChange(event) { this.inputChange.emit(event); } onInputFocus(event) { this.inputFocus.emit(event); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: NgxsmkDatepickerInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: NgxsmkDatepickerInputComponent, isStandalone: true, selector: "ngxsmk-datepicker-input", inputs: { isNative: "isNative", disabled: "disabled", classes: "classes", nativeInputType: "nativeInputType", formattedValue: "formattedValue", placeholder: "placeholder", id: "id", name: "name", autocomplete: "autocomplete", required: "required", minDateNative: "minDateNative", maxDateNative: "maxDateNative", ariaLabel: "ariaLabel", ariaDescribedBy: "ariaDescribedBy", errorState: "errorState", clearAriaLabel: "clearAriaLabel", clearLabel: "clearLabel", isCalendarOpen: "isCalendarOpen", allowTyping: "allowTyping", typedInputValue: "typedInputValue", displayValue: "displayValue", showCalendarButton: "showCalendarButton", calendarAriaLabel: "calendarAriaLabel", validationErrorMessage: "validationErrorMessage" }, outputs: { nativeInputChange: "nativeInputChange", inputBlur: "inputBlur", clearValue: "clearValue", toggleCalendar: "toggleCalendar", pointerDown: "pointerDown", pointerUp: "pointerUp", inputGroupFocus: "inputGroupFocus", inputKeyDown: "inputKeyDown", inputChange: "inputChange", inputFocus: "inputFocus" }, viewQueries: [{ propertyName: "nativeInput", first: true, predicate: ["nativeInput"], descendants: true }, { propertyName: "customInput", first: true, predicate: ["customInput"], descendants: true }], ngImport: i0, template: ` @if (isNative) { <div class="ngxsmk-input-group ngxsmk-native-input-group" [class.disabled]="disabled" [ngClass]="classes?.inputGroup" > <input [type]="nativeInputType" #nativeInput [value]="formattedValue" [placeholder]="placeholder" [id]="id" [name]="name" [autocomplete]="autocomplete" [disabled]="disabled" [required]="required" [attr.min]="minDateNative" [attr.max]="maxDateNative" [attr.aria-label]="ariaLabel" [attr.aria-required]="required" [attr.aria-invalid]="errorState" [attr.aria-describedby]="ariaDescribedBy" class="ngxsmk-display-input ngxsmk-native-input" [ngClass]="classes?.input" (change)="onNativeInputChange($event)" (blur)="onInputBlur($event)" /> @if (formattedValue) { <button type="button" class="ngxsmk-clear-button" (click)="onClearValue($event)" [disabled]="disabled" [attr.aria-label]="clearAriaLabel" [title]="clearLabel" [ngClass]="classes?.clearBtn" > <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16"> <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M368 368L144 144M368 144L144 368" /> </svg> </button> } </div> } @else { <div class="ngxsmk-input-and-error"> <div class="ngxsmk-input-group" (click)="onToggleCalendar($event)" (pointerdown)="onPointerDown($event)" (pointerup)="onPointerUp($event)" (focus)="onInputGroupFocus()" (keydown.enter)="onToggleCalendar($event)" (keydown.space)="onToggleCalendar($event); $event.preventDefault()" [class.disabled]="disabled" role="button" [attr.aria-disabled]="disabled" aria-haspopup="dialog" [attr.aria-expanded]="isCalendarOpen" tabindex="0" [ngClass]="classes?.inputGroup" > <input type="text" #customInput [value]="allowTyping ? typedInputValue || displayValue : displayValue" [placeholder]="placeholder" [id]="id" [name]="name" [autocomplete]="autocomplete" [readonly]="!allowTyping" [disabled]="disabled" [required]="required" [attr.aria-label]="ariaLabel" [attr.aria-required]="required" [attr.aria-invalid]="errorState" [attr.aria-describedby]="ariaDescribedBy" class="ngxsmk-display-input" [ngClass]="classes?.input" (keydown.enter)="onInputKeyDown($event)" (keydown.space)="onInputKeyDown($event)" (keydown.escape)="onInputKeyDown($event)" (input)="onInputChange($event)" (blur)="onInputBlur($event)" (focus)="onInputFocus($event)" /> @if (displayValue) { <button type="button" class="ngxsmk-clear-button" (click)="onClearValue($event)" (touchstart)="$event.stopPropagation()" (touchend)="$event.stopPropagation()" (pointerdown)="$event.stopPropagation()" (pointerup)="$event.stopPropagation()" [disabled]="disabled" [attr.aria-label]="clearAriaLabel" [title]="clearLabel" [ngClass]="classes?.clearBtn" > <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16"> <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M368 368L144 144M368 144L144 368" /> </svg> </button> } @if (showCalendarButton) { <button type="button" class="ngxsmk-calendar-button" (click)="onToggleCalendar($event); $event.stopPropagation()" [disabled]="disabled" [attr.aria-label]="calendarAriaLabel" [title]="calendarAriaLabel" [ngClass]="classes?.calendarBtn" > <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M96 80H416c26.51 0 48 21.49 48 48V416c0 26.51-21.49 48-48 48H96c-26.51 0-48-21.49-48-48V128c0-26.51 21.49-48 48-48zM160 32v64M352 32v64M464 192H48M200 256h112M200 320h112M200 384h112M152 256h.01M152 320h.01M152 384h.01" /> </svg> </button> } </div> @if (validationErrorMessage) { <div class="ngxsmk-validation-error" role="alert" [attr.aria-live]="'polite'"> {{ validationErrorMessage }} </div> } </div> } `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: NgxsmkDatepickerInputComponent, decorators: [{ type: Component, args: [{ selector: 'ngxsmk-datepicker-input', standalone: true, imports: [NgClass], template: ` @if (isNative) { <div class="ngxsmk-input-group ngxsmk-native-input-group" [class.disabled]="disabled" [ngClass]="classes?.inputGroup" > <input [type]="nativeInputType" #nativeInput [value]="formattedValue" [placeholder]="placeholder" [id]="id" [name]="name" [autocomplete]="autocomplete" [disabled]="disabled" [required]="required" [attr.min]="minDateNative" [attr.max]="maxDateNative" [attr.aria-label]="ariaLabel" [attr.aria-required]="required" [attr.aria-invalid]="errorState" [attr.aria-describedby]="ariaDescribedBy" class="ngxsmk-display-input ngxsmk-native-input" [ngClass]="classes?.input" (change)="onNativeInputChange($event)" (blur)="onInputBlur($event)" /> @if (formattedValue) { <button type="button" class="ngxsmk-clear-button" (click)="onClearValue($event)" [disabled]="disabled" [attr.aria-label]="clearAriaLabel" [title]="clearLabel" [ngClass]="classes?.clearBtn" > <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16"> <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M368 368L144 144M368 144L144 368" /> </svg> </button> } </div> } @else { <div class="ngxsmk-input-and-error"> <div class="ngxsmk-input-group" (click)="onToggleCalendar($event)" (pointerdown)="onPointerDown($event)" (pointerup)="onPointerUp($event)" (focus)="onInputGroupFocus()" (keydown.enter)="onToggleCalendar($event)" (keydown.space)="onToggleCalendar($event); $event.preventDefault()" [class.disabled]="disabled" role="button" [attr.aria-disabled]="disabled" aria-haspopup="dialog" [attr.aria-expanded]="isCalendarOpen" tabindex="0" [ngClass]="classes?.inputGroup" > <input type="text" #customInput [value]="allowTyping ? typedInputValue || displayValue : displayValue" [placeholder]="placeholder" [id]="id" [name]="name" [autocomplete]="autocomplete" [readonly]="!allowTyping" [disabled]="disabled" [required]="required" [attr.aria-label]="ariaLabel" [attr.aria-required]="required" [attr.aria-invalid]="errorState" [attr.aria-describedby]="ariaDescribedBy" class="ngxsmk-display-input" [ngClass]="classes?.input" (keydown.enter)="onInputKeyDown($event)" (keydown.space)="onInputKeyDown($event)" (keydown.escape)="onInputKeyDown($event)" (input)="onInputChange($event)" (blur)="onInputBlur($event)" (focus)="onInputFocus($event)" /> @if (displayValue) { <button type="button" class="ngxsmk-clear-button" (click)="onClearValue($event)" (touchstart)="$event.stopPropagation()" (touchend)="$event.stopPropagation()" (pointerdown)="$event.stopPropagation()" (pointerup)="$event.stopPropagation()" [disabled]="disabled" [attr.aria-label]="clearAriaLabel" [title]="clearLabel" [ngClass]="classes?.clearBtn" > <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16"> <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M368 368L144 144M368 144L144 368" /> </svg> </button> } @if (showCalendarButton) { <button type="button" class="ngxsmk-calendar-button" (click)="onToggleCalendar($event); $event.stopPropagation()" [disabled]="disabled" [attr.aria-label]="calendarAriaLabel" [title]="calendarAriaLabel" [ngClass]="classes?.calendarBtn" > <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M96 80H416c26.51 0 48 21.49 48 48V416c0 26.51-21.49 48-48 48H96c-26.51 0-48-21.49-48-48V128c0-26.51 21.49-48 48-48zM160 32v64M352 32v64M464 192H48M200 256h112M200 320h112M200 384h112M152 256h.01M152 320h.01M152 384h.01" /> </svg> </button> } </div> @if (validationErrorMessage) { <div class="ngxsmk-validation-error" role="alert" [attr.aria-live]="'polite'"> {{ validationErrorMessage }} </div> } </div> } `, changeDetection: ChangeDetectionStrategy.OnPush, }] }], propDecorators: { isNative: [{ type: Input }], disabled: [{ type: Input }], classes: [{ type: Input }], nativeInputType: [{ type: Input }], formattedValue: [{ type: Input }], placeholder: [{ type: Input }], id: [{ type: Input }], name: [{ type: Input }], autocomplete: [{ type: Input }], required: [{ type: Input }], minDateNative: [{ type: Input }], maxDateNative: [{ type: Input }], ariaLabel: [{ type: Input }], ariaDescribedBy: [{ type: Input }], errorState: [{ type: Input }], clearAriaLabel: [{ type: Input }], clearLabel: [{ type: Input }], isCalendarOpen: [{ type: Input }], allowTyping: [{ type: Input }], typedInputValue: [{ type: Input }], displayValue: [{ type: Input }], showCalendarButton: [{ type: Input }], calendarAriaLabel: [{ type: Input }], validationErrorMessage: [{ type: Input }], nativeInputChange: [{ type: Output }], inputBlur: [{ type: Output }], clearValue: [{ type: Output }], toggleCalendar: [{ type: Output }], pointerDown: [{ type: Output }], pointerUp: [{ type: Output }], inputGroupFocus: [{ type: Output }], inputKeyDown: [{ type: Output }], inputChange: [{ type: Output }], inputFocus: [{ type: Output }], nativeInput: [{ type: ViewChild, args: ['nativeInput'] }], customInput: [{ type: ViewChild, args: ['customInput'] }] } }); class CustomSelectComponent { constructor() { this.options = []; this.disabled = false; this.valueChange = new EventEmitter(); this.isOpen = false; this.elementRef = inject(ElementRef); this.platformId = inject(PLATFORM_ID); this.document = inject(DOCUMENT); this.isBrowser = isPlatformBrowser(this.platformId); this.resizeObserver = null; this.scrollListener = null; } ngAfterViewInit() { if (this.isBrowser) { this.setupResizeObserver(); this.setupScrollListener(); } } ngOnDestroy() { if (this.resizeObserver) { this.resizeObserver.disconnect(); } if (this.scrollListener && this.isBrowser) { window.removeEventListener('scroll', this.scrollListener, true); } } setupResizeObserver() { if (this.isBrowser && typeof ResizeObserver !== 'undefined') { this.resizeObserver = new ResizeObserver(() => { if (this.isOpen) { this.updatePanelPosition(); } }); if (this.container?.nativeElement) { this.resizeObserver.observe(this.container.nativeElement); } } } setupScrollListener() { if (this.isBrowser) { this.scrollListener = () => { if (this.isOpen) { // Absolute positioning doesn't need updates on scroll } }; window.addEventListener('scroll', this.scrollListener, { passive: true, capture: true }); } } updatePanelPosition() { // No special positioning needed for standard dropdowns // CSS handles top: 100% + 4px } onDocumentClick(event) { if (this.isBrowser) { const target = event.target; if (target && !this.elementRef.nativeElement.contains(target)) { this.isOpen = false; } // Logic removed: forcing closure when calendar is open prevented dropdown from opening } } onDocumentTouchStart(event) { // On mobile, close dropdown when calendar opens if (this.isBrowser && this.isOpen) { const calendarBackdrop = this.document.querySelector('.ngxsmk-backdrop'); if (calendarBackdrop) { const target = event.target; // Only close if touch is outside the dropdown if (target && !this.elementRef.nativeElement.contains(target)) { this.isOpen = false; } } } } get displayValue() { const selectedOption = this.options.find((opt) => opt.value === this.value); return selectedOption ? selectedOption.label : ''; } toggleDropdown() { if (this.disabled) return; this.isOpen = !this.isOpen; if (this.isOpen) { setTimeout(() => { this.updatePanelPosition(); this.scrollToSelected(); }, 0); } } scrollToSelected() { if (!this.isBrowser || !this.panel?.nativeElement) return; const selectedEl = this.panel.nativeElement.querySelector('.selected'); if (selectedEl) { selectedEl.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } } selectOption(option) { this.value = option.value; this.valueChange.emit(this.value); this.isOpen = false; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: CustomSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: CustomSelectComponent, isStandalone: true, selector: "ngxsmk-custom-select", inputs: { options: "options", value: "value", disabled: "disabled" }, outputs: { valueChange: "valueChange" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:touchstart": "onDocumentTouchStart($event)" }, properties: { "attr.data-open": "isOpen" } }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true }, { propertyName: "button", first: true, predicate: ["button"], descendants: true }, { propertyName: "panel", first: true, predicate: ["panel"], descendants: true }], ngImport: i0, template: ` <div class="ngxsmk-select-container" [class.is-open]="isOpen" (click)="toggleDropdown()" (keydown.enter)="toggleDropdown()" (keydown.space)="toggleDropdown(); $event.preventDefault()" tabindex="0" role="button" [attr.aria-expanded]="isOpen" #container > <button type="button" class="ngxsmk-select-display" [disabled]="disabled" #button> <span>{{ displayValue }}</span> <svg class="ngxsmk-arrow-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M112 184l144 144 144-144" /> </svg> </button> @if (isOpen) { <div class="ngxsmk-options-panel" #panel> <ul> @for (option of options; track option.value) { <li [class.selected]="option.value === value" (click)="selectOption(option); $event.stopPropagation()" (keydown.enter)="selectOption(option); $event.stopPropagation()" (keydown.space)="selectOption(option); $event.stopPropagation(); $event.preventDefault()" [attr.tabindex]="0" role="option" [attr.aria-selected]="option.value === value" > {{ option.label }} </li> } </ul> </div> } </div> `, isInline: true, encapsulation: i0.ViewEncapsulation.None }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: CustomSelectComponent, decorators: [{ type: Component, args: [{ selector: 'ngxsmk-custom-select', standalone: true, imports: [], encapsulation: ViewEncapsulation.None, host: { '[attr.data-open]': 'isOpen', }, template: ` <div class="ngxsmk-select-container" [class.is-open]="isOpen" (click)="toggleDropdown()" (keydown.enter)="toggleDropdown()" (keydown.space)="toggleDropdown(); $event.preventDefault()" tabindex="0" role="button" [attr.aria-expanded]="isOpen" #container > <button type="button" class="ngxsmk-select-display" [disabled]="disabled" #button> <span>{{ displayValue }}</span> <svg class="ngxsmk-arrow-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M112 184l144 144 144-144" /> </svg> </button> @if (isOpen) { <div class="ngxsmk-options-panel" #panel> <ul> @for (option of options; track option.value) { <li [class.selected]="option.value === value" (click)="selectOption(option); $event.stopPropagation()" (keydown.enter)="selectOption(option); $event.stopPropagation()" (keydown.space)="selectOption(option); $event.stopPropagation(); $event.preventDefault()" [attr.tabindex]="0" role="option" [attr.aria-selected]="option.value === value" > {{ option.label }} </li> } </ul> </div> } </div> `, }] }], propDecorators: { options: [{ type: Input }], value: [{ type: Input }], disabled: [{ type: Input }], valueChange: [{ type: Output }], container: [{ type: ViewChild, args: ['container', { static: false }] }], button: [{ type: ViewChild, args: ['button', { static: false }] }], panel: [{ type: ViewChild, args: ['panel', { static: false }] }], onDocumentClick: [{ type: HostListener, args: ['document:click', ['$event']] }], onDocumentTouchStart: [{ type: HostListener, args: ['document:touchstart', ['$event']] }] } }); /** * The header section of the calendar, containing navigation and selection controls. * * Includes: * - Month selection dropdown * - Year selection dropdown * - Previous/Next month navigation buttons */ class CalendarHeaderComponent { constructor() { this.monthOptions = []; this.yearOptions = []; this.currentMonth = 0; this.currentYear = new Date().getFullYear(); this.disabled = false; this.isBackArrowDisabled = false; this.prevMonthAriaLabel = ''; this.nextMonthAriaLabel = ''; this.currentYearChange = new EventEmitter(); this.currentMonthChange = new EventEmitter(); this.previousMonth = new EventEmitter(); this.nextMonth = new EventEmitter(); } onMonthSelect(value) { this.currentMonthChange.emit(value); } onYearSelect(value) { this.currentYearChange.emit(value); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: CalendarHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.11", type: CalendarHeaderComponent, isStandalone: true, selector: "ngxsmk-calendar-header", inputs: { monthOptions: "monthOptions", yearOptions: "yearOptions", currentMonth: "currentMonth", currentYear: "currentYear", disabled: "disabled", isBackArrowDisabled: "isBackArrowDisabled", prevMonthAriaLabel: "prevMonthAriaLabel", nextMonthAriaLabel: "nextMonthAriaLabel", headerClass: "headerClass", navPrevClass: "navPrevClass", navNextClass: "navNextClass" }, outputs: { currentYearChange: "currentYearChange", currentMonthChange: "currentMonthChange", previousMonth: "previousMonth", nextMonth: "nextMonth" }, viewQueries: [{ propertyName: "monthSelect", first: true, predicate: ["monthSelect"], descendants: true }, { propertyName: "yearSelect", first: true, predicate: ["yearSelect"], descendants: true }], ngImport: i0, template: ` <div class="ngxsmk-header" [ngClass]="headerClass"> <div class="ngxsmk-month-year-selects"> <ngxsmk-custom-select #monthSelect class="month-select" [options]="monthOptions" [(value)]="currentMonth" [disabled]="disabled" (valueChange)="onMonthSelect($event)" > </ngxsmk-custom-select> <ngxsmk-custom-select #yearSelect class="year-select" [options]="yearOptions" [(value)]="currentYear" [disabled]="disabled" (valueChange)="onYearSelect($event)" > </ngxsmk-custom-select> </div> <div class="ngxsmk-nav-buttons"> <button type="button" class="ngxsmk-nav-button" (click)="previousMonth.emit()" [disabled]="disabled || isBackArrowDisabled" [attr.aria-label]="prevMonthAriaLabel" [title]="prevMonthAriaLabel" [ngClass]="navPrevClass" > <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M328 112L184 256l144 144" /> </svg> </button> <button type="button" class="ngxsmk-nav-button" (click)="nextMonth.emit()" [disabled]="disabled" [attr.aria-label]="nextMonthAriaLabel" [title]="nextMonthAriaLabel" [ngClass]="navNextClass" > <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M184 112l144 144-144 144" /> </svg> </button> </div> </div> `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: CustomSelectComponent, selector: "ngxsmk-custom-select", inputs: ["optio