@oneluiz/dual-datepicker 3.6.0 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/calendar-grid/calendar-grid.cache.d.ts +39 -0
- package/core/calendar-grid/calendar-grid.factory.d.ts +26 -0
- package/core/calendar-grid/calendar-grid.types.d.ts +57 -0
- package/core/calendar-grid/index.d.ts +50 -0
- package/core/calendar-grid/range-highlighter.cache.d.ts +105 -0
- package/core/calendar-grid/range-highlighter.d.ts +85 -0
- package/core/calendar-grid/range-highlighter.types.d.ts +182 -0
- package/core/calendar-grid/virtual-weeks.logic.d.ts +116 -0
- package/core/calendar-grid/virtual-weeks.types.d.ts +71 -0
- package/core/index.d.ts +11 -0
- package/dual-datepicker.component.d.ts +78 -1
- package/esm2022/core/calendar-grid/calendar-grid.cache.mjs +92 -0
- package/esm2022/core/calendar-grid/calendar-grid.factory.mjs +97 -0
- package/esm2022/core/calendar-grid/calendar-grid.types.mjs +8 -0
- package/esm2022/core/calendar-grid/index.mjs +51 -0
- package/esm2022/core/calendar-grid/range-highlighter.cache.mjs +198 -0
- package/esm2022/core/calendar-grid/range-highlighter.mjs +185 -0
- package/esm2022/core/calendar-grid/range-highlighter.types.mjs +11 -0
- package/esm2022/core/calendar-grid/virtual-weeks.logic.mjs +149 -0
- package/esm2022/core/calendar-grid/virtual-weeks.types.mjs +11 -0
- package/esm2022/core/index.mjs +14 -1
- package/esm2022/dual-datepicker.component.mjs +177 -38
- package/fesm2022/oneluiz-dual-datepicker.mjs +965 -39
- package/fesm2022/oneluiz-dual-datepicker.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, Injectable, inject, signal, computed, EventEmitter, effect, APP_INITIALIZER, forwardRef, HostListener, Output, Input, Component, makeEnvironmentProviders } from '@angular/core';
|
|
2
|
+
import { InjectionToken, Injectable, inject, signal, computed, Inject, EventEmitter, effect, APP_INITIALIZER, forwardRef, HostListener, Output, Input, Component, makeEnvironmentProviders } from '@angular/core';
|
|
3
3
|
import * as i1 from '@angular/common';
|
|
4
4
|
import { CommonModule } from '@angular/common';
|
|
5
5
|
import { FormsModule, ReactiveFormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
@@ -1816,6 +1816,784 @@ const BUILT_IN_PRESETS = [
|
|
|
1816
1816
|
YEAR_TO_DATE_PRESET
|
|
1817
1817
|
];
|
|
1818
1818
|
|
|
1819
|
+
/**
|
|
1820
|
+
* Calendar Grid Types
|
|
1821
|
+
*
|
|
1822
|
+
* Defines the structure for memoized calendar month grids.
|
|
1823
|
+
* Separates base grid structure (cacheable) from decorations (dynamic).
|
|
1824
|
+
*/
|
|
1825
|
+
|
|
1826
|
+
/**
|
|
1827
|
+
* Calendar Grid Factory
|
|
1828
|
+
*
|
|
1829
|
+
* Generates calendar month grids using DateAdapter for deterministic,
|
|
1830
|
+
* timezone-safe date operations.
|
|
1831
|
+
*
|
|
1832
|
+
* Grid structure:
|
|
1833
|
+
* - Always 6 weeks x 7 days (42 cells) for layout stability
|
|
1834
|
+
* - Includes padding days from previous/next month
|
|
1835
|
+
* - No decorations (selected, disabled, etc.) - those are applied separately
|
|
1836
|
+
*/
|
|
1837
|
+
class CalendarGridFactory {
|
|
1838
|
+
adapter;
|
|
1839
|
+
constructor(adapter) {
|
|
1840
|
+
this.adapter = adapter;
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Create a calendar grid for a given month
|
|
1844
|
+
*
|
|
1845
|
+
* @param monthDate - Any date within the target month (will be normalized to start of month)
|
|
1846
|
+
* @param weekStart - First day of week (0 = Sunday, 1 = Monday, etc.)
|
|
1847
|
+
* @param locale - Locale identifier (optional, for future use)
|
|
1848
|
+
* @returns CalendarGrid - 6 weeks x 7 days grid
|
|
1849
|
+
*/
|
|
1850
|
+
createGrid(monthDate, weekStart = 0, locale) {
|
|
1851
|
+
// Normalize to start of month
|
|
1852
|
+
const year = this.adapter.getYear(monthDate);
|
|
1853
|
+
const month = this.adapter.getMonth(monthDate);
|
|
1854
|
+
const firstDayOfMonth = new Date(year, month, 1);
|
|
1855
|
+
const normalizedFirst = this.adapter.normalize(firstDayOfMonth);
|
|
1856
|
+
// Get days in month (day 0 of next month = last day of current month)
|
|
1857
|
+
const lastDayOfMonth = new Date(year, month + 1, 0);
|
|
1858
|
+
const daysInMonth = this.adapter.getDate(lastDayOfMonth);
|
|
1859
|
+
// Get first day of week offset
|
|
1860
|
+
const firstDayOfWeek = this.adapter.getDay(normalizedFirst);
|
|
1861
|
+
const offset = this.calculateOffset(firstDayOfWeek, weekStart);
|
|
1862
|
+
// Generate 42 cells (6 weeks x 7 days)
|
|
1863
|
+
const cells = [];
|
|
1864
|
+
let currentDate = this.adapter.addDays(normalizedFirst, -offset);
|
|
1865
|
+
for (let i = 0; i < 42; i++) {
|
|
1866
|
+
const cellDate = this.adapter.normalize(currentDate);
|
|
1867
|
+
const cellYear = this.adapter.getYear(cellDate);
|
|
1868
|
+
const cellMonth = this.adapter.getMonth(cellDate);
|
|
1869
|
+
const cellDay = this.adapter.getDate(cellDate);
|
|
1870
|
+
const cellDayOfWeek = this.adapter.getDay(cellDate);
|
|
1871
|
+
cells.push({
|
|
1872
|
+
date: cellDate,
|
|
1873
|
+
inCurrentMonth: cellYear === year && cellMonth === month,
|
|
1874
|
+
iso: this.adapter.toISODate(cellDate),
|
|
1875
|
+
day: cellDay,
|
|
1876
|
+
month: cellMonth,
|
|
1877
|
+
year: cellYear,
|
|
1878
|
+
dayOfWeek: cellDayOfWeek
|
|
1879
|
+
});
|
|
1880
|
+
currentDate = this.adapter.addDays(currentDate, 1);
|
|
1881
|
+
}
|
|
1882
|
+
// Split into weeks
|
|
1883
|
+
const weeks = [];
|
|
1884
|
+
for (let i = 0; i < 6; i++) {
|
|
1885
|
+
weeks.push(cells.slice(i * 7, (i + 1) * 7));
|
|
1886
|
+
}
|
|
1887
|
+
return {
|
|
1888
|
+
month: { year, month },
|
|
1889
|
+
weekStart,
|
|
1890
|
+
locale,
|
|
1891
|
+
weeks,
|
|
1892
|
+
cells
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
/**
|
|
1896
|
+
* Calculate offset (number of padding days from previous month)
|
|
1897
|
+
*
|
|
1898
|
+
* @param firstDayOfWeek - Day of week for first day of month (0-6)
|
|
1899
|
+
* @param weekStart - Desired week start (0-6)
|
|
1900
|
+
* @returns Number of padding days needed
|
|
1901
|
+
*/
|
|
1902
|
+
calculateOffset(firstDayOfWeek, weekStart) {
|
|
1903
|
+
let offset = firstDayOfWeek - weekStart;
|
|
1904
|
+
if (offset < 0) {
|
|
1905
|
+
offset += 7;
|
|
1906
|
+
}
|
|
1907
|
+
return offset;
|
|
1908
|
+
}
|
|
1909
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CalendarGridFactory, deps: [{ token: DATE_ADAPTER }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1910
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CalendarGridFactory, providedIn: 'root' });
|
|
1911
|
+
}
|
|
1912
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CalendarGridFactory, decorators: [{
|
|
1913
|
+
type: Injectable,
|
|
1914
|
+
args: [{ providedIn: 'root' }]
|
|
1915
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
1916
|
+
type: Inject,
|
|
1917
|
+
args: [DATE_ADAPTER]
|
|
1918
|
+
}] }] });
|
|
1919
|
+
|
|
1920
|
+
/**
|
|
1921
|
+
* Calendar Grid Cache
|
|
1922
|
+
*
|
|
1923
|
+
* LRU cache for calendar month grids to avoid recomputing the same month grid
|
|
1924
|
+
* multiple times when only decorations (selected dates, hover, etc.) change.
|
|
1925
|
+
*
|
|
1926
|
+
* Strategy:
|
|
1927
|
+
* - Key: year-month-weekStart-locale
|
|
1928
|
+
* - LRU eviction (least recently used) when limit is reached
|
|
1929
|
+
* - Default limit: 24 months (covers 1 year forward + 1 year back)
|
|
1930
|
+
*
|
|
1931
|
+
* Performance impact:
|
|
1932
|
+
* - Eliminates ~90% of grid recalculations in typical usage
|
|
1933
|
+
* - Memory footprint: ~10KB per cached month (negligible)
|
|
1934
|
+
*/
|
|
1935
|
+
class CalendarGridCache {
|
|
1936
|
+
factory;
|
|
1937
|
+
cache = new Map();
|
|
1938
|
+
maxSize = 24;
|
|
1939
|
+
constructor(factory) {
|
|
1940
|
+
this.factory = factory;
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* Get or create a calendar grid
|
|
1944
|
+
*
|
|
1945
|
+
* @param monthDate - Any date within the target month
|
|
1946
|
+
* @param weekStart - First day of week (0 = Sunday, 1 = Monday, etc.)
|
|
1947
|
+
* @param locale - Locale identifier (optional)
|
|
1948
|
+
* @returns CalendarGrid - cached or newly created
|
|
1949
|
+
*/
|
|
1950
|
+
get(monthDate, weekStart = 0, locale) {
|
|
1951
|
+
const key = this.buildKey(monthDate, weekStart, locale);
|
|
1952
|
+
// Check cache
|
|
1953
|
+
const cached = this.cache.get(key);
|
|
1954
|
+
if (cached) {
|
|
1955
|
+
// Move to end (LRU)
|
|
1956
|
+
this.cache.delete(key);
|
|
1957
|
+
this.cache.set(key, cached);
|
|
1958
|
+
return cached;
|
|
1959
|
+
}
|
|
1960
|
+
// Generate new grid
|
|
1961
|
+
const grid = this.factory.createGrid(monthDate, weekStart, locale);
|
|
1962
|
+
// Store in cache
|
|
1963
|
+
this.cache.set(key, grid);
|
|
1964
|
+
// Evict oldest if over limit (LRU)
|
|
1965
|
+
if (this.cache.size > this.maxSize) {
|
|
1966
|
+
const firstKey = this.cache.keys().next().value;
|
|
1967
|
+
this.cache.delete(firstKey);
|
|
1968
|
+
}
|
|
1969
|
+
return grid;
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Build cache key from month parameters
|
|
1973
|
+
*
|
|
1974
|
+
* Format: "year-month-weekStart-locale"
|
|
1975
|
+
* Example: "2026-1-0-en" (Feb 2026, Sunday start, English)
|
|
1976
|
+
*/
|
|
1977
|
+
buildKey(monthDate, weekStart, locale) {
|
|
1978
|
+
const year = monthDate.getFullYear();
|
|
1979
|
+
const month = monthDate.getMonth();
|
|
1980
|
+
return `${year}-${month}-${weekStart}${locale ? '-' + locale : ''}`;
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Clear entire cache (for testing or manual reset)
|
|
1984
|
+
*/
|
|
1985
|
+
clear() {
|
|
1986
|
+
this.cache.clear();
|
|
1987
|
+
}
|
|
1988
|
+
/**
|
|
1989
|
+
* Get cache size (for debugging/testing)
|
|
1990
|
+
*/
|
|
1991
|
+
size() {
|
|
1992
|
+
return this.cache.size;
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Check if a specific month is cached
|
|
1996
|
+
*/
|
|
1997
|
+
has(monthDate, weekStart = 0, locale) {
|
|
1998
|
+
const key = this.buildKey(monthDate, weekStart, locale);
|
|
1999
|
+
return this.cache.has(key);
|
|
2000
|
+
}
|
|
2001
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CalendarGridCache, deps: [{ token: CalendarGridFactory }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2002
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CalendarGridCache, providedIn: 'root' });
|
|
2003
|
+
}
|
|
2004
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CalendarGridCache, decorators: [{
|
|
2005
|
+
type: Injectable,
|
|
2006
|
+
args: [{ providedIn: 'root' }]
|
|
2007
|
+
}], ctorParameters: () => [{ type: CalendarGridFactory }] });
|
|
2008
|
+
|
|
2009
|
+
/**
|
|
2010
|
+
* Range Highlighter Types
|
|
2011
|
+
*
|
|
2012
|
+
* Type definitions for calendar cell decorations with range highlighting.
|
|
2013
|
+
* Used by RangeHighlighter to cache decorated grids and avoid recomputations.
|
|
2014
|
+
*
|
|
2015
|
+
* @module core/calendar-grid/range-highlighter.types
|
|
2016
|
+
* @version 3.8.0
|
|
2017
|
+
*/
|
|
2018
|
+
|
|
2019
|
+
/**
|
|
2020
|
+
* Range Highlighter Service
|
|
2021
|
+
*
|
|
2022
|
+
* Decorates calendar grids with range highlights, hover previews, and disabled states.
|
|
2023
|
+
* Pure computation layer - no caching (see RangeHighlighterCache for memoization).
|
|
2024
|
+
*
|
|
2025
|
+
* @module core/calendar-grid/range-highlighter
|
|
2026
|
+
* @version 3.8.0
|
|
2027
|
+
*/
|
|
2028
|
+
/**
|
|
2029
|
+
* Range Highlighter
|
|
2030
|
+
*
|
|
2031
|
+
* Applies decorations to calendar grids:
|
|
2032
|
+
* - isSelectedStart, isSelectedEnd (range boundaries)
|
|
2033
|
+
* - isInRange (cells within selected range)
|
|
2034
|
+
* - isInHoverRange (hover preview)
|
|
2035
|
+
* - isDisabled (constraints, custom predicates)
|
|
2036
|
+
*
|
|
2037
|
+
* Design:
|
|
2038
|
+
* - Pure functions (same input = same output)
|
|
2039
|
+
* - Uses DateAdapter for all date operations (SSR-safe)
|
|
2040
|
+
* - No side effects, no state
|
|
2041
|
+
* - Fast (~1ms for 42 cells on mobile)
|
|
2042
|
+
*
|
|
2043
|
+
* Usage:
|
|
2044
|
+
* ```typescript
|
|
2045
|
+
* const grid = calendarGridCache.get(monthDate, 0);
|
|
2046
|
+
* const decorated = rangeHighlighter.decorate(grid, {
|
|
2047
|
+
* start: startDate,
|
|
2048
|
+
* end: endDate,
|
|
2049
|
+
* hoverDate: '2026-01-20',
|
|
2050
|
+
* disabledDates: [...]
|
|
2051
|
+
* });
|
|
2052
|
+
* ```
|
|
2053
|
+
*/
|
|
2054
|
+
class RangeHighlighter {
|
|
2055
|
+
adapter;
|
|
2056
|
+
constructor(adapter) {
|
|
2057
|
+
this.adapter = adapter;
|
|
2058
|
+
}
|
|
2059
|
+
/**
|
|
2060
|
+
* Decorate calendar grid with range highlights
|
|
2061
|
+
*
|
|
2062
|
+
* @param grid Base calendar grid (from CalendarGridCache)
|
|
2063
|
+
* @param params Decoration parameters (start, end, hover, disabled, etc.)
|
|
2064
|
+
* @returns Decorated grid with computed properties
|
|
2065
|
+
*/
|
|
2066
|
+
decorate(grid, params) {
|
|
2067
|
+
// Normalize dates (ensure start of day, consistent ISO strings)
|
|
2068
|
+
const startISO = params.start ? this.adapter.toISODate(params.start) : null;
|
|
2069
|
+
const endISO = params.end ? this.adapter.toISODate(params.end) : null;
|
|
2070
|
+
const minISO = params.minDate ? this.adapter.toISODate(params.minDate) : null;
|
|
2071
|
+
const maxISO = params.maxDate ? this.adapter.toISODate(params.maxDate) : null;
|
|
2072
|
+
const hoverISO = params.hoverDate || null;
|
|
2073
|
+
// Compute hover range boundaries (if applicable)
|
|
2074
|
+
const hoverRange = this.computeHoverRange(startISO, hoverISO, params.selectingStartDate || false, params.multiRange || false);
|
|
2075
|
+
// Decorate all cells
|
|
2076
|
+
const decoratedCells = grid.cells.map((cell) => {
|
|
2077
|
+
// Padding cells (previous/next month) get default decorations
|
|
2078
|
+
if (!cell.inCurrentMonth) {
|
|
2079
|
+
return {
|
|
2080
|
+
...cell,
|
|
2081
|
+
isSelectedStart: false,
|
|
2082
|
+
isSelectedEnd: false,
|
|
2083
|
+
isInRange: false,
|
|
2084
|
+
isInHoverRange: false,
|
|
2085
|
+
isDisabled: false
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
// Current month cells: apply full decoration logic
|
|
2089
|
+
const cellISO = cell.iso;
|
|
2090
|
+
return {
|
|
2091
|
+
...cell,
|
|
2092
|
+
isSelectedStart: startISO === cellISO,
|
|
2093
|
+
isSelectedEnd: endISO === cellISO,
|
|
2094
|
+
isInRange: this.isInRange(cellISO, startISO, endISO),
|
|
2095
|
+
isInHoverRange: this.isInHoverRange(cellISO, hoverRange),
|
|
2096
|
+
isDisabled: this.isDisabled(cell.date, minISO, maxISO, params.disabledDates)
|
|
2097
|
+
};
|
|
2098
|
+
});
|
|
2099
|
+
// Organize cells into weeks (6 × 7)
|
|
2100
|
+
const weeks = [];
|
|
2101
|
+
for (let i = 0; i < decoratedCells.length; i += 7) {
|
|
2102
|
+
weeks.push(decoratedCells.slice(i, i + 7));
|
|
2103
|
+
}
|
|
2104
|
+
return {
|
|
2105
|
+
base: grid,
|
|
2106
|
+
cells: decoratedCells,
|
|
2107
|
+
weeks
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
/**
|
|
2111
|
+
* Check if cell is within selected range
|
|
2112
|
+
*
|
|
2113
|
+
* @param cellISO Cell date (ISO format)
|
|
2114
|
+
* @param startISO Start date (ISO or null)
|
|
2115
|
+
* @param endISO End date (ISO or null)
|
|
2116
|
+
* @returns True if cell is in range [start, end] (inclusive)
|
|
2117
|
+
*/
|
|
2118
|
+
isInRange(cellISO, startISO, endISO) {
|
|
2119
|
+
if (!startISO || !endISO)
|
|
2120
|
+
return false;
|
|
2121
|
+
return cellISO >= startISO && cellISO <= endISO;
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* Check if cell is within hover preview range
|
|
2125
|
+
*
|
|
2126
|
+
* @param cellISO Cell date (ISO format)
|
|
2127
|
+
* @param hoverRange Hover range boundaries (or null)
|
|
2128
|
+
* @returns True if cell is in hover range
|
|
2129
|
+
*/
|
|
2130
|
+
isInHoverRange(cellISO, hoverRange) {
|
|
2131
|
+
if (!hoverRange)
|
|
2132
|
+
return false;
|
|
2133
|
+
return cellISO >= hoverRange.min && cellISO <= hoverRange.max;
|
|
2134
|
+
}
|
|
2135
|
+
/**
|
|
2136
|
+
* Check if cell is disabled
|
|
2137
|
+
*
|
|
2138
|
+
* @param date Cell date object
|
|
2139
|
+
* @param minISO Minimum allowed date (ISO or null)
|
|
2140
|
+
* @param maxISO Maximum allowed date (ISO or null)
|
|
2141
|
+
* @param disabledDates Disabled dates (array, function, or null)
|
|
2142
|
+
* @returns True if cell is disabled
|
|
2143
|
+
*/
|
|
2144
|
+
isDisabled(date, minISO, maxISO, disabledDates) {
|
|
2145
|
+
const dateISO = this.adapter.toISODate(date);
|
|
2146
|
+
// Check min/max constraints
|
|
2147
|
+
if (minISO && dateISO < minISO)
|
|
2148
|
+
return true;
|
|
2149
|
+
if (maxISO && dateISO > maxISO)
|
|
2150
|
+
return true;
|
|
2151
|
+
// Check disabled dates
|
|
2152
|
+
if (!disabledDates)
|
|
2153
|
+
return false;
|
|
2154
|
+
if (typeof disabledDates === 'function') {
|
|
2155
|
+
// Custom predicate
|
|
2156
|
+
return disabledDates(date);
|
|
2157
|
+
}
|
|
2158
|
+
else if (Array.isArray(disabledDates)) {
|
|
2159
|
+
// Array of disabled dates (exact day match)
|
|
2160
|
+
return disabledDates.some(disabledDate => {
|
|
2161
|
+
return this.adapter.getYear(date) === this.adapter.getYear(disabledDate) &&
|
|
2162
|
+
this.adapter.getMonth(date) === this.adapter.getMonth(disabledDate) &&
|
|
2163
|
+
this.adapter.getDate(date) === this.adapter.getDate(disabledDate);
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
return false;
|
|
2167
|
+
}
|
|
2168
|
+
/**
|
|
2169
|
+
* Compute hover preview range
|
|
2170
|
+
*
|
|
2171
|
+
* When user hovers over a date (and not selecting start):
|
|
2172
|
+
* - Show preview range from start to hover
|
|
2173
|
+
* - Range is always [min, max] where min <= max
|
|
2174
|
+
*
|
|
2175
|
+
* @param startISO Current start date (ISO or null)
|
|
2176
|
+
* @param hoverISO Hovered date (ISO or null)
|
|
2177
|
+
* @param selectingStart True if selecting start date (hover disabled)
|
|
2178
|
+
* @param multiRange True if in multi-range mode
|
|
2179
|
+
* @returns Hover range boundaries or null
|
|
2180
|
+
*/
|
|
2181
|
+
computeHoverRange(startISO, hoverISO, selectingStart, multiRange) {
|
|
2182
|
+
// No hover preview when selecting start
|
|
2183
|
+
if (selectingStart || !hoverISO || !startISO)
|
|
2184
|
+
return null;
|
|
2185
|
+
// Compute min/max (always normalized order)
|
|
2186
|
+
const min = startISO < hoverISO ? startISO : hoverISO;
|
|
2187
|
+
const max = startISO > hoverISO ? startISO : hoverISO;
|
|
2188
|
+
return { min, max };
|
|
2189
|
+
}
|
|
2190
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RangeHighlighter, deps: [{ token: DATE_ADAPTER }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2191
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RangeHighlighter, providedIn: 'root' });
|
|
2192
|
+
}
|
|
2193
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RangeHighlighter, decorators: [{
|
|
2194
|
+
type: Injectable,
|
|
2195
|
+
args: [{ providedIn: 'root' }]
|
|
2196
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
2197
|
+
type: Inject,
|
|
2198
|
+
args: [DATE_ADAPTER]
|
|
2199
|
+
}] }] });
|
|
2200
|
+
|
|
2201
|
+
/**
|
|
2202
|
+
* Range Highlighter Cache
|
|
2203
|
+
*
|
|
2204
|
+
* LRU cache for decorated calendar grids.
|
|
2205
|
+
* Eliminates redundant decoration computations when range/constraints don't change.
|
|
2206
|
+
*
|
|
2207
|
+
* @module core/calendar-grid/range-highlighter.cache
|
|
2208
|
+
* @version 3.8.0
|
|
2209
|
+
*/
|
|
2210
|
+
/**
|
|
2211
|
+
* Range Highlighter Cache
|
|
2212
|
+
*
|
|
2213
|
+
* Memoizes decorated grids to avoid recomputing decorations.
|
|
2214
|
+
*
|
|
2215
|
+
* Cache strategy:
|
|
2216
|
+
* - LRU eviction (Map preserves insertion order)
|
|
2217
|
+
* - Max 48 entries (~1-2 years of decorated grids)
|
|
2218
|
+
* - Key: month + start + end + hover + disabled signature
|
|
2219
|
+
*
|
|
2220
|
+
* Performance:
|
|
2221
|
+
* - Cache hit: ~0.1ms (object lookup)
|
|
2222
|
+
* - Cache miss: ~1ms (decoration + store)
|
|
2223
|
+
* - Hit rate: Expected >80% in typical usage
|
|
2224
|
+
*
|
|
2225
|
+
* Memory:
|
|
2226
|
+
* - ~15KB per decorated grid
|
|
2227
|
+
* - Max 48 grids = ~720KB
|
|
2228
|
+
* - Auto-eviction prevents leaks
|
|
2229
|
+
*
|
|
2230
|
+
* Edge cases handled:
|
|
2231
|
+
* - Function predicates: Not cacheable (recompute every time)
|
|
2232
|
+
* - Large disabled arrays: Sorted + joined for stable key
|
|
2233
|
+
* - Hover changes: Separate cache entries
|
|
2234
|
+
* - Multi-range mode: Included in key
|
|
2235
|
+
*
|
|
2236
|
+
* Usage:
|
|
2237
|
+
* ```typescript
|
|
2238
|
+
* const grid = calendarGridCache.get(monthDate, 0);
|
|
2239
|
+
* const decorated = rangeHighlighterCache.get(grid, {
|
|
2240
|
+
* start: startDate,
|
|
2241
|
+
* end: endDate,
|
|
2242
|
+
* hoverDate: '2026-01-20',
|
|
2243
|
+
* disabledDates: [...]
|
|
2244
|
+
* });
|
|
2245
|
+
* // Same params = same object reference (===)
|
|
2246
|
+
* ```
|
|
2247
|
+
*/
|
|
2248
|
+
class RangeHighlighterCache {
|
|
2249
|
+
highlighter;
|
|
2250
|
+
adapter;
|
|
2251
|
+
cache = new Map();
|
|
2252
|
+
maxSize = 48; // ~1-2 years of decorated grids
|
|
2253
|
+
constructor(highlighter, adapter) {
|
|
2254
|
+
this.highlighter = highlighter;
|
|
2255
|
+
this.adapter = adapter;
|
|
2256
|
+
}
|
|
2257
|
+
/**
|
|
2258
|
+
* Get decorated grid (cached or computed)
|
|
2259
|
+
*
|
|
2260
|
+
* If cache hit: Returns existing DecoratedGrid (same object reference)
|
|
2261
|
+
* If cache miss: Computes decorations, stores in cache, returns new grid
|
|
2262
|
+
*
|
|
2263
|
+
* @param grid Base calendar grid (from CalendarGridCache)
|
|
2264
|
+
* @param params Decoration parameters
|
|
2265
|
+
* @returns Decorated grid with range highlights
|
|
2266
|
+
*/
|
|
2267
|
+
get(grid, params) {
|
|
2268
|
+
// Build cache key
|
|
2269
|
+
const cacheKey = this.buildKey(grid, params);
|
|
2270
|
+
// Check cache (function predicates can't be cached reliably)
|
|
2271
|
+
const hasFunction = typeof params.disabledDates === 'function';
|
|
2272
|
+
if (!hasFunction && this.cache.has(cacheKey.full)) {
|
|
2273
|
+
// Cache hit: Return existing grid
|
|
2274
|
+
const cached = this.cache.get(cacheKey.full);
|
|
2275
|
+
// LRU update: Delete and re-insert (moves to end)
|
|
2276
|
+
this.cache.delete(cacheKey.full);
|
|
2277
|
+
this.cache.set(cacheKey.full, cached);
|
|
2278
|
+
return cached;
|
|
2279
|
+
}
|
|
2280
|
+
// Cache miss: Compute decorations
|
|
2281
|
+
const decorated = this.highlighter.decorate(grid, params);
|
|
2282
|
+
// Store in cache (only if not using function predicate)
|
|
2283
|
+
if (!hasFunction) {
|
|
2284
|
+
this.cache.set(cacheKey.full, decorated);
|
|
2285
|
+
// Evict oldest if over limit
|
|
2286
|
+
if (this.cache.size > this.maxSize) {
|
|
2287
|
+
const oldestKey = this.cache.keys().next().value;
|
|
2288
|
+
this.cache.delete(oldestKey);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
return decorated;
|
|
2292
|
+
}
|
|
2293
|
+
/**
|
|
2294
|
+
* Build cache key from grid and params
|
|
2295
|
+
*
|
|
2296
|
+
* Key structure:
|
|
2297
|
+
* `${monthKey}|${startISO}|${endISO}|${minISO}|${maxISO}|${hoverISO}|${disabledSig}`
|
|
2298
|
+
*
|
|
2299
|
+
* Example:
|
|
2300
|
+
* "2026-1-0-|2026-01-15|2026-01-25|null|null|2026-01-20|2026-01-10,2026-01-11"
|
|
2301
|
+
*
|
|
2302
|
+
* @param grid Base calendar grid
|
|
2303
|
+
* @param params Decoration parameters
|
|
2304
|
+
* @returns Cache key components
|
|
2305
|
+
*/
|
|
2306
|
+
buildKey(grid, params) {
|
|
2307
|
+
// Month key (from base grid)
|
|
2308
|
+
const monthKey = `${grid.month.year}-${grid.month.month}-${grid.weekStart}-${grid.locale || ''}`;
|
|
2309
|
+
// Date ISOs (null if not provided)
|
|
2310
|
+
const startISO = params.start ? this.adapter.toISODate(params.start) : 'null';
|
|
2311
|
+
const endISO = params.end ? this.adapter.toISODate(params.end) : 'null';
|
|
2312
|
+
const minISO = params.minDate ? this.adapter.toISODate(params.minDate) : 'null';
|
|
2313
|
+
const maxISO = params.maxDate ? this.adapter.toISODate(params.maxDate) : 'null';
|
|
2314
|
+
const hoverISO = params.hoverDate || 'null';
|
|
2315
|
+
// Disabled dates signature
|
|
2316
|
+
const disabledSignature = this.buildDisabledSignature(params.disabledDates);
|
|
2317
|
+
// Multi-range flag (affects hover behavior)
|
|
2318
|
+
const multiRangeFlag = params.multiRange ? '1' : '0';
|
|
2319
|
+
const selectingStartFlag = params.selectingStartDate ? '1' : '0';
|
|
2320
|
+
// Full key (pipe-separated for readability)
|
|
2321
|
+
const full = `${monthKey}|${startISO}|${endISO}|${minISO}|${maxISO}|${hoverISO}|${disabledSignature}|${multiRangeFlag}|${selectingStartFlag}`;
|
|
2322
|
+
return {
|
|
2323
|
+
monthKey,
|
|
2324
|
+
startISO,
|
|
2325
|
+
endISO,
|
|
2326
|
+
minISO,
|
|
2327
|
+
maxISO,
|
|
2328
|
+
hoverISO,
|
|
2329
|
+
disabledSignature,
|
|
2330
|
+
full
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
/**
|
|
2334
|
+
* Build signature for disabled dates
|
|
2335
|
+
*
|
|
2336
|
+
* Strategies:
|
|
2337
|
+
* - null/undefined: 'none'
|
|
2338
|
+
* - Function: 'function' (not cacheable)
|
|
2339
|
+
* - Array: Sorted ISO dates joined with commas
|
|
2340
|
+
*
|
|
2341
|
+
* @param disabledDates Disabled dates specification
|
|
2342
|
+
* @returns Signature string for cache key
|
|
2343
|
+
*/
|
|
2344
|
+
buildDisabledSignature(disabledDates) {
|
|
2345
|
+
if (!disabledDates)
|
|
2346
|
+
return 'none';
|
|
2347
|
+
if (typeof disabledDates === 'function') {
|
|
2348
|
+
return 'function';
|
|
2349
|
+
}
|
|
2350
|
+
if (Array.isArray(disabledDates)) {
|
|
2351
|
+
if (disabledDates.length === 0)
|
|
2352
|
+
return 'none';
|
|
2353
|
+
// Convert to ISO dates, sort, join
|
|
2354
|
+
const isoArray = disabledDates
|
|
2355
|
+
.map(date => this.adapter.toISODate(date))
|
|
2356
|
+
.sort();
|
|
2357
|
+
return isoArray.join(',');
|
|
2358
|
+
}
|
|
2359
|
+
return 'none';
|
|
2360
|
+
}
|
|
2361
|
+
/**
|
|
2362
|
+
* Clear all cached grids
|
|
2363
|
+
*/
|
|
2364
|
+
clear() {
|
|
2365
|
+
this.cache.clear();
|
|
2366
|
+
}
|
|
2367
|
+
/**
|
|
2368
|
+
* Get current cache size
|
|
2369
|
+
*/
|
|
2370
|
+
size() {
|
|
2371
|
+
return this.cache.size;
|
|
2372
|
+
}
|
|
2373
|
+
/**
|
|
2374
|
+
* Check if key is cached
|
|
2375
|
+
*
|
|
2376
|
+
* @param grid Base calendar grid
|
|
2377
|
+
* @param params Decoration parameters
|
|
2378
|
+
* @returns True if this grid+params is cached
|
|
2379
|
+
*/
|
|
2380
|
+
has(grid, params) {
|
|
2381
|
+
const key = this.buildKey(grid, params);
|
|
2382
|
+
return this.cache.has(key.full);
|
|
2383
|
+
}
|
|
2384
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RangeHighlighterCache, deps: [{ token: RangeHighlighter }, { token: DATE_ADAPTER }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2385
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RangeHighlighterCache, providedIn: 'root' });
|
|
2386
|
+
}
|
|
2387
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RangeHighlighterCache, decorators: [{
|
|
2388
|
+
type: Injectable,
|
|
2389
|
+
args: [{ providedIn: 'root' }]
|
|
2390
|
+
}], ctorParameters: () => [{ type: RangeHighlighter }, { type: undefined, decorators: [{
|
|
2391
|
+
type: Inject,
|
|
2392
|
+
args: [DATE_ADAPTER]
|
|
2393
|
+
}] }] });
|
|
2394
|
+
|
|
2395
|
+
/**
|
|
2396
|
+
* Virtual Weeks Types
|
|
2397
|
+
*
|
|
2398
|
+
* Type definitions for windowed week rendering (virtual scrolling).
|
|
2399
|
+
* Reduces DOM nodes by rendering only visible weeks instead of full month.
|
|
2400
|
+
*
|
|
2401
|
+
* @module core/calendar-grid/virtual-weeks.types
|
|
2402
|
+
* @version 3.9.0
|
|
2403
|
+
*/
|
|
2404
|
+
|
|
2405
|
+
/**
|
|
2406
|
+
* Virtual Weeks Logic (Pure Functions)
|
|
2407
|
+
*
|
|
2408
|
+
* Pure computation layer for windowed week rendering.
|
|
2409
|
+
* No side effects, fully testable with node:test.
|
|
2410
|
+
*
|
|
2411
|
+
* @module core/calendar-grid/virtual-weeks.logic
|
|
2412
|
+
* @version 3.9.0
|
|
2413
|
+
*/
|
|
2414
|
+
/**
|
|
2415
|
+
* Get visible weeks from total weeks array
|
|
2416
|
+
*
|
|
2417
|
+
* Pure function: Given weeks array and window config, returns visible slice.
|
|
2418
|
+
*
|
|
2419
|
+
* @param weeks Total weeks array (usually 6 weeks)
|
|
2420
|
+
* @param startIndex Start index of visible window (0-based)
|
|
2421
|
+
* @param windowSize How many weeks to show
|
|
2422
|
+
* @returns Visible weeks slice
|
|
2423
|
+
*
|
|
2424
|
+
* @example
|
|
2425
|
+
* ```typescript
|
|
2426
|
+
* const allWeeks = [week0, week1, week2, week3, week4, week5]; // 6 weeks
|
|
2427
|
+
* const visible = getVisibleWeeks(allWeeks, 0, 3);
|
|
2428
|
+
* // Returns [week0, week1, week2]
|
|
2429
|
+
*
|
|
2430
|
+
* const visible2 = getVisibleWeeks(allWeeks, 3, 3);
|
|
2431
|
+
* // Returns [week3, week4, week5]
|
|
2432
|
+
* ```
|
|
2433
|
+
*/
|
|
2434
|
+
function getVisibleWeeks(weeks, startIndex, windowSize) {
|
|
2435
|
+
if (!weeks || weeks.length === 0) {
|
|
2436
|
+
return [];
|
|
2437
|
+
}
|
|
2438
|
+
// If no windowing (windowSize undefined or >= total weeks), return all
|
|
2439
|
+
if (windowSize === undefined || windowSize >= weeks.length) {
|
|
2440
|
+
return weeks;
|
|
2441
|
+
}
|
|
2442
|
+
// Clamp startIndex to valid range
|
|
2443
|
+
const clampedStart = clampWeekStart(startIndex, weeks.length, windowSize);
|
|
2444
|
+
// Return slice
|
|
2445
|
+
return weeks.slice(clampedStart, clampedStart + windowSize);
|
|
2446
|
+
}
|
|
2447
|
+
/**
|
|
2448
|
+
* Clamp week start index to valid range
|
|
2449
|
+
*
|
|
2450
|
+
* Ensures startIndex is within bounds [0, totalWeeks - windowSize].
|
|
2451
|
+
* Prevents scrolling beyond available weeks.
|
|
2452
|
+
*
|
|
2453
|
+
* @param startIndex Desired start index
|
|
2454
|
+
* @param totalWeeks Total weeks available
|
|
2455
|
+
* @param windowSize Window size
|
|
2456
|
+
* @returns Clamped start index
|
|
2457
|
+
*
|
|
2458
|
+
* @example
|
|
2459
|
+
* ```typescript
|
|
2460
|
+
* clampWeekStart(0, 6, 3); // 0 (valid)
|
|
2461
|
+
* clampWeekStart(3, 6, 3); // 3 (valid, shows weeks 3-5)
|
|
2462
|
+
* clampWeekStart(4, 6, 3); // 3 (clamped, can't show beyond week 5)
|
|
2463
|
+
* clampWeekStart(-1, 6, 3); // 0 (clamped, can't go negative)
|
|
2464
|
+
* ```
|
|
2465
|
+
*/
|
|
2466
|
+
function clampWeekStart(startIndex, totalWeeks, windowSize) {
|
|
2467
|
+
if (windowSize >= totalWeeks) {
|
|
2468
|
+
return 0; // No windowing needed
|
|
2469
|
+
}
|
|
2470
|
+
const maxStart = totalWeeks - windowSize;
|
|
2471
|
+
return Math.max(0, Math.min(startIndex, maxStart));
|
|
2472
|
+
}
|
|
2473
|
+
/**
|
|
2474
|
+
* Navigate week window (scroll up/down)
|
|
2475
|
+
*
|
|
2476
|
+
* Returns new start index after navigation.
|
|
2477
|
+
* Handles clamping automatically.
|
|
2478
|
+
*
|
|
2479
|
+
* @param currentStart Current start index
|
|
2480
|
+
* @param direction Navigation direction (+1 = down/later, -1 = up/earlier)
|
|
2481
|
+
* @param totalWeeks Total weeks available
|
|
2482
|
+
* @param windowSize Window size
|
|
2483
|
+
* @returns New start index after navigation
|
|
2484
|
+
*
|
|
2485
|
+
* @example
|
|
2486
|
+
* ```typescript
|
|
2487
|
+
* // Start at week 0, navigate down
|
|
2488
|
+
* navigateWeekWindow(0, 1, 6, 3); // Returns 1 (now showing weeks 1-3)
|
|
2489
|
+
*
|
|
2490
|
+
* // At week 3 (last valid position), navigate down
|
|
2491
|
+
* navigateWeekWindow(3, 1, 6, 3); // Returns 3 (can't go further)
|
|
2492
|
+
*
|
|
2493
|
+
* // At week 1, navigate up
|
|
2494
|
+
* navigateWeekWindow(1, -1, 6, 3); // Returns 0 (now showing weeks 0-2)
|
|
2495
|
+
* ```
|
|
2496
|
+
*/
|
|
2497
|
+
function navigateWeekWindow(currentStart, direction, totalWeeks, windowSize) {
|
|
2498
|
+
const newStart = currentStart + direction;
|
|
2499
|
+
return clampWeekStart(newStart, totalWeeks, windowSize);
|
|
2500
|
+
}
|
|
2501
|
+
/**
|
|
2502
|
+
* Get virtual week window state
|
|
2503
|
+
*
|
|
2504
|
+
* Computes full window state including navigation capabilities.
|
|
2505
|
+
*
|
|
2506
|
+
* @param startIndex Current start index
|
|
2507
|
+
* @param totalWeeks Total weeks available
|
|
2508
|
+
* @param windowSize Window size
|
|
2509
|
+
* @returns Complete window state
|
|
2510
|
+
*
|
|
2511
|
+
* @example
|
|
2512
|
+
* ```typescript
|
|
2513
|
+
* const state = getVirtualWeekWindow(0, 6, 3);
|
|
2514
|
+
* // {
|
|
2515
|
+
* // startIndex: 0,
|
|
2516
|
+
* // windowSize: 3,
|
|
2517
|
+
* // totalWeeks: 6,
|
|
2518
|
+
* // canNavigateUp: false, // Already at top
|
|
2519
|
+
* // canNavigateDown: true // Can scroll down
|
|
2520
|
+
* // }
|
|
2521
|
+
*
|
|
2522
|
+
* const state2 = getVirtualWeekWindow(3, 6, 3);
|
|
2523
|
+
* // {
|
|
2524
|
+
* // startIndex: 3,
|
|
2525
|
+
* // windowSize: 3,
|
|
2526
|
+
* // totalWeeks: 6,
|
|
2527
|
+
* // canNavigateUp: true, // Can scroll up
|
|
2528
|
+
* // canNavigateDown: false // Already at bottom
|
|
2529
|
+
* // }
|
|
2530
|
+
* ```
|
|
2531
|
+
*/
|
|
2532
|
+
function getVirtualWeekWindow(startIndex, totalWeeks, windowSize) {
|
|
2533
|
+
const clampedStart = clampWeekStart(startIndex, totalWeeks, windowSize);
|
|
2534
|
+
const maxStart = Math.max(0, totalWeeks - windowSize);
|
|
2535
|
+
return {
|
|
2536
|
+
startIndex: clampedStart,
|
|
2537
|
+
windowSize,
|
|
2538
|
+
totalWeeks,
|
|
2539
|
+
canNavigateUp: clampedStart > 0,
|
|
2540
|
+
canNavigateDown: clampedStart < maxStart
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
/**
|
|
2544
|
+
* Check if virtual weeks mode is enabled
|
|
2545
|
+
*
|
|
2546
|
+
* @param windowSize Window size from config (undefined = disabled)
|
|
2547
|
+
* @param totalWeeks Total weeks available
|
|
2548
|
+
* @returns True if windowing should be applied
|
|
2549
|
+
*/
|
|
2550
|
+
function isVirtualWeeksEnabled(windowSize, totalWeeks) {
|
|
2551
|
+
return windowSize !== undefined && windowSize < totalWeeks;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
/**
|
|
2555
|
+
* Calendar Grid Module
|
|
2556
|
+
*
|
|
2557
|
+
* Performance-optimized calendar month grid generation with memoization.
|
|
2558
|
+
*
|
|
2559
|
+
* v3.7.0: Grid Structure Cache
|
|
2560
|
+
* - CalendarGridFactory for deterministic 42-cell grids
|
|
2561
|
+
* - CalendarGridCache with LRU (24 months)
|
|
2562
|
+
*
|
|
2563
|
+
* v3.8.0: Range Highlight Cache
|
|
2564
|
+
* - RangeHighlighter for decoration logic
|
|
2565
|
+
* - RangeHighlighterCache with LRU (48 grids)
|
|
2566
|
+
* - Separates grid structure from decorations
|
|
2567
|
+
*
|
|
2568
|
+
* v3.9.0: Virtual Weeks (Windowed Rendering)
|
|
2569
|
+
* - Virtual-weeks logic for reduced DOM complexity
|
|
2570
|
+
* - Render only visible weeks (configurable window)
|
|
2571
|
+
* - ~50% reduction in DOM nodes with windowSize=3
|
|
2572
|
+
*
|
|
2573
|
+
* Usage:
|
|
2574
|
+
* ```typescript
|
|
2575
|
+
* constructor(
|
|
2576
|
+
* private gridCache: CalendarGridCache,
|
|
2577
|
+
* private highlighterCache: RangeHighlighterCache
|
|
2578
|
+
* ) {}
|
|
2579
|
+
*
|
|
2580
|
+
* const grid = this.gridCache.get(monthDate, weekStart);
|
|
2581
|
+
* const decorated = this.highlighterCache.get(grid, {
|
|
2582
|
+
* start, end, hoverDate, disabledDates
|
|
2583
|
+
* });
|
|
2584
|
+
*
|
|
2585
|
+
* // Optional: Windowed rendering (v3.9.0+)
|
|
2586
|
+
* const windowSize = 3; // Render only 3 weeks
|
|
2587
|
+
* const visibleWeeks = getVisibleWeeks(
|
|
2588
|
+
* decorated.weeks,
|
|
2589
|
+
* weekStartIndex,
|
|
2590
|
+
* windowSize
|
|
2591
|
+
* );
|
|
2592
|
+
* // decorated.cells[0].iso => '2026-02-01'
|
|
2593
|
+
* // decorated.cells[0].isInRange => true
|
|
2594
|
+
* ```
|
|
2595
|
+
*/
|
|
2596
|
+
|
|
1819
2597
|
class DualDatepickerComponent {
|
|
1820
2598
|
elementRef;
|
|
1821
2599
|
placeholder = 'Select date range';
|
|
@@ -1865,12 +2643,35 @@ class DualDatepickerComponent {
|
|
|
1865
2643
|
minuteStep = 15; // Step for minute selector (1, 5, 15, 30)
|
|
1866
2644
|
defaultStartTime = '00:00'; // Default start time HH:mm
|
|
1867
2645
|
defaultEndTime = '23:59'; // Default end time HH:mm
|
|
2646
|
+
/**
|
|
2647
|
+
* Virtual Weeks Configuration (v3.9.0+)
|
|
2648
|
+
*
|
|
2649
|
+
* Enables windowed rendering to reduce DOM complexity and improve mobile performance.
|
|
2650
|
+
* Only renders a subset of calendar weeks instead of all 6.
|
|
2651
|
+
*
|
|
2652
|
+
* Example: `{ windowSize: 3 }` renders only 3 weeks (21 cells) instead of 6 weeks (42 cells),
|
|
2653
|
+
* reducing DOM nodes by ~50% per calendar.
|
|
2654
|
+
*
|
|
2655
|
+
* @default undefined (disabled - renders all 6 weeks for backward compatibility)
|
|
2656
|
+
*
|
|
2657
|
+
* Example:
|
|
2658
|
+
* ```html
|
|
2659
|
+
* <ngx-dual-datepicker
|
|
2660
|
+
* [virtualWeeks]="{ windowSize: 3 }">
|
|
2661
|
+
* </ngx-dual-datepicker>
|
|
2662
|
+
* ```
|
|
2663
|
+
*/
|
|
2664
|
+
virtualWeeks;
|
|
1868
2665
|
dateRangeChange = new EventEmitter();
|
|
1869
2666
|
dateRangeSelected = new EventEmitter();
|
|
1870
2667
|
multiDateRangeChange = new EventEmitter();
|
|
1871
2668
|
multiDateRangeSelected = new EventEmitter();
|
|
1872
2669
|
// Date adapter injection
|
|
1873
2670
|
dateAdapter = inject(DATE_ADAPTER$1);
|
|
2671
|
+
// Calendar grid cache (v3.7.0+) - memoizes month grid generation
|
|
2672
|
+
gridCache = inject(CalendarGridCache);
|
|
2673
|
+
// Range highlighter cache (v3.8.0+) - memoizes decorations
|
|
2674
|
+
highlighterCache = inject(RangeHighlighterCache);
|
|
1874
2675
|
// Headless store for date range state (v3.5.0+)
|
|
1875
2676
|
rangeStore = inject(DualDateRangeStore);
|
|
1876
2677
|
// UI-only signals
|
|
@@ -1883,6 +2684,51 @@ class DualDatepickerComponent {
|
|
|
1883
2684
|
showStartTimePicker = signal(false);
|
|
1884
2685
|
showEndTimePicker = signal(false);
|
|
1885
2686
|
hoverDate = signal(null);
|
|
2687
|
+
/**
|
|
2688
|
+
* Virtual Weeks State (v3.9.0+)
|
|
2689
|
+
*
|
|
2690
|
+
* Signals to track which week window is currently visible for each calendar.
|
|
2691
|
+
* - weekStart = 0: Shows first N weeks (windowSize)
|
|
2692
|
+
* - weekStart = 1: Shows weeks 1 to N+1, etc.
|
|
2693
|
+
*
|
|
2694
|
+
* Reset to 0 when month changes for consistent UX.
|
|
2695
|
+
*/
|
|
2696
|
+
previousMonthWeekStart = signal(0);
|
|
2697
|
+
currentMonthWeekStart = signal(0);
|
|
2698
|
+
/**
|
|
2699
|
+
* Computed: Visible weeks for windowed rendering (v3.9.0+)
|
|
2700
|
+
*
|
|
2701
|
+
* If virtualWeeks is enabled, returns only the visible subset of weeks.
|
|
2702
|
+
* Otherwise, returns all weeks for backward compatibility.
|
|
2703
|
+
*
|
|
2704
|
+
* Example: If windowSize=3 and weekStart=0, returns first 3 weeks (rows 0-2).
|
|
2705
|
+
*/
|
|
2706
|
+
previousMonthVisibleDays = computed(() => {
|
|
2707
|
+
const allDays = this.previousMonthDays();
|
|
2708
|
+
if (!this.virtualWeeks || !isVirtualWeeksEnabled(this.virtualWeeks.windowSize, 6)) {
|
|
2709
|
+
return allDays;
|
|
2710
|
+
}
|
|
2711
|
+
// Calendar has 6 weeks (42 cells = 7 days × 6 weeks)
|
|
2712
|
+
const allWeeks = [];
|
|
2713
|
+
for (let i = 0; i < 6; i++) {
|
|
2714
|
+
allWeeks.push(allDays.slice(i * 7, (i + 1) * 7));
|
|
2715
|
+
}
|
|
2716
|
+
const visibleWeeks = getVisibleWeeks(allWeeks, this.previousMonthWeekStart(), this.virtualWeeks.windowSize);
|
|
2717
|
+
return visibleWeeks.flat();
|
|
2718
|
+
});
|
|
2719
|
+
currentMonthVisibleDays = computed(() => {
|
|
2720
|
+
const allDays = this.currentMonthDays();
|
|
2721
|
+
if (!this.virtualWeeks || !isVirtualWeeksEnabled(this.virtualWeeks.windowSize, 6)) {
|
|
2722
|
+
return allDays;
|
|
2723
|
+
}
|
|
2724
|
+
// Calendar has 6 weeks (42 cells = 7 days × 6 weeks)
|
|
2725
|
+
const allWeeks = [];
|
|
2726
|
+
for (let i = 0; i < 6; i++) {
|
|
2727
|
+
allWeeks.push(allDays.slice(i * 7, (i + 1) * 7));
|
|
2728
|
+
}
|
|
2729
|
+
const visibleWeeks = getVisibleWeeks(allWeeks, this.currentMonthWeekStart(), this.virtualWeeks.windowSize);
|
|
2730
|
+
return visibleWeeks.flat();
|
|
2731
|
+
});
|
|
1886
2732
|
// Computed time properties from store
|
|
1887
2733
|
get startHour() {
|
|
1888
2734
|
const time = this.rangeStore.startTime();
|
|
@@ -2372,43 +3218,63 @@ class DualDatepickerComponent {
|
|
|
2372
3218
|
generateCalendars() {
|
|
2373
3219
|
this.previousMonthDays.set(this.generateMonthCalendar(this.previousMonth()));
|
|
2374
3220
|
this.currentMonthDays.set(this.generateMonthCalendar(this.currentMonth()));
|
|
3221
|
+
// Reset virtual week windows when month changes (v3.9.0+)
|
|
3222
|
+
// Always start at first week for consistent UX
|
|
3223
|
+
if (this.virtualWeeks) {
|
|
3224
|
+
this.previousMonthWeekStart.set(0);
|
|
3225
|
+
this.currentMonthWeekStart.set(0);
|
|
3226
|
+
}
|
|
2375
3227
|
}
|
|
3228
|
+
/**
|
|
3229
|
+
* Generate calendar grid with decorations (v3.8.0+)
|
|
3230
|
+
*
|
|
3231
|
+
* Uses CalendarGridCache for base grid structure (memoized by month),
|
|
3232
|
+
* then uses RangeHighlighterCache for decorations (memoized by range/constraints).
|
|
3233
|
+
*
|
|
3234
|
+
* Performance:
|
|
3235
|
+
* - Grid structure: Cached by month (same month = same grid object)
|
|
3236
|
+
* - Decorations: Cached by range+hover+disabled (same state = same decorated grid)
|
|
3237
|
+
* - Result: ~95% cache hit rate in typical usage
|
|
3238
|
+
*
|
|
3239
|
+
* Cache keys:
|
|
3240
|
+
* - Grid: `${year}-${month}-${weekStart}`
|
|
3241
|
+
* - Decorations: `${monthKey}|${start}|${end}|${hover}|${disabled}`
|
|
3242
|
+
*/
|
|
2376
3243
|
generateMonthCalendar(date) {
|
|
2377
|
-
|
|
2378
|
-
const
|
|
2379
|
-
|
|
2380
|
-
const
|
|
2381
|
-
const
|
|
2382
|
-
const
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
//
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
date: dateStr,
|
|
3244
|
+
// Get base grid from cache (weekStart = 0 for Sunday, no locale for now)
|
|
3245
|
+
const grid = this.gridCache.get(date, 0);
|
|
3246
|
+
// Get dates from store
|
|
3247
|
+
const startDate = this.rangeStore.startDate();
|
|
3248
|
+
const endDate = this.rangeStore.endDate();
|
|
3249
|
+
const hoverISO = this.hoverDate();
|
|
3250
|
+
// Get decorated grid from cache (handles all decoration logic)
|
|
3251
|
+
const decorated = this.highlighterCache.get(grid, {
|
|
3252
|
+
start: startDate,
|
|
3253
|
+
end: endDate,
|
|
3254
|
+
hoverDate: hoverISO,
|
|
3255
|
+
disabledDates: this.disabledDates,
|
|
3256
|
+
multiRange: this.multiRange,
|
|
3257
|
+
selectingStartDate: this.selectingStartDate()
|
|
3258
|
+
// minDate/maxDate omitted for now (not in current API)
|
|
3259
|
+
});
|
|
3260
|
+
// Map decorated cells to legacy format (for backward compatibility)
|
|
3261
|
+
const monthDays = decorated.cells.map((cell) => {
|
|
3262
|
+
if (!cell.inCurrentMonth) {
|
|
3263
|
+
// Padding cell (previous/next month)
|
|
3264
|
+
return { day: null, isCurrentMonth: false };
|
|
3265
|
+
}
|
|
3266
|
+
// Current month cell - use cached decorations
|
|
3267
|
+
return {
|
|
3268
|
+
day: cell.day,
|
|
3269
|
+
date: cell.iso,
|
|
2404
3270
|
isCurrentMonth: true,
|
|
2405
|
-
isStart:
|
|
2406
|
-
isEnd:
|
|
2407
|
-
inRange:
|
|
2408
|
-
inHoverRange:
|
|
2409
|
-
isDisabled:
|
|
2410
|
-
}
|
|
2411
|
-
}
|
|
3271
|
+
isStart: cell.isSelectedStart,
|
|
3272
|
+
isEnd: cell.isSelectedEnd,
|
|
3273
|
+
inRange: cell.isInRange,
|
|
3274
|
+
inHoverRange: cell.isInHoverRange,
|
|
3275
|
+
isDisabled: cell.isDisabled
|
|
3276
|
+
};
|
|
3277
|
+
});
|
|
2412
3278
|
return monthDays;
|
|
2413
3279
|
}
|
|
2414
3280
|
isInRange(dateStr) {
|
|
@@ -2424,6 +3290,54 @@ class DualDatepickerComponent {
|
|
|
2424
3290
|
return dateStr >= this.startDate && dateStr <= this.endDate;
|
|
2425
3291
|
}
|
|
2426
3292
|
}
|
|
3293
|
+
/**
|
|
3294
|
+
* Virtual Weeks Navigation (v3.9.0+)
|
|
3295
|
+
*
|
|
3296
|
+
* Navigate the visible week window up/down for windowed rendering.
|
|
3297
|
+
*
|
|
3298
|
+
* @param monthIndex 0 = previous month, 1 = current month
|
|
3299
|
+
* @param direction -1 = scroll up (previous weeks), +1 = scroll down (next weeks)
|
|
3300
|
+
*
|
|
3301
|
+
* Example: If windowSize=3 and weekStart=0, navigateWeeks(0, +1) shows weeks 1-3.
|
|
3302
|
+
*/
|
|
3303
|
+
navigateWeeks(monthIndex, direction) {
|
|
3304
|
+
if (!this.virtualWeeks)
|
|
3305
|
+
return;
|
|
3306
|
+
const totalWeeks = 6; // Standard calendar grid has 6 weeks
|
|
3307
|
+
const currentStart = monthIndex === 0
|
|
3308
|
+
? this.previousMonthWeekStart()
|
|
3309
|
+
: this.currentMonthWeekStart();
|
|
3310
|
+
const newStart = navigateWeekWindow(currentStart, direction, totalWeeks, this.virtualWeeks.windowSize);
|
|
3311
|
+
if (monthIndex === 0) {
|
|
3312
|
+
this.previousMonthWeekStart.set(newStart);
|
|
3313
|
+
}
|
|
3314
|
+
else {
|
|
3315
|
+
this.currentMonthWeekStart.set(newStart);
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
/**
|
|
3319
|
+
* Check if week navigation is available (v3.9.0+)
|
|
3320
|
+
*
|
|
3321
|
+
* @param monthIndex 0 = previous month, 1 = current month
|
|
3322
|
+
* @param direction -1 = up (can scroll to previous weeks?), +1 = down (can scroll to next weeks?)
|
|
3323
|
+
* @returns true if navigation is available in that direction
|
|
3324
|
+
*/
|
|
3325
|
+
canNavigateWeeks(monthIndex, direction) {
|
|
3326
|
+
if (!this.virtualWeeks)
|
|
3327
|
+
return false;
|
|
3328
|
+
const totalWeeks = 6;
|
|
3329
|
+
const currentStart = monthIndex === 0
|
|
3330
|
+
? this.previousMonthWeekStart()
|
|
3331
|
+
: this.currentMonthWeekStart();
|
|
3332
|
+
if (direction < 0) {
|
|
3333
|
+
// Can scroll up if not at start
|
|
3334
|
+
return currentStart > 0;
|
|
3335
|
+
}
|
|
3336
|
+
else {
|
|
3337
|
+
// Can scroll down if window doesn't extend past end
|
|
3338
|
+
return currentStart + this.virtualWeeks.windowSize < totalWeeks;
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
2427
3341
|
isDateDisabled(date) {
|
|
2428
3342
|
if (!this.disabledDates)
|
|
2429
3343
|
return false;
|
|
@@ -2862,7 +3776,7 @@ class DualDatepickerComponent {
|
|
|
2862
3776
|
this.isDisabled.set(isDisabled);
|
|
2863
3777
|
}
|
|
2864
3778
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DualDatepickerComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
2865
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: DualDatepickerComponent, isStandalone: true, selector: "ngx-dual-datepicker", inputs: { placeholder: "placeholder", startDate: "startDate", endDate: "endDate", showPresets: "showPresets", showClearButton: "showClearButton", multiRange: "multiRange", closeOnSelection: "closeOnSelection", closeOnPresetSelection: "closeOnPresetSelection", closeOnClickOutside: "closeOnClickOutside", enableKeyboardNavigation: "enableKeyboardNavigation", presets: "presets", theme: "theme", inputBackgroundColor: "inputBackgroundColor", inputTextColor: "inputTextColor", inputBorderColor: "inputBorderColor", inputBorderColorHover: "inputBorderColorHover", inputBorderColorFocus: "inputBorderColorFocus", inputPadding: "inputPadding", locale: "locale", disabledDates: "disabledDates", displayFormat: "displayFormat", requireApply: "requireApply", enableTimePicker: "enableTimePicker", timeFormat: "timeFormat", minuteStep: "minuteStep", defaultStartTime: "defaultStartTime", defaultEndTime: "defaultEndTime" }, outputs: { dateRangeChange: "dateRangeChange", dateRangeSelected: "dateRangeSelected", multiDateRangeChange: "multiDateRangeChange", multiDateRangeSelected: "multiDateRangeSelected" }, host: { listeners: { "document:click": "onClickOutside($event)", "keydown": "handleKeyboardNavigation($event)" } }, providers: [
|
|
3779
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: DualDatepickerComponent, isStandalone: true, selector: "ngx-dual-datepicker", inputs: { placeholder: "placeholder", startDate: "startDate", endDate: "endDate", showPresets: "showPresets", showClearButton: "showClearButton", multiRange: "multiRange", closeOnSelection: "closeOnSelection", closeOnPresetSelection: "closeOnPresetSelection", closeOnClickOutside: "closeOnClickOutside", enableKeyboardNavigation: "enableKeyboardNavigation", presets: "presets", theme: "theme", inputBackgroundColor: "inputBackgroundColor", inputTextColor: "inputTextColor", inputBorderColor: "inputBorderColor", inputBorderColorHover: "inputBorderColorHover", inputBorderColorFocus: "inputBorderColorFocus", inputPadding: "inputPadding", locale: "locale", disabledDates: "disabledDates", displayFormat: "displayFormat", requireApply: "requireApply", enableTimePicker: "enableTimePicker", timeFormat: "timeFormat", minuteStep: "minuteStep", defaultStartTime: "defaultStartTime", defaultEndTime: "defaultEndTime", virtualWeeks: "virtualWeeks" }, outputs: { dateRangeChange: "dateRangeChange", dateRangeSelected: "dateRangeSelected", multiDateRangeChange: "multiDateRangeChange", multiDateRangeSelected: "multiDateRangeSelected" }, host: { listeners: { "document:click": "onClickOutside($event)", "keydown": "handleKeyboardNavigation($event)" } }, providers: [
|
|
2866
3780
|
{
|
|
2867
3781
|
provide: NG_VALUE_ACCESSOR,
|
|
2868
3782
|
useExisting: forwardRef(() => DualDatepickerComponent),
|
|
@@ -2883,7 +3797,7 @@ class DualDatepickerComponent {
|
|
|
2883
3797
|
},
|
|
2884
3798
|
deps: [PresetRegistry]
|
|
2885
3799
|
}
|
|
2886
|
-
], usesOnChanges: true, ngImport: i0, template: "<div class=\"datepicker-wrapper\" \n [class]=\"'theme-' + theme\"\n [style.--input-border-hover]=\"inputBorderColorHover\"\n [style.--input-border-focus]=\"inputBorderColorFocus\">\n <input \n type=\"text\" \n class=\"datepicker-input\" \n [value]=\"dateRangeText()\" \n (click)=\"toggleDatePicker()\" \n [placeholder]=\"placeholder\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"placeholder\"\n [attr.aria-expanded]=\"showDatePicker()\"\n [attr.aria-haspopup]=\"'dialog'\"\n role=\"combobox\"\n [ngStyle]=\"{\n 'background-color': inputBackgroundColor,\n 'color': inputTextColor,\n 'border-color': inputBorderColor,\n 'padding': inputPadding\n }\"\n readonly>\n\n @if (showDatePicker()) {\n <div class=\"date-picker-dropdown\">\n @if (showPresets) {\n <div class=\"date-picker-presets\">\n @for (preset of presets; track preset.label) {\n <button type=\"button\" (click)=\"selectPresetRange(preset)\">{{ preset.label }}</button>\n }\n <button type=\"button\" class=\"btn-close-calendar\" (click)=\"closeDatePicker()\" title=\"Close\">\n \u00D7\n </button>\n </div>\n }\n\n @if (!showPresets) {\n <div class=\"date-picker-header-only-close\">\n <button type=\"button\" class=\"btn-close-calendar\" (click)=\"closeDatePicker()\" title=\"Close\">\n \u00D7\n </button>\n </div>\n }\n\n <!-- Calendars -->\n <div class=\"date-picker-calendars\">\n <!-- Previous month calendar -->\n <div class=\"date-picker-calendar\">\n <div class=\"date-picker-header\">\n <button type=\"button\" (click)=\"changeMonth(-1)\">\n <svg width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path fill-rule=\"evenodd\" d=\"M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z\"/>\n </svg>\n </button>\n <span>{{ previousMonthName() }}</span>\n <button type=\"button\" style=\"visibility: hidden;\">\n <svg width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path fill-rule=\"evenodd\" d=\"M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z\"/>\n </svg>\n </button>\n </div>\n <div class=\"date-picker-weekdays\">\n @for (dayName of weekDayNames(); track $index) {\n <span>{{ dayName }}</span>\n }\n </div>\n <div class=\"date-picker-days\">\n @for (dayObj of previousMonthDays(); track dayObj.date || $index) {\n <button \n type=\"button\"\n class=\"date-picker-day\" \n [class.empty]=\"!dayObj.isCurrentMonth\"\n [class.selected]=\"dayObj.isStart || dayObj.isEnd\"\n [class.in-range]=\"dayObj.inRange && !dayObj.isStart && !dayObj.isEnd\"\n [class.in-hover-range]=\"dayObj.inHoverRange && !dayObj.isStart && !dayObj.isEnd\"\n [class.disabled]=\"dayObj.isDisabled\"\n [class.keyboard-focused]=\"enableKeyboardNavigation && hasKeyboardFocus(dayObj.date, 0)\"\n [attr.tabindex]=\"enableKeyboardNavigation && dayObj.isCurrentMonth && hasKeyboardFocus(dayObj.date, 0) ? 0 : -1\"\n [attr.aria-label]=\"formatDateDisplay(dayObj.date)\"\n [attr.aria-selected]=\"dayObj.isStart || dayObj.isEnd\"\n [attr.aria-current]=\"dayObj.isStart ? 'date' : null\"\n [attr.aria-disabled]=\"dayObj.isDisabled\"\n (click)=\"selectDay(dayObj)\"\n (mouseenter)=\"onDayHover(dayObj)\"\n (mouseleave)=\"clearDayHover()\"\n [disabled]=\"!dayObj.isCurrentMonth || dayObj.isDisabled\">\n {{ dayObj.day }}\n </button>\n }\n </div>\n </div>\n\n <!-- Current month calendar -->\n <div class=\"date-picker-calendar\">\n <div class=\"date-picker-header\">\n <button type=\"button\" style=\"visibility: hidden;\">\n <svg width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path fill-rule=\"evenodd\" d=\"M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z\"/>\n </svg>\n </button>\n <span>{{ currentMonthName() }}</span>\n <button type=\"button\" (click)=\"changeMonth(1)\">\n <svg width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path fill-rule=\"evenodd\" d=\"M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z\"/>\n </svg>\n </button>\n </div>\n <div class=\"date-picker-weekdays\">\n @for (dayName of weekDayNames(); track $index) {\n <span>{{ dayName }}</span>\n }\n </div>\n <div class=\"date-picker-days\">\n @for (dayObj of currentMonthDays(); track dayObj.date || $index) {\n <button \n type=\"button\"\n class=\"date-picker-day\" \n [class.empty]=\"!dayObj.isCurrentMonth\"\n [class.selected]=\"dayObj.isStart || dayObj.isEnd\"\n [class.in-range]=\"dayObj.inRange && !dayObj.isStart && !dayObj.isEnd\"\n [class.in-hover-range]=\"dayObj.inHoverRange && !dayObj.isStart && !dayObj.isEnd\"\n [class.disabled]=\"dayObj.isDisabled\"\n [class.keyboard-focused]=\"enableKeyboardNavigation && hasKeyboardFocus(dayObj.date, 1)\"\n [attr.tabindex]=\"enableKeyboardNavigation && dayObj.isCurrentMonth && hasKeyboardFocus(dayObj.date, 1) ? 0 : -1\"\n [attr.aria-label]=\"formatDateDisplay(dayObj.date)\"\n [attr.aria-selected]=\"dayObj.isStart || dayObj.isEnd\"\n [attr.aria-current]=\"dayObj.isStart ? 'date' : null\"\n [attr.aria-disabled]=\"dayObj.isDisabled\"\n (click)=\"selectDay(dayObj)\"\n (mouseenter)=\"onDayHover(dayObj)\"\n (mouseleave)=\"clearDayHover()\"\n [disabled]=\"!dayObj.isCurrentMonth || dayObj.isDisabled\">\n {{ dayObj.day }}\n </button>\n }\n </div>\n </div>\n </div>\n\n <!-- Multi-Range List -->\n @if (multiRange && selectedRanges().length > 0) {\n <div class=\"multi-range-list\">\n <div class=\"multi-range-header\">\n <span class=\"multi-range-title\">Selected Ranges ({{ selectedRanges().length }})</span>\n </div>\n <div class=\"multi-range-items\">\n @for (range of selectedRanges(); track $index) {\n <div class=\"multi-range-item\">\n <span class=\"multi-range-text\">{{ range.rangeText }}</span>\n <button \n type=\"button\" \n class=\"btn-remove-range\" \n (click)=\"removeRange($index)\"\n title=\"Remove this range\">\n <svg width=\"14\" height=\"14\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z\"/>\n </svg>\n </button>\n </div>\n }\n </div>\n </div>\n }\n\n <!-- Time Picker -->\n @if (enableTimePicker) {\n <div class=\"time-picker-container\">\n <!-- Start Time -->\n <div class=\"time-picker-section\">\n <div class=\"time-picker-label\">Start Time</div>\n <div class=\"time-picker-inputs\">\n <div class=\"time-input-group\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-up\" \n (click)=\"incrementStartHour()\"\n title=\"Increment hour\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 4.86l-4.796 5.481c-.566.647-.106 1.659.753 1.659h9.592a1 1 0 0 0 .753-1.659l-4.796-5.48a1 1 0 0 0-1.506 0z\"/>\n </svg>\n </button>\n <input\n type=\"text\"\n class=\"time-input\"\n [value]=\"startHour.toString().padStart(2, '0')\"\n readonly\n title=\"Start hour\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-down\" \n (click)=\"decrementStartHour()\"\n title=\"Decrement hour\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 11.14l-4.796-5.481C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z\"/>\n </svg>\n </button>\n </div>\n <span class=\"time-separator\">:</span>\n <div class=\"time-input-group\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-up\" \n (click)=\"incrementStartMinute()\"\n title=\"Increment minute\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 4.86l-4.796 5.481c-.566.647-.106 1.659.753 1.659h9.592a1 1 0 0 0 .753-1.659l-4.796-5.48a1 1 0 0 0-1.506 0z\"/>\n </svg>\n </button>\n <input\n type=\"text\"\n class=\"time-input\"\n [value]=\"startMinute.toString().padStart(2, '0')\"\n readonly\n title=\"Start minute\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-down\" \n (click)=\"decrementStartMinute()\"\n title=\"Decrement minute\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 11.14l-4.796-5.481C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z\"/>\n </svg>\n </button>\n </div>\n @if (timeFormat === '12h') {\n <div class=\"time-period\">\n {{ startHour >= 12 ? 'PM' : 'AM' }}\n </div>\n }\n </div>\n </div>\n\n <!-- Separator -->\n <div class=\"time-separator-vertical\"></div>\n\n <!-- End Time -->\n <div class=\"time-picker-section\">\n <div class=\"time-picker-label\">End Time</div>\n <div class=\"time-picker-inputs\">\n <div class=\"time-input-group\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-up\" \n (click)=\"incrementEndHour()\"\n title=\"Increment hour\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 4.86l-4.796 5.481c-.566.647-.106 1.659.753 1.659h9.592a1 1 0 0 0 .753-1.659l-4.796-5.48a1 1 0 0 0-1.506 0z\"/>\n </svg>\n </button>\n <input\n type=\"text\"\n class=\"time-input\"\n [value]=\"endHour.toString().padStart(2, '0')\"\n readonly\n title=\"End hour\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-down\" \n (click)=\"decrementEndHour()\"\n title=\"Decrement hour\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 11.14l-4.796-5.481C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z\"/>\n </svg>\n </button>\n </div>\n <span class=\"time-separator\">:</span>\n <div class=\"time-input-group\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-up\" \n (click)=\"incrementEndMinute()\"\n title=\"Increment minute\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 4.86l-4.796 5.481c-.566.647-.106 1.659.753 1.659h9.592a1 1 0 0 0 .753-1.659l-4.796-5.48a1 1 0 0 0-1.506 0z\"/>\n </svg>\n </button>\n <input\n type=\"text\"\n class=\"time-input\"\n [value]=\"endMinute.toString().padStart(2, '0')\"\n readonly\n title=\"End minute\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-down\" \n (click)=\"decrementEndMinute()\"\n title=\"Decrement minute\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 11.14l-4.796-5.481C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z\"/>\n </svg>\n </button>\n </div>\n @if (timeFormat === '12h') {\n <div class=\"time-period\">\n {{ endHour >= 12 ? 'PM' : 'AM' }}\n </div>\n }\n </div>\n </div>\n </div>\n }\n\n <!-- Footer with buttons -->\n @if (showClearButton || multiRange || requireApply) {\n <div class=\"date-picker-footer\">\n @if (requireApply && !multiRange) {\n <div class=\"apply-footer-actions\">\n <button \n type=\"button\" \n class=\"btn-cancel\" \n (click)=\"cancelSelection()\" \n [disabled]=\"!hasPendingChanges()\"\n title=\"Cancel selection\">\n Cancel\n </button>\n <button \n type=\"button\" \n class=\"btn-apply\" \n (click)=\"applySelection()\" \n [disabled]=\"!hasPendingChanges() || !pendingStartDate || !pendingEndDate\"\n title=\"Apply selection\">\n Apply\n </button>\n </div>\n }\n @if (multiRange) {\n <div class=\"multi-range-footer-actions\">\n <button type=\"button\" class=\"btn-clear\" (click)=\"clear()\" title=\"Clear all ranges\">\n Clear All\n </button>\n <button type=\"button\" class=\"btn-done\" (click)=\"closeDatePicker()\" title=\"Done selecting\">\n Done\n </button>\n </div>\n }\n @if (!multiRange && !requireApply && showClearButton) {\n <button type=\"button\" class=\"btn-clear\" (click)=\"clear()\" title=\"Clear selection\">\n Clear\n </button>\n }\n </div>\n }\n </div>\n }\n</div>\n", styles: [".datepicker-wrapper{position:relative;width:100%}.datepicker-wrapper .datepicker-input{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;cursor:pointer}.datepicker-wrapper .datepicker-input:hover{border-color:var(--input-border-hover, #ced4da)}.datepicker-wrapper .datepicker-input:focus{border-color:var(--input-border-focus, #80bdff);box-shadow:0 0 0 .2rem #007bff40;outline:0}.datepicker-wrapper .datepicker-input::placeholder{color:#6c757d;opacity:1}.datepicker-wrapper .datepicker-input:disabled,.datepicker-wrapper .datepicker-input[readonly]{background-color:#e9ecef;opacity:1}.date-picker-dropdown{position:absolute;top:100%;left:0;margin-top:4px;background:#fff;border:1px solid #e1e4e8;border-radius:8px;box-shadow:0 4px 12px #00000014,0 0 1px #00000014;padding:16px;z-index:1060;min-width:680px}@media (max-width: 768px){.date-picker-dropdown{min-width:100%;left:0;right:0}}.date-picker-header-only-close{display:flex;justify-content:flex-end;margin-bottom:12px}.date-picker-header-only-close .btn-close-calendar{background-color:transparent;border:1px solid transparent;color:#6b7280;padding:6px 10px;border-radius:6px;cursor:pointer;transition:all .15s ease;font-size:1.5rem;line-height:1}.date-picker-header-only-close .btn-close-calendar:hover{background-color:#fee;border-color:#fcc;color:#dc2626;transform:translateY(-1px);box-shadow:0 2px 4px #dc26261a}.date-picker-header-only-close .btn-close-calendar:active{transform:translateY(0);box-shadow:none}.date-picker-presets{display:flex;gap:6px;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid #e5e7eb;align-items:center}@media (max-width: 768px){.date-picker-presets{flex-wrap:wrap}}.date-picker-presets button{font-size:.75rem;padding:6px 14px;border:none;background-color:#f9fafb;color:#374151;border-radius:6px;transition:all .15s ease;font-weight:500;cursor:pointer;border:1px solid #e5e7eb}.date-picker-presets button:hover{background-color:#f3f4f6;border-color:#d1d5db;transform:translateY(-1px);box-shadow:0 2px 4px #0000000f}.date-picker-presets button:active{transform:translateY(0);box-shadow:none}.date-picker-presets .btn-close-calendar{margin-left:auto;background-color:transparent;border:1px solid transparent;color:#6b7280;padding:6px 10px;font-size:1.5rem;line-height:1}.date-picker-presets .btn-close-calendar:hover{background-color:#fee;border-color:#fcc;color:#dc2626;transform:translateY(-1px);box-shadow:0 2px 4px #dc26261a}.date-picker-presets .btn-close-calendar:active{transform:translateY(0);box-shadow:none}.date-picker-calendars{display:flex;gap:32px}@media (max-width: 768px){.date-picker-calendars{flex-direction:column;gap:16px}}.date-picker-calendar{flex:1}.date-picker-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding:0 4px}.date-picker-header span{font-size:.813rem;font-weight:600;color:#111827}.date-picker-header button{padding:4px;color:#6b7280;text-decoration:none;border-radius:6px;transition:all .15s ease;border:none;background:transparent;cursor:pointer}.date-picker-header button:hover{background-color:#f3f4f6;color:#111827}.date-picker-weekdays{display:grid;grid-template-columns:repeat(7,1fr);gap:2px;margin-bottom:4px}.date-picker-weekdays span{text-align:center;font-size:.625rem;font-weight:600;color:#6b7280;padding:6px}.date-picker-days{display:grid;grid-template-columns:repeat(7,1fr);gap:2px}.date-picker-day{aspect-ratio:1;border:none;background:transparent;border-radius:50%;font-size:.75rem;cursor:pointer;transition:all .15s ease;color:#374151;font-weight:400;position:relative}.date-picker-day:hover:not(:disabled):not(.selected){background-color:#f3f4f6;color:#111827}.date-picker-day.empty{visibility:hidden}.date-picker-day.selected{background-color:#222;color:#fff;font-weight:600}.date-picker-day.in-range{background-color:#f9fafb;border-radius:0}.date-picker-day.in-hover-range{background-color:#e0e7ff;border-radius:0;opacity:.7;position:relative}.date-picker-day.in-hover-range:after{content:\"\";position:absolute;inset:0;border:1px dashed #6366f1;pointer-events:none}.date-picker-day:disabled{cursor:not-allowed;opacity:.3}.date-picker-day.disabled{cursor:not-allowed;opacity:.4;color:#9ca3af;background-color:#f9fafb;text-decoration:line-through}.date-picker-day.disabled:hover{background-color:#f9fafb;color:#9ca3af}.date-picker-day.keyboard-focused{outline:2px solid #3b82f6;outline-offset:2px;z-index:1}.date-picker-day.keyboard-focused:not(.selected){background-color:#eff6ff}.date-picker-day:focus-visible{outline:2px solid #3b82f6;outline-offset:2px;z-index:1}.date-picker-footer{padding:12px;border-top:1px solid #e1e4e8;display:flex;justify-content:center;gap:8px}.date-picker-footer .btn-clear{padding:8px 16px;background-color:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;font-size:.875rem;font-weight:500;color:#24292f;cursor:pointer;transition:all .15s ease}.date-picker-footer .btn-clear:hover{background-color:#f3f4f6;border-color:#8c959f;transform:translateY(-1px);box-shadow:0 2px 4px #0000000d}.date-picker-footer .btn-clear:active{transform:translateY(0);box-shadow:none;background-color:#e9ecef}.date-picker-footer .btn-done{padding:8px 16px;background-color:#222;border:1px solid #222;border-radius:6px;font-size:.875rem;font-weight:500;color:#fff;cursor:pointer;transition:all .15s ease}.date-picker-footer .btn-done:hover{background-color:#000;border-color:#000;transform:translateY(-1px);box-shadow:0 2px 4px #00000026}.date-picker-footer .btn-done:active{transform:translateY(0);box-shadow:none}.date-picker-footer .btn-apply{padding:8px 24px;background-color:#2563eb;border:1px solid #2563eb;border-radius:6px;font-size:.875rem;font-weight:600;color:#fff;cursor:pointer;transition:all .15s ease}.date-picker-footer .btn-apply:hover:not(:disabled){background-color:#1d4ed8;border-color:#1d4ed8;transform:translateY(-1px);box-shadow:0 2px 8px #2563eb40}.date-picker-footer .btn-apply:active:not(:disabled){transform:translateY(0);box-shadow:none}.date-picker-footer .btn-apply:disabled{opacity:.5;cursor:not-allowed}.date-picker-footer .btn-cancel{padding:8px 24px;background-color:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;font-size:.875rem;font-weight:500;color:#24292f;cursor:pointer;transition:all .15s ease}.date-picker-footer .btn-cancel:hover:not(:disabled){background-color:#f3f4f6;border-color:#8c959f;transform:translateY(-1px);box-shadow:0 2px 4px #0000000d}.date-picker-footer .btn-cancel:active:not(:disabled){transform:translateY(0);box-shadow:none;background-color:#e9ecef}.date-picker-footer .btn-cancel:disabled{opacity:.5;cursor:not-allowed}.date-picker-footer .apply-footer-actions{display:flex;gap:8px;width:100%;justify-content:flex-end}.date-picker-footer .multi-range-footer-actions{display:flex;gap:8px;width:100%;justify-content:space-between}.multi-range-list{border-top:1px solid #e1e4e8;border-bottom:1px solid #e1e4e8;padding:12px;margin-top:12px}.multi-range-list .multi-range-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.multi-range-list .multi-range-header .multi-range-title{font-size:.8125rem;font-weight:600;color:#24292f;text-transform:uppercase;letter-spacing:.025em}.multi-range-list .multi-range-items{display:flex;flex-direction:column;gap:6px;max-height:150px;overflow-y:auto}.multi-range-list .multi-range-items::-webkit-scrollbar{width:6px}.multi-range-list .multi-range-items::-webkit-scrollbar-track{background:#f1f3f4;border-radius:4px}.multi-range-list .multi-range-items::-webkit-scrollbar-thumb{background:#cbd5e0;border-radius:4px}.multi-range-list .multi-range-items::-webkit-scrollbar-thumb:hover{background:#a0aec0}.multi-range-list .multi-range-item{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background-color:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;transition:all .15s ease}.multi-range-list .multi-range-item:hover{background-color:#f3f4f6;border-color:#8c959f}.multi-range-list .multi-range-item .multi-range-text{font-size:.875rem;color:#24292f;font-weight:500}.multi-range-list .multi-range-item .btn-remove-range{padding:4px;background-color:transparent;border:1px solid transparent;border-radius:4px;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#6b7280;transition:all .15s ease}.multi-range-list .multi-range-item .btn-remove-range:hover{background-color:#fee;border-color:#fcc;color:#dc2626}.multi-range-list .multi-range-item .btn-remove-range:hover svg{transform:scale(1.1)}.multi-range-list .multi-range-item .btn-remove-range:active{transform:scale(.95)}.multi-range-list .multi-range-item .btn-remove-range svg{transition:transform .15s ease}.time-picker-container{display:flex;align-items:center;justify-content:center;gap:16px;padding:16px;border-top:1px solid #e5e7eb;border-bottom:1px solid #e5e7eb;margin:12px 0;background-color:#f9fafb}.time-picker-container .time-picker-section{display:flex;flex-direction:column;gap:8px;flex:1;align-items:center}.time-picker-container .time-picker-section .time-picker-label{font-size:.875rem;font-weight:600;color:#374151;text-align:center}.time-picker-container .time-picker-section .time-picker-inputs{display:flex;align-items:center;gap:8px}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group{display:flex;flex-direction:column;gap:4px;align-items:center}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-input{width:48px;padding:8px 6px;text-align:center;font-size:1.25rem;font-weight:600;color:#1f2937;background-color:#fff;border:2px solid #e5e7eb;border-radius:6px;outline:none;transition:all .15s ease;cursor:default;-webkit-user-select:none;user-select:none}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-input:focus{border-color:var(--primary-color, #3b82f6);box-shadow:0 0 0 3px #3b82f61a}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-btn{padding:4px 8px;background-color:#fff;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#6b7280;transition:all .15s ease}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-btn:hover{background-color:var(--primary-color, #3b82f6);border-color:var(--primary-color, #3b82f6);color:#fff;transform:translateY(-1px);box-shadow:0 2px 4px #0000001a}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-btn:active{transform:translateY(0);box-shadow:none}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-btn svg{pointer-events:none}.time-picker-container .time-picker-section .time-picker-inputs .time-separator{font-size:1.5rem;font-weight:700;color:#9ca3af;margin:0 4px}.time-picker-container .time-picker-section .time-picker-inputs .time-period{font-size:.875rem;font-weight:600;color:#6b7280;padding:8px 12px;background-color:#f3f4f6;border-radius:6px;min-width:48px;text-align:center}.time-picker-container .time-separator-vertical{width:1px;height:80px;background-color:#e5e7eb}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: ReactiveFormsModule }] });
|
|
3800
|
+
], usesOnChanges: true, ngImport: i0, template: "<div class=\"datepicker-wrapper\" \n [class]=\"'theme-' + theme\"\n [style.--input-border-hover]=\"inputBorderColorHover\"\n [style.--input-border-focus]=\"inputBorderColorFocus\">\n <input \n type=\"text\" \n class=\"datepicker-input\" \n [value]=\"dateRangeText()\" \n (click)=\"toggleDatePicker()\" \n [placeholder]=\"placeholder\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"placeholder\"\n [attr.aria-expanded]=\"showDatePicker()\"\n [attr.aria-haspopup]=\"'dialog'\"\n role=\"combobox\"\n [ngStyle]=\"{\n 'background-color': inputBackgroundColor,\n 'color': inputTextColor,\n 'border-color': inputBorderColor,\n 'padding': inputPadding\n }\"\n readonly>\n\n @if (showDatePicker()) {\n <div class=\"date-picker-dropdown\">\n @if (showPresets) {\n <div class=\"date-picker-presets\">\n @for (preset of presets; track preset.label) {\n <button type=\"button\" (click)=\"selectPresetRange(preset)\">{{ preset.label }}</button>\n }\n <button type=\"button\" class=\"btn-close-calendar\" (click)=\"closeDatePicker()\" title=\"Close\">\n \u00D7\n </button>\n </div>\n }\n\n @if (!showPresets) {\n <div class=\"date-picker-header-only-close\">\n <button type=\"button\" class=\"btn-close-calendar\" (click)=\"closeDatePicker()\" title=\"Close\">\n \u00D7\n </button>\n </div>\n }\n\n <!-- Calendars -->\n <div class=\"date-picker-calendars\">\n <!-- Previous month calendar -->\n <div class=\"date-picker-calendar\">\n <div class=\"date-picker-header\">\n <button type=\"button\" (click)=\"changeMonth(-1)\">\n <svg width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path fill-rule=\"evenodd\" d=\"M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z\"/>\n </svg>\n </button>\n <span>{{ previousMonthName() }}</span>\n <button type=\"button\" style=\"visibility: hidden;\">\n <svg width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path fill-rule=\"evenodd\" d=\"M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z\"/>\n </svg>\n </button>\n </div>\n <div class=\"date-picker-weekdays\">\n @for (dayName of weekDayNames(); track $index) {\n <span>{{ dayName }}</span>\n }\n </div>\n <div class=\"date-picker-days\">\n @for (dayObj of previousMonthVisibleDays(); track dayObj.date || $index) {\n <button \n type=\"button\"\n class=\"date-picker-day\" \n [class.empty]=\"!dayObj.isCurrentMonth\"\n [class.selected]=\"dayObj.isStart || dayObj.isEnd\"\n [class.in-range]=\"dayObj.inRange && !dayObj.isStart && !dayObj.isEnd\"\n [class.in-hover-range]=\"dayObj.inHoverRange && !dayObj.isStart && !dayObj.isEnd\"\n [class.disabled]=\"dayObj.isDisabled\"\n [class.keyboard-focused]=\"enableKeyboardNavigation && hasKeyboardFocus(dayObj.date, 0)\"\n [attr.tabindex]=\"enableKeyboardNavigation && dayObj.isCurrentMonth && hasKeyboardFocus(dayObj.date, 0) ? 0 : -1\"\n [attr.aria-label]=\"formatDateDisplay(dayObj.date)\"\n [attr.aria-selected]=\"dayObj.isStart || dayObj.isEnd\"\n [attr.aria-current]=\"dayObj.isStart ? 'date' : null\"\n [attr.aria-disabled]=\"dayObj.isDisabled\"\n (click)=\"selectDay(dayObj)\"\n (mouseenter)=\"onDayHover(dayObj)\"\n (mouseleave)=\"clearDayHover()\"\n [disabled]=\"!dayObj.isCurrentMonth || dayObj.isDisabled\">\n {{ dayObj.day }}\n </button>\n }\n </div>\n </div>\n\n <!-- Current month calendar -->\n <div class=\"date-picker-calendar\">\n <div class=\"date-picker-header\">\n <button type=\"button\" style=\"visibility: hidden;\">\n <svg width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path fill-rule=\"evenodd\" d=\"M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z\"/>\n </svg>\n </button>\n <span>{{ currentMonthName() }}</span>\n <button type=\"button\" (click)=\"changeMonth(1)\">\n <svg width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path fill-rule=\"evenodd\" d=\"M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z\"/>\n </svg>\n </button>\n </div>\n <div class=\"date-picker-weekdays\">\n @for (dayName of weekDayNames(); track $index) {\n <span>{{ dayName }}</span>\n }\n </div>\n <div class=\"date-picker-days\">\n @for (dayObj of currentMonthVisibleDays(); track dayObj.date || $index) {\n <button \n type=\"button\"\n class=\"date-picker-day\" \n [class.empty]=\"!dayObj.isCurrentMonth\"\n [class.selected]=\"dayObj.isStart || dayObj.isEnd\"\n [class.in-range]=\"dayObj.inRange && !dayObj.isStart && !dayObj.isEnd\"\n [class.in-hover-range]=\"dayObj.inHoverRange && !dayObj.isStart && !dayObj.isEnd\"\n [class.disabled]=\"dayObj.isDisabled\"\n [class.keyboard-focused]=\"enableKeyboardNavigation && hasKeyboardFocus(dayObj.date, 1)\"\n [attr.tabindex]=\"enableKeyboardNavigation && dayObj.isCurrentMonth && hasKeyboardFocus(dayObj.date, 1) ? 0 : -1\"\n [attr.aria-label]=\"formatDateDisplay(dayObj.date)\"\n [attr.aria-selected]=\"dayObj.isStart || dayObj.isEnd\"\n [attr.aria-current]=\"dayObj.isStart ? 'date' : null\"\n [attr.aria-disabled]=\"dayObj.isDisabled\"\n (click)=\"selectDay(dayObj)\"\n (mouseenter)=\"onDayHover(dayObj)\"\n (mouseleave)=\"clearDayHover()\"\n [disabled]=\"!dayObj.isCurrentMonth || dayObj.isDisabled\">\n {{ dayObj.day }}\n </button>\n }\n </div>\n </div>\n </div>\n\n <!-- Multi-Range List -->\n @if (multiRange && selectedRanges().length > 0) {\n <div class=\"multi-range-list\">\n <div class=\"multi-range-header\">\n <span class=\"multi-range-title\">Selected Ranges ({{ selectedRanges().length }})</span>\n </div>\n <div class=\"multi-range-items\">\n @for (range of selectedRanges(); track $index) {\n <div class=\"multi-range-item\">\n <span class=\"multi-range-text\">{{ range.rangeText }}</span>\n <button \n type=\"button\" \n class=\"btn-remove-range\" \n (click)=\"removeRange($index)\"\n title=\"Remove this range\">\n <svg width=\"14\" height=\"14\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z\"/>\n </svg>\n </button>\n </div>\n }\n </div>\n </div>\n }\n\n <!-- Time Picker -->\n @if (enableTimePicker) {\n <div class=\"time-picker-container\">\n <!-- Start Time -->\n <div class=\"time-picker-section\">\n <div class=\"time-picker-label\">Start Time</div>\n <div class=\"time-picker-inputs\">\n <div class=\"time-input-group\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-up\" \n (click)=\"incrementStartHour()\"\n title=\"Increment hour\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 4.86l-4.796 5.481c-.566.647-.106 1.659.753 1.659h9.592a1 1 0 0 0 .753-1.659l-4.796-5.48a1 1 0 0 0-1.506 0z\"/>\n </svg>\n </button>\n <input\n type=\"text\"\n class=\"time-input\"\n [value]=\"startHour.toString().padStart(2, '0')\"\n readonly\n title=\"Start hour\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-down\" \n (click)=\"decrementStartHour()\"\n title=\"Decrement hour\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 11.14l-4.796-5.481C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z\"/>\n </svg>\n </button>\n </div>\n <span class=\"time-separator\">:</span>\n <div class=\"time-input-group\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-up\" \n (click)=\"incrementStartMinute()\"\n title=\"Increment minute\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 4.86l-4.796 5.481c-.566.647-.106 1.659.753 1.659h9.592a1 1 0 0 0 .753-1.659l-4.796-5.48a1 1 0 0 0-1.506 0z\"/>\n </svg>\n </button>\n <input\n type=\"text\"\n class=\"time-input\"\n [value]=\"startMinute.toString().padStart(2, '0')\"\n readonly\n title=\"Start minute\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-down\" \n (click)=\"decrementStartMinute()\"\n title=\"Decrement minute\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 11.14l-4.796-5.481C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z\"/>\n </svg>\n </button>\n </div>\n @if (timeFormat === '12h') {\n <div class=\"time-period\">\n {{ startHour >= 12 ? 'PM' : 'AM' }}\n </div>\n }\n </div>\n </div>\n\n <!-- Separator -->\n <div class=\"time-separator-vertical\"></div>\n\n <!-- End Time -->\n <div class=\"time-picker-section\">\n <div class=\"time-picker-label\">End Time</div>\n <div class=\"time-picker-inputs\">\n <div class=\"time-input-group\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-up\" \n (click)=\"incrementEndHour()\"\n title=\"Increment hour\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 4.86l-4.796 5.481c-.566.647-.106 1.659.753 1.659h9.592a1 1 0 0 0 .753-1.659l-4.796-5.48a1 1 0 0 0-1.506 0z\"/>\n </svg>\n </button>\n <input\n type=\"text\"\n class=\"time-input\"\n [value]=\"endHour.toString().padStart(2, '0')\"\n readonly\n title=\"End hour\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-down\" \n (click)=\"decrementEndHour()\"\n title=\"Decrement hour\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 11.14l-4.796-5.481C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z\"/>\n </svg>\n </button>\n </div>\n <span class=\"time-separator\">:</span>\n <div class=\"time-input-group\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-up\" \n (click)=\"incrementEndMinute()\"\n title=\"Increment minute\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 4.86l-4.796 5.481c-.566.647-.106 1.659.753 1.659h9.592a1 1 0 0 0 .753-1.659l-4.796-5.48a1 1 0 0 0-1.506 0z\"/>\n </svg>\n </button>\n <input\n type=\"text\"\n class=\"time-input\"\n [value]=\"endMinute.toString().padStart(2, '0')\"\n readonly\n title=\"End minute\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-down\" \n (click)=\"decrementEndMinute()\"\n title=\"Decrement minute\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 11.14l-4.796-5.481C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z\"/>\n </svg>\n </button>\n </div>\n @if (timeFormat === '12h') {\n <div class=\"time-period\">\n {{ endHour >= 12 ? 'PM' : 'AM' }}\n </div>\n }\n </div>\n </div>\n </div>\n }\n\n <!-- Footer with buttons -->\n @if (showClearButton || multiRange || requireApply) {\n <div class=\"date-picker-footer\">\n @if (requireApply && !multiRange) {\n <div class=\"apply-footer-actions\">\n <button \n type=\"button\" \n class=\"btn-cancel\" \n (click)=\"cancelSelection()\" \n [disabled]=\"!hasPendingChanges()\"\n title=\"Cancel selection\">\n Cancel\n </button>\n <button \n type=\"button\" \n class=\"btn-apply\" \n (click)=\"applySelection()\" \n [disabled]=\"!hasPendingChanges() || !pendingStartDate || !pendingEndDate\"\n title=\"Apply selection\">\n Apply\n </button>\n </div>\n }\n @if (multiRange) {\n <div class=\"multi-range-footer-actions\">\n <button type=\"button\" class=\"btn-clear\" (click)=\"clear()\" title=\"Clear all ranges\">\n Clear All\n </button>\n <button type=\"button\" class=\"btn-done\" (click)=\"closeDatePicker()\" title=\"Done selecting\">\n Done\n </button>\n </div>\n }\n @if (!multiRange && !requireApply && showClearButton) {\n <button type=\"button\" class=\"btn-clear\" (click)=\"clear()\" title=\"Clear selection\">\n Clear\n </button>\n }\n </div>\n }\n </div>\n }\n</div>\n", styles: [".datepicker-wrapper{position:relative;width:100%}.datepicker-wrapper .datepicker-input{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;cursor:pointer}.datepicker-wrapper .datepicker-input:hover{border-color:var(--input-border-hover, #ced4da)}.datepicker-wrapper .datepicker-input:focus{border-color:var(--input-border-focus, #80bdff);box-shadow:0 0 0 .2rem #007bff40;outline:0}.datepicker-wrapper .datepicker-input::placeholder{color:#6c757d;opacity:1}.datepicker-wrapper .datepicker-input:disabled,.datepicker-wrapper .datepicker-input[readonly]{background-color:#e9ecef;opacity:1}.date-picker-dropdown{position:absolute;top:100%;left:0;margin-top:4px;background:#fff;border:1px solid #e1e4e8;border-radius:8px;box-shadow:0 4px 12px #00000014,0 0 1px #00000014;padding:16px;z-index:1060;min-width:680px}@media (max-width: 768px){.date-picker-dropdown{min-width:100%;left:0;right:0}}.date-picker-header-only-close{display:flex;justify-content:flex-end;margin-bottom:12px}.date-picker-header-only-close .btn-close-calendar{background-color:transparent;border:1px solid transparent;color:#6b7280;padding:6px 10px;border-radius:6px;cursor:pointer;transition:all .15s ease;font-size:1.5rem;line-height:1}.date-picker-header-only-close .btn-close-calendar:hover{background-color:#fee;border-color:#fcc;color:#dc2626;transform:translateY(-1px);box-shadow:0 2px 4px #dc26261a}.date-picker-header-only-close .btn-close-calendar:active{transform:translateY(0);box-shadow:none}.date-picker-presets{display:flex;gap:6px;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid #e5e7eb;align-items:center}@media (max-width: 768px){.date-picker-presets{flex-wrap:wrap}}.date-picker-presets button{font-size:.75rem;padding:6px 14px;border:none;background-color:#f9fafb;color:#374151;border-radius:6px;transition:all .15s ease;font-weight:500;cursor:pointer;border:1px solid #e5e7eb}.date-picker-presets button:hover{background-color:#f3f4f6;border-color:#d1d5db;transform:translateY(-1px);box-shadow:0 2px 4px #0000000f}.date-picker-presets button:active{transform:translateY(0);box-shadow:none}.date-picker-presets .btn-close-calendar{margin-left:auto;background-color:transparent;border:1px solid transparent;color:#6b7280;padding:6px 10px;font-size:1.5rem;line-height:1}.date-picker-presets .btn-close-calendar:hover{background-color:#fee;border-color:#fcc;color:#dc2626;transform:translateY(-1px);box-shadow:0 2px 4px #dc26261a}.date-picker-presets .btn-close-calendar:active{transform:translateY(0);box-shadow:none}.date-picker-calendars{display:flex;gap:32px}@media (max-width: 768px){.date-picker-calendars{flex-direction:column;gap:16px}}.date-picker-calendar{flex:1;contain:layout paint}.date-picker-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding:0 4px}.date-picker-header span{font-size:.813rem;font-weight:600;color:#111827}.date-picker-header button{padding:4px;color:#6b7280;text-decoration:none;border-radius:6px;transition:all .15s ease;border:none;background:transparent;cursor:pointer}.date-picker-header button:hover{background-color:#f3f4f6;color:#111827}.date-picker-weekdays{display:grid;grid-template-columns:repeat(7,1fr);gap:2px;margin-bottom:4px}.date-picker-weekdays span{text-align:center;font-size:.625rem;font-weight:600;color:#6b7280;padding:6px}.date-picker-days{display:grid;grid-template-columns:repeat(7,1fr);gap:2px;contain:layout paint}.date-picker-day{aspect-ratio:1;border:none;background:transparent;border-radius:50%;font-size:.75rem;cursor:pointer;transition:all .15s ease;color:#374151;font-weight:400;position:relative}.date-picker-day:hover:not(:disabled):not(.selected){background-color:#f3f4f6;color:#111827}.date-picker-day.empty{visibility:hidden}.date-picker-day.selected{background-color:#222;color:#fff;font-weight:600}.date-picker-day.in-range{background-color:#f9fafb;border-radius:0}.date-picker-day.in-hover-range{background-color:#e0e7ff;border-radius:0;opacity:.7;position:relative}.date-picker-day.in-hover-range:after{content:\"\";position:absolute;inset:0;border:1px dashed #6366f1;pointer-events:none}.date-picker-day:disabled{cursor:not-allowed;opacity:.3}.date-picker-day.disabled{cursor:not-allowed;opacity:.4;color:#9ca3af;background-color:#f9fafb;text-decoration:line-through}.date-picker-day.disabled:hover{background-color:#f9fafb;color:#9ca3af}.date-picker-day.keyboard-focused{outline:2px solid #3b82f6;outline-offset:2px;z-index:1}.date-picker-day.keyboard-focused:not(.selected){background-color:#eff6ff}.date-picker-day:focus-visible{outline:2px solid #3b82f6;outline-offset:2px;z-index:1}.date-picker-footer{padding:12px;border-top:1px solid #e1e4e8;display:flex;justify-content:center;gap:8px}.date-picker-footer .btn-clear{padding:8px 16px;background-color:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;font-size:.875rem;font-weight:500;color:#24292f;cursor:pointer;transition:all .15s ease}.date-picker-footer .btn-clear:hover{background-color:#f3f4f6;border-color:#8c959f;transform:translateY(-1px);box-shadow:0 2px 4px #0000000d}.date-picker-footer .btn-clear:active{transform:translateY(0);box-shadow:none;background-color:#e9ecef}.date-picker-footer .btn-done{padding:8px 16px;background-color:#222;border:1px solid #222;border-radius:6px;font-size:.875rem;font-weight:500;color:#fff;cursor:pointer;transition:all .15s ease}.date-picker-footer .btn-done:hover{background-color:#000;border-color:#000;transform:translateY(-1px);box-shadow:0 2px 4px #00000026}.date-picker-footer .btn-done:active{transform:translateY(0);box-shadow:none}.date-picker-footer .btn-apply{padding:8px 24px;background-color:#2563eb;border:1px solid #2563eb;border-radius:6px;font-size:.875rem;font-weight:600;color:#fff;cursor:pointer;transition:all .15s ease}.date-picker-footer .btn-apply:hover:not(:disabled){background-color:#1d4ed8;border-color:#1d4ed8;transform:translateY(-1px);box-shadow:0 2px 8px #2563eb40}.date-picker-footer .btn-apply:active:not(:disabled){transform:translateY(0);box-shadow:none}.date-picker-footer .btn-apply:disabled{opacity:.5;cursor:not-allowed}.date-picker-footer .btn-cancel{padding:8px 24px;background-color:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;font-size:.875rem;font-weight:500;color:#24292f;cursor:pointer;transition:all .15s ease}.date-picker-footer .btn-cancel:hover:not(:disabled){background-color:#f3f4f6;border-color:#8c959f;transform:translateY(-1px);box-shadow:0 2px 4px #0000000d}.date-picker-footer .btn-cancel:active:not(:disabled){transform:translateY(0);box-shadow:none;background-color:#e9ecef}.date-picker-footer .btn-cancel:disabled{opacity:.5;cursor:not-allowed}.date-picker-footer .apply-footer-actions{display:flex;gap:8px;width:100%;justify-content:flex-end}.date-picker-footer .multi-range-footer-actions{display:flex;gap:8px;width:100%;justify-content:space-between}.multi-range-list{border-top:1px solid #e1e4e8;border-bottom:1px solid #e1e4e8;padding:12px;margin-top:12px}.multi-range-list .multi-range-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.multi-range-list .multi-range-header .multi-range-title{font-size:.8125rem;font-weight:600;color:#24292f;text-transform:uppercase;letter-spacing:.025em}.multi-range-list .multi-range-items{display:flex;flex-direction:column;gap:6px;max-height:150px;overflow-y:auto}.multi-range-list .multi-range-items::-webkit-scrollbar{width:6px}.multi-range-list .multi-range-items::-webkit-scrollbar-track{background:#f1f3f4;border-radius:4px}.multi-range-list .multi-range-items::-webkit-scrollbar-thumb{background:#cbd5e0;border-radius:4px}.multi-range-list .multi-range-items::-webkit-scrollbar-thumb:hover{background:#a0aec0}.multi-range-list .multi-range-item{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background-color:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;transition:all .15s ease}.multi-range-list .multi-range-item:hover{background-color:#f3f4f6;border-color:#8c959f}.multi-range-list .multi-range-item .multi-range-text{font-size:.875rem;color:#24292f;font-weight:500}.multi-range-list .multi-range-item .btn-remove-range{padding:4px;background-color:transparent;border:1px solid transparent;border-radius:4px;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#6b7280;transition:all .15s ease}.multi-range-list .multi-range-item .btn-remove-range:hover{background-color:#fee;border-color:#fcc;color:#dc2626}.multi-range-list .multi-range-item .btn-remove-range:hover svg{transform:scale(1.1)}.multi-range-list .multi-range-item .btn-remove-range:active{transform:scale(.95)}.multi-range-list .multi-range-item .btn-remove-range svg{transition:transform .15s ease}.time-picker-container{display:flex;align-items:center;justify-content:center;gap:16px;padding:16px;border-top:1px solid #e5e7eb;border-bottom:1px solid #e5e7eb;margin:12px 0;background-color:#f9fafb}.time-picker-container .time-picker-section{display:flex;flex-direction:column;gap:8px;flex:1;align-items:center}.time-picker-container .time-picker-section .time-picker-label{font-size:.875rem;font-weight:600;color:#374151;text-align:center}.time-picker-container .time-picker-section .time-picker-inputs{display:flex;align-items:center;gap:8px}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group{display:flex;flex-direction:column;gap:4px;align-items:center}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-input{width:48px;padding:8px 6px;text-align:center;font-size:1.25rem;font-weight:600;color:#1f2937;background-color:#fff;border:2px solid #e5e7eb;border-radius:6px;outline:none;transition:all .15s ease;cursor:default;-webkit-user-select:none;user-select:none}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-input:focus{border-color:var(--primary-color, #3b82f6);box-shadow:0 0 0 3px #3b82f61a}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-btn{padding:4px 8px;background-color:#fff;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#6b7280;transition:all .15s ease}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-btn:hover{background-color:var(--primary-color, #3b82f6);border-color:var(--primary-color, #3b82f6);color:#fff;transform:translateY(-1px);box-shadow:0 2px 4px #0000001a}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-btn:active{transform:translateY(0);box-shadow:none}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-btn svg{pointer-events:none}.time-picker-container .time-picker-section .time-picker-inputs .time-separator{font-size:1.5rem;font-weight:700;color:#9ca3af;margin:0 4px}.time-picker-container .time-picker-section .time-picker-inputs .time-period{font-size:.875rem;font-weight:600;color:#6b7280;padding:8px 12px;background-color:#f3f4f6;border-radius:6px;min-width:48px;text-align:center}.time-picker-container .time-separator-vertical{width:1px;height:80px;background-color:#e5e7eb}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: ReactiveFormsModule }] });
|
|
2887
3801
|
}
|
|
2888
3802
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DualDatepickerComponent, decorators: [{
|
|
2889
3803
|
type: Component,
|
|
@@ -2908,7 +3822,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
2908
3822
|
},
|
|
2909
3823
|
deps: [PresetRegistry]
|
|
2910
3824
|
}
|
|
2911
|
-
], template: "<div class=\"datepicker-wrapper\" \n [class]=\"'theme-' + theme\"\n [style.--input-border-hover]=\"inputBorderColorHover\"\n [style.--input-border-focus]=\"inputBorderColorFocus\">\n <input \n type=\"text\" \n class=\"datepicker-input\" \n [value]=\"dateRangeText()\" \n (click)=\"toggleDatePicker()\" \n [placeholder]=\"placeholder\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"placeholder\"\n [attr.aria-expanded]=\"showDatePicker()\"\n [attr.aria-haspopup]=\"'dialog'\"\n role=\"combobox\"\n [ngStyle]=\"{\n 'background-color': inputBackgroundColor,\n 'color': inputTextColor,\n 'border-color': inputBorderColor,\n 'padding': inputPadding\n }\"\n readonly>\n\n @if (showDatePicker()) {\n <div class=\"date-picker-dropdown\">\n @if (showPresets) {\n <div class=\"date-picker-presets\">\n @for (preset of presets; track preset.label) {\n <button type=\"button\" (click)=\"selectPresetRange(preset)\">{{ preset.label }}</button>\n }\n <button type=\"button\" class=\"btn-close-calendar\" (click)=\"closeDatePicker()\" title=\"Close\">\n \u00D7\n </button>\n </div>\n }\n\n @if (!showPresets) {\n <div class=\"date-picker-header-only-close\">\n <button type=\"button\" class=\"btn-close-calendar\" (click)=\"closeDatePicker()\" title=\"Close\">\n \u00D7\n </button>\n </div>\n }\n\n <!-- Calendars -->\n <div class=\"date-picker-calendars\">\n <!-- Previous month calendar -->\n <div class=\"date-picker-calendar\">\n <div class=\"date-picker-header\">\n <button type=\"button\" (click)=\"changeMonth(-1)\">\n <svg width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path fill-rule=\"evenodd\" d=\"M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z\"/>\n </svg>\n </button>\n <span>{{ previousMonthName() }}</span>\n <button type=\"button\" style=\"visibility: hidden;\">\n <svg width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path fill-rule=\"evenodd\" d=\"M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z\"/>\n </svg>\n </button>\n </div>\n <div class=\"date-picker-weekdays\">\n @for (dayName of weekDayNames(); track $index) {\n <span>{{ dayName }}</span>\n }\n </div>\n <div class=\"date-picker-days\">\n @for (dayObj of previousMonthDays(); track dayObj.date || $index) {\n <button \n type=\"button\"\n class=\"date-picker-day\" \n [class.empty]=\"!dayObj.isCurrentMonth\"\n [class.selected]=\"dayObj.isStart || dayObj.isEnd\"\n [class.in-range]=\"dayObj.inRange && !dayObj.isStart && !dayObj.isEnd\"\n [class.in-hover-range]=\"dayObj.inHoverRange && !dayObj.isStart && !dayObj.isEnd\"\n [class.disabled]=\"dayObj.isDisabled\"\n [class.keyboard-focused]=\"enableKeyboardNavigation && hasKeyboardFocus(dayObj.date, 0)\"\n [attr.tabindex]=\"enableKeyboardNavigation && dayObj.isCurrentMonth && hasKeyboardFocus(dayObj.date, 0) ? 0 : -1\"\n [attr.aria-label]=\"formatDateDisplay(dayObj.date)\"\n [attr.aria-selected]=\"dayObj.isStart || dayObj.isEnd\"\n [attr.aria-current]=\"dayObj.isStart ? 'date' : null\"\n [attr.aria-disabled]=\"dayObj.isDisabled\"\n (click)=\"selectDay(dayObj)\"\n (mouseenter)=\"onDayHover(dayObj)\"\n (mouseleave)=\"clearDayHover()\"\n [disabled]=\"!dayObj.isCurrentMonth || dayObj.isDisabled\">\n {{ dayObj.day }}\n </button>\n }\n </div>\n </div>\n\n <!-- Current month calendar -->\n <div class=\"date-picker-calendar\">\n <div class=\"date-picker-header\">\n <button type=\"button\" style=\"visibility: hidden;\">\n <svg width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path fill-rule=\"evenodd\" d=\"M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z\"/>\n </svg>\n </button>\n <span>{{ currentMonthName() }}</span>\n <button type=\"button\" (click)=\"changeMonth(1)\">\n <svg width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path fill-rule=\"evenodd\" d=\"M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z\"/>\n </svg>\n </button>\n </div>\n <div class=\"date-picker-weekdays\">\n @for (dayName of weekDayNames(); track $index) {\n <span>{{ dayName }}</span>\n }\n </div>\n <div class=\"date-picker-days\">\n @for (dayObj of currentMonthDays(); track dayObj.date || $index) {\n <button \n type=\"button\"\n class=\"date-picker-day\" \n [class.empty]=\"!dayObj.isCurrentMonth\"\n [class.selected]=\"dayObj.isStart || dayObj.isEnd\"\n [class.in-range]=\"dayObj.inRange && !dayObj.isStart && !dayObj.isEnd\"\n [class.in-hover-range]=\"dayObj.inHoverRange && !dayObj.isStart && !dayObj.isEnd\"\n [class.disabled]=\"dayObj.isDisabled\"\n [class.keyboard-focused]=\"enableKeyboardNavigation && hasKeyboardFocus(dayObj.date, 1)\"\n [attr.tabindex]=\"enableKeyboardNavigation && dayObj.isCurrentMonth && hasKeyboardFocus(dayObj.date, 1) ? 0 : -1\"\n [attr.aria-label]=\"formatDateDisplay(dayObj.date)\"\n [attr.aria-selected]=\"dayObj.isStart || dayObj.isEnd\"\n [attr.aria-current]=\"dayObj.isStart ? 'date' : null\"\n [attr.aria-disabled]=\"dayObj.isDisabled\"\n (click)=\"selectDay(dayObj)\"\n (mouseenter)=\"onDayHover(dayObj)\"\n (mouseleave)=\"clearDayHover()\"\n [disabled]=\"!dayObj.isCurrentMonth || dayObj.isDisabled\">\n {{ dayObj.day }}\n </button>\n }\n </div>\n </div>\n </div>\n\n <!-- Multi-Range List -->\n @if (multiRange && selectedRanges().length > 0) {\n <div class=\"multi-range-list\">\n <div class=\"multi-range-header\">\n <span class=\"multi-range-title\">Selected Ranges ({{ selectedRanges().length }})</span>\n </div>\n <div class=\"multi-range-items\">\n @for (range of selectedRanges(); track $index) {\n <div class=\"multi-range-item\">\n <span class=\"multi-range-text\">{{ range.rangeText }}</span>\n <button \n type=\"button\" \n class=\"btn-remove-range\" \n (click)=\"removeRange($index)\"\n title=\"Remove this range\">\n <svg width=\"14\" height=\"14\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z\"/>\n </svg>\n </button>\n </div>\n }\n </div>\n </div>\n }\n\n <!-- Time Picker -->\n @if (enableTimePicker) {\n <div class=\"time-picker-container\">\n <!-- Start Time -->\n <div class=\"time-picker-section\">\n <div class=\"time-picker-label\">Start Time</div>\n <div class=\"time-picker-inputs\">\n <div class=\"time-input-group\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-up\" \n (click)=\"incrementStartHour()\"\n title=\"Increment hour\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 4.86l-4.796 5.481c-.566.647-.106 1.659.753 1.659h9.592a1 1 0 0 0 .753-1.659l-4.796-5.48a1 1 0 0 0-1.506 0z\"/>\n </svg>\n </button>\n <input\n type=\"text\"\n class=\"time-input\"\n [value]=\"startHour.toString().padStart(2, '0')\"\n readonly\n title=\"Start hour\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-down\" \n (click)=\"decrementStartHour()\"\n title=\"Decrement hour\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 11.14l-4.796-5.481C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z\"/>\n </svg>\n </button>\n </div>\n <span class=\"time-separator\">:</span>\n <div class=\"time-input-group\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-up\" \n (click)=\"incrementStartMinute()\"\n title=\"Increment minute\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 4.86l-4.796 5.481c-.566.647-.106 1.659.753 1.659h9.592a1 1 0 0 0 .753-1.659l-4.796-5.48a1 1 0 0 0-1.506 0z\"/>\n </svg>\n </button>\n <input\n type=\"text\"\n class=\"time-input\"\n [value]=\"startMinute.toString().padStart(2, '0')\"\n readonly\n title=\"Start minute\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-down\" \n (click)=\"decrementStartMinute()\"\n title=\"Decrement minute\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 11.14l-4.796-5.481C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z\"/>\n </svg>\n </button>\n </div>\n @if (timeFormat === '12h') {\n <div class=\"time-period\">\n {{ startHour >= 12 ? 'PM' : 'AM' }}\n </div>\n }\n </div>\n </div>\n\n <!-- Separator -->\n <div class=\"time-separator-vertical\"></div>\n\n <!-- End Time -->\n <div class=\"time-picker-section\">\n <div class=\"time-picker-label\">End Time</div>\n <div class=\"time-picker-inputs\">\n <div class=\"time-input-group\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-up\" \n (click)=\"incrementEndHour()\"\n title=\"Increment hour\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 4.86l-4.796 5.481c-.566.647-.106 1.659.753 1.659h9.592a1 1 0 0 0 .753-1.659l-4.796-5.48a1 1 0 0 0-1.506 0z\"/>\n </svg>\n </button>\n <input\n type=\"text\"\n class=\"time-input\"\n [value]=\"endHour.toString().padStart(2, '0')\"\n readonly\n title=\"End hour\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-down\" \n (click)=\"decrementEndHour()\"\n title=\"Decrement hour\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 11.14l-4.796-5.481C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z\"/>\n </svg>\n </button>\n </div>\n <span class=\"time-separator\">:</span>\n <div class=\"time-input-group\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-up\" \n (click)=\"incrementEndMinute()\"\n title=\"Increment minute\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 4.86l-4.796 5.481c-.566.647-.106 1.659.753 1.659h9.592a1 1 0 0 0 .753-1.659l-4.796-5.48a1 1 0 0 0-1.506 0z\"/>\n </svg>\n </button>\n <input\n type=\"text\"\n class=\"time-input\"\n [value]=\"endMinute.toString().padStart(2, '0')\"\n readonly\n title=\"End minute\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-down\" \n (click)=\"decrementEndMinute()\"\n title=\"Decrement minute\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 11.14l-4.796-5.481C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z\"/>\n </svg>\n </button>\n </div>\n @if (timeFormat === '12h') {\n <div class=\"time-period\">\n {{ endHour >= 12 ? 'PM' : 'AM' }}\n </div>\n }\n </div>\n </div>\n </div>\n }\n\n <!-- Footer with buttons -->\n @if (showClearButton || multiRange || requireApply) {\n <div class=\"date-picker-footer\">\n @if (requireApply && !multiRange) {\n <div class=\"apply-footer-actions\">\n <button \n type=\"button\" \n class=\"btn-cancel\" \n (click)=\"cancelSelection()\" \n [disabled]=\"!hasPendingChanges()\"\n title=\"Cancel selection\">\n Cancel\n </button>\n <button \n type=\"button\" \n class=\"btn-apply\" \n (click)=\"applySelection()\" \n [disabled]=\"!hasPendingChanges() || !pendingStartDate || !pendingEndDate\"\n title=\"Apply selection\">\n Apply\n </button>\n </div>\n }\n @if (multiRange) {\n <div class=\"multi-range-footer-actions\">\n <button type=\"button\" class=\"btn-clear\" (click)=\"clear()\" title=\"Clear all ranges\">\n Clear All\n </button>\n <button type=\"button\" class=\"btn-done\" (click)=\"closeDatePicker()\" title=\"Done selecting\">\n Done\n </button>\n </div>\n }\n @if (!multiRange && !requireApply && showClearButton) {\n <button type=\"button\" class=\"btn-clear\" (click)=\"clear()\" title=\"Clear selection\">\n Clear\n </button>\n }\n </div>\n }\n </div>\n }\n</div>\n", styles: [".datepicker-wrapper{position:relative;width:100%}.datepicker-wrapper .datepicker-input{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;cursor:pointer}.datepicker-wrapper .datepicker-input:hover{border-color:var(--input-border-hover, #ced4da)}.datepicker-wrapper .datepicker-input:focus{border-color:var(--input-border-focus, #80bdff);box-shadow:0 0 0 .2rem #007bff40;outline:0}.datepicker-wrapper .datepicker-input::placeholder{color:#6c757d;opacity:1}.datepicker-wrapper .datepicker-input:disabled,.datepicker-wrapper .datepicker-input[readonly]{background-color:#e9ecef;opacity:1}.date-picker-dropdown{position:absolute;top:100%;left:0;margin-top:4px;background:#fff;border:1px solid #e1e4e8;border-radius:8px;box-shadow:0 4px 12px #00000014,0 0 1px #00000014;padding:16px;z-index:1060;min-width:680px}@media (max-width: 768px){.date-picker-dropdown{min-width:100%;left:0;right:0}}.date-picker-header-only-close{display:flex;justify-content:flex-end;margin-bottom:12px}.date-picker-header-only-close .btn-close-calendar{background-color:transparent;border:1px solid transparent;color:#6b7280;padding:6px 10px;border-radius:6px;cursor:pointer;transition:all .15s ease;font-size:1.5rem;line-height:1}.date-picker-header-only-close .btn-close-calendar:hover{background-color:#fee;border-color:#fcc;color:#dc2626;transform:translateY(-1px);box-shadow:0 2px 4px #dc26261a}.date-picker-header-only-close .btn-close-calendar:active{transform:translateY(0);box-shadow:none}.date-picker-presets{display:flex;gap:6px;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid #e5e7eb;align-items:center}@media (max-width: 768px){.date-picker-presets{flex-wrap:wrap}}.date-picker-presets button{font-size:.75rem;padding:6px 14px;border:none;background-color:#f9fafb;color:#374151;border-radius:6px;transition:all .15s ease;font-weight:500;cursor:pointer;border:1px solid #e5e7eb}.date-picker-presets button:hover{background-color:#f3f4f6;border-color:#d1d5db;transform:translateY(-1px);box-shadow:0 2px 4px #0000000f}.date-picker-presets button:active{transform:translateY(0);box-shadow:none}.date-picker-presets .btn-close-calendar{margin-left:auto;background-color:transparent;border:1px solid transparent;color:#6b7280;padding:6px 10px;font-size:1.5rem;line-height:1}.date-picker-presets .btn-close-calendar:hover{background-color:#fee;border-color:#fcc;color:#dc2626;transform:translateY(-1px);box-shadow:0 2px 4px #dc26261a}.date-picker-presets .btn-close-calendar:active{transform:translateY(0);box-shadow:none}.date-picker-calendars{display:flex;gap:32px}@media (max-width: 768px){.date-picker-calendars{flex-direction:column;gap:16px}}.date-picker-calendar{flex:1}.date-picker-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding:0 4px}.date-picker-header span{font-size:.813rem;font-weight:600;color:#111827}.date-picker-header button{padding:4px;color:#6b7280;text-decoration:none;border-radius:6px;transition:all .15s ease;border:none;background:transparent;cursor:pointer}.date-picker-header button:hover{background-color:#f3f4f6;color:#111827}.date-picker-weekdays{display:grid;grid-template-columns:repeat(7,1fr);gap:2px;margin-bottom:4px}.date-picker-weekdays span{text-align:center;font-size:.625rem;font-weight:600;color:#6b7280;padding:6px}.date-picker-days{display:grid;grid-template-columns:repeat(7,1fr);gap:2px}.date-picker-day{aspect-ratio:1;border:none;background:transparent;border-radius:50%;font-size:.75rem;cursor:pointer;transition:all .15s ease;color:#374151;font-weight:400;position:relative}.date-picker-day:hover:not(:disabled):not(.selected){background-color:#f3f4f6;color:#111827}.date-picker-day.empty{visibility:hidden}.date-picker-day.selected{background-color:#222;color:#fff;font-weight:600}.date-picker-day.in-range{background-color:#f9fafb;border-radius:0}.date-picker-day.in-hover-range{background-color:#e0e7ff;border-radius:0;opacity:.7;position:relative}.date-picker-day.in-hover-range:after{content:\"\";position:absolute;inset:0;border:1px dashed #6366f1;pointer-events:none}.date-picker-day:disabled{cursor:not-allowed;opacity:.3}.date-picker-day.disabled{cursor:not-allowed;opacity:.4;color:#9ca3af;background-color:#f9fafb;text-decoration:line-through}.date-picker-day.disabled:hover{background-color:#f9fafb;color:#9ca3af}.date-picker-day.keyboard-focused{outline:2px solid #3b82f6;outline-offset:2px;z-index:1}.date-picker-day.keyboard-focused:not(.selected){background-color:#eff6ff}.date-picker-day:focus-visible{outline:2px solid #3b82f6;outline-offset:2px;z-index:1}.date-picker-footer{padding:12px;border-top:1px solid #e1e4e8;display:flex;justify-content:center;gap:8px}.date-picker-footer .btn-clear{padding:8px 16px;background-color:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;font-size:.875rem;font-weight:500;color:#24292f;cursor:pointer;transition:all .15s ease}.date-picker-footer .btn-clear:hover{background-color:#f3f4f6;border-color:#8c959f;transform:translateY(-1px);box-shadow:0 2px 4px #0000000d}.date-picker-footer .btn-clear:active{transform:translateY(0);box-shadow:none;background-color:#e9ecef}.date-picker-footer .btn-done{padding:8px 16px;background-color:#222;border:1px solid #222;border-radius:6px;font-size:.875rem;font-weight:500;color:#fff;cursor:pointer;transition:all .15s ease}.date-picker-footer .btn-done:hover{background-color:#000;border-color:#000;transform:translateY(-1px);box-shadow:0 2px 4px #00000026}.date-picker-footer .btn-done:active{transform:translateY(0);box-shadow:none}.date-picker-footer .btn-apply{padding:8px 24px;background-color:#2563eb;border:1px solid #2563eb;border-radius:6px;font-size:.875rem;font-weight:600;color:#fff;cursor:pointer;transition:all .15s ease}.date-picker-footer .btn-apply:hover:not(:disabled){background-color:#1d4ed8;border-color:#1d4ed8;transform:translateY(-1px);box-shadow:0 2px 8px #2563eb40}.date-picker-footer .btn-apply:active:not(:disabled){transform:translateY(0);box-shadow:none}.date-picker-footer .btn-apply:disabled{opacity:.5;cursor:not-allowed}.date-picker-footer .btn-cancel{padding:8px 24px;background-color:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;font-size:.875rem;font-weight:500;color:#24292f;cursor:pointer;transition:all .15s ease}.date-picker-footer .btn-cancel:hover:not(:disabled){background-color:#f3f4f6;border-color:#8c959f;transform:translateY(-1px);box-shadow:0 2px 4px #0000000d}.date-picker-footer .btn-cancel:active:not(:disabled){transform:translateY(0);box-shadow:none;background-color:#e9ecef}.date-picker-footer .btn-cancel:disabled{opacity:.5;cursor:not-allowed}.date-picker-footer .apply-footer-actions{display:flex;gap:8px;width:100%;justify-content:flex-end}.date-picker-footer .multi-range-footer-actions{display:flex;gap:8px;width:100%;justify-content:space-between}.multi-range-list{border-top:1px solid #e1e4e8;border-bottom:1px solid #e1e4e8;padding:12px;margin-top:12px}.multi-range-list .multi-range-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.multi-range-list .multi-range-header .multi-range-title{font-size:.8125rem;font-weight:600;color:#24292f;text-transform:uppercase;letter-spacing:.025em}.multi-range-list .multi-range-items{display:flex;flex-direction:column;gap:6px;max-height:150px;overflow-y:auto}.multi-range-list .multi-range-items::-webkit-scrollbar{width:6px}.multi-range-list .multi-range-items::-webkit-scrollbar-track{background:#f1f3f4;border-radius:4px}.multi-range-list .multi-range-items::-webkit-scrollbar-thumb{background:#cbd5e0;border-radius:4px}.multi-range-list .multi-range-items::-webkit-scrollbar-thumb:hover{background:#a0aec0}.multi-range-list .multi-range-item{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background-color:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;transition:all .15s ease}.multi-range-list .multi-range-item:hover{background-color:#f3f4f6;border-color:#8c959f}.multi-range-list .multi-range-item .multi-range-text{font-size:.875rem;color:#24292f;font-weight:500}.multi-range-list .multi-range-item .btn-remove-range{padding:4px;background-color:transparent;border:1px solid transparent;border-radius:4px;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#6b7280;transition:all .15s ease}.multi-range-list .multi-range-item .btn-remove-range:hover{background-color:#fee;border-color:#fcc;color:#dc2626}.multi-range-list .multi-range-item .btn-remove-range:hover svg{transform:scale(1.1)}.multi-range-list .multi-range-item .btn-remove-range:active{transform:scale(.95)}.multi-range-list .multi-range-item .btn-remove-range svg{transition:transform .15s ease}.time-picker-container{display:flex;align-items:center;justify-content:center;gap:16px;padding:16px;border-top:1px solid #e5e7eb;border-bottom:1px solid #e5e7eb;margin:12px 0;background-color:#f9fafb}.time-picker-container .time-picker-section{display:flex;flex-direction:column;gap:8px;flex:1;align-items:center}.time-picker-container .time-picker-section .time-picker-label{font-size:.875rem;font-weight:600;color:#374151;text-align:center}.time-picker-container .time-picker-section .time-picker-inputs{display:flex;align-items:center;gap:8px}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group{display:flex;flex-direction:column;gap:4px;align-items:center}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-input{width:48px;padding:8px 6px;text-align:center;font-size:1.25rem;font-weight:600;color:#1f2937;background-color:#fff;border:2px solid #e5e7eb;border-radius:6px;outline:none;transition:all .15s ease;cursor:default;-webkit-user-select:none;user-select:none}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-input:focus{border-color:var(--primary-color, #3b82f6);box-shadow:0 0 0 3px #3b82f61a}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-btn{padding:4px 8px;background-color:#fff;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#6b7280;transition:all .15s ease}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-btn:hover{background-color:var(--primary-color, #3b82f6);border-color:var(--primary-color, #3b82f6);color:#fff;transform:translateY(-1px);box-shadow:0 2px 4px #0000001a}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-btn:active{transform:translateY(0);box-shadow:none}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-btn svg{pointer-events:none}.time-picker-container .time-picker-section .time-picker-inputs .time-separator{font-size:1.5rem;font-weight:700;color:#9ca3af;margin:0 4px}.time-picker-container .time-picker-section .time-picker-inputs .time-period{font-size:.875rem;font-weight:600;color:#6b7280;padding:8px 12px;background-color:#f3f4f6;border-radius:6px;min-width:48px;text-align:center}.time-picker-container .time-separator-vertical{width:1px;height:80px;background-color:#e5e7eb}\n"] }]
|
|
3825
|
+
], template: "<div class=\"datepicker-wrapper\" \n [class]=\"'theme-' + theme\"\n [style.--input-border-hover]=\"inputBorderColorHover\"\n [style.--input-border-focus]=\"inputBorderColorFocus\">\n <input \n type=\"text\" \n class=\"datepicker-input\" \n [value]=\"dateRangeText()\" \n (click)=\"toggleDatePicker()\" \n [placeholder]=\"placeholder\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"placeholder\"\n [attr.aria-expanded]=\"showDatePicker()\"\n [attr.aria-haspopup]=\"'dialog'\"\n role=\"combobox\"\n [ngStyle]=\"{\n 'background-color': inputBackgroundColor,\n 'color': inputTextColor,\n 'border-color': inputBorderColor,\n 'padding': inputPadding\n }\"\n readonly>\n\n @if (showDatePicker()) {\n <div class=\"date-picker-dropdown\">\n @if (showPresets) {\n <div class=\"date-picker-presets\">\n @for (preset of presets; track preset.label) {\n <button type=\"button\" (click)=\"selectPresetRange(preset)\">{{ preset.label }}</button>\n }\n <button type=\"button\" class=\"btn-close-calendar\" (click)=\"closeDatePicker()\" title=\"Close\">\n \u00D7\n </button>\n </div>\n }\n\n @if (!showPresets) {\n <div class=\"date-picker-header-only-close\">\n <button type=\"button\" class=\"btn-close-calendar\" (click)=\"closeDatePicker()\" title=\"Close\">\n \u00D7\n </button>\n </div>\n }\n\n <!-- Calendars -->\n <div class=\"date-picker-calendars\">\n <!-- Previous month calendar -->\n <div class=\"date-picker-calendar\">\n <div class=\"date-picker-header\">\n <button type=\"button\" (click)=\"changeMonth(-1)\">\n <svg width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path fill-rule=\"evenodd\" d=\"M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z\"/>\n </svg>\n </button>\n <span>{{ previousMonthName() }}</span>\n <button type=\"button\" style=\"visibility: hidden;\">\n <svg width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path fill-rule=\"evenodd\" d=\"M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z\"/>\n </svg>\n </button>\n </div>\n <div class=\"date-picker-weekdays\">\n @for (dayName of weekDayNames(); track $index) {\n <span>{{ dayName }}</span>\n }\n </div>\n <div class=\"date-picker-days\">\n @for (dayObj of previousMonthVisibleDays(); track dayObj.date || $index) {\n <button \n type=\"button\"\n class=\"date-picker-day\" \n [class.empty]=\"!dayObj.isCurrentMonth\"\n [class.selected]=\"dayObj.isStart || dayObj.isEnd\"\n [class.in-range]=\"dayObj.inRange && !dayObj.isStart && !dayObj.isEnd\"\n [class.in-hover-range]=\"dayObj.inHoverRange && !dayObj.isStart && !dayObj.isEnd\"\n [class.disabled]=\"dayObj.isDisabled\"\n [class.keyboard-focused]=\"enableKeyboardNavigation && hasKeyboardFocus(dayObj.date, 0)\"\n [attr.tabindex]=\"enableKeyboardNavigation && dayObj.isCurrentMonth && hasKeyboardFocus(dayObj.date, 0) ? 0 : -1\"\n [attr.aria-label]=\"formatDateDisplay(dayObj.date)\"\n [attr.aria-selected]=\"dayObj.isStart || dayObj.isEnd\"\n [attr.aria-current]=\"dayObj.isStart ? 'date' : null\"\n [attr.aria-disabled]=\"dayObj.isDisabled\"\n (click)=\"selectDay(dayObj)\"\n (mouseenter)=\"onDayHover(dayObj)\"\n (mouseleave)=\"clearDayHover()\"\n [disabled]=\"!dayObj.isCurrentMonth || dayObj.isDisabled\">\n {{ dayObj.day }}\n </button>\n }\n </div>\n </div>\n\n <!-- Current month calendar -->\n <div class=\"date-picker-calendar\">\n <div class=\"date-picker-header\">\n <button type=\"button\" style=\"visibility: hidden;\">\n <svg width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path fill-rule=\"evenodd\" d=\"M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z\"/>\n </svg>\n </button>\n <span>{{ currentMonthName() }}</span>\n <button type=\"button\" (click)=\"changeMonth(1)\">\n <svg width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path fill-rule=\"evenodd\" d=\"M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z\"/>\n </svg>\n </button>\n </div>\n <div class=\"date-picker-weekdays\">\n @for (dayName of weekDayNames(); track $index) {\n <span>{{ dayName }}</span>\n }\n </div>\n <div class=\"date-picker-days\">\n @for (dayObj of currentMonthVisibleDays(); track dayObj.date || $index) {\n <button \n type=\"button\"\n class=\"date-picker-day\" \n [class.empty]=\"!dayObj.isCurrentMonth\"\n [class.selected]=\"dayObj.isStart || dayObj.isEnd\"\n [class.in-range]=\"dayObj.inRange && !dayObj.isStart && !dayObj.isEnd\"\n [class.in-hover-range]=\"dayObj.inHoverRange && !dayObj.isStart && !dayObj.isEnd\"\n [class.disabled]=\"dayObj.isDisabled\"\n [class.keyboard-focused]=\"enableKeyboardNavigation && hasKeyboardFocus(dayObj.date, 1)\"\n [attr.tabindex]=\"enableKeyboardNavigation && dayObj.isCurrentMonth && hasKeyboardFocus(dayObj.date, 1) ? 0 : -1\"\n [attr.aria-label]=\"formatDateDisplay(dayObj.date)\"\n [attr.aria-selected]=\"dayObj.isStart || dayObj.isEnd\"\n [attr.aria-current]=\"dayObj.isStart ? 'date' : null\"\n [attr.aria-disabled]=\"dayObj.isDisabled\"\n (click)=\"selectDay(dayObj)\"\n (mouseenter)=\"onDayHover(dayObj)\"\n (mouseleave)=\"clearDayHover()\"\n [disabled]=\"!dayObj.isCurrentMonth || dayObj.isDisabled\">\n {{ dayObj.day }}\n </button>\n }\n </div>\n </div>\n </div>\n\n <!-- Multi-Range List -->\n @if (multiRange && selectedRanges().length > 0) {\n <div class=\"multi-range-list\">\n <div class=\"multi-range-header\">\n <span class=\"multi-range-title\">Selected Ranges ({{ selectedRanges().length }})</span>\n </div>\n <div class=\"multi-range-items\">\n @for (range of selectedRanges(); track $index) {\n <div class=\"multi-range-item\">\n <span class=\"multi-range-text\">{{ range.rangeText }}</span>\n <button \n type=\"button\" \n class=\"btn-remove-range\" \n (click)=\"removeRange($index)\"\n title=\"Remove this range\">\n <svg width=\"14\" height=\"14\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z\"/>\n </svg>\n </button>\n </div>\n }\n </div>\n </div>\n }\n\n <!-- Time Picker -->\n @if (enableTimePicker) {\n <div class=\"time-picker-container\">\n <!-- Start Time -->\n <div class=\"time-picker-section\">\n <div class=\"time-picker-label\">Start Time</div>\n <div class=\"time-picker-inputs\">\n <div class=\"time-input-group\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-up\" \n (click)=\"incrementStartHour()\"\n title=\"Increment hour\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 4.86l-4.796 5.481c-.566.647-.106 1.659.753 1.659h9.592a1 1 0 0 0 .753-1.659l-4.796-5.48a1 1 0 0 0-1.506 0z\"/>\n </svg>\n </button>\n <input\n type=\"text\"\n class=\"time-input\"\n [value]=\"startHour.toString().padStart(2, '0')\"\n readonly\n title=\"Start hour\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-down\" \n (click)=\"decrementStartHour()\"\n title=\"Decrement hour\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 11.14l-4.796-5.481C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z\"/>\n </svg>\n </button>\n </div>\n <span class=\"time-separator\">:</span>\n <div class=\"time-input-group\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-up\" \n (click)=\"incrementStartMinute()\"\n title=\"Increment minute\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 4.86l-4.796 5.481c-.566.647-.106 1.659.753 1.659h9.592a1 1 0 0 0 .753-1.659l-4.796-5.48a1 1 0 0 0-1.506 0z\"/>\n </svg>\n </button>\n <input\n type=\"text\"\n class=\"time-input\"\n [value]=\"startMinute.toString().padStart(2, '0')\"\n readonly\n title=\"Start minute\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-down\" \n (click)=\"decrementStartMinute()\"\n title=\"Decrement minute\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 11.14l-4.796-5.481C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z\"/>\n </svg>\n </button>\n </div>\n @if (timeFormat === '12h') {\n <div class=\"time-period\">\n {{ startHour >= 12 ? 'PM' : 'AM' }}\n </div>\n }\n </div>\n </div>\n\n <!-- Separator -->\n <div class=\"time-separator-vertical\"></div>\n\n <!-- End Time -->\n <div class=\"time-picker-section\">\n <div class=\"time-picker-label\">End Time</div>\n <div class=\"time-picker-inputs\">\n <div class=\"time-input-group\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-up\" \n (click)=\"incrementEndHour()\"\n title=\"Increment hour\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 4.86l-4.796 5.481c-.566.647-.106 1.659.753 1.659h9.592a1 1 0 0 0 .753-1.659l-4.796-5.48a1 1 0 0 0-1.506 0z\"/>\n </svg>\n </button>\n <input\n type=\"text\"\n class=\"time-input\"\n [value]=\"endHour.toString().padStart(2, '0')\"\n readonly\n title=\"End hour\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-down\" \n (click)=\"decrementEndHour()\"\n title=\"Decrement hour\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 11.14l-4.796-5.481C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z\"/>\n </svg>\n </button>\n </div>\n <span class=\"time-separator\">:</span>\n <div class=\"time-input-group\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-up\" \n (click)=\"incrementEndMinute()\"\n title=\"Increment minute\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 4.86l-4.796 5.481c-.566.647-.106 1.659.753 1.659h9.592a1 1 0 0 0 .753-1.659l-4.796-5.48a1 1 0 0 0-1.506 0z\"/>\n </svg>\n </button>\n <input\n type=\"text\"\n class=\"time-input\"\n [value]=\"endMinute.toString().padStart(2, '0')\"\n readonly\n title=\"End minute\">\n <button \n type=\"button\" \n class=\"time-btn time-btn-down\" \n (click)=\"decrementEndMinute()\"\n title=\"Decrement minute\">\n <svg width=\"12\" height=\"12\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path d=\"M7.247 11.14l-4.796-5.481C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z\"/>\n </svg>\n </button>\n </div>\n @if (timeFormat === '12h') {\n <div class=\"time-period\">\n {{ endHour >= 12 ? 'PM' : 'AM' }}\n </div>\n }\n </div>\n </div>\n </div>\n }\n\n <!-- Footer with buttons -->\n @if (showClearButton || multiRange || requireApply) {\n <div class=\"date-picker-footer\">\n @if (requireApply && !multiRange) {\n <div class=\"apply-footer-actions\">\n <button \n type=\"button\" \n class=\"btn-cancel\" \n (click)=\"cancelSelection()\" \n [disabled]=\"!hasPendingChanges()\"\n title=\"Cancel selection\">\n Cancel\n </button>\n <button \n type=\"button\" \n class=\"btn-apply\" \n (click)=\"applySelection()\" \n [disabled]=\"!hasPendingChanges() || !pendingStartDate || !pendingEndDate\"\n title=\"Apply selection\">\n Apply\n </button>\n </div>\n }\n @if (multiRange) {\n <div class=\"multi-range-footer-actions\">\n <button type=\"button\" class=\"btn-clear\" (click)=\"clear()\" title=\"Clear all ranges\">\n Clear All\n </button>\n <button type=\"button\" class=\"btn-done\" (click)=\"closeDatePicker()\" title=\"Done selecting\">\n Done\n </button>\n </div>\n }\n @if (!multiRange && !requireApply && showClearButton) {\n <button type=\"button\" class=\"btn-clear\" (click)=\"clear()\" title=\"Clear selection\">\n Clear\n </button>\n }\n </div>\n }\n </div>\n }\n</div>\n", styles: [".datepicker-wrapper{position:relative;width:100%}.datepicker-wrapper .datepicker-input{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;cursor:pointer}.datepicker-wrapper .datepicker-input:hover{border-color:var(--input-border-hover, #ced4da)}.datepicker-wrapper .datepicker-input:focus{border-color:var(--input-border-focus, #80bdff);box-shadow:0 0 0 .2rem #007bff40;outline:0}.datepicker-wrapper .datepicker-input::placeholder{color:#6c757d;opacity:1}.datepicker-wrapper .datepicker-input:disabled,.datepicker-wrapper .datepicker-input[readonly]{background-color:#e9ecef;opacity:1}.date-picker-dropdown{position:absolute;top:100%;left:0;margin-top:4px;background:#fff;border:1px solid #e1e4e8;border-radius:8px;box-shadow:0 4px 12px #00000014,0 0 1px #00000014;padding:16px;z-index:1060;min-width:680px}@media (max-width: 768px){.date-picker-dropdown{min-width:100%;left:0;right:0}}.date-picker-header-only-close{display:flex;justify-content:flex-end;margin-bottom:12px}.date-picker-header-only-close .btn-close-calendar{background-color:transparent;border:1px solid transparent;color:#6b7280;padding:6px 10px;border-radius:6px;cursor:pointer;transition:all .15s ease;font-size:1.5rem;line-height:1}.date-picker-header-only-close .btn-close-calendar:hover{background-color:#fee;border-color:#fcc;color:#dc2626;transform:translateY(-1px);box-shadow:0 2px 4px #dc26261a}.date-picker-header-only-close .btn-close-calendar:active{transform:translateY(0);box-shadow:none}.date-picker-presets{display:flex;gap:6px;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid #e5e7eb;align-items:center}@media (max-width: 768px){.date-picker-presets{flex-wrap:wrap}}.date-picker-presets button{font-size:.75rem;padding:6px 14px;border:none;background-color:#f9fafb;color:#374151;border-radius:6px;transition:all .15s ease;font-weight:500;cursor:pointer;border:1px solid #e5e7eb}.date-picker-presets button:hover{background-color:#f3f4f6;border-color:#d1d5db;transform:translateY(-1px);box-shadow:0 2px 4px #0000000f}.date-picker-presets button:active{transform:translateY(0);box-shadow:none}.date-picker-presets .btn-close-calendar{margin-left:auto;background-color:transparent;border:1px solid transparent;color:#6b7280;padding:6px 10px;font-size:1.5rem;line-height:1}.date-picker-presets .btn-close-calendar:hover{background-color:#fee;border-color:#fcc;color:#dc2626;transform:translateY(-1px);box-shadow:0 2px 4px #dc26261a}.date-picker-presets .btn-close-calendar:active{transform:translateY(0);box-shadow:none}.date-picker-calendars{display:flex;gap:32px}@media (max-width: 768px){.date-picker-calendars{flex-direction:column;gap:16px}}.date-picker-calendar{flex:1;contain:layout paint}.date-picker-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding:0 4px}.date-picker-header span{font-size:.813rem;font-weight:600;color:#111827}.date-picker-header button{padding:4px;color:#6b7280;text-decoration:none;border-radius:6px;transition:all .15s ease;border:none;background:transparent;cursor:pointer}.date-picker-header button:hover{background-color:#f3f4f6;color:#111827}.date-picker-weekdays{display:grid;grid-template-columns:repeat(7,1fr);gap:2px;margin-bottom:4px}.date-picker-weekdays span{text-align:center;font-size:.625rem;font-weight:600;color:#6b7280;padding:6px}.date-picker-days{display:grid;grid-template-columns:repeat(7,1fr);gap:2px;contain:layout paint}.date-picker-day{aspect-ratio:1;border:none;background:transparent;border-radius:50%;font-size:.75rem;cursor:pointer;transition:all .15s ease;color:#374151;font-weight:400;position:relative}.date-picker-day:hover:not(:disabled):not(.selected){background-color:#f3f4f6;color:#111827}.date-picker-day.empty{visibility:hidden}.date-picker-day.selected{background-color:#222;color:#fff;font-weight:600}.date-picker-day.in-range{background-color:#f9fafb;border-radius:0}.date-picker-day.in-hover-range{background-color:#e0e7ff;border-radius:0;opacity:.7;position:relative}.date-picker-day.in-hover-range:after{content:\"\";position:absolute;inset:0;border:1px dashed #6366f1;pointer-events:none}.date-picker-day:disabled{cursor:not-allowed;opacity:.3}.date-picker-day.disabled{cursor:not-allowed;opacity:.4;color:#9ca3af;background-color:#f9fafb;text-decoration:line-through}.date-picker-day.disabled:hover{background-color:#f9fafb;color:#9ca3af}.date-picker-day.keyboard-focused{outline:2px solid #3b82f6;outline-offset:2px;z-index:1}.date-picker-day.keyboard-focused:not(.selected){background-color:#eff6ff}.date-picker-day:focus-visible{outline:2px solid #3b82f6;outline-offset:2px;z-index:1}.date-picker-footer{padding:12px;border-top:1px solid #e1e4e8;display:flex;justify-content:center;gap:8px}.date-picker-footer .btn-clear{padding:8px 16px;background-color:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;font-size:.875rem;font-weight:500;color:#24292f;cursor:pointer;transition:all .15s ease}.date-picker-footer .btn-clear:hover{background-color:#f3f4f6;border-color:#8c959f;transform:translateY(-1px);box-shadow:0 2px 4px #0000000d}.date-picker-footer .btn-clear:active{transform:translateY(0);box-shadow:none;background-color:#e9ecef}.date-picker-footer .btn-done{padding:8px 16px;background-color:#222;border:1px solid #222;border-radius:6px;font-size:.875rem;font-weight:500;color:#fff;cursor:pointer;transition:all .15s ease}.date-picker-footer .btn-done:hover{background-color:#000;border-color:#000;transform:translateY(-1px);box-shadow:0 2px 4px #00000026}.date-picker-footer .btn-done:active{transform:translateY(0);box-shadow:none}.date-picker-footer .btn-apply{padding:8px 24px;background-color:#2563eb;border:1px solid #2563eb;border-radius:6px;font-size:.875rem;font-weight:600;color:#fff;cursor:pointer;transition:all .15s ease}.date-picker-footer .btn-apply:hover:not(:disabled){background-color:#1d4ed8;border-color:#1d4ed8;transform:translateY(-1px);box-shadow:0 2px 8px #2563eb40}.date-picker-footer .btn-apply:active:not(:disabled){transform:translateY(0);box-shadow:none}.date-picker-footer .btn-apply:disabled{opacity:.5;cursor:not-allowed}.date-picker-footer .btn-cancel{padding:8px 24px;background-color:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;font-size:.875rem;font-weight:500;color:#24292f;cursor:pointer;transition:all .15s ease}.date-picker-footer .btn-cancel:hover:not(:disabled){background-color:#f3f4f6;border-color:#8c959f;transform:translateY(-1px);box-shadow:0 2px 4px #0000000d}.date-picker-footer .btn-cancel:active:not(:disabled){transform:translateY(0);box-shadow:none;background-color:#e9ecef}.date-picker-footer .btn-cancel:disabled{opacity:.5;cursor:not-allowed}.date-picker-footer .apply-footer-actions{display:flex;gap:8px;width:100%;justify-content:flex-end}.date-picker-footer .multi-range-footer-actions{display:flex;gap:8px;width:100%;justify-content:space-between}.multi-range-list{border-top:1px solid #e1e4e8;border-bottom:1px solid #e1e4e8;padding:12px;margin-top:12px}.multi-range-list .multi-range-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.multi-range-list .multi-range-header .multi-range-title{font-size:.8125rem;font-weight:600;color:#24292f;text-transform:uppercase;letter-spacing:.025em}.multi-range-list .multi-range-items{display:flex;flex-direction:column;gap:6px;max-height:150px;overflow-y:auto}.multi-range-list .multi-range-items::-webkit-scrollbar{width:6px}.multi-range-list .multi-range-items::-webkit-scrollbar-track{background:#f1f3f4;border-radius:4px}.multi-range-list .multi-range-items::-webkit-scrollbar-thumb{background:#cbd5e0;border-radius:4px}.multi-range-list .multi-range-items::-webkit-scrollbar-thumb:hover{background:#a0aec0}.multi-range-list .multi-range-item{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background-color:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;transition:all .15s ease}.multi-range-list .multi-range-item:hover{background-color:#f3f4f6;border-color:#8c959f}.multi-range-list .multi-range-item .multi-range-text{font-size:.875rem;color:#24292f;font-weight:500}.multi-range-list .multi-range-item .btn-remove-range{padding:4px;background-color:transparent;border:1px solid transparent;border-radius:4px;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#6b7280;transition:all .15s ease}.multi-range-list .multi-range-item .btn-remove-range:hover{background-color:#fee;border-color:#fcc;color:#dc2626}.multi-range-list .multi-range-item .btn-remove-range:hover svg{transform:scale(1.1)}.multi-range-list .multi-range-item .btn-remove-range:active{transform:scale(.95)}.multi-range-list .multi-range-item .btn-remove-range svg{transition:transform .15s ease}.time-picker-container{display:flex;align-items:center;justify-content:center;gap:16px;padding:16px;border-top:1px solid #e5e7eb;border-bottom:1px solid #e5e7eb;margin:12px 0;background-color:#f9fafb}.time-picker-container .time-picker-section{display:flex;flex-direction:column;gap:8px;flex:1;align-items:center}.time-picker-container .time-picker-section .time-picker-label{font-size:.875rem;font-weight:600;color:#374151;text-align:center}.time-picker-container .time-picker-section .time-picker-inputs{display:flex;align-items:center;gap:8px}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group{display:flex;flex-direction:column;gap:4px;align-items:center}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-input{width:48px;padding:8px 6px;text-align:center;font-size:1.25rem;font-weight:600;color:#1f2937;background-color:#fff;border:2px solid #e5e7eb;border-radius:6px;outline:none;transition:all .15s ease;cursor:default;-webkit-user-select:none;user-select:none}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-input:focus{border-color:var(--primary-color, #3b82f6);box-shadow:0 0 0 3px #3b82f61a}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-btn{padding:4px 8px;background-color:#fff;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#6b7280;transition:all .15s ease}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-btn:hover{background-color:var(--primary-color, #3b82f6);border-color:var(--primary-color, #3b82f6);color:#fff;transform:translateY(-1px);box-shadow:0 2px 4px #0000001a}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-btn:active{transform:translateY(0);box-shadow:none}.time-picker-container .time-picker-section .time-picker-inputs .time-input-group .time-btn svg{pointer-events:none}.time-picker-container .time-picker-section .time-picker-inputs .time-separator{font-size:1.5rem;font-weight:700;color:#9ca3af;margin:0 4px}.time-picker-container .time-picker-section .time-picker-inputs .time-period{font-size:.875rem;font-weight:600;color:#6b7280;padding:8px 12px;background-color:#f3f4f6;border-radius:6px;min-width:48px;text-align:center}.time-picker-container .time-separator-vertical{width:1px;height:80px;background-color:#e5e7eb}\n"] }]
|
|
2912
3826
|
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { placeholder: [{
|
|
2913
3827
|
type: Input
|
|
2914
3828
|
}], startDate: [{
|
|
@@ -2963,6 +3877,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
2963
3877
|
type: Input
|
|
2964
3878
|
}], defaultEndTime: [{
|
|
2965
3879
|
type: Input
|
|
3880
|
+
}], virtualWeeks: [{
|
|
3881
|
+
type: Input
|
|
2966
3882
|
}], dateRangeChange: [{
|
|
2967
3883
|
type: Output
|
|
2968
3884
|
}], dateRangeSelected: [{
|
|
@@ -3504,6 +4420,16 @@ function providePresetPackage(packageName, presets) {
|
|
|
3504
4420
|
* - Built-in presets as plugins
|
|
3505
4421
|
* - Provider functions for custom presets
|
|
3506
4422
|
*
|
|
4423
|
+
* v3.7.0: Calendar Grid Cache
|
|
4424
|
+
* - CalendarGridFactory for deterministic grid generation
|
|
4425
|
+
* - CalendarGridCache with LRU memoization
|
|
4426
|
+
* - Separates grid structure from decorations for performance
|
|
4427
|
+
*
|
|
4428
|
+
* v3.8.0: Range Highlight Cache
|
|
4429
|
+
* - RangeHighlighter for decoration logic (pure computation)
|
|
4430
|
+
* - RangeHighlighterCache with LRU (48 grids)
|
|
4431
|
+
* - Eliminates redundant decoration recomputations
|
|
4432
|
+
*
|
|
3507
4433
|
* Import from here for clean barrel exports
|
|
3508
4434
|
*/
|
|
3509
4435
|
|
|
@@ -3515,5 +4441,5 @@ function providePresetPackage(packageName, presets) {
|
|
|
3515
4441
|
* Generated bundle index. Do not edit.
|
|
3516
4442
|
*/
|
|
3517
4443
|
|
|
3518
|
-
export { BUILT_IN_PRESETS, CommonPresets, DATE_ADAPTER$1 as DATE_ADAPTER, DATE_CLOCK, DateAdapter, DualDateRangeStore, DualDatepickerComponent, LAST_14_DAYS_PRESET, LAST_30_DAYS_PRESET, LAST_60_DAYS_PRESET, LAST_7_DAYS_PRESET, LAST_90_DAYS_PRESET, LAST_MONTH_PRESET, LAST_QUARTER_PRESET, LAST_WEEK_PRESET, LAST_YEAR_PRESET, MONTH_TO_DATE_PRESET, NativeDateAdapter$1 as NativeDateAdapter, PresetEngine, PresetRegistry, QUARTER_TO_DATE_PRESET, SystemClock, THIS_MONTH_PRESET, THIS_QUARTER_PRESET, THIS_WEEK_PRESET, THIS_YEAR_PRESET, TODAY_PRESET, YEAR_TO_DATE_PRESET, YESTERDAY_PRESET, applyBounds, createPreset, formatISODate, getLastMonth, getLastNDays, getLastNMonths, getLastNYears, getLastQuarter, getLastWeek, getLastYear, getMonthToDate, getQuarterToDate, getThisMonth, getThisQuarter, getThisWeek, getThisYear, getToday, getYearToDate, getYesterday, isDateDisabled, isRangePresetPlugin, parseISODate, presetEngine, provideBuiltInPresets, provideCustomPresets, providePresetPackage, validateDateBounds, validateRangeBounds, validateRangeOrder };
|
|
4444
|
+
export { BUILT_IN_PRESETS, CalendarGridCache, CalendarGridFactory, CommonPresets, DATE_ADAPTER$1 as DATE_ADAPTER, DATE_CLOCK, DateAdapter, DualDateRangeStore, DualDatepickerComponent, LAST_14_DAYS_PRESET, LAST_30_DAYS_PRESET, LAST_60_DAYS_PRESET, LAST_7_DAYS_PRESET, LAST_90_DAYS_PRESET, LAST_MONTH_PRESET, LAST_QUARTER_PRESET, LAST_WEEK_PRESET, LAST_YEAR_PRESET, MONTH_TO_DATE_PRESET, NativeDateAdapter$1 as NativeDateAdapter, PresetEngine, PresetRegistry, QUARTER_TO_DATE_PRESET, RangeHighlighter, RangeHighlighterCache, SystemClock, THIS_MONTH_PRESET, THIS_QUARTER_PRESET, THIS_WEEK_PRESET, THIS_YEAR_PRESET, TODAY_PRESET, YEAR_TO_DATE_PRESET, YESTERDAY_PRESET, applyBounds, clampWeekStart, createPreset, formatISODate, getLastMonth, getLastNDays, getLastNMonths, getLastNYears, getLastQuarter, getLastWeek, getLastYear, getMonthToDate, getQuarterToDate, getThisMonth, getThisQuarter, getThisWeek, getThisYear, getToday, getVirtualWeekWindow, getVisibleWeeks, getYearToDate, getYesterday, isDateDisabled, isRangePresetPlugin, isVirtualWeeksEnabled, navigateWeekWindow, parseISODate, presetEngine, provideBuiltInPresets, provideCustomPresets, providePresetPackage, validateDateBounds, validateRangeBounds, validateRangeOrder };
|
|
3519
4445
|
//# sourceMappingURL=oneluiz-dual-datepicker.mjs.map
|