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
JavaScript
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